Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

22
app/tool/saga/BUILD Normal file
View File

@@ -0,0 +1,22 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/tool/saga/cmd:all-srcs",
"//app/tool/saga/conf:all-srcs",
"//app/tool/saga/dao:all-srcs",
"//app/tool/saga/http:all-srcs",
"//app/tool/saga/model:all-srcs",
"//app/tool/saga/service:all-srcs",
],
tags = ["automanaged"],
)

397
app/tool/saga/CHANGELOG.md Normal file
View File

@@ -0,0 +1,397 @@
# v5.23.3
1. 解决企业微信发送失败问题即部门ID号更改导致wechat接口获取用户为空引起。
# v5.23.2
1. 解决某些情况下saga提示合并成功但实际未合并成功的问题。
2. 解决pipeline hook时的panic错误。
# v5.23.1
1. 修复saga panic错误。
# v5.23.0
1. 增加合并时生成的commit信息中包括MR的title和描述。
2. 优化权限信息获取方式。
# v5.22.9
1. 解决pipeline状态改变发送通知时偶发的panic错误。
# v5.22.8
1. 增加超级权限用户的定制功能。
2. 增加等待合并或者正在执行合并的MR数量提示。
# v5.22.7
1. 解决saga发送通知时偶发的panic错误。
# v5.22.6
1. 解决 saga 偶尔发生panic导致caster实例重启问题。
# v5.22.5
1. 增加权限文件删除、移动时,对应的权限信息变更情况。
# v5.22.4
1. 临时恢复目标分支和配置的正则表达式分支判断失误的问题。
# v5.22.3
1. 解决目标分支和配置的正则表达式分支判断失误的问题。
2. 解决使用了目标分支直接作为权限分支的问题。
# v5.22.2
1. 增加SAGA的UT代码。
# v5.22.1
1. 解决delay merge功能出现的问题
# v5.22.0
1增加配置权限仅限于当前目录的定制即权限约束不再向下递归
2增加label准入的定制即发版阶段设置了label的MR才允许合入
3增加sven平台配置更改立即生效以及配置更改后的权限信息自动同步
4增加delay合并功能的定制即+mr后等pipeline跑过后自动合入并且不需要retry pipeline
5调整了部分代码结构
# v5.21.1
1. 增加hbase存储时的容错
# v5.21.0
1. 增加hbase存储
2. 增加权限关联分支
3. 增加pipeline是否关联saga流程的定制
4. 增加最少review人数的定制
5. 优化代码结构和流程
# v5.20.7
1. 解决role info重复显示的问题
# v5.20.6
1. 优化role info的显示去除all的todo
# v5.20.5
1. 解决role info显示的格式问题
# v5.20.4
1. 增加role info中owner的显示
2. 增加target分支不在白名单中的提示
# v5.20.3
1. 更改路由从V1到V2
# v5.20.2
1. +mr支持读取+1
2. 优化更新权限接口
3. 去掉多余的更新权限方法
4. 执行失败的时候立即释放锁
# v5.20.1
1. 过滤重复+merge
2. 修正note的显示问题
3. 修正owners检查的逻辑
4. 修改灰度指令+ok/+mr
# v5.20.0
1. 去掉build+lint
2. 增加retry机制
3. saga支持多实例
4. 去saga本地git操作全改为api操作
5. saga报告改到pipeline里执行
6. 统一为redis(之前微信使用到的mc暂时未去掉后期saga-admin封装好接口后再去掉)
7. assiged通知暂时屏蔽后续找到好的技术方案再加进去
8. 增加友好提示信息
# v5.19.9
1. retry机制改为webhook实现
2. changelog、Swagger改到pipeline执行
# v5.19.8
1. 增加retry机制
2. 去掉build+lint
# v5.19.7
1. +merge之前判断pipeline是否通过
2. 通知根据pipeline状态是否改变来发
# v5.19.6
1. skip to audit the non-exist repo and print error log
# v5.19.5
1. add pipeline notification for all repository
# v5.19.4
1. fix swagger check bug again
# v5.19.3
1. fix taskchain
2. fix swagger check bug
# v5.19.2
1. 支持Pipeline失败通知
# v5.19.1
1. 支持自动同步企业微信名单
# v5.19.0
1. 增加 swagger 规则检查
# v5.18.9
1. 获取需要添加的企业微信名单并保存在缓存中,然后定期将名单发送给指定的人
# v5.18.8
1. 修复热更update和handle mr时死锁问题
# v5.18.7
1. 修复reload repo时空指针异常
# v5.18.6
1. saga对webhook自主管理在文件中配置需要audit的webhook
2. 优化config load错误时错误日志
# v5.18.5
1. repo 配置支持ignore filelist
# v5.18.4
1. merge成功和失败时发送企业微信通知
# v5.18.3
1. 测试pipeline里pre-merge功能
# v5.18.2
1. 修复android-v4在热更时误判为变化
# v5.18.1
1. 监听文件改动,支持热更
# v5.18.0
1. 支持热更
# v5.17.2
1. 修复contributor.go被识别为CONTRIBUTORS.md的情况
# v5.17.1
1. 修复Reviewers和Owners为空
# v5.17.0
1. 使用gorm代替mysql
2. 新增企业微信通知接口
# v5.16.3
1. 关闭saga触发pipeline功能
# v5.16.2
1. 修复 golint 不生效
# v5.16.1
1. 修复 eslint
# v5.16.0
1. 增加 daemonSimple 防止gitlab 邮箱轰炸
2. 增加 gitlab reward emoji 作为review标志
3. 切换gitlab接口到v4
# v5.15.3
1. 修复go build ui err
# v5.15.2
1. 修复reset error
# v5.15.1
1. 修复go build constraints
# v5.15.0
1. 重构鉴权系统
2. 支持repos默认配置
# v5.14.2
1. 去掉path check
2. 优化大mr ut策略
# v5.14.1
1. 支持target branches正则表达式
# v5.14.0
1. 新增MR定制化target branches功能
# v5.13.1
1. 更新 path check
2. 修复 gitlab 适配
# v5.13.0
1. http router 切换到 bm
# v5.12.1
1. 修复新创建trigger后空指针的panic
# v5.12.0
1. 增加path检查新部门ep
2. 将 linter 拆分为二进制版本供gitlab ci使用
# v5.11.1
1. 修复MR未能触发gitlab pipeline的问题
# v5.11.0
1. 加入MR触发gitlab pipeline
# v5.10.2
1. 优化eslint流程
# v5.10.1
1. 优化eslint输出
2. 优化staticcheck
# v5.10.0
1. 加入php静态检查
2. 加入eslint静态检查
3. 加入 assign 通知 , review 双向通知
4. 加入path检查、解析
5. 加入changelog解析appid、version版本
6. go vet 所有规则开放
# v5.9.3
1. 修复go build重名问题
# v5.9.2
1. 删除statsd依赖
# v5.9.1
1. 优化启动环境变量
# v5.9.0
1. 支持任意类型repo接入
2. 支持合并时最小review数检查
# v5.8.13
1. 修复go build 作用域
# v5.8.12
1. 重构check工具
2. 改进分值计算
# v5.8.11
1. 增加 accpet ut 检查
# v5.8.10
1. 提升 go build 速度
2. 覆盖单元测试 build 检查
3. 优化 task 运行日志显示
# v5.8.9
1. 修复ut selector call
# v5.8.8
1. 放过revert分支
# v5.8.7
1. 优化ut算法
# v5.8.6
1. 修复panic
# v5.8.5
1. 修复ut在go build失败后仍然工作的bug
2. 修复覆盖率显示问题
# v5.8.4
1. 修复ut assign nil panic
# v5.8.3
1. 修复ut assign bug.
# v5.8.2
1. 修复ut assign bug
# v5.8.1
1. 修复ut检查错误
# v5.8.0
1. 增加静态单元测试覆盖率检查
2. 修复兼容xxx_test的pkg命名的单元测试
3. 更详细和友好的提示
4. health检查定时任务
# v5.7.3
1. 修复merge没有检查unittest的错误
2. unittest兼容xxx_test的pkg命名
# v5.7.2
1. 修复conf
# v5.7.1
1. unittest纳入merge规范检查项
2. clean up code
3. 修改report
# v5.7.0
1. 对接rider retag
2. 支持rider构建时retag+rider v1.0.0
# v5.6.1
1. 改进代码风格
2. 增加若干注释
# v5.6.0
1. 增加unittest检查
# v5.5.1
1. cleanup code
2. 替换merge命令
# v5.5.0
1. 重构merge鉴权支持contributors解析的方式
2. 支持多人合作merge
# v5.4.0
1. 添加自动发布功能(+deploy [env])
# v5.3.0
1. 增加若干静态检查工具simple,unused,gofmt,cyclo
# v5.2.0
1. 重构rider自动构建流程(+rider)
2. 接入发布api
3. 修复若干bug
# v5.1.1
1. 修复saga diff pkg 检测算法
# v5.1.0
1. 增加任务过程展示
2. 并行化go check工具执行
# v5.0.0
1. 重构任务系统
# v4.3.1
1. 增加目录权限白名单
2. 更新Accept MR接口
# v4.3.0
1. 支持gitlab comment hook
2. 升级gitlab新版API
3. report加入折叠功能
# v4.2.0
1. 增加CHANGELOG检查
# v4.1.0
1. 增加分支名检查不合规的直接关闭MR
# v4.0.0
1. # vendor纳入build测试
2. 加入staticcheck
3. 定期健康检查,如发现问题邮件通知
4. 去掉 go test未来在rider中跑测试
5. 接入服务树
# v3.0.0
1. 项目文件变更后邮件发送
2. CONTRIBUTORS owner解析
# v2.0.0
1. 加入更多代码检查工具go vet , golint , go test -cover
2. 更精准的Affected PKG
3. 报告内容优化
4. DAG优化bug修复
5. DAG通过事件、周期重构
6. 增加若干log
7. 错误处理依赖github.com/pkg/errors
# v1.0.0
1. 初始化项目

View File

@@ -0,0 +1,12 @@
# Owner
muyang
zhanglin
# Author
muyang
yubaihai
wangweizhen
wuwei
# Reviewer
muyang

17
app/tool/saga/OWNERS Normal file
View File

@@ -0,0 +1,17 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- muyang
- wangweizhen
- wuwei
- yubaihai
- zhanglin
labels:
- tool
options:
no_parent_owners: true
reviewers:
- muyang
- wangweizhen
- wuwei
- yubaihai

34
app/tool/saga/README.md Normal file
View File

@@ -0,0 +1,34 @@
# saga
##### 项目简介
> 1.提供大仓库pkg依赖关系DAG
> 2.提供gitlab MR自动构建、测试、覆盖率、代码静态检查
##### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
##### 依赖包
> 1.公共仓库go-common
> 2.github.com/xanzy/go-gitlab
##### 依赖服务
> 1.gitlab
##### 特别说明
> 1.运行环境ssh key需要绑定到gitlab账户下
> 2.运行环境PATH有运行的go,golint的路径
> 3.eslint
1. 安装nodejs
2. 设置path
3. npm run lint
> 4.phplint
1. 安装 PHP& PEAR
2. 安装CodeSniffer
pear install PHP_CodeSniffer
3. 配置CodeSniffer
phpcs --config-set default_standard PSR2
phpcs --config-set show_warnings 0
phpcs --config-set severity 1
4. 校验代码
phpcs * (*为待校验的文件名可以只检验本次MR涉及改动的文件)

View File

@@ -0,0 +1,43 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_binary",
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["saga-test.toml"],
importpath = "go-common/app/tool/saga/cmd",
tags = ["automanaged"],
visibility = ["//visibility:private"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/http:go_default_library",
"//app/tool/saga/service:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
"//library/os/signal:go_default_library",
"//library/syscall:go_default_library",
],
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

55
app/tool/saga/cmd/main.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"flag"
"os"
"time"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/http"
"go-common/app/tool/saga/service"
"go-common/library/log"
"go-common/library/net/trace"
"go-common/library/os/signal"
"go-common/library/syscall"
)
var (
s *service.Service
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
log.Init(conf.Conf.Log)
defer log.Close()
trace.Init(conf.Conf.Tracer)
defer trace.Close()
s = service.New()
http.Init(s)
log.Info("saga (version: %s) start")
signalHandler()
}
func signalHandler() {
var (
ch = make(chan os.Signal, 1)
)
signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGSTOP)
for {
si := <-ch
switch si {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
log.Info("get a signal %s, stop the saga process", si.String())
s.Close()
s.Wait()
time.Sleep(time.Second)
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,185 @@
version = "5.14.1"
[log]
[bm]
addr = "0.0.0.0:7000"
timeout = "1s"
maxListen = 100
[memcache]
mrRecordExpire = "720h"
[memcache.mr]
name = "mr"
proto = "tcp"
addr = "172.18.33.130:11211"
idle = 5
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "80s"
[redis]
active = 100
idle = 100
idleTimeout = "3s"
waitTimeout = "3s"
wait = false
name = "redis"
proto = "tcp"
addr = "10.23.103.180:6379"
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
[hbase]
master = ""
meta = ""
dialTimeout = "1s"
readTimeout = "1500ms"
readsTimeout = "600ms"
writeTimeout = "3000ms"
writesTimeout = "600ms"
[hbase.zookeeper]
root = ""
addrs = ["10.23.103.153:2181","172.18.33.168:2181","172.18.33.169:2181"]
timeout = "30s"
[httpClient]
key = "fakeKey"
secret = "fakeSecret"
dial = "1s"
timeout = "10s"
keepAlive = "60s"
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
switchoff = false
[orm]
dsn = "root:123456@tcp(172.18.33.130:3306)/saga?timeout=200ms&readTimeout=200ms&writeTimeout=200ms&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 5
idle = 5
idleTimeout = "4h"
[identify]
[identify.app]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
[identify.memcache]
name = "go-business/identify"
proto = "unix"
addr = "/tmp/shd-platform-identify-web-mc.sock"
active = 1024
idle = 64
dialTimeout = "30ms"
readTimeout = "70ms"
writeTimeout = "70ms"
idleTimeout = "80s"
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
dial = "30ms"
timeout = "50ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[property]
taskTimeout = "1m"
codePath = "/data/code"
toolPath = "/data/tool/go/bin:/root/gopath/bin:/root/nodejs/node-v8.9.3-linux-x64/bin"
goroot = "/data/tool/go"
[property.rider]
token = "sWLFPxns"
secret = "rmIrNLqQSmUGRuFV"
buildURL = "http://ops-rider.bilibili.co/api/app/saga_build_app"
resultURL = "http://ops-rider.bilibili.co/api/app/get_build_info/%d"
retagURL = "http://ops-rider.bilibili.co/api/app/saga_retag_app"
releaseURL = "http://ops-rider.bilibili.co/api/sagarelease/start_release_app"
releaseResultURL = "http://ops-rider.bilibili.co/api/releaseset/envflow_taskstatus/%d"
pollPeriod = "5s"
[property.serviceTree]
tokenURL = "http://easyst.bilibili.co/v1/token"
infoURL = "http://easyst.bilibili.co/v1/node/tree"
username = "saga"
platformID = "VhF33E/rYfLEsJxPh/MAuyY+XR>j&$kBaQXhV@F?yufxdMyvM3"
reloadTick = "1h"
[property.dag]
buildTick = "1h"
repoURL = "git@gitlab.bilibili.co:platform/go-common.git"
repoName = "go-common"
repoGitlabName = "Kratos"
[property.gitlab]
api = "http://gitlab.bilibili.co/api/v4"
token = "z3nN4s4BVX5oNYXKbEPL"
[[property.webhooks]]
url = "http://api.bilibili.co/x/internal/v1/saga/gitlab/mr"
mergeRequestsEvents = true
[[property.webhooks]]
url = "http://api.bilibili.co/x/internal/v1/saga/gitlab/comment"
noteEvents = true
[[property.webhooks]]
url = "http://api.bilibili.co/x/internal/v1/saga/gitlab/pipeline"
pipelineEvents = true
[property.mail]
host = "smtp.exmail.qq.com"
port = 465
address = "saga@bilibili.com"
pwd = ""
name = "SAGA"
[property.ut]
rate = 0.3
[property.wechat]
appId = 1000047
appSecret = "WveODxk3xpT9box48wcxkmArx3mu6d4vJHdJkNy_iTk"
[property.contact]
appId = 9527
appSecret = "humOPReDjdLaGwIyXO1NyJIbzf09ESO2Izn40jqjBg8"
[property.healthcheck]
checkCron = "0 0 5 ? * ?"
[[property.healthcheck.alertAddrs]]
name = "HinaKaze"
address = "muyang@bilibili.com"
[property.reportrequiredvisible]
checkCron = "0 0 0 ? * 0"
[[property.reportrequiredvisible.alertAddrs]]
name = "zhanglin"
address = "zhanglin@bilibili.com"
[[property.reportrequiredvisible.alertAddrs]]
name = "fangrongchang"
address = "fangrongchang@bilibili.com"
[[property.reportrequiredvisible.alertAddrs]]
name = "tangyongqiang"
address = "tangyongqiang@bilibili.com"
[property.synccontact]
checkCron = "0 0 10 * * ?"
[[property.repos]]
url = "git@gitlab.bilibili.co:platform/go-common.git"
name = "go-common"
gName = "Kratos"
language = "go"
minReviewer= 1
IgnoreFiles = ["BUILD"]
[[property.repos]]
url = "git@gitlab.bilibili.co:zhanglin/bililive-android-livevideo-ci-test.git"
name = "bililive-android-livevideo-ci-test"
language = "android"
IgnoreFiles = ["BUILD"]

View File

@@ -0,0 +1,50 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/tool/saga/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/hbase.v2:go_default_library",
"//library/database/orm:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/trace:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["conf_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

197
app/tool/saga/conf/conf.go Normal file
View File

@@ -0,0 +1,197 @@
package conf
import (
"flag"
"regexp"
"sync"
"time"
"go-common/app/tool/saga/model"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/hbase.v2"
"go-common/library/database/orm"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/trace"
xtime "go-common/library/time"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
)
const (
configKey = "saga.toml"
)
var (
confPath string
client *conf.Client
// Conf store the global config
Conf = &Config{}
reload chan bool
)
func init() {
flag.StringVar(&confPath, "conf", "", "config path")
reload = make(chan bool, 10)
}
// Config def.
type Config struct {
Tracer *trace.Config
BM *bm.ServerConfig
HTTPClient *bm.ClientConfig
Memcache *Memcache
Redis *redis.Config
HBase *HBaseConfig
Log *log.Config
Property *Property
sync.RWMutex
// orm
ORM *orm.Config
}
// Memcache config.
type Memcache struct {
MR *memcache.Config
MRRecordExpire xtime.Duration
}
// HBaseConfig for new hbase client.
type HBaseConfig struct {
*hbase.Config
WriteTimeout xtime.Duration
ReadTimeout xtime.Duration
}
// Property config for biz logic.
type Property struct {
TaskInterval xtime.Duration // 任务轮询时间
TaskTimeout xtime.Duration // 任务超时时间
PollPipeline xtime.Duration //pipeline轮询间隔时间
Gitlab *struct {
API string // gitlab api host
Token string // saga 账户 access token
}
WebHooks []*model.WebHook
Mail *struct {
Host string
Port int
Address string
Pwd string
Name string
}
HealthCheck *struct {
CheckCron string
AlertAddrs []*model.MailAddress
}
ReportRequiredVisible *struct {
CheckCron string
AlertAddrs []*model.MailAddress
}
SyncContact *struct {
CheckCron string
}
UT *struct {
Rate float64
}
Wechat *model.AppConfig
Contact *model.AppConfig
Repos []*model.RepoConfig
}
// Init init conf.
func Init() (err error) {
if confPath == "" {
return configCenter()
}
if _, err = toml.DecodeFile(confPath, &Conf); err != nil {
log.Error("toml.DecodeFile(%s) err(%+v)", confPath, err)
return
}
Conf = doDefault(Conf)
return
}
func configCenter() (err error) {
if client, err = conf.New(); err != nil {
panic(err)
}
if err = load(); err != nil {
return
}
//client.WatchAll()
client.Watch(configKey)
go func() {
for range client.Event() {
log.Info("config reload")
if err = load(); err != nil {
log.Error("config reload error (+%v)", err)
} else {
reload <- true
}
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Value(configKey); !ok {
err = errors.Errorf("load config center error [%s]", configKey)
return
}
if _, err = toml.Decode(s, &tmpConf); err != nil {
err = errors.Wrapf(err, "could not decode config err(%+v)", err)
return
}
Conf = doDefault(tmpConf)
return
}
func doDefault(c *Config) *Config {
if int64(c.Property.TaskInterval) == 0 {
c.Property.TaskInterval = xtime.Duration(3 * time.Second)
}
if int64(c.Property.TaskTimeout) == 0 {
c.Property.TaskTimeout = xtime.Duration(time.Minute)
}
if int64(c.Property.PollPipeline) == 0 {
c.Property.PollPipeline = xtime.Duration(10 * time.Second)
}
for _, r := range c.Property.Repos {
if r.GName == "" {
r.GName = r.Name
}
if r.Language == "" {
r.Language = "any"
}
if r.MinReviewer < 0 {
r.MinReviewer = 0
}
if r.LockTimeout == 0 {
r.LockTimeout = 600
}
if len(r.AuthBranches) == 0 {
r.AuthBranches = []string{"master"}
}
if len(r.TargetBranches) == 0 {
r.TargetBranches = r.AuthBranches
}
for _, b := range r.TargetBranches {
r.TargetBranchRegexes = append(r.TargetBranchRegexes, regexp.MustCompile(b))
}
}
return c
}
// ReloadEvents return the reload chan
func ReloadEvents() <-chan bool {
return reload
}

View File

@@ -0,0 +1,33 @@
package conf
import (
"flag"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func init() {
var err error
flag.Set("conf", "../cmd/saga-test.toml")
if err = Init(); err != nil {
panic(err)
}
}
func TestConf(t *testing.T) {
Convey("test conf", t, func() {
So(Conf, ShouldNotBeNil)
So(Conf.Property, ShouldNotBeNil)
So(Conf.Property.Gitlab, ShouldNotBeNil)
So(Conf.Property.Repos, ShouldNotBeNil)
So(Conf.Property.Repos[0].MinReviewer, ShouldEqual, 1)
So(Conf.Property.Repos[1].MinReviewer, ShouldEqual, 0)
So(Conf.Property.Repos[0].AuthBranches[0], ShouldEqual, "master")
So(Conf.Property.Repos[1].AuthBranches[0], ShouldEqual, "master")
So(Conf.Property.Mail, ShouldNotBeNil)
So(Conf.Property.HealthCheck, ShouldNotBeNil)
So(Conf.Property.HealthCheck.AlertAddrs, ShouldNotBeEmpty)
So(Conf.Property.Wechat, ShouldNotBeNil)
})
}

View File

@@ -0,0 +1,67 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"dao.go",
"db_wechat.go",
"hbase.go",
"http.go",
"lock.go",
"mc.go",
"redis.go",
],
importpath = "go-common/app/tool/saga/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/hbase.v2:go_default_library",
"//library/database/orm:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/github.com/jinzhu/gorm:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/tsuna/gohbase/hrpc:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"dao_test.go",
"hbase_test.go",
"lock_test.go",
"redis_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//library/cache/redis:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

84
app/tool/saga/dao/dao.go Normal file
View File

@@ -0,0 +1,84 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"net/http"
"time"
"go-common/app/tool/saga/conf"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/database/hbase.v2"
"go-common/library/database/orm"
bm "go-common/library/net/http/blademaster"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
// Dao def
type Dao struct {
// cache
httpClient *bm.Client
mysql *gorm.DB
mcMR *memcache.Pool
redis *redis.Pool
hbase *hbase.Client
mcMRRecordExpire int32
}
// New create instance of Dao
func New() (d *Dao) {
d = &Dao{
httpClient: bm.NewClient(conf.Conf.HTTPClient),
mysql: orm.NewMySQL(conf.Conf.ORM),
mcMR: memcache.NewPool(conf.Conf.Memcache.MR),
redis: redis.NewPool(conf.Conf.Redis),
hbase: hbase.NewClient(conf.Conf.HBase.Config),
mcMRRecordExpire: int32(time.Duration(conf.Conf.Memcache.MRRecordExpire) / time.Second),
}
return
}
// Ping dao.
func (d *Dao) Ping(c context.Context) (err error) {
if err = d.pingRedis(c); err != nil {
return
}
if err = d.pingMC(c); err != nil {
return
}
return d.mysql.DB().Ping()
}
// Close dao.
func (d *Dao) Close() {
if d.mcMR != nil {
d.mcMR.Close()
}
if d.redis != nil {
d.redis.Close()
}
if d.mysql != nil {
d.mysql.Close()
}
if d.hbase != nil {
d.hbase.Close()
}
}
func (d *Dao) newRequest(method, url string, v interface{}) (req *http.Request, err error) {
body := &bytes.Buffer{}
if method != http.MethodGet {
if err = json.NewEncoder(body).Encode(v); err != nil {
err = errors.WithStack(err)
return
}
}
if req, err = http.NewRequest(method, url, body); err != nil {
err = errors.WithStack(err)
}
return
}

View File

@@ -0,0 +1,38 @@
package dao
import (
"context"
"flag"
"os"
"testing"
"go-common/app/tool/saga/conf"
)
var (
d *Dao
ctx = context.Background()
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "test.ep.saga")
flag.Set("conf_token", "1d10044ab80b21d37ea67445f0475237")
flag.Set("tree_id", "56733")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "fat")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../cmd/saga-test.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New()
defer d.Close()
os.Exit(m.Run())
}

View File

@@ -0,0 +1,88 @@
package dao
import (
"regexp"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
pkgerr "github.com/pkg/errors"
)
var (
regUserID = regexp.MustCompile(`^\d+$`)
)
// QueryUserByUserName query user by user name
func (d *Dao) QueryUserByUserName(userName string) (contactInfo *model.ContactInfo, err error) {
contactInfo = &model.ContactInfo{}
err = pkgerr.WithStack(d.mysql.Where(&model.ContactInfo{UserName: userName}).First(contactInfo).Error)
return
}
// QueryUserByID query user by user ID
func (d *Dao) QueryUserByID(userID string) (contactInfo *model.ContactInfo, err error) {
contactInfo = &model.ContactInfo{}
err = pkgerr.WithStack(d.mysql.Where(&model.ContactInfo{UserID: userID}).First(contactInfo).Error)
return
}
// UserIds query user ids for the user names
func (d *Dao) UserIds(userNames []string) (userIds string, err error) {
var (
userName string
ids []string
contactInfo *model.ContactInfo
)
if len(userNames) == 0 {
err = errors.Errorf("UserIds: userNames is empty!")
return
}
for _, userName = range userNames {
if contactInfo, err = d.QueryUserByUserName(userName); err != nil {
log.Error("UserIds: no such user (%s) in db, err (%s)", userName, err.Error())
}
log.Info("UserIds: username (%s), userid (%s)", userName, contactInfo.UserID)
if contactInfo.UserID != "" && regUserID.MatchString(contactInfo.UserID) {
ids = append(ids, contactInfo.UserID)
}
}
if len(ids) > 0 {
userIds = strings.Join(ids, "|")
err = nil
} else {
err = errors.Wrapf(err, "UserIds: failed to find all the users in db, what a pity!")
}
return
}
// ContactInfos query all the records in contact_infos
func (d *Dao) ContactInfos() (contactInfos []*model.ContactInfo, err error) {
err = pkgerr.WithStack(d.mysql.Find(&contactInfos).Error)
return
}
// CreateContact create contact info record
func (d *Dao) CreateContact(contact *model.ContactInfo) (err error) {
err = pkgerr.WithStack(d.mysql.Create(contact).Error)
return
}
// DelContact delete the contact info with the specified UserID
func (d *Dao) DelContact(contact *model.ContactInfo) (err error) {
err = pkgerr.WithStack(d.mysql.Delete(contact).Error)
return
}
// UptContact update the contact information
func (d *Dao) UptContact(contact *model.ContactInfo) (err error) {
err = pkgerr.WithStack(d.mysql.Model(&model.ContactInfo{}).Where(&model.ContactInfo{UserID: contact.UserID}).Updates(*contact).Error)
return
}

105
app/tool/saga/dao/hbase.go Normal file
View File

@@ -0,0 +1,105 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"time"
"go-common/app/tool/saga/conf"
"go-common/library/log"
"github.com/pkg/errors"
"github.com/tsuna/gohbase/hrpc"
)
const (
_sagaTable = "ep:saga"
_ColFamily = "saga_auth"
_cSagaPathOwner = "path_owner"
_cSagaPathReviewer = "path_reviewer"
)
// sagaAuthKey ...
func sagaAuthKey(projID int, branch string, path string) string {
return fmt.Sprintf("saga_auth_%d_%s_%s", projID, branch, path)
}
// SetPathAuthH ...
func (d *Dao) SetPathAuthH(c context.Context, projID int, branch string, path string, owners []string, reviewers []string) (err error) {
var (
key = sagaAuthKey(projID, branch, path)
auth = make(map[string][]byte)
bOwner []byte
bReviewer []byte
)
if bOwner, err = json.Marshal(owners); err != nil {
return errors.WithStack(err)
}
if bReviewer, err = json.Marshal(reviewers); err != nil {
return errors.WithStack(err)
}
auth[_cSagaPathOwner] = bOwner
auth[_cSagaPathReviewer] = bReviewer
values := map[string]map[string][]byte{_ColFamily: auth}
ctx, cancel := context.WithTimeout(c, time.Duration(conf.Conf.HBase.WriteTimeout))
defer cancel()
if _, err = d.hbase.PutStr(ctx, _sagaTable, key, values); err != nil {
return errors.Wrapf(err, "hbase PutStr error (key: %s values: %v)", key, values)
}
return
}
// PathAuthH ...
func (d *Dao) PathAuthH(ctx context.Context, projID int, branch string, path string) (owners []string, reviewers []string, err error) {
var (
key = sagaAuthKey(projID, branch, path)
result *hrpc.Result
)
ctx, cancel := context.WithTimeout(ctx, time.Duration(conf.Conf.HBase.ReadTimeout))
defer cancel()
if result, err = d.hbase.GetStr(ctx, _sagaTable, key); err != nil {
err = errors.Wrapf(err, "hbase GetStr error (key: %s)", key)
return
}
for _, c := range result.Cells {
switch string(c.Qualifier) {
case _cSagaPathOwner:
if err = json.Unmarshal(c.Value, &owners); err != nil {
err = errors.WithStack(err)
return
}
log.Info("Get key: (%s), owners Info: (%+v)", key, owners)
case _cSagaPathReviewer:
if err = json.Unmarshal(c.Value, &reviewers); err != nil {
err = errors.WithStack(err)
return
}
log.Info("Get key: (%s), reviewers Info: (%+v)", key, reviewers)
}
}
return
}
// DeletePathAuthH ...
func (d *Dao) DeletePathAuthH(c context.Context, projID int, branch string, path string) (err error) {
key := sagaAuthKey(projID, branch, path)
ctx, cancel := context.WithTimeout(c, time.Duration(conf.Conf.HBase.WriteTimeout))
defer cancel()
auth := make(map[string][]byte)
auth[_cSagaPathOwner] = nil
auth[_cSagaPathReviewer] = nil
values := map[string]map[string][]byte{_ColFamily: auth}
if _, err = d.hbase.Delete(ctx, _sagaTable, key, values); err != nil {
err = errors.Wrapf(err, "hbase delete error (key: %s)", key)
}
return
}

View File

@@ -0,0 +1,133 @@
package dao
import (
"context"
"fmt"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoSagaAuthKey(t *testing.T) {
convey.Convey("sagaAuthKey", t, func(ctx convey.C) {
var (
projID = int(111)
branch = "test"
path = "."
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := sagaAuthKey(projID, branch, path)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_auth_111_test_.")
})
})
})
}
func TestDaoSetPathAuthH(t *testing.T) {
convey.Convey("SetPathAuthH", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(111)
branch = "master"
path = "."
owners = []string{"zhanglin", "wuwei"}
reviewers = []string{"tangyongqiang", "changhengyuan"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.SetPathAuthH(c, projID, branch, path, owners, reviewers)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoPathAuthH(t *testing.T) {
convey.Convey("PathAuthH", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(111)
branch = "master"
path = "."
owner = []string{"zhanglin", "wuwei"}
reviewer = []string{"tangyongqiang", "changhengyuan"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
owners, reviewers, err := d.PathAuthH(c, projID, branch, path)
ctx.Convey("Then err should be nil.owners,reviewers should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(fmt.Sprint(reviewers), convey.ShouldEqual, fmt.Sprint(reviewer))
ctx.So(fmt.Sprint(owners), convey.ShouldEqual, fmt.Sprint(owner))
})
})
})
}
func TestDaoDeletePathAuthH(t *testing.T) {
convey.Convey("DeletePathAuthH", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(111)
branch = "master"
path = "."
owners = []string{"zhanglin", "wuwei"}
reviewers = []string{"tangyongqiang", "changhengyuan"}
path2 = "test"
owners2 = []string{"zhang", "wu"}
reviewers2 = []string{"tang", "chang"}
)
ctx.Convey("When data not exist", func(ctx convey.C) {
err := d.DeletePathAuthH(c, projID, branch, path)
ctx.Convey("delete err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
ctx.Convey("When data exist", func(ctx convey.C) {
err := d.SetPathAuthH(c, projID, branch, path, owners, reviewers)
ctx.Convey("save path auth.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
err = d.SetPathAuthH(c, projID, branch, path2, owners2, reviewers2)
ctx.Convey("save path auth 2.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
os, rs, err := d.PathAuthH(c, projID, branch, path)
ctx.Convey("get path auth", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(fmt.Sprint(rs), convey.ShouldEqual, fmt.Sprint(reviewers))
ctx.So(fmt.Sprint(os), convey.ShouldEqual, fmt.Sprint(owners))
})
os, rs, err = d.PathAuthH(c, projID, branch, path2)
ctx.Convey("get path auth 2.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(fmt.Sprint(rs), convey.ShouldEqual, fmt.Sprint(reviewers2))
ctx.So(fmt.Sprint(os), convey.ShouldEqual, fmt.Sprint(owners2))
})
err = d.DeletePathAuthH(c, projID, branch, path)
ctx.Convey("delete auth path.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
os, rs, err = d.PathAuthH(c, projID, branch, path)
ctx.Convey("get again path auth.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(fmt.Sprint(rs), convey.ShouldEqual, "[]")
ctx.So(fmt.Sprint(os), convey.ShouldEqual, "[]")
ctx.So(len(rs), convey.ShouldEqual, 0)
ctx.So(len(os), convey.ShouldEqual, 0)
})
os, rs, err = d.PathAuthH(c, projID, branch, path2)
ctx.Convey("get again path auth 2.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(fmt.Sprint(rs), convey.ShouldEqual, fmt.Sprint(reviewers2))
ctx.So(fmt.Sprint(os), convey.ShouldEqual, fmt.Sprint(owners2))
})
})
})
}

188
app/tool/saga/dao/http.go Normal file
View File

@@ -0,0 +1,188 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"fmt"
xhttp "net/http"
"net/url"
"strconv"
"go-common/app/tool/saga/model"
"github.com/pkg/errors"
)
const (
qyWechatURL = "https://qyapi.weixin.qq.com"
corpID = "wx0833ac9926284fa5" // 企业微信Bilibili的企业ID
departmentID = "12" // 公司统一用部门ID
)
// WechatPushMsg wechat push text message to specified user
func (d *Dao) WechatPushMsg(c context.Context, token string, txtMsg *model.TxtNotification) (invalidUser string, err error) {
var (
u string
params = url.Values{}
res struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
InvalidUser string `json:"invaliduser"`
InvalidParty string `json:"invalidparty"`
InvalidTag string `json:"invalidtag"`
}
)
u = qyWechatURL + "/cgi-bin/message/send"
params.Set("access_token", token)
if err = d.PostJSON(c, u, "", params, &res, txtMsg); err != nil {
return
}
if res.ErrCode != 0 || res.InvalidUser != "" || res.InvalidParty != "" || res.InvalidTag != "" {
invalidUser = res.InvalidUser
err = errors.Errorf("WechatPushMsg: errcode: %d, errmsg: %s, invalidUser: %s, invalidParty: %s, invalidTag: %s",
res.ErrCode, res.ErrMsg, res.InvalidUser, res.InvalidParty, res.InvalidTag)
return
}
return
}
// PostJSON post http request with json params.
func (d *Dao) PostJSON(c context.Context, uri, ip string, params url.Values, res interface{}, v interface{}) (err error) {
var (
body = &bytes.Buffer{}
req *xhttp.Request
url string
en string
)
if err = json.NewEncoder(body).Encode(v); err != nil {
return
}
url = uri
if en = params.Encode(); en != "" {
url += "?" + en
}
if req, err = xhttp.NewRequest(xhttp.MethodPost, url, body); err != nil {
return
}
if err = d.httpClient.Do(c, req, &res); err != nil {
return
}
return
}
// WechatAccessToken query access token with the specified secret
func (d *Dao) WechatAccessToken(c context.Context, secret string) (token string, expire int32, err error) {
var (
u string
params = url.Values{}
res struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
AccessToken string `json:"access_token"`
ExpiresIn int32 `json:"expires_in"`
}
)
u = qyWechatURL + "/cgi-bin/gettoken"
params.Set("corpid", corpID)
params.Set("corpsecret", secret)
if err = d.httpClient.Get(c, u, "", params, &res); err != nil {
return
}
if res.ErrCode != 0 {
err = errors.Errorf("WechatAccessToken: errcode: %d, errmsg: %s", res.ErrCode, res.ErrMsg)
return
}
token = res.AccessToken
expire = res.ExpiresIn
return
}
// WechatContacts query all the contacts
func (d *Dao) WechatContacts(c context.Context, accessToken string) (contacts []*model.ContactInfo, err error) {
var (
u string
params = url.Values{}
res struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
UserList []*model.ContactInfo `json:"userlist"`
}
)
u = qyWechatURL + "/cgi-bin/user/list"
params.Set("access_token", accessToken)
params.Set("department_id", departmentID)
params.Set("fetch_child", "1")
if err = d.httpClient.Get(c, u, "", params, &res); err != nil {
return
}
if res.ErrCode != 0 {
err = errors.Errorf("WechatContacts: errcode: %d, errmsg: %s", res.ErrCode, res.ErrMsg)
return
}
contacts = res.UserList
return
}
// WechatSagaVisible get all the user ids who can visiable saga
func (d *Dao) WechatSagaVisible(c context.Context, accessToken string, agentID int) (users []*model.UserInfo, err error) {
var (
u string
params = url.Values{}
res struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
VisibleUsers model.AllowUserInfo `json:"allow_userinfos"`
}
)
u = qyWechatURL + "/cgi-bin/agent/get"
params.Set("access_token", accessToken)
params.Set("agentid", strconv.Itoa(agentID))
if err = d.httpClient.Get(c, u, "", params, &res); err != nil {
return
}
if res.ErrCode != 0 {
err = errors.Errorf("WechatSagaVisible: errcode: %d, errmsg: %s", res.ErrCode, res.ErrMsg)
return
}
users = res.VisibleUsers.Users
return
}
// RepoFiles ...TODO 该方法放在gitlab里比较好
func (d *Dao) RepoFiles(c context.Context, Host string, token string, repo *model.RepoInfo) (files []string, err error) {
var (
u string
req *xhttp.Request
)
u = fmt.Sprintf("http://%s/%s/%s/files/%s?format=json", Host, repo.Group, repo.Name, repo.Branch)
if req, err = d.newRequest("GET", u, nil); err != nil {
return
}
req.Header.Set("PRIVATE-TOKEN", token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if err = d.httpClient.Do(c, req, &files); err != nil {
return
}
return
}

31
app/tool/saga/dao/lock.go Normal file
View File

@@ -0,0 +1,31 @@
package dao
import (
"context"
"go-common/library/cache/redis"
"go-common/library/log"
)
//TryLock ...
func (d *Dao) TryLock(c context.Context, key string, value string, timeout int) (ok bool, err error) {
var conn = d.redis.Get(c)
defer conn.Close()
_, err = redis.String(conn.Do("SET", key, value, "EX", timeout, "NX"))
if err == redis.ErrNil {
log.Info("TryLock redis key(%s) is ErrNil!", key)
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// UnLock ...
func (d *Dao) UnLock(c context.Context, key string) (err error) {
var conn = d.redis.Get(c)
defer conn.Close()
_, err = conn.Do("DEL", key)
return
}

View File

@@ -0,0 +1,35 @@
package dao
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestLock(t *testing.T) {
var (
key string
ok bool
err error
)
Convey("TEST Lock", t, func() {
ok, err = d.TryLock(ctx, key, "test", 1)
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
ok, err = d.TryLock(ctx, key, "test", 1)
So(err, ShouldBeNil)
So(ok, ShouldBeFalse)
err = d.UnLock(ctx, key)
So(err, ShouldBeNil)
ok, err = d.TryLock(ctx, key, "test", 1)
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
ok, err = d.TryLock(ctx, key, "test", 1)
So(err, ShouldBeNil)
So(ok, ShouldBeFalse)
})
}

181
app/tool/saga/dao/mc.go Normal file
View File

@@ -0,0 +1,181 @@
package dao
import (
"context"
"fmt"
"go-common/app/tool/saga/model"
"go-common/library/cache/memcache"
"go-common/library/log"
"github.com/pkg/errors"
)
const (
requiredViableUsersKey = "saga_wechat_require_visible_users_key"
)
func (d *Dao) pingMC(c context.Context) (err error) {
conn := d.mcMR.Get(c)
defer conn.Close()
if err = conn.Set(&memcache.Item{Key: "ping", Value: []byte{1}, Expiration: 0}); err != nil {
err = errors.Wrap(err, "conn.Store(set,ping,1)")
}
return
}
func mrRecordKey(mrID int) string {
return fmt.Sprintf("saga_mr_%d", mrID)
}
// MRRecordCache get MRRecord from mc
func (d *Dao) MRRecordCache(c context.Context, mrID int) (record *model.MRRecord, err error) {
var (
key = mrRecordKey(mrID)
conn = d.mcMR.Get(c)
reply *memcache.Item
)
defer conn.Close()
reply, err = conn.Get(key)
if err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
err = errors.Wrapf(err, "conn.Get(get,%s)", key)
return
}
record = &model.MRRecord{}
if err = conn.Scan(reply, record); err != nil {
err = errors.Wrapf(err, "reply.Scan(%s)", string(reply.Value))
}
return
}
// SetMRRecordCache set MRRecord to mc
func (d *Dao) SetMRRecordCache(c context.Context, record *model.MRRecord) (err error) {
var (
key = mrRecordKey(record.MRID)
conn = d.mcMR.Get(c)
)
defer conn.Close()
if err = conn.Set(&memcache.Item{Key: key, Object: record, Expiration: 0, Flags: memcache.FlagJSON}); err != nil {
err = errors.Wrapf(err, "conn.Add(%s,%v)", key, record)
return
}
return
}
func weixinTokenKey(key string) string {
return fmt.Sprintf("saga_weixin_token_%s", key)
}
// AccessToken get access token from mc
func (d *Dao) AccessToken(c context.Context, key string) (token string, err error) {
var (
wkey = weixinTokenKey(key)
conn = d.mcMR.Get(c)
reply *memcache.Item
)
defer conn.Close()
reply, err = conn.Get(wkey)
if err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
err = errors.Wrapf(err, "conn.Get(get,%s)", wkey)
return
}
if err = conn.Scan(reply, &token); err != nil {
err = errors.Wrapf(err, "reply.Scan(%s)", string(reply.Value))
}
return
}
// SetAccessToken set the access token to mc
func (d *Dao) SetAccessToken(c context.Context, key string, token string, expire int32) (err error) {
var (
wkey = weixinTokenKey(key)
conn = d.mcMR.Get(c)
item *memcache.Item
)
defer conn.Close()
item = &memcache.Item{Key: wkey, Object: token, Expiration: expire, Flags: memcache.FlagJSON}
if err = conn.Set(item); err != nil {
err = errors.Wrapf(err, "conn.Add(%s,%v)", wkey, token)
return
}
return
}
// RequireVisibleUsers get wechat require visible users from memcache
func (d *Dao) RequireVisibleUsers(c context.Context, userMap *map[string]model.RequireVisibleUser) (err error) {
var (
conn = d.mcMR.Get(c)
reply *memcache.Item
)
defer conn.Close()
reply, err = conn.Get(requiredViableUsersKey)
if err != nil {
if err == memcache.ErrNotFound {
log.Info("no such key (%s) in cache, err (%s)", requiredViableUsersKey, err.Error())
err = nil
}
return
}
if err = conn.Scan(reply, userMap); err != nil {
err = errors.Wrapf(err, "reply.Scan(%s)", string(reply.Value))
}
return
}
// SetRequireVisibleUsers set wechat require visible users to memcache
func (d *Dao) SetRequireVisibleUsers(c context.Context, contactInfo *model.ContactInfo) (err error) {
var (
conn = d.mcMR.Get(c)
item *memcache.Item
userMap = make(map[string]model.RequireVisibleUser)
)
defer conn.Close()
if err = d.RequireVisibleUsers(c, &userMap); err != nil {
log.Error("get require visible user error(%v)", err)
return
}
user := model.RequireVisibleUser{
UserName: contactInfo.UserName,
NickName: contactInfo.NickName,
}
userMap[contactInfo.UserID] = user
item = &memcache.Item{Key: requiredViableUsersKey, Object: userMap, Expiration: 0, Flags: memcache.FlagJSON}
if err = conn.Set(item); err != nil {
err = errors.Wrapf(err, "conn.Set(%s,%v)", requiredViableUsersKey, userMap)
return
}
return
}
// DeleteRequireVisibleUsers delete the wechat require visible key in memcache
func (d *Dao) DeleteRequireVisibleUsers(c context.Context) (err error) {
var (
conn = d.mcMR.Get(c)
)
defer conn.Close()
err = conn.Delete(requiredViableUsersKey)
if err != nil {
err = errors.Wrapf(err, "conn.Delete(%s)", requiredViableUsersKey)
}
return
}

465
app/tool/saga/dao/redis.go Normal file
View File

@@ -0,0 +1,465 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"go-common/app/tool/saga/model"
"go-common/library/cache/redis"
"github.com/pkg/errors"
)
func mergeTaskKey(taskType int) string {
return fmt.Sprintf("saga_task_%d", taskType)
}
func mrIIDKey(mrIID int) string {
return fmt.Sprintf("saga_mrIID_%d", mrIID)
}
func (d *Dao) pingRedis(c context.Context) (err error) {
conn := d.redis.Get(c)
_, err = conn.Do("SET", "PING", "PONG")
conn.Close()
return
}
// AddMRIID ...
func (d *Dao) AddMRIID(c context.Context, mrIID int, expire int) (err error) {
var (
key = mrIIDKey(mrIID)
conn = d.redis.Get(c)
)
defer conn.Close()
if err = conn.Send("SET", key, mrIID); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
if _, err = conn.Do("EXPIRE", key, expire); err != nil {
return
}
return
}
// ExistMRIID ...
func (d *Dao) ExistMRIID(c context.Context, mrIID int) (ok bool, err error) {
var (
key = mrIIDKey(mrIID)
conn = d.redis.Get(c)
)
defer conn.Close()
if _, err = redis.Int(conn.Do("GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
}
return false, err
}
return true, nil
}
// DeleteMRIID ...
func (d *Dao) DeleteMRIID(c context.Context, mrIID int) (err error) {
var (
key = mrIIDKey(mrIID)
conn = d.redis.Get(c)
)
defer conn.Close()
if err = conn.Send("DEL", key); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// PushMergeTask ...
func (d *Dao) PushMergeTask(c context.Context, taskType int, taskInfo *model.TaskInfo) (err error) {
var (
key = mergeTaskKey(taskType)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(taskInfo); err != nil {
err = errors.WithStack(err)
return
}
if err = conn.Send("LPUSH", key, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// DeleteMergeTask ...
func (d *Dao) DeleteMergeTask(c context.Context, taskType int, taskInfo *model.TaskInfo) (err error) {
var (
key = mergeTaskKey(taskType)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(taskInfo); err != nil {
err = errors.WithStack(err)
return
}
if err = conn.Send("LREM", key, 0, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// MergeTasks ...
func (d *Dao) MergeTasks(c context.Context, taskType int) (count int, taskInfos []*model.TaskInfo, err error) {
var (
key = mergeTaskKey(taskType)
values [][]byte
conn = d.redis.Get(c)
)
defer conn.Close()
if count, err = redis.Int(conn.Do("LLEN", key)); err != nil {
return
}
if values, err = redis.ByteSlices(conn.Do("LRANGE", key, 0, -1)); err != nil {
if err == redis.ErrNil {
err = nil
}
return
}
taskInfos = make([]*model.TaskInfo, 0, count)
for _, value := range values {
taskInfo := &model.TaskInfo{}
if err = json.Unmarshal(value, &taskInfo); err != nil {
err = errors.WithStack(err)
return
}
taskInfos = append(taskInfos, taskInfo)
//taskInfos = append([]*model.TaskInfo{taskInfo}, taskInfos...)
}
return
}
func mergeInfoKey(projID int, branch string) string {
return fmt.Sprintf("saga_mergeInfo_%d_%s", projID, branch)
}
func pathOwnerKey(projID int, branch string, path string) string {
return fmt.Sprintf("saga_PathOwner_%d_%s_%s", projID, branch, path)
}
func pathReviewerKey(projID int, branch string, path string) string {
return fmt.Sprintf("saga_PathReviewer_%d_%s_%s", projID, branch, path)
}
func authInfoKey(projID int, mrIID int) string {
return fmt.Sprintf("saga_auth_%d_%d", projID, mrIID)
}
// SetMergeInfo ...
func (d *Dao) SetMergeInfo(c context.Context, projID int, branch string, mergeInfo *model.MergeInfo) (err error) {
var (
key = mergeInfoKey(projID, branch)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(mergeInfo); err != nil {
err = errors.WithStack(err)
return
}
if err = conn.Send("SET", key, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// MergeInfo ...
func (d *Dao) MergeInfo(c context.Context, projID int, branch string) (ok bool, mergeInfo *model.MergeInfo, err error) {
var (
key = mergeInfoKey(projID, branch)
value []byte
conn = d.redis.Get(c)
)
defer conn.Close()
if value, err = redis.Bytes(conn.Do("GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
}
return
}
mergeInfo = &model.MergeInfo{}
if err = json.Unmarshal(value, &mergeInfo); err != nil {
err = errors.WithStack(err)
return
}
ok = true
return
}
// DeleteMergeInfo ...
func (d *Dao) DeleteMergeInfo(c context.Context, projID int, branch string) (err error) {
var (
key = mergeInfoKey(projID, branch)
conn = d.redis.Get(c)
)
defer conn.Close()
if err = conn.Send("DEL", key); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// SetPathOwner ...
func (d *Dao) SetPathOwner(c context.Context, projID int, branch string, path string, owners []string) (err error) {
var (
key = pathOwnerKey(projID, branch, path)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(owners); err != nil {
err = errors.WithStack(err)
return
}
if err = conn.Send("SET", key, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// PathOwner ...
func (d *Dao) PathOwner(c context.Context, projID int, branch string, path string) (owners []string, err error) {
var (
key = pathOwnerKey(projID, branch, path)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = redis.Bytes(conn.Do("GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
}
return
}
if err = json.Unmarshal(bs, &owners); err != nil {
err = errors.WithStack(err)
return
}
return
}
// SetPathReviewer ...
func (d *Dao) SetPathReviewer(c context.Context, projID int, branch string, path string, reviewers []string) (err error) {
var (
key = pathReviewerKey(projID, branch, path)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(reviewers); err != nil {
return errors.WithStack(err)
}
if err = conn.Send("SET", key, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// PathReviewer ...
func (d *Dao) PathReviewer(c context.Context, projID int, branch string, path string) (reviewers []string, err error) {
var (
key = pathReviewerKey(projID, branch, path)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = redis.Bytes(conn.Do("GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
}
return
}
if err = json.Unmarshal(bs, &reviewers); err != nil {
err = errors.WithStack(err)
return
}
return
}
// pathAuthKey ...
func pathAuthKey(projID int, branch string, path string) string {
return fmt.Sprintf("saga_path_auth_%d_%s_%s", projID, branch, path)
}
// PathAuthR ...
func (d *Dao) PathAuthR(c context.Context, projID int, branch string, path string) (authUsers *model.AuthUsers, err error) {
var (
key = pathAuthKey(projID, branch, path)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = redis.Bytes(conn.Do("GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
}
return
}
authUsers = new(model.AuthUsers)
if err = json.Unmarshal(bs, authUsers); err != nil {
err = errors.WithStack(err)
return
}
return
}
// SetPathAuthR ...
func (d *Dao) SetPathAuthR(c context.Context, projID int, branch string, path string, authUsers *model.AuthUsers) (err error) {
var (
key = pathAuthKey(projID, branch, path)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(authUsers); err != nil {
return errors.WithStack(err)
}
if err = conn.Send("SET", key, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// DeletePathAuthR ...
func (d *Dao) DeletePathAuthR(c context.Context, projID int, branch string, path string) (err error) {
var (
key = pathAuthKey(projID, branch, path)
conn = d.redis.Get(c)
)
defer conn.Close()
if err = conn.Send("DEL", key); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// SetReportStatus ...
func (d *Dao) SetReportStatus(c context.Context, projID int, mrIID int, result bool) (err error) {
var (
key = authInfoKey(projID, mrIID)
conn = d.redis.Get(c)
bs []byte
)
defer conn.Close()
if bs, err = json.Marshal(result); err != nil {
return errors.WithStack(err)
}
if err = conn.Send("SET", key, bs); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}
// ReportStatus ...
func (d *Dao) ReportStatus(c context.Context, projID int, mrIID int) (result bool, err error) {
var (
key = authInfoKey(projID, mrIID)
value []byte
conn = d.redis.Get(c)
)
defer conn.Close()
if value, err = redis.Bytes(conn.Do("GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
}
return
}
if err = json.Unmarshal(value, &result); err != nil {
err = errors.WithStack(err)
return
}
return
}
// DeleteReportStatus ...
func (d *Dao) DeleteReportStatus(c context.Context, projID int, mrIID int) (err error) {
var (
key = authInfoKey(projID, mrIID)
conn = d.redis.Get(c)
)
defer conn.Close()
if err = conn.Send("DEL", key); err != nil {
return
}
if err = conn.Flush(); err != nil {
return
}
if _, err = conn.Receive(); err != nil {
return
}
return
}

View File

@@ -0,0 +1,475 @@
package dao
import (
"context"
"encoding/json"
"testing"
"go-common/app/tool/saga/model"
"go-common/library/cache/redis"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoMergeTaskKey(t *testing.T) {
convey.Convey("mergeTaskKey", t, func(ctx convey.C) {
var (
taskType = int(111)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := mergeTaskKey(taskType)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_task_111")
})
})
})
}
func TestDaoMrIIDKey(t *testing.T) {
convey.Convey("mrIIDKey", t, func(ctx convey.C) {
var (
mrIID = int(222)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := mrIIDKey(mrIID)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_mrIID_222")
})
})
})
}
func TestDaoPingRedis(t *testing.T) {
convey.Convey("pingRedis", t, func(ctx convey.C) {
var (
c = context.Background()
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.pingRedis(c)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoAddMRIID(t *testing.T) {
convey.Convey("AddMRIID", t, func(ctx convey.C) {
var (
c = context.Background()
mrIID = int(333)
expire = int(1800)
conn = d.redis.Get(c)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.AddMRIID(c, mrIID, expire)
mrID, err := redis.Int(conn.Do("GET", "saga_mrIID_333"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// 检查redis存储结果
ctx.So(mrID, convey.ShouldEqual, mrIID)
})
})
})
}
func TestDaoExistMRIID(t *testing.T) {
convey.Convey("ExistMRIID", t, func(ctx convey.C) {
var (
c = context.Background()
mrIID = int(444)
conn = d.redis.Get(c)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
conn.Do("DEL", "saga_mrIID_444")
ok, err := d.ExistMRIID(c, mrIID)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(ok, convey.ShouldBeFalse)
})
})
ctx.Convey("When MRIID exist", func(ctx convey.C) {
conn.Do("SET", "saga_mrIID_444", 1)
ok, err := d.ExistMRIID(c, mrIID)
ctx.Convey("Then ok should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(ok, convey.ShouldBeTrue)
})
})
})
}
func TestDaoDeleteMRIID(t *testing.T) {
convey.Convey("DeleteMRIID", t, func(ctx convey.C) {
var (
c = context.Background()
mrIID = int(555)
conn = d.redis.Get(c)
)
ctx.Convey("When data not exist", func(ctx convey.C) {
err := d.DeleteMRIID(c, mrIID)
value, _ := redis.Int(conn.Do("GET", "saga_mrIID_555"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, 0)
})
})
ctx.Convey("When data exist", func(ctx convey.C) {
redis.String(conn.Do("SET", "saga_mrIID_555", "Test"))
err := d.DeleteMRIID(c, mrIID)
value, _ := redis.Int(conn.Do("GET", "saga_mrIID_555"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, 0)
})
})
})
}
func TestDaoPushMergeTask(t *testing.T) {
convey.Convey("PushMergeTask", t, func(ctx convey.C) {
var (
c = context.Background()
taskType = int(666)
taskInfo = &model.TaskInfo{NoteID: 111, Event: nil, Repo: nil}
conn = d.redis.Get(c)
)
bs, _ := json.Marshal(taskInfo)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.PushMergeTask(c, taskType, taskInfo)
value, _ := redis.Bytes(conn.Do("LPOP", "saga_task_666"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// push 运行函数 pop 检验结果
ctx.So(string(value), convey.ShouldEqual, string(bs))
})
})
})
}
func TestDaoDeleteMergeTask(t *testing.T) {
convey.Convey("DeleteMergeTask", t, func(ctx convey.C) {
var (
c = context.Background()
taskType = int(777)
taskInfo = &model.TaskInfo{NoteID: 777, Event: nil, Repo: nil}
conn = d.redis.Get(c)
)
ctx.Convey("When data is not exist", func(ctx convey.C) {
err := d.DeleteMergeTask(c, taskType, taskInfo)
value, _ := redis.Int(conn.Do("LPOP", "saga_task_777"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, 0)
})
})
ctx.Convey("When data is exist", func(ctx convey.C) {
taskInfoJSON, _ := json.Marshal(taskInfo)
redis.String(conn.Do("LPUSH", "saga_task_777", taskInfoJSON))
err := d.DeleteMergeTask(c, taskType, taskInfo)
value, _ := redis.Int(conn.Do("LPOP", "saga_task_777"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, 0)
})
})
})
}
func TestDaoMergeTasks(t *testing.T) {
convey.Convey("MergeTasks", t, func(ctx convey.C) {
var (
c = context.Background()
taskType = int(888)
conn = d.redis.Get(c)
taskInfo = &model.TaskInfo{NoteID: 888, Event: nil, Repo: nil}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
taskInfoJSON, _ := json.Marshal(taskInfo)
conn.Do("LPUSH", "saga_task_888", taskInfoJSON)
count, taskInfos, err := d.MergeTasks(c, taskType)
taskInfoFirst, _ := json.Marshal(taskInfos[0])
ctx.Convey("Then err should be nil.count,taskInfos should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(string(taskInfoFirst), convey.ShouldEqual, string(taskInfoJSON))
ctx.So(count, convey.ShouldNotEqual, 0)
})
})
})
}
func TestDaoMergeInfoKey(t *testing.T) {
convey.Convey("mergeInfoKey", t, func(ctx convey.C) {
var (
projID = int(999)
branch = "test"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := mergeInfoKey(projID, branch)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_mergeInfo_999_test")
})
})
})
}
func TestDaoPathOwnerKey(t *testing.T) {
convey.Convey("pathOwnerKey", t, func(ctx convey.C) {
var (
projID = int(1111)
branch = "test"
path = "."
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := pathOwnerKey(projID, branch, path)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_PathOwner_1111_test_.")
})
})
})
}
func TestDaoPathReviewerKey(t *testing.T) {
convey.Convey("pathReviewerKey", t, func(ctx convey.C) {
var (
projID = int(2222)
branch = "test"
path = "."
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := pathReviewerKey(projID, branch, path)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_PathReviewer_2222_test_.")
})
})
})
}
func TestDaoAuthInfoKey(t *testing.T) {
convey.Convey("authInfoKey", t, func(ctx convey.C) {
var (
projID = int(3333)
mrIID = int(3333)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := authInfoKey(projID, mrIID)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_auth_3333_3333")
})
})
})
}
func TestDaoSetMergeInfo(t *testing.T) {
convey.Convey("SetMergeInfo", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(4444)
branch = "test"
mergeInfo = &model.MergeInfo{}
conn = d.redis.Get(c)
)
redis.String(conn.Do(""))
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.SetMergeInfo(c, projID, branch, mergeInfo)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoMergeInfo(t *testing.T) {
convey.Convey("MergeInfo", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(5555)
branch = "test"
conn = d.redis.Get(c)
info = []byte(`{"NoteID":111,"Event":null,"Repo":null}`)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
conn.Do("SET", "saga_mergeInfo_5555_test", info)
ok, mergeInfo, err := d.MergeInfo(c, projID, branch)
ctx.Convey("Then err should be nil.ok,mergeInfo should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(mergeInfo, convey.ShouldNotBeNil)
ctx.So(ok, convey.ShouldBeTrue)
})
})
})
}
func TestDaoDeleteMergeInfo(t *testing.T) {
convey.Convey("DeleteMergeInfo", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(6666)
branch = "test"
conn = d.redis.Get(c)
info = []byte(`{"NoteID":111,"Event":null,"Repo":null}`)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
conn.Do("SET", "saga_mergeInfo_6666_test", info)
err := d.DeleteMergeInfo(c, projID, branch)
value, _ := redis.Int(conn.Do("GET", "saga_mergeInfo_6666_test"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, 0)
})
})
})
}
func TestDaoPathAuthKey(t *testing.T) {
convey.Convey("pathAuthKey", t, func(ctx convey.C) {
var (
projID = int(7777)
branch = "test"
path = "."
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := pathAuthKey(projID, branch, path)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, "saga_path_auth_7777_test_.")
})
})
})
}
func TestDaoPathAuthR(t *testing.T) {
convey.Convey("PathAuthR", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(8888)
branch = "test"
path = "."
conn = d.redis.Get(c)
)
redis.Int(conn.Do("SET", "saga_path_auth_8888_test_.", `{"Owners": ["c"]}`))
ctx.Convey("When everything goes positive", func(ctx convey.C) {
authUsers, err := d.PathAuthR(c, projID, branch, path)
authUsersJSON, _ := json.Marshal(authUsers)
ctx.Convey("Then err should be nil.authUsers should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(string(authUsersJSON), convey.ShouldEqual, `{"Owners":["c"],"Reviewers":null}`)
ctx.So(authUsers, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoSetPathAuthR(t *testing.T) {
convey.Convey("SetPathAuthR", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(9999)
branch = "test"
path = "."
authUsers = &model.AuthUsers{Owners: []string{"testOwner"}, Reviewers: []string{"testReviewers"}}
conn = d.redis.Get(c)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.SetPathAuthR(c, projID, branch, path, authUsers)
value, _ := redis.String(conn.Do("GET", "saga_path_auth_9999_test_."))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, `{"Owners":["testOwner"],"Reviewers":["testReviewers"]}`)
})
})
})
}
func TestDaoDeletePathAuthR(t *testing.T) {
convey.Convey("DeletePathAuthR", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(88)
branch = "test"
path = "."
authUsers = &model.AuthUsers{Owners: []string{"testOwner"}, Reviewers: []string{"testReviewers"}}
conn = d.redis.Get(c)
)
ctx.Convey("When data not exist", func(ctx convey.C) {
err := d.DeletePathAuthR(c, projID, branch, path)
value, _ := redis.String(conn.Do("GET", "saga_path_auth_88_test_."))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, "")
})
})
ctx.Convey("When data exist", func(ctx convey.C) {
err := d.SetPathAuthR(c, projID, branch, path, authUsers)
value, _ := redis.String(conn.Do("GET", "saga_path_auth_88_test_."))
ctx.Convey("Then err should be nil 1.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, `{"Owners":["testOwner"],"Reviewers":["testReviewers"]}`)
})
err = d.DeletePathAuthR(c, projID, branch, path)
value, _ = redis.String(conn.Do("GET", "saga_path_auth_88_test_."))
ctx.Convey("Then err should be nil 2.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, "")
})
})
})
}
func TestDaoSetReportStatus(t *testing.T) {
convey.Convey("SetReportStatus", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(11111)
mrIID = int(11111)
result bool
conn = d.redis.Get(c)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := d.SetReportStatus(c, projID, mrIID, result)
value, _ := redis.String(conn.Do("GET", "saga_auth_11111_11111"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, "false")
})
})
})
}
func TestDaoReportStatus(t *testing.T) {
convey.Convey("ReportStatus", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(22222)
mrIID = int(22222)
conn = d.redis.Get(c)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
conn.Do("SET", "saga_auth_22222_22222", "true")
result, err := d.ReportStatus(c, projID, mrIID)
ctx.Convey("Then err should be nil.result should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(result, convey.ShouldEqual, true)
})
})
})
}
func TestDaoDeleteReportStatus(t *testing.T) {
convey.Convey("DeleteReportStatus", t, func(ctx convey.C) {
var (
c = context.Background()
projID = int(33333)
mrIID = int(33333)
conn = d.redis.Get(c)
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
conn.Do("SET", "saga_auth_33333_33333", "true")
err := d.DeleteReportStatus(c, projID, mrIID)
value, _ := redis.Int(conn.Do("GET", "saga_auth_33333_33333"))
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(value, convey.ShouldEqual, 0)
})
})
})
}

View File

@@ -0,0 +1,40 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"api.go",
"gitlab.go",
"http.go",
],
importpath = "go-common/app/tool/saga/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/binding:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

21
app/tool/saga/http/api.go Normal file
View File

@@ -0,0 +1,21 @@
package http
import (
"go-common/app/tool/saga/model"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/binding"
)
func buildContributors(c *bm.Context) {
var (
err error
repo = &model.RepoInfo{}
)
if err = c.BindWith(repo, binding.JSON); err != nil {
log.Error("BindWith error(%v)", err)
return
}
c.JSON(nil, svc.HandleBuildContributors(c, repo))
}

View File

@@ -0,0 +1,70 @@
package http
import (
"encoding/json"
"io/ioutil"
"go-common/app/tool/saga/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/binding"
)
func gitlabComment(c *bm.Context) {
var (
bytes []byte
err error
hookComment = &model.HookComment{}
)
if bytes, err = ioutil.ReadAll(c.Request.Body); err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
c.JSON(nil, ecode.RequestErr)
return
}
if err = json.Unmarshal(bytes, hookComment); err != nil {
log.Error("json.Unmarshal() error(%v)", err)
c.JSON(nil, ecode.RequestErr)
return
}
if hookComment == nil || hookComment.User == nil || hookComment.ObjectAttributes == nil || hookComment.MergeRequest == nil {
log.Error("hookComment event not standard")
c.JSON(nil, ecode.RequestErr)
return
}
log.Info("Got new Gitlab Comment Event kind(%s) attr(%+v) user(%+v)", hookComment.ObjectKind, hookComment.ObjectAttributes, hookComment.User)
c.JSON(nil, svc.HandleGitlabComment(c, hookComment))
}
func gitlabPipeline(c *bm.Context) {
var (
err error
hookPipeline = &model.HookPipeline{}
)
if err = c.BindWith(hookPipeline, binding.JSON); err != nil {
return
}
if hookPipeline == nil || hookPipeline.User == nil || hookPipeline.ObjectAttributes == nil || hookPipeline.Project == nil {
log.Error("hookPipeline event not standard")
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, svc.PipelineChanged(c, hookPipeline))
}
func gitlabMR(c *bm.Context) {
var (
err error
hookMR = &model.HookMR{}
)
if err = c.BindWith(hookMR, binding.JSON); err != nil {
return
}
if hookMR == nil || hookMR.ObjectAttributes == nil || hookMR.Project == nil {
log.Error("hookMR event not standard")
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, svc.MergeRequest(c, hookMR))
}

View File

@@ -0,0 +1,57 @@
package http
import (
"net/http"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
svc *service.Service
idfSvc *verify.Verify
)
// Init init http sever instance.
func Init(s *service.Service) {
svc = s
idfSvc = verify.New(nil)
e := bm.DefaultServer(conf.Conf.BM)
internalRouter(e)
if err := e.Start(); err != nil {
log.Error("xhttp.Serve error(%v)", err)
panic(err)
}
}
// internalRouter init internal router.
func internalRouter(e *bm.Engine) {
e.Ping(ping)
e.Register(register)
group1 := e.Group("/x/internal/v2/saga/gitlab")
{
group1.POST("/comment", gitlabComment)
group1.POST("/pipeline", gitlabPipeline)
group1.POST("/mr", gitlabMR)
}
group2 := e.Group("/x/internal/v2/saga/api")
{
group2.POST("/contributors", buildContributors)
}
}
// ping check server ok.
func ping(c *bm.Context) {
if err := svc.Ping(c); err != nil {
log.Error("saga ping error(%+v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}
func register(c *bm.Context) {
c.JSON(nil, nil)
}

View File

@@ -0,0 +1,44 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"gitlab.go",
"gitlab_comment.go",
"gitlab_mr.go",
"gitlab_pipeline.go",
"gitlab_push.go",
"mail.go",
"model.go",
"wechat.go",
],
importpath = "go-common/app/tool/saga/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["model_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,77 @@
package model
// User def
type User struct {
Name string `json:"name"`
UserName string `json:"username"`
AvatarURL string `json:"avatar_url"`
}
// Project def
type Project struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
WebURL string `json:"web_url"`
AvatarURL string `json:"avatar_url"`
GitSSHURL string `json:"git_ssh_url"`
GitHTTPURL string `json:"git_http_url"`
Namespace string `json:"namespace"`
VisibilityLevel int64 `json:"visibility_level"`
PathWithNamespace string `json:"path_with_namespace"`
DefaultBranch string `json:"default_branch"`
Homepage string `json:"homepage"`
URL string `json:"url"`
SSHURL string `json:"ssh_url"`
HTTPURL string `json:"http_url"`
}
// Repository def
type Repository struct {
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Homepage string `json:"homepage"`
GitHTTPURL string `json:"git_http_url"`
GitSSHURL string `json:"git_ssh_url"`
VisibilityLevel int64 `json:"visibility_level"`
}
// Commit def
type Commit struct {
ID string `json:"id"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
URL string `json:"url"`
Author *Author `json:"author"`
Added []string `json:"added"`
Modified []string `json:"modified"`
Removed []string `json:"removed"`
}
// Author def
type Author struct {
Name string `json:"name"`
Email string `json:"email"`
}
// WebHook def
type WebHook struct {
URL string `json:"url,omitempty"`
PushEvents bool `json:"push_events,omitempty"`
IssuesEvents bool `json:"issues_events,omitempty"`
ConfidentialIssuesEvents bool `json:"confidential_issues_events,omitempty"`
MergeRequestsEvents bool `json:"merge_requests_events,omitempty"`
TagPushEvents bool `json:"tag_push_events,omitempty"`
NoteEvents bool `json:"note_events,omitempty"`
JobEvents bool `json:"job_events,omitempty"`
PipelineEvents bool `json:"pipeline_events,omitempty"`
WikiPageEvents bool `json:"wiki_page_events,omitempty"`
}
// RepoInfo ...
type RepoInfo struct {
Group string `json:"group"`
Name string `json:"name"`
Branch string `json:"branch"`
}

View File

@@ -0,0 +1,53 @@
package model
const (
//HookCommentTypeMR ...
HookCommentTypeMR = "MergeRequest"
)
const (
// CommentTypeStandard iota
CommentTypeStandard = iota
// CommentTypeMisaka ...
CommentTypeMisaka
// CommentTypeMmerge ...
CommentTypeMmerge
// CommentTypeMerge ...
CommentTypeMerge
// CommentTypeRider ...
CommentTypeRider
// CommentTypeDeploy ...
CommentTypeDeploy
// CommentTypeAddOne ...
CommentTypeAddOne
)
// HookComment struct
type HookComment struct {
ObjectKind string `json:"object_kind"`
User *User `json:"user"`
ProjectID int64 `json:"project_id"`
Project *Project `json:"project"`
Repository *Repository `json:"repository"`
ObjectAttributes *Comment `json:"object_attributes"`
MergeRequest *MergeRequest `json:"merge_request"`
Commit *Commit `json:"commit"`
}
// Comment struct
type Comment struct {
ID int64 `json:"id"`
Note string `json:"note"`
NoteableType string `json:"noteable_type"`
AuthorID int64 `json:"author_id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ProjectID int64 `json:"project_id"`
Attachment string `json:"attachment"`
LineCode string `json:"line_code"`
CommitID string `json:"commit_id"`
NoteableID int64 `json:"noteable_id"`
System bool `json:"system"`
STDiff string `json:"st_diff"`
URL string `json:"url"`
}

View File

@@ -0,0 +1,113 @@
package model
const (
// MRActionOpen ...
MRActionOpen = "open"
// MRActionReopen ...
MRActionReopen = "reopen"
// MRActionMerge ...
MRActionMerge = "merge"
)
const (
// MRStateOpened ...
MRStateOpened = "opened"
// MRStateClosed ...
MRStateClosed = "closed"
// MRStateMerged ...
MRStateMerged = "merged"
)
const (
// MRMergeOK ...
MRMergeOK = "can_be_merged"
// MRMergeFailed ...
MRMergeFailed = "cannot_be_merged"
// MRMergeUnchecked ...
MRMergeUnchecked = "unchecked"
)
// HookMR def
type HookMR struct {
ObjectKind string `json:"object_kind"`
Project *Project `json:"project"`
User *User `json:"user"`
ObjectAttributes *MergeRequest `json:"object_attributes"`
Assignee *User `json:"assignee"`
}
// MergeRequest struct
type MergeRequest struct {
ID int64 `json:"id"`
TargetBranch string `json:"target_branch"`
SourceBranch string `json:"source_branch"`
SourceProjectID int64 `json:"source_project_id"`
AuthorID int64 `json:"author_id"`
AssigneeID int64 `json:"assignee_id"`
Title string `json:"title"`
CreateAt string `json:"created_at"`
UpdateAt string `json:"updated_at"`
STCommits int64 `json:"st_commits"`
STDiffs int64 `json:"st_diffs"`
MilestoneID int64 `json:"milestone_id"`
State string `json:"state"`
MergeStatus string `json:"merge_status"`
TargetProjectID int64 `json:"target_project_id"`
IID int64 `json:"iid"`
Description string `json:"description"`
Source *Project `json:"source"`
Target *Project `json:"target"`
LastCommit *Commit `json:"last_commit"`
WorkInProgress bool `json:"work_in_progress"`
URL string `json:"url"`
Action string `json:"action"` // "open","update","close"
Sha string `json:"sha"`
}
// MRRecord def
type MRRecord struct {
ProjectID int `json:"pid"`
MRID int `json:"mrid"`
LastCommit string `json:"lc"`
Mail bool `json:"mail"` // 是否发送过邮件
NoteID int `json:"note"`
Report struct {
TimeSpend int64 `json:"rts"`
MergeFlag bool `json:"rmf"`
BuildFlag bool `json:"rbf"`
StaticCheckFlag bool `json:"rsf"`
VetFlag bool `json:"rvf"`
LintFlag bool `json:"rlf"`
RuleFlag bool `json:"rrf"`
} `json:"report"`
Rider struct {
BuildID int64 `json:"ribi"`
BuildFlag bool `json:"ribf"`
BuildCommit string `json:"ribc"`
DeployID int64 `json:"ridi"`
DeployFlag bool `json:"ridf"`
DeployCommit string `json:"ridc"`
} `json:"rider"`
Reviwers []Reviewer `json:"mus"`
ReviewNotify struct {
Reviewer []string `json:"rnr"`
Assign string `json:"rna"`
} `json:"rn"`
}
// Reviewer struct
type Reviewer struct {
Name string `json:"mun"`
CommitID string `json:"muci"`
}
const (
// MRTypeCommon iota
MRTypeCommon = iota
// MRTypeBiz ...
MRTypeBiz
// MRTypeRevert ...
MRTypeRevert
// MRTypeInvalid ...
MRTypeInvalid
)

View File

@@ -0,0 +1,56 @@
package model
const (
// HookPipelineType ...
HookPipelineType = "pipeline"
// PipelineFailed ...
PipelineFailed = "failed"
// PipelineSuccess ...
PipelineSuccess = "success"
// PipelineSkipped ...
PipelineSkipped = "skipped"
// PipelineCanceled ...
PipelineCanceled = "canceled"
// PipelineRunning ...
PipelineRunning = "running"
// PipelinePending ...
PipelinePending = "pending"
// MergeStatusOk ...
MergeStatusOk = "can_be_merged"
// MergeStateOpened ...
MergeStateOpened = "opened"
)
// QueryStatus ...
type QueryStatus int
// query pipeline type.
const (
QueryProcessing QueryStatus = iota
QuerySuccess
QuerySuccessRmNote
QueryID
)
// HookPipeline webhook for pipeline
type HookPipeline struct {
ObjectKind string `json:"object_kind"`
User *User `json:"user"`
Project *Project `json:"project"`
ObjectAttributes *Pipeline `json:"object_attributes"`
Commit *Commit `json:"commit"`
}
// Pipeline object_attributes for pipeline
type Pipeline struct {
ID int64 `json:"id"`
Ref string `json:"ref"`
Tag bool `json:"tag"`
Sha string `json:"sha"`
BeforeSha string `json:"before_sha"`
Status string `json:"status"`
Stages []string `json:"stages"`
CreatedAt string `json:"created_at"`
FinishedAt string `json:"finished_at"`
Duration uint64 `json:"duration"`
}

View File

@@ -0,0 +1,20 @@
package model
// HookPush def
type HookPush struct {
ObjectKind string `json:"object_kind"`
Before string `json:"before"`
After string `json:"after"`
Ref string `json:"ref"`
CheckoutSHA string `json:"checkout_sha"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
UserUserName string `json:"user_username"`
UserEmail string `json:"user_email"`
UserAvatar string `json:"user_avatar"`
ProjectID int64 `json:"project_id"`
Project *Project `json:"project"`
Repository *Repository `json:"repository"`
Commits []*Commit `json:"commits"`
TotalCommitsCount int64 `json:"total_commits_count"`
}

View File

@@ -0,0 +1,27 @@
package model
// Mail def.
type Mail struct {
ToAddress []*MailAddress
Subject string
Body string
}
// MailAddress def.
type MailAddress struct {
Address string
Name string
}
// MailData def.
type MailData struct {
UserName string
SourceBranch string
TargetBranch string
Title string
Description string
URL string
Info string
PipelineStatus string
PipeStatus string
}

View File

@@ -0,0 +1,165 @@
package model
import (
"reflect"
"regexp"
)
// reids锁
const (
SagaTask = "_SagaTask_%d"
SagaRepoLockKey = "_SagaRepoLockKey_%d"
SagaLockValue = "_SagaLockValue"
)
// gitlab指令
const (
SagaCommandPlusOne = "+ok"
SagaCommandMerge = "+mr"
SagaCommandPlusOne1 = "+1"
SagaCommandMerge1 = "+merge"
)
// 任务状态
const (
TaskStatusFailed = 1 // 任务失败
TaskStatusSuccess = 2 // 任务成功
TaskStatusRunning = 3 // 任务运行中
TaskStatusWaiting = 4 // 任务等待
)
// CONTRIBUTORS define
const (
SagaContributorsName = "CONTRIBUTORS.md"
)
// RepoConfig def
type RepoConfig struct {
URL string
Group string
Name string
GName string // gitlab仓库别名
Language string
AuthBranches []string // 鉴权分支
TargetBranches []string // 分支白名单
TargetBranchRegexes []*regexp.Regexp
LockTimeout int32
MinReviewer int
RelatePipeline bool
DelayMerge bool
LimitAuth bool
AllowLabel string
SuperAuthUsers []string
}
// RequireReviewFolder ...
type RequireReviewFolder struct {
Folder string
Owners []string
Reviewers []string
}
// AuthUsers ...
type AuthUsers struct {
Owners []string
Reviewers []string
}
// ContactInfo def
type ContactInfo struct {
ID string `json:"id,omitempty" gorm:"column:id"`
UserName string `json:"english_name" gorm:"column:user_name"`
UserID string `json:"userid" gorm:"column:user_id"`
NickName string `json:"name" gorm:"column:nick_name"`
VisibleSaga bool `json:"visible_saga" gorm:"column:visible_saga"`
}
// RequireVisibleUser def
type RequireVisibleUser struct {
UserName string
NickName string
}
// AlmostEqual return the compare result with fields
func (contact *ContactInfo) AlmostEqual(other *ContactInfo) bool {
if contact.UserID == other.UserID &&
contact.UserName == other.UserName &&
contact.NickName == other.NickName {
return true
}
return false
}
// TaskInfo ...
type TaskInfo struct {
NoteID int
Event *HookComment
Repo *Repo
}
// MergeInfo ...
type MergeInfo struct {
PipelineID int
NoteID int
AuthorID int
UserName string
MRIID int
ProjID int
URL string
AuthBranches []string
SourceBranch string
TargetBranch string
MinReviewer int
LockTimeout int32
Title string
Description string
}
// Repo structure
type Repo struct {
Config *RepoConfig
}
// Update if config is changed
func (r *Repo) Update(conf *RepoConfig) bool {
if r.confEqual(conf) {
return false
}
r.Config = conf
return true
}
func (r *Repo) confEqual(conf *RepoConfig) bool {
if r.Config.URL == conf.URL &&
r.Config.Group == conf.Group &&
r.Config.Name == conf.Name &&
r.Config.GName == conf.GName &&
r.Config.Language == conf.Language &&
reflect.DeepEqual(r.Config.AuthBranches, conf.AuthBranches) &&
reflect.DeepEqual(r.Config.TargetBranches, conf.TargetBranches) &&
r.Config.LockTimeout == conf.LockTimeout &&
r.Config.MinReviewer == conf.MinReviewer &&
r.Config.RelatePipeline == conf.RelatePipeline &&
r.Config.DelayMerge == conf.DelayMerge &&
r.Config.LimitAuth == conf.LimitAuth &&
r.Config.AllowLabel == conf.AllowLabel &&
reflect.DeepEqual(r.Config.SuperAuthUsers, conf.SuperAuthUsers) {
return true
}
return false
}
// AuthUpdate ...
func (r *Repo) AuthUpdate(conf *RepoConfig) bool {
if r.Config.Group == conf.Group &&
r.Config.Name == conf.Name &&
reflect.DeepEqual(r.Config.AuthBranches, conf.AuthBranches) {
return false
}
return true
}
// WebHookUpdate ...
func (r *Repo) WebHookUpdate(conf *RepoConfig) bool {
return r.Config.URL != conf.URL
}

View File

@@ -0,0 +1,227 @@
package model
import (
"encoding/json"
"testing"
)
func TestGitlab(t *testing.T) {
var (
jsonStr = `{
"object_kind": "merge_request",
"user": {
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"object_attributes": {
"id": 99,
"target_branch": "master",
"source_branch": "ms-viewport",
"source_project_id": 14,
"author_id": 51,
"assignee_id": 6,
"title": "MS-Viewport",
"created_at": "2013-12-03T17:23:34Z",
"updated_at": "2013-12-03T17:23:34Z",
"st_commits": null,
"st_diffs": null,
"milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 14,
"iid": 1,
"description": "",
"source":{
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
"avatar_url":null,
"git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
"git_http_url":"http://example.com/awesome_space/awesome_project.git",
"namespace":"Awesome Space",
"visibility_level":20,
"path_with_namespace":"awesome_space/awesome_project",
"default_branch":"master",
"homepage":"http://example.com/awesome_space/awesome_project",
"url":"http://example.com/awesome_space/awesome_project.git",
"ssh_url":"git@example.com:awesome_space/awesome_project.git",
"http_url":"http://example.com/awesome_space/awesome_project.git"
},
"target": {
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
"avatar_url":null,
"git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
"git_http_url":"http://example.com/awesome_space/awesome_project.git",
"namespace":"Awesome Space",
"visibility_level":20,
"path_with_namespace":"awesome_space/awesome_project",
"default_branch":"master",
"homepage":"http://example.com/awesome_space/awesome_project",
"url":"http://example.com/awesome_space/awesome_project.git",
"ssh_url":"git@example.com:awesome_space/awesome_project.git",
"http_url":"http://example.com/awesome_space/awesome_project.git"
},
"last_commit": {
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"message": "fixed readme",
"timestamp": "2012-01-03T23:36:29+02:00",
"url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"author": {
"name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)"
}
},
"work_in_progress": false,
"url": "http://example.com/diaspora/merge_requests/1",
"action": "open",
"assignee": {
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
}
}
`
mrHook = &HookMR{}
err error
)
err = json.Unmarshal([]byte(jsonStr), mrHook)
if err != nil {
t.Fatal(err)
}
t.Log(mrHook)
t.Log(mrHook.User)
t.Log(mrHook.ObjectAttributes)
var (
commentJSONStr = `{
"object_kind": "note",
"user": {
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project_id": 5,
"project":{
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
"avatar_url":null,
"git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
"git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
"namespace":"Gitlab Org",
"visibility_level":10,
"path_with_namespace":"gitlab-org/gitlab-test",
"default_branch":"master",
"homepage":"http://example.com/gitlab-org/gitlab-test",
"url":"http://example.com/gitlab-org/gitlab-test.git",
"ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
"http_url":"http://example.com/gitlab-org/gitlab-test.git"
},
"repository":{
"name": "Gitlab Test",
"url": "http://localhost/gitlab-org/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
"homepage": "http://example.com/gitlab-org/gitlab-test"
},
"object_attributes": {
"id": 1244,
"note": "This MR needs work.",
"noteable_type": "MergeRequest",
"author_id": 1,
"created_at": "2015-05-17 18:21:36 UTC",
"updated_at": "2015-05-17 18:21:36 UTC",
"project_id": 5,
"attachment": null,
"line_code": null,
"commit_id": "",
"noteable_id": 7,
"system": false,
"st_diff": null,
"url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
},
"merge_request": {
"id": 7,
"target_branch": "markdown",
"source_branch": "master",
"source_project_id": 5,
"author_id": 8,
"assignee_id": 28,
"title": "Tempora et eos debitis quae laborum et.",
"created_at": "2015-03-01 20:12:53 UTC",
"updated_at": "2015-03-21 18:27:27 UTC",
"milestone_id": 11,
"state": "opened",
"merge_status": "cannot_be_merged",
"target_project_id": 5,
"iid": 1,
"description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
"position": 0,
"source":{
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
"avatar_url":null,
"git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
"git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
"namespace":"Gitlab Org",
"visibility_level":10,
"path_with_namespace":"gitlab-org/gitlab-test",
"default_branch":"master",
"homepage":"http://example.com/gitlab-org/gitlab-test",
"url":"http://example.com/gitlab-org/gitlab-test.git",
"ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
"http_url":"http://example.com/gitlab-org/gitlab-test.git"
},
"target": {
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
"avatar_url":null,
"git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
"git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
"namespace":"Gitlab Org",
"visibility_level":10,
"path_with_namespace":"gitlab-org/gitlab-test",
"default_branch":"master",
"homepage":"http://example.com/gitlab-org/gitlab-test",
"url":"http://example.com/gitlab-org/gitlab-test.git",
"ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
"http_url":"http://example.com/gitlab-org/gitlab-test.git"
},
"last_commit": {
"id": "562e173be03b8ff2efb05345d12df18815438a4b",
"message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
"timestamp": "2015-04-08T21:00:25-07:00",
"url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
"author": {
"name": "John Smith",
"email": "john@example.com"
}
},
"work_in_progress": false,
"assignee": {
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
}
}
`
hookComment = &HookComment{}
)
err = json.Unmarshal([]byte(commentJSONStr), hookComment)
if err != nil {
t.Fatal(err)
}
t.Log(hookComment)
t.Log(hookComment.User)
t.Log(hookComment.Project)
t.Log(hookComment.Repository)
t.Log(hookComment.ObjectAttributes)
t.Log(hookComment.MergeRequest)
t.Log(hookComment.Commit)
}

View File

@@ -0,0 +1,38 @@
package model
// AppConfig def
type AppConfig struct {
AppID int // 企业微信SAGA应用的appId
AppSecret string // 企业微信SAGA应用的secret
}
// Notification def
type Notification struct {
ToUser string `json:"touser"`
ToParty string `json:"toparty"`
ToTag string `json:"totag"`
MsgType string `json:"msgtype"`
AgentID int `json:"agentid"`
}
// Text def
type Text struct {
Content string `json:"content"`
}
// TxtNotification 文本消息
type TxtNotification struct {
Notification
Body Text `json:"text"`
Safe int `json:"safe"`
}
// AllowUserInfo 应用可见名单列表
type AllowUserInfo struct {
Users []*UserInfo `json:"user"`
}
// UserInfo only contain userid now
type UserInfo struct {
UserID string `json:"userid"`
}

View File

@@ -0,0 +1,73 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"comment.go",
"contributors.go",
"mr.go",
"pipeline.go",
"service.go",
"wechat.go",
],
importpath = "go-common/app/tool/saga/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/command:go_default_library",
"//app/tool/saga/service/gitlab:go_default_library",
"//app/tool/saga/service/mail:go_default_library",
"//app/tool/saga/service/notification:go_default_library",
"//app/tool/saga/service/wechat:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/robfig/cron:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/tool/saga/service/command:all-srcs",
"//app/tool/saga/service/gitlab:all-srcs",
"//app/tool/saga/service/mail:all-srcs",
"//app/tool/saga/service/notification:all-srcs",
"//app/tool/saga/service/wechat:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"comment_test.go",
"contributors_test.go",
"service_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"command.go",
"contributors.go",
"merge.go",
"review.go",
],
importpath = "go-common/app/tool/saga/service/command",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/gitlab:go_default_library",
"//app/tool/saga/service/notification:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/xanzy/go-gitlab:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"command_test.go",
"contributors_test.go",
"review_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/gitlab:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,113 @@
package command
import (
"context"
"fmt"
"runtime/debug"
"time"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/gitlab"
"go-common/library/log"
)
// Command Command def.
type Command struct {
dao *dao.Dao
gitlab *gitlab.Gitlab
cmds map[string]cmdFunc
}
type cmdFunc func(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error)
// New ...
func New(dao *dao.Dao, gitlab *gitlab.Gitlab) (c *Command) {
c = &Command{
dao: dao,
gitlab: gitlab,
cmds: make(map[string]cmdFunc),
}
return
}
// Exec ...
func (c *Command) Exec(ctx context.Context, cmd string, event *model.HookComment, repo *model.Repo) (err error) {
var (
f cmdFunc
ok bool
projID = int(event.MergeRequest.SourceProjectID)
mrIID = int(event.MergeRequest.IID)
)
if f, ok = c.cmds[cmd]; !ok {
return
}
if err = f(ctx, event, repo); err != nil {
c.gitlab.CreateMRNote(projID, mrIID, fmt.Sprintf("<pre>SAGA 异常:%+v %s</pre>", err, debug.Stack()))
return
}
return
}
func (c *Command) register(cmd string, f cmdFunc) {
c.cmds[cmd] = f
}
// Registers ...
func (c *Command) Registers() {
c.register(model.SagaCommandPlusOne, c.runPlusOne)
c.register(model.SagaCommandMerge, c.runTryMerge)
c.register(model.SagaCommandMerge1, c.runTryMerge)
c.register(model.SagaCommandPlusOne1, c.runPlusOne)
}
// ListenTask ...
func (c *Command) ListenTask() {
var (
ctx = context.TODO()
err error
t *time.Timer
ok bool
taskInfos []*model.TaskInfo
)
defer func() {
if x := recover(); x != nil {
log.Error("ListenTask: %+v %s", x, debug.Stack())
}
}()
t = time.NewTimer(time.Duration(conf.Conf.Property.TaskInterval))
for range t.C {
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusWaiting); err != nil {
log.Error("request MergeTasks: %+v", err)
continue
}
for _, taskInfo := range taskInfos {
if ok, err = c.dao.TryLock(ctx, fmt.Sprintf(model.SagaRepoLockKey, int(taskInfo.Event.ProjectID)), model.SagaLockValue, int(taskInfo.Repo.Config.LockTimeout)); err != nil {
log.Error("TryLock ProjectID: %d, MRIID: %d, err: %+v", taskInfo.Event.ProjectID, taskInfo.Event.MergeRequest.IID, err)
continue
}
if !ok {
log.Info("TryLock ok: %t, ProjectID: %d, MRIID: %d", ok, taskInfo.Event.ProjectID, taskInfo.Event.MergeRequest.IID)
continue
}
log.Info("request MRIID: %d, MergeTasks:%+v", taskInfo.Event.MergeRequest.IID, taskInfo)
go func(taskInfo *model.TaskInfo) {
var (
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
mrIID = int(taskInfo.Event.MergeRequest.IID)
branch = taskInfo.Event.MergeRequest.SourceBranch
)
if err = c.execMergeTask(taskInfo); err != nil {
c.gitlab.UpdateMRNote(projID, mrIID, taskInfo.NoteID, fmt.Sprintf("<pre>SAGA 异常:%+v</pre>", err))
if err = c.resetMergeStatus(projID, mrIID, branch, false); err != nil {
log.Error("resetMergeStatus error: %+v", err)
}
log.Error("execMergeTask: %+v", err)
}
}(taskInfo)
}
t.Reset(time.Duration(conf.Conf.Property.TaskInterval))
}
}

View File

@@ -0,0 +1,209 @@
package command
import (
"context"
"encoding/json"
"flag"
"path/filepath"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/gitlab"
"github.com/smartystreets/goconvey/convey"
)
var (
c *Command
gitlabHookCommentTest = []byte(`{
"object_kind":"note",
"event_type":"note",
"user":{
"name":"changhengyuan",
"username":"changhengyuan",
"avatar_url":"https://www.gravatar.com/avatar/d3218d34473c6fb4d18a770f14e59a89?s=80\u0026d=identicon"
},
"project_id":35,
"project":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"},
"object_attributes":{
"id":3040,
"note":"test",
"noteable_type":"MergeRequest",
"author_id":15,
"created_at":"2018-09-26 06:55:13 UTC",
"updated_at":"2018-09-26 06:55:13 UTC",
"project_id":35,
"attachment":null,
"line_code":null,
"commit_id":"",
"noteable_id":390,
"system":false,
"st_diff":null,
"updated_by_id":null,
"type":null,
"position":null,
"original_position":null,
"resolved_at":null,
"resolved_by_id":null,
"discussion_id":"450c34e4c0f9e925bdc6a24c2ae4920d7a394ebc",
"change_position":null,
"resolved_by_push":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52#note_3040"
},
"repository":{
"name":"test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"description":"",
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga"
},
"merge_request":{
"assignee_id":null,
"author_id":15,
"created_at":"2018-09-26 06:41:55 UTC",
"description":"",
"head_pipeline_id":4510,
"id":390,
"iid":52,
"last_edited_at":null,
"last_edited_by_id":null,
"merge_commit_sha":null,
"merge_error":null,
"merge_params":{
"force_remove_source_branch":"0"
},
"merge_status":"cannot_be_merged",
"merge_user_id":null,
"merge_when_pipeline_succeeds":false,
"milestone_id":null,
"source_branch":"test-branch",
"source_project_id":35,
"state":"opened",
"target_branch":"master",
"target_project_id":35,
"time_estimate":0,
"title":"Test branch",
"updated_at":"2018-09-26 06:54:33 UTC",
"updated_by_id":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52",
"source":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"target":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"last_commit":{
"id":"51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"message":"add CONTRIBUTORS.md\n",
"timestamp":"2018-09-17T18:02:13+08:00",
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/commit/51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"author":{
"name":"哔哩哔哩",
"email":"bilibili@bilibilideMac-mini.local"
}
},
"work_in_progress":false,
"total_time_spent":0,
"human_total_time_spent":null,
"human_time_estimate":null}}`)
)
func init() {
dir, _ := filepath.Abs("../../cmd/saga-test.toml")
flag.Set("conf", dir)
conf.Init()
c = New(&dao.Dao{}, &gitlab.Gitlab{})
}
func TestCommandNew(t *testing.T) {
convey.Convey("New", t, func(ctx convey.C) {
ctx.Convey("When everything goes positive", func(ctx convey.C) {
ctx.Convey("Then c should not be nil.", func(ctx convey.C) {
ctx.So(c, convey.ShouldNotBeNil)
})
})
})
}
func TestCommandExec(t *testing.T) {
convey.Convey("Exec", t, func(ctx convey.C) {
var (
ct = context.Background()
cmd = "+1"
event = &model.HookComment{}
repo = &model.Repo{}
c = &Command{}
)
_ = json.Unmarshal(gitlabHookCommentTest, event)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := c.Exec(ct, cmd, event, repo)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestCommandRegister(t *testing.T) {
convey.Convey("register", t, func(ctx convey.C) {
var (
cmd = "test_cmd"
f cmdFunc
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
c.register(cmd, f)
ctx.Convey("No return values", func(ctx convey.C) {
cmd, ok := c.cmds["test_cmd"]
ctx.So(ok, convey.ShouldEqual, true)
ctx.So(cmd, convey.ShouldEqual, f)
})
})
})
}

View File

@@ -0,0 +1,470 @@
package command
import (
"context"
"fmt"
"path/filepath"
"strings"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
)
type contributor struct {
Owner []string
Author []string
Reviewer []string
}
func readContributor(content []byte) (c *contributor) {
var (
lines []string
lineStr string
curSection string
)
c = &contributor{}
lines = strings.Split(string(content), "\n")
for _, lineStr = range lines {
if lineStr == "" {
continue
}
if strings.Contains(strings.ToLower(lineStr), "owner") {
curSection = "owner"
continue
}
if strings.Contains(strings.ToLower(lineStr), "author") {
curSection = "author"
continue
}
if strings.Contains(strings.ToLower(lineStr), "reviewer") {
curSection = "reviewer"
continue
}
switch curSection {
case "owner":
c.Owner = append(c.Owner, strings.TrimSpace(lineStr))
case "author":
c.Author = append(c.Author, strings.TrimSpace(lineStr))
case "reviewer":
c.Reviewer = append(c.Reviewer, strings.TrimSpace(lineStr))
}
}
return
}
// BuildContributor ...
func (c *Command) BuildContributor(repo *model.RepoInfo) (err error) {
var (
host string
token string
files []string
projID int
branch = repo.Branch
)
if host, token, err = c.gitlab.HostToken(); err != nil {
return
}
if files, err = c.dao.RepoFiles(context.TODO(), host, token, repo); err != nil {
return
}
if projID, err = c.gitlab.ProjectID(fmt.Sprintf("git@%s:%s/%s.git", host, repo.Group, repo.Name)); err != nil {
return
}
if err = c.SaveContributor(projID, branch, files, false); err != nil {
return
}
return
}
func hasbranch(branch string, branchs []string) bool {
for _, b := range branchs {
if strings.EqualFold(b, branch) {
return true
}
}
return false
}
// UpdateContributor ...
func (c *Command) UpdateContributor(projID int, mrIID int, sourceBranch string, targetBranch string, authBranches []string) (err error) {
var changeFiles []string
var deleteFiles []string
if !hasbranch(targetBranch, authBranches) {
log.Info("UpdateContributor not authBranches: %d, %s, %+v", projID, targetBranch, authBranches)
return
}
if changeFiles, deleteFiles, err = c.gitlab.MRChanges(projID, mrIID); err != nil {
return
}
//log.Info("UpdateContributor: projID: %d, changeFiles: %+v, deleteFiles: %+v", projID, changeFiles, deleteFiles)
if err = c.SaveContributor(projID, targetBranch, changeFiles, false); err != nil {
return
}
if err = c.SaveContributor(projID, targetBranch, deleteFiles, true); err != nil {
return
}
return
}
// SaveContributor ...
func (c *Command) SaveContributor(projID int, branch string, files []string, clean bool) (err error) {
var (
ctx = context.TODO()
raw []byte
cb *contributor
)
for _, file := range files {
if strings.EqualFold(filepath.Base(file), model.SagaContributorsName) {
path := filepath.Dir(file)
if clean {
//delete redis key
if err = c.dao.DeletePathAuthR(ctx, projID, branch, path); err != nil {
log.Error("delete Auth Redis key error(%+v) project ID:%d, branch:%s, path:%s", err, projID, branch, path)
err = nil
}
//delete hbase key
if err = c.dao.DeletePathAuthH(ctx, projID, branch, path); err != nil {
log.Error("delete Auth hbase key error(%+v) project ID:%d, branch:%s, path:%s", err, projID, branch, path)
err = nil
}
} else {
if raw, err = c.gitlab.RepoRawFile(projID, branch, file); err != nil {
return
}
cb = readContributor(raw)
log.Info("SaveContributor projectID:%d, branch:%s, path:%s, owner:%+v, reviewer:%+v", projID, branch, path, cb.Owner, cb.Reviewer)
//add to redis
authUser := &model.AuthUsers{
Owners: cb.Owner,
Reviewers: cb.Reviewer,
}
if err = c.dao.SetPathAuthR(ctx, projID, branch, path, authUser); err != nil {
log.Error("Set Auth to Redis error(%+v) project ID:%d, path:%s, owner:%+v, reviewer:%+v", err, projID, path, cb.Owner, cb.Reviewer)
err = nil
}
//add to hbase
if err = c.dao.SetPathAuthH(ctx, projID, branch, path, cb.Owner, cb.Reviewer); err != nil {
log.Error("Set Auth to Hbase error(%+v) projectID:%d, branch:%s, path:%s, owner:%+v, reviewer:%+v", err, projID, branch, path, cb.Owner, cb.Reviewer)
err = nil
}
}
}
}
return
}
// checkSuperAuth ...
func checkSuperAuth(username string, reviewedUsers []string, superUsers []string) (ok bool) {
for _, super := range superUsers {
if strings.EqualFold(super, username) {
return true
}
}
for _, r := range reviewedUsers {
for _, s := range superUsers {
if strings.EqualFold(r, s) {
return true
}
}
}
return false
}
// checkAllPathAuth ...
func (c *Command) checkAllPathAuth(taskInfo *model.TaskInfo) (ok bool, err error) {
var (
comment string
files []string
folders map[string]string
owners []string
reviewers []string
requireOwners []string
requireReviewers []string
reviewedUsers []string
requireReviewFolders []*model.RequireReviewFolder
username = taskInfo.Event.User.UserName
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
mrIID = int(taskInfo.Event.MergeRequest.IID)
sourceBranch = taskInfo.Event.MergeRequest.SourceBranch
targetBranch = taskInfo.Event.MergeRequest.TargetBranch
authBranch = c.GetAuthBranch(targetBranch, taskInfo.Repo.Config.AuthBranches)
url = taskInfo.Event.ObjectAttributes.URL
minReviewer = taskInfo.Repo.Config.MinReviewer
limitAuth = taskInfo.Repo.Config.LimitAuth
minReviewTip bool
isOwner bool
)
log.Info("checkAllPathAuth start ... MRIID: %d", mrIID)
if reviewedUsers, err = c.reviewedUsers(projID, mrIID); err != nil {
return
}
if len(taskInfo.Repo.Config.SuperAuthUsers) > 0 {
log.Info("checkAllPathAuth MRIID: %d, reviewedUsers: %v, SuperAuthUsers: %v", mrIID, reviewedUsers, taskInfo.Repo.Config.SuperAuthUsers)
ok = checkSuperAuth(username, reviewedUsers, taskInfo.Repo.Config.SuperAuthUsers)
if !ok {
comment, err = c.showRequireSuperAuthComment(taskInfo)
go notification.WechatAuthor(c.dao, username, url, sourceBranch, targetBranch, comment)
}
return
}
if files, err = c.gitlab.CompareDiff(projID, targetBranch, sourceBranch); err != nil {
return
}
log.Info("checkAllPathAuth projID:%d, MRIID:%d, reviewedUsers:%+v, files:%+v, targetBranch:%s, sourceBranch:%s", projID, mrIID, reviewedUsers, files, targetBranch, sourceBranch)
// 去重目录,校验目录权限
folders = make(map[string]string)
for _, file := range files {
folder := filepath.Dir(file)
if _, has := folders[folder]; !has {
folders[folder] = folder
authEnough := false
ownerPathReviewed := false
ownerReviewedStat := false
reviewedCount := 0
requireOwners = []string{}
requireReviewers = []string{}
dir := folder
for {
if owners, reviewers, err = c.GetPathAuth(projID, authBranch, dir); err != nil {
return
}
isOwner, ownerPathReviewed = reviewedOwner(owners, reviewedUsers, username)
num := reviewedNum(reviewers, reviewedUsers)
reviewedCount = reviewedCount + num
if isOwner {
log.Info("checkAllPathAuth user is owner, MRIID: %d, user: %s, owners %v, path: %s", mrIID, username, owners, dir)
authEnough = true
break
}
if ownerPathReviewed {
ownerReviewedStat = true
if reviewedCount >= minReviewer {
log.Info("checkAllPathAuth owner reviewed and reach minReviewer, user: %s, owners %+v, path: %s, reviewedUsers: %+v, reviewedCount: %d, minReviewer: %d", username, owners, dir, reviewedUsers, reviewedCount, minReviewer)
authEnough = true
break
}
}
if (len(requireOwners) == 0) && (len(owners) > 0) {
log.Info("checkAllPathAuth required owners %v, path: %s, MRIID: %d", owners, dir, mrIID)
requireOwners = owners
}
if (len(requireReviewers) == 0) && (len(reviewers) > 0) {
log.Info("checkAllPathAuth required reviewers %v, path: %s, MRIID: %d", reviewers, dir, mrIID)
requireReviewers = reviewers
}
if dir == "." {
break
}
if (len(owners) > 0) && limitAuth {
break
}
dir = filepath.Dir(dir)
}
if !authEnough {
requireAuth := &model.RequireReviewFolder{}
requireAuth.Folder = folder
if !ownerReviewedStat {
requireAuth.Owners = requireOwners
}
if reviewedCount < minReviewer {
requireAuth.Reviewers = requireReviewers
minReviewTip = true
}
requireReviewFolders = append(requireReviewFolders, requireAuth)
}
}
}
if len(requireReviewFolders) > 0 {
comment, err = c.showRequireAuthComment(taskInfo, requireReviewFolders, minReviewTip)
go notification.WechatAuthor(c.dao, username, url, sourceBranch, targetBranch, comment)
return
}
ok = true
return
}
// GetAllPathAuth ...
func (c *Command) GetAllPathAuth(projID int, mrIID int, authBranch string) (pathOwners []model.RequireReviewFolder, err error) {
var (
files []string
deleteFiles []string
folders map[string]string
pathOwner []string
pathReviewer []string
)
if files, deleteFiles, err = c.gitlab.MRChanges(projID, mrIID); err != nil {
return
}
files = append(files, deleteFiles...)
// 去重目录,校验目录权限
folders = make(map[string]string)
for _, file := range files {
folder := filepath.Dir(file)
if _, has := folders[folder]; !has {
folders[folder] = folder
dir := folder
for {
if pathOwner, pathReviewer, err = c.GetPathAuth(projID, authBranch, dir); err != nil {
return
}
if len(pathOwner) > 0 {
exist := false
for _, os := range pathOwners {
if os.Folder == dir {
exist = true
break
}
}
if exist {
break
}
requireOwner := model.RequireReviewFolder{}
requireOwner.Folder = dir
requireOwner.Owners = pathOwner
if len(pathReviewer) > 0 {
requireOwner.Reviewers = pathReviewer
}
pathOwners = append(pathOwners, requireOwner)
break
}
if dir == "." {
break
}
dir = filepath.Dir(dir)
}
}
}
return
}
// GetPathAuth ...
func (c *Command) GetPathAuth(projID int, branch string, path string) (owners []string, reviewers []string, err error) {
var (
ctx = context.TODO()
authUser *model.AuthUsers
)
if authUser, err = c.dao.PathAuthR(ctx, projID, branch, path); err != nil || authUser == nil {
if err != nil {
log.Error("GetPathAuthInfo error project ID:%d, branch:%s, path: %s (err: %+v) ", projID, branch, path, err)
}
if owners, reviewers, err = c.dao.PathAuthH(ctx, projID, branch, path); err != nil {
return
}
if len(owners) <= 0 && len(reviewers) <= 0 {
return
}
if authUser == nil {
authUser = new(model.AuthUsers)
}
authUser.Owners = owners
authUser.Reviewers = reviewers
if err1 := c.dao.SetPathAuthR(ctx, projID, branch, path, authUser); err1 != nil {
log.Error("SetPathAuthR error project ID:%d, branch:%s, path: %s (err1: %+v) ", projID, branch, path, err1)
}
return
}
if authUser != nil {
owners = authUser.Owners
reviewers = authUser.Reviewers
}
return
}
// GetAuthBranch ...
func (c *Command) GetAuthBranch(targetBranch string, authBranches []string) (authBranch string) {
for _, r := range authBranches {
if r == targetBranch {
return r
}
}
return authBranches[0]
}
// showRequireAuthComment ...
func (c *Command) showRequireAuthComment(taskInfo *model.TaskInfo, requireReviewFolders []*model.RequireReviewFolder, minReviewTip bool) (comment string, err error) {
var (
username = taskInfo.Event.User.UserName
mrIID = int(taskInfo.Event.MergeRequest.IID)
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
noteID = taskInfo.NoteID
minReviewer = taskInfo.Repo.Config.MinReviewer
)
if minReviewTip {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败无权限的文件目录如下并且最少需要review人数 [%d] 个,请寻找对应的大佬 +1 后再 +merge</pre>", username, minReviewer)
} else {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,无权限的文件目录如下,请寻找对应的大佬 +1 后再 +merge</pre>", username)
}
comment += "\n"
for _, reviewFolder := range requireReviewFolders {
comment += fmt.Sprintf("+ %s ", reviewFolder.Folder)
if len(reviewFolder.Owners) > 0 {
comment += "OWNER: "
for _, o := range reviewFolder.Owners {
if o == "all" {
comment += "所有人" + " 或 "
} else {
comment += o + " 或 "
}
}
comment = comment[:len(comment)-len(" 或 ")]
}
if len(reviewFolder.Reviewers) > 0 {
comment += "REVIEWER: "
for _, o := range reviewFolder.Reviewers {
if o == "all" {
comment += "所有人" + " 与 "
} else {
comment += o + " 与 "
}
}
comment = comment[:len(comment)-len(" 与 ")]
//comment += fmt.Sprintf("<pre>; 已review人数 [%d] 个</pre>", minReviewer)
}
comment += "\n\n"
}
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
// showRequireSuperAuthComment ...
func (c *Command) showRequireSuperAuthComment(taskInfo *model.TaskInfo) (comment string, err error) {
var (
username = taskInfo.Event.User.UserName
mrIID = int(taskInfo.Event.MergeRequest.IID)
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
noteID = taskInfo.NoteID
)
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,已配置超级权限用户,请寻找其中至少一位大佬 +1 后再 +merge</pre>", username)
comment += "\n"
comment += fmt.Sprintf("+ SUPERMAN: ")
for _, user := range taskInfo.Repo.Config.SuperAuthUsers {
comment += fmt.Sprintf("%s 或 ", user)
}
comment = comment[:len(comment)-len(" 或 ")]
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}

View File

@@ -0,0 +1,38 @@
package command
import (
"fmt"
"io/ioutil"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestCommandReadContributor(t *testing.T) {
convey.Convey("readContributor", t, func(ctx convey.C) {
content, _ := ioutil.ReadFile("../../CONTRIBUTORS.md")
ctx.Convey("When everything goes positive", func(ctx convey.C) {
cc := readContributor(content)
ctx.Convey("Then c should not be nil.", func(ctx convey.C) {
ctx.So(fmt.Sprint(cc.Author), convey.ShouldEqual, `[muyang yubaihai wangweizhen wuwei]`)
ctx.So(fmt.Sprint(cc.Owner), convey.ShouldEqual, `[muyang zhanglin]`)
ctx.So(fmt.Sprint(cc.Reviewer), convey.ShouldEqual, `[muyang]`)
})
})
})
}
func TestCommandHasBranch(t *testing.T) {
convey.Convey("hasbranch", t, func(ctx convey.C) {
var (
branch = "test"
branchs = []string{"master", "test"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := hasbranch(branch, branchs)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, true)
})
})
})
}

View File

@@ -0,0 +1,529 @@
package command
import (
"context"
"fmt"
"runtime/debug"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
ggitlab "github.com/xanzy/go-gitlab"
)
func (c *Command) runTryMerge(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error) {
var (
ok bool
canMerge bool
projID = int(event.MergeRequest.SourceProjectID)
mrIID = int(event.MergeRequest.IID)
wip = event.MergeRequest.WorkInProgress
noteID int
taskInfo = &model.TaskInfo{
Event: event,
Repo: repo,
}
)
log.Info("runTryMerge start ... MRIID: %d, Repo Config: %+v", mrIID, repo.Config)
if ok, err = c.dao.ExistMRIID(ctx, mrIID); err != nil || ok {
return
}
if noteID, err = c.gitlab.CreateMRNote(projID, mrIID, "<pre>SAGA 开始执行,请大佬稍后......</pre>"); err != nil {
return
}
taskInfo.NoteID = noteID
// 1, check wip
if wip {
c.gitlab.UpdateMRNote(projID, mrIID, noteID, "<pre>警告当前MR处于WIP状态请待开发结束后再merge</pre>")
return
}
// 2, check labels
if ok, err = c.checkLabels(projID, mrIID, noteID, repo); err != nil || !ok {
return
}
// 3, check merge status
if canMerge, err = c.checkMergeStatus(projID, mrIID, noteID); err != nil || !canMerge {
return
}
// 4, check pipeline status
if repo.Config.RelatePipeline {
if repo.Config.DelayMerge {
if ok, _, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QueryProcessing); err != nil || !ok {
return
}
} else {
if ok, _, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QuerySuccess); err != nil || !ok {
return
}
}
}
// 5, check path auth
if ok, err = c.checkAllPathAuth(taskInfo); err != nil || !ok {
return
}
// 6, show current mr queue info
c.showMRQueueInfo(ctx, taskInfo)
if err = c.dao.PushMergeTask(ctx, model.TaskStatusWaiting, taskInfo); err != nil {
return
}
if err = c.dao.AddMRIID(ctx, mrIID, int(repo.Config.LockTimeout)); err != nil {
return
}
log.Info("runTryMerge merge task 已加入 waiting 任务列队中... MRIID: %d", mrIID)
return
}
func (c *Command) execMergeTask(taskInfo *model.TaskInfo) (err error) {
var (
ctx = context.TODO()
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
mrIID = int(taskInfo.Event.MergeRequest.IID)
sourceBranch = taskInfo.Event.MergeRequest.SourceBranch
pipeline = &ggitlab.Pipeline{}
noteID = taskInfo.NoteID
mergeInfo = &model.MergeInfo{
ProjID: projID,
MRIID: mrIID,
URL: taskInfo.Event.ObjectAttributes.URL,
AuthBranches: taskInfo.Repo.Config.AuthBranches,
SourceBranch: taskInfo.Event.MergeRequest.SourceBranch,
TargetBranch: taskInfo.Event.MergeRequest.TargetBranch,
AuthorID: int(taskInfo.Event.MergeRequest.AuthorID),
UserName: taskInfo.Event.User.UserName,
MinReviewer: taskInfo.Repo.Config.MinReviewer,
LockTimeout: taskInfo.Repo.Config.LockTimeout,
Title: taskInfo.Event.MergeRequest.Title,
Description: taskInfo.Event.MergeRequest.Description,
}
)
mergeInfo.NoteID = noteID
// 从等待任务列队移除
if err = c.dao.DeleteMergeTask(ctx, model.TaskStatusWaiting, taskInfo); err != nil {
return
}
// 加入到正在执行任务列队
if err = c.dao.PushMergeTask(ctx, model.TaskStatusRunning, taskInfo); err != nil {
return
}
if taskInfo.Repo.Config.RelatePipeline {
if taskInfo.Repo.Config.DelayMerge {
if err = c.HookDelayMerge(projID, sourceBranch, mergeInfo); err != nil {
return
}
return
}
if err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, "<pre>SAGA 提示为了保证合进主干后能正常编译正在重跑pipeline等待时间取决于pipeline运行时间请耐心等待</pre>"); err != nil {
return
}
if pipeline, err = c.retryPipeline(taskInfo.Event); err != nil {
return
}
mergeInfo.PipelineID = pipeline.ID
if err = c.dao.SetMergeInfo(ctx, projID, sourceBranch, mergeInfo); err != nil {
return
}
} else {
if err = c.HookMerge(projID, sourceBranch, mergeInfo); err != nil {
return
}
}
return
}
func (c *Command) retryPipeline(event *model.HookComment) (pipeline *ggitlab.Pipeline, err error) {
var (
trigger *ggitlab.PipelineTrigger
triggers []*ggitlab.PipelineTrigger
projID = int(event.MergeRequest.SourceProjectID)
sourceBranch = event.MergeRequest.SourceBranch
)
if triggers, err = c.gitlab.Triggers(projID); err != nil {
return
}
if len(triggers) == 0 {
log.Info("No triggers were found for project %d, try to create it now.", projID)
if trigger, err = c.gitlab.CreateTrigger(projID); err != nil {
return
}
triggers = []*ggitlab.PipelineTrigger{trigger}
}
trigger = triggers[0]
if trigger.Owner == nil || trigger.Owner.ID == 0 {
log.Info("Legacy trigger (without owner), take ownership now.")
if trigger, err = c.gitlab.TakeOwnership(projID, trigger.ID); err != nil {
return
}
}
if pipeline, err = c.gitlab.TriggerPipeline(projID, sourceBranch, trigger.Token); err != nil {
return
}
return
}
// HookPipeline ...
func (c *Command) HookPipeline(projID int, branch string, pipelineID int) (err error) {
var (
ok bool
canMerge bool
mergeInfo *model.MergeInfo
)
defer func() {
if x := recover(); x != nil {
log.Error("HookPipeline: %+v %s", x, debug.Stack())
}
}()
if ok, mergeInfo, err = c.dao.MergeInfo(context.TODO(), projID, branch); err != nil || !ok {
return
}
log.Info("HookPipeline projID: %d, MRIID: %d, branch: %s, pipelineId: %d", projID, mergeInfo.MRIID, branch, mergeInfo.PipelineID)
if pipelineID < mergeInfo.PipelineID {
return
}
defer func() {
if err = c.resetMergeStatus(projID, mergeInfo.MRIID, branch, true); err != nil {
log.Error("resetMergeStatus MRIID: %d, error: %+v", mergeInfo.MRIID, err)
}
}()
// 1, check pipeline id
if ok, _, err = c.checkPipeline(projID, mergeInfo.MRIID, mergeInfo.NoteID, mergeInfo.PipelineID, model.QueryID); err != nil || !ok {
return
}
// 2, check pipeline status
if ok, _, err = c.checkPipeline(projID, mergeInfo.MRIID, mergeInfo.NoteID, 0, model.QuerySuccess); err != nil || !ok {
return
}
// 3, check merge status
if canMerge, err = c.checkMergeStatus(projID, mergeInfo.MRIID, mergeInfo.NoteID); err != nil || !canMerge {
return
}
log.Info("HookPipeline acceptMerge ... MRIID: %d", mergeInfo.MRIID)
if ok, err = c.acceptMerge(mergeInfo); err != nil || !ok {
return
}
return
}
// HookMerge ...
func (c *Command) HookMerge(projID int, branch string, mergeInfo *model.MergeInfo) (err error) {
var (
ok bool
canMerge bool
)
defer func() {
if x := recover(); x != nil {
log.Error("HookMerge: %+v %s", x, debug.Stack())
}
}()
defer func() {
if err = c.resetMergeStatus(projID, mergeInfo.MRIID, branch, true); err != nil {
log.Error("resetMergeStatus MRIID: %d, error: %+v", mergeInfo.MRIID, err)
}
}()
log.Info("HookMerge projID: %d, MRIID: %d, branch: %s", projID, mergeInfo.MRIID, branch)
if canMerge, err = c.checkMergeStatus(projID, mergeInfo.MRIID, mergeInfo.NoteID); err != nil || !canMerge {
return
}
log.Info("HookMerge acceptMerge ... MRIID: %d", mergeInfo.MRIID)
if ok, err = c.acceptMerge(mergeInfo); err != nil || !ok {
return
}
return
}
// HookDelayMerge ...
func (c *Command) HookDelayMerge(projID int, branch string, mergeInfo *model.MergeInfo) (err error) {
var (
ctx = context.TODO()
ok bool
noteID = mergeInfo.NoteID
mrIID = mergeInfo.MRIID
pipelineID int
status string
)
defer func() {
if x := recover(); x != nil {
log.Error("HookDelayMerge: %+v %s", x, debug.Stack())
}
}()
//if ok, pipelineID, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QuerySuccessRmNote); err != nil {
//return
//}
if pipelineID, status, err = c.gitlab.MRPipelineStatus(projID, mrIID); err != nil {
return
}
if status == model.PipelineSuccess || status == model.PipelineSkipped {
ok = true
} else if status != model.PipelineRunning && status != model.PipelinePending {
comment := fmt.Sprintf("<pre>警告pipeline状态异常请确保pipeline状态正常后再执行merge操作</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
log.Info("HookDelayMerge projID: %d, MRIID: %d, branch: %s, pipeline status: %t", projID, mergeInfo.MRIID, branch, ok)
if ok {
if err = c.HookMerge(projID, branch, mergeInfo); err != nil {
return
}
} else {
mergeInfo.PipelineID = pipelineID
if err = c.dao.SetMergeInfo(ctx, projID, branch, mergeInfo); err != nil {
return
}
}
return
}
func (c *Command) acceptMerge(mergeInfo *model.MergeInfo) (ok bool, err error) {
var (
comment string
author string
canMerge bool
state string
authorID = mergeInfo.AuthorID
username = mergeInfo.UserName
projID = mergeInfo.ProjID
mrIID = mergeInfo.MRIID
url = mergeInfo.URL
sourceBranch = mergeInfo.SourceBranch
targetBranch = mergeInfo.TargetBranch
noteID = mergeInfo.NoteID
content = mergeInfo.Title
)
if author, err = c.gitlab.UserName(authorID); err != nil {
return
}
if canMerge, err = c.checkMergeStatus(projID, mrIID, noteID); err != nil {
return
}
if !canMerge {
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment)
return
}
if len(mergeInfo.Description) > 0 {
content = content + "\n\n" + mergeInfo.Description
}
mergeMSG := fmt.Sprintf("Merge branch [%s] into [%s] by [%s]\n%s", sourceBranch, targetBranch, username, content)
if state, err = c.gitlab.AcceptMR(projID, mrIID, mergeMSG); err != nil || state != model.MRStateMerged {
if err != nil {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败当前状态不允许合并请查看上方merge按钮旁的提示</pre>", username)
} else {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,请检查当前状态或同步目标分支代码后再试!</pre>", username)
}
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
ok = true
comment = fmt.Sprintf("<pre>[%s]尝试合并成功!</pre>", username)
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
func (c *Command) resetMergeStatus(projID int, MRIID int, branch string, taskRunning bool) (err error) {
var (
ctx = context.TODO()
)
log.Info("resetMergeStatus projID: %d, MRIID: %d start", projID, MRIID)
if err = c.dao.UnLock(ctx, fmt.Sprintf(model.SagaRepoLockKey, projID)); err != nil {
log.Error("UnLock error: %+v", err)
}
if err = c.dao.DeleteMergeInfo(ctx, projID, branch); err != nil {
log.Error("DeleteMergeInfo error: %+v", err)
}
if err = c.dao.DeleteMRIID(ctx, MRIID); err != nil {
log.Error("Delete MRIID :%d, error: %+v", MRIID, err)
}
if taskRunning {
if err = c.DeleteRunningTask(projID, MRIID); err != nil {
log.Error("DeleteRunningTask: %+v", err)
}
}
log.Info("resetMergeStatus projID: %d, MRIID: %d end!", projID, MRIID)
return
}
// DeleteRunningTask ...
func (c *Command) DeleteRunningTask(projID int, mrID int) (err error) {
var (
ctx = context.TODO()
taskInfos []*model.TaskInfo
)
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusRunning); err != nil {
return
}
for _, taskInfo := range taskInfos {
pID := int(taskInfo.Event.MergeRequest.SourceProjectID)
mID := int(taskInfo.Event.MergeRequest.IID)
if pID == projID && mID == mrID {
// 从正在运行的任务列队中移除
err = c.dao.DeleteMergeTask(ctx, model.TaskStatusRunning, taskInfo)
return
}
}
return
}
func (c *Command) checkMergeStatus(projID int, mrIID int, noteID int) (canMerge bool, err error) {
var (
wip bool
state string
status string
comment string
)
if wip, state, status, err = c.gitlab.MergeStatus(projID, mrIID); err != nil {
return
}
if wip {
comment = "<pre>SAGA 尝试合并失败当前MR是一项正在进行的工作若已完成请先点击“Resolve WIP status”按钮处理后再+merge</pre>"
} else if state != model.MergeStateOpened {
comment = "<pre>SAGA 尝试合并失败当前MR已经关闭或者已经合并</pre>"
} else if status != model.MergeStatusOk {
comment = "<pre>SAGA 尝试合并失败,请先解决合并冲突!</pre>"
} else {
canMerge = true
}
if len(comment) > 0 {
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
}
return
}
// checkLabels ios or android need checkout label when release app stage
func (c *Command) checkLabels(projID int, mrIID int, noteID int, repo *model.Repo) (ok bool, err error) {
var (
labels []string
comment = fmt.Sprintf("<pre>警告SAGA 无法执行+merge发版阶段只允许合入指定label的MR</pre>")
)
if len(repo.Config.AllowLabel) <= 0 {
ok = true
return
}
if labels, err = c.gitlab.MergeLabels(projID, mrIID); err != nil {
return
}
log.Info("checkMrLabels MRIID: %d, labels: %+v", mrIID, labels)
for _, label := range labels {
if label == repo.Config.AllowLabel {
ok = true
return
}
}
if err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment); err != nil {
return
}
return
}
func (c *Command) checkPipeline(projID int, mrIID int, noteID int, lastPipelineID int, queryStatus model.QueryStatus) (ok bool, pipelineID int, err error) {
var status string
if pipelineID, status, err = c.gitlab.MRPipelineStatus(projID, mrIID); err != nil {
return
}
log.Info("checkPipeline MRIID: %d, queryStatus: %d, pipeline status: %s", mrIID, queryStatus, status)
// query pipeline id index
if queryStatus == model.QueryID {
if pipelineID > lastPipelineID {
comment := fmt.Sprintf("<pre>警告SAGA 检测到重新提交代码了,+merge中断请重新review代码</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
ok = true
return
}
// query process status
if queryStatus == model.QueryProcessing {
if status == model.PipelineRunning || status == model.PipelinePending {
comment := fmt.Sprintf("<pre>警告pipeline正在运行中暂不能立即merge待pipeline运行通过后会自动执行merge操作</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
ok = true
return
} else if status == model.PipelineSuccess || status == model.PipelineSkipped {
ok = true
return
}
comment := fmt.Sprintf("<pre>警告pipeline状态异常请确保pipeline状态正常后再执行merge操作</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
// query success status
if queryStatus == model.QuerySuccess {
if status != model.PipelineSuccess {
comment := fmt.Sprintf("<pre>警告SAGA 无法执行+mergepipeline还未成功请大佬先让pipeline执行通过</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
ok = false
return
}
}
ok = true
return
}
// showMRQueueInfo ...
func (c *Command) showMRQueueInfo(ctx context.Context, taskInfo *model.TaskInfo) (err error) {
var (
mrIID = int(taskInfo.Event.MergeRequest.IID)
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
noteID = taskInfo.NoteID
taskInfos []*model.TaskInfo
comment string
waitNum int
runningNum int
)
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusWaiting); err != nil {
return
}
for _, waitTaskInfo := range taskInfos {
if waitTaskInfo.Event.ProjectID == taskInfo.Event.ProjectID {
waitNum++
}
}
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusRunning); err != nil {
return
}
for _, runningTaskInfo := range taskInfos {
if runningTaskInfo.Event.ProjectID == taskInfo.Event.ProjectID {
runningNum++
}
}
if waitNum > 0 {
comment = fmt.Sprintf("<pre>SAGA 提示:当前还有 [%d] 个 MR 等待合并,请大佬耐心等待!</pre>", waitNum)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
} else if runningNum > 0 {
comment = fmt.Sprintf("<pre>SAGA 提示:当前还有 [%d] 个 MR 正在执行,请大佬耐心等待!</pre>", runningNum)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
}
return
}

View File

@@ -0,0 +1,91 @@
package command
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
)
func (c *Command) runPlusOne(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error) {
var (
author string
url = event.ObjectAttributes.URL
commit = event.MergeRequest.LastCommit.ID
reviewer = event.User.UserName
authorID = int(event.MergeRequest.AuthorID)
sourceBranch = event.MergeRequest.SourceBranch
targetBranch = event.MergeRequest.TargetBranch
wip = event.MergeRequest.WorkInProgress
)
log.Info("runPlusOne start ...")
if wip {
c.gitlab.CreateMRNote(event.Project.ID, int(event.MergeRequest.IID), fmt.Sprintf("<pre>警告当前MR处于WIP状态请待开发结束后再review</pre>"))
return
}
if author, err = c.gitlab.UserName(authorID); err != nil {
log.Error("%+v", err)
return
}
log.Info("runPlusOne notification author: %s", author)
if author != "" {
go func() {
notification.MailPlusOne(author, reviewer, url, commit, sourceBranch, targetBranch)
}()
go func() {
notification.WechatPlusOne(c.dao, author, reviewer, url, commit, sourceBranch, targetBranch)
}()
}
return
}
func reviewedOwner(owners []string, reviewedUsers []string, username string) (isowner bool, reviewed bool) {
for _, owner := range owners {
if strings.EqualFold(owner, username) || strings.EqualFold(owner, "all") {
return true, true
}
}
for _, owner := range owners {
for _, user := range reviewedUsers {
if strings.EqualFold(user, owner) {
return false, true
}
}
}
return false, false
}
func (c *Command) reviewedUsers(projID int, mrIID int) (reviewedUsers []string, err error) {
var awardUsers []string
if reviewedUsers, err = c.gitlab.PlusUsernames(projID, mrIID); err != nil {
return
}
if awardUsers, err = c.gitlab.AwardEmojiUsernames(projID, mrIID); err != nil {
return
}
OUTER:
for _, au := range awardUsers {
for _, mu := range reviewedUsers {
if au == mu {
continue OUTER
}
}
reviewedUsers = append(reviewedUsers, au)
}
return
}
func reviewedNum(reviewers []string, reviewedUsers []string) (num int) {
for _, reviewer := range reviewers {
for _, user := range reviewedUsers {
if strings.EqualFold(user, reviewer) || strings.EqualFold(reviewer, "all") {
num++
}
}
}
return
}

View File

@@ -0,0 +1,53 @@
package command
import (
"fmt"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestCommandReviewedOwner(t *testing.T) {
convey.Convey("ownerReviewed", t, func(ctx convey.C) {
var (
owners = []string{"a", "b"}
reviewedUsers = []string{"a"}
username = "d"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
isowner, reviewed := reviewedOwner(owners, reviewedUsers, username)
fmt.Println(isowner, reviewed)
ctx.Convey("Then isowner,reviewed should not be nil.", func(ctx convey.C) {
ctx.So(reviewed, convey.ShouldBeTrue)
ctx.So(isowner, convey.ShouldBeFalse)
})
})
})
}
func TestCommandReviewedNum(t *testing.T) {
convey.Convey("reviewedNum", t, func(ctx convey.C) {
var (
reviewers = []string{"a", "b"}
reviewedUsers = []string{"c"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
num := reviewedNum(reviewers, reviewedUsers)
ctx.Convey("Then num should not be nil.", func(ctx convey.C) {
ctx.So(num, convey.ShouldEqual, 0)
})
})
})
convey.Convey("reviewedNum", t, func(ctx convey.C) {
var (
reviewers = []string{"a", "b"}
reviewedUsers = []string{"a"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
num := reviewedNum(reviewers, reviewedUsers)
ctx.Convey("Then num should not be nil.", func(ctx convey.C) {
ctx.So(num, convey.ShouldEqual, 1)
})
})
})
}

View File

@@ -0,0 +1,43 @@
package service
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
)
// HandleGitlabComment handle comment webhook
func (s *Service) HandleGitlabComment(c context.Context, event *model.HookComment) (err error) {
var (
comment = strings.TrimSpace(event.ObjectAttributes.Note)
projName = event.MergeRequest.Source.Name
targetBranch = event.MergeRequest.TargetBranch
gitRepo *model.Repo
ok bool
)
if event.ObjectAttributes.NoteableType != model.HookCommentTypeMR {
log.Info("Comment hook noteableType [%s] ignore", event.ObjectAttributes.NoteableType)
return
}
if event.MergeRequest.State == model.MRStateMerged {
log.Info("Gitlab MR [%d] has been merged", event.MergeRequest.ID)
return
}
if gitRepo, ok = s.gitRepoMap[projName]; !ok {
log.Info("Gitlab MR (%d) unknown projName (%s)", event.MergeRequest.ID, projName)
return
}
if !s.validTargetBranch(targetBranch, gitRepo) {
log.Info("Target branch (%s) is not in white list, won't serve comment!", targetBranch)
return
}
fmt.Println("HandleGitlabComment:", event)
if err = s.cmd.Exec(c, comment, event, gitRepo); err != nil {
log.Error("Command Exec cmd: %s err: (%+v)", comment, err)
return
}
return
}

View File

@@ -0,0 +1,30 @@
package service
import (
"context"
"encoding/json"
"testing"
"go-common/app/tool/saga/model"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceHandleGitlabComment(t *testing.T) {
convey.Convey("HandleGitlabComment", t, func(ctx convey.C) {
var (
c = context.Background()
event = &model.HookComment{}
err error
s Service
)
err = json.Unmarshal(GitlabHookCommentTest, event)
convey.So(err, convey.ShouldBeNil)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := s.HandleGitlabComment(c, event)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,63 @@
package service
import (
"context"
"runtime/debug"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
)
// HandleBuildContributors ...
func (s *Service) HandleBuildContributors(c context.Context, repo *model.RepoInfo) (err error) {
if strings.TrimSpace(repo.Group) == "" {
err = errors.Errorf("repo Group is not valid")
return
}
if strings.TrimSpace(repo.Name) == "" {
err = errors.Errorf("repo Name is not valid")
return
}
if strings.TrimSpace(repo.Branch) == "" {
err = errors.Errorf("repo Branch is not valid")
return
}
go func() {
defer func() {
if x := recover(); x != nil {
log.Error("BuildContributor: %+v %s", x, debug.Stack())
}
}()
if err = s.cmd.BuildContributor(repo); err != nil {
log.Error("BuildContributor %+v", err)
}
}()
return
}
// BuildContributors ...
func (s *Service) BuildContributors(repos []*model.Repo) (err error) {
var (
repo *model.Repo
branch string
)
log.Info("BuildContributors start ...")
for _, repo = range repos {
for _, branch = range repo.Config.AuthBranches {
repoInfo := &model.RepoInfo{
Group: repo.Config.Group,
Name: repo.Config.Name,
Branch: branch,
}
log.Info("BuildContributors project [%s], group [%s], Name [%s], branch [%s]", repo.Config.URL, repo.Config.Group, repo.Config.Name, branch)
if err = s.HandleBuildContributors(context.TODO(), repoInfo); err != nil {
log.Error("BuildContributors err (%+v)", err)
}
}
}
return
}

View File

@@ -0,0 +1,22 @@
package service
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceHandleBuildContributors(t *testing.T) {
convey.Convey("HandleBuildContributors", t, func(ctx convey.C) {
var (
c = context.Background()
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := s.HandleBuildContributors(c, repoTest)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,45 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["gitlab.go"],
importpath = "go-common/app/tool/saga/service/gitlab",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/model:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/xanzy/go-gitlab:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["gitlab_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,545 @@
package gitlab
import (
"net/url"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
"github.com/xanzy/go-gitlab"
)
// Gitlab def
type Gitlab struct {
url string
token string
client *gitlab.Client
}
// New new gitlab structure
func New(url string, token string) (g *Gitlab) {
g = &Gitlab{
url: url,
token: token,
client: gitlab.NewClient(nil, token),
}
g.client.SetBaseURL(url)
return
}
// HostToken ...
func (g *Gitlab) HostToken() (host string, token string, err error) {
var u *url.URL
if u, err = url.Parse(g.url); err != nil {
return
}
host = u.Host
token = g.token
return
}
// LastGreenCommit get last green pipeline commit
func (g *Gitlab) LastGreenCommit(ciProjectID string, ciCommitRefName string) (commit string, err error) {
var (
status = gitlab.Success
pipelines gitlab.PipelineList
)
opt := &gitlab.ListProjectPipelinesOptions{
Status: &status,
Ref: gitlab.String(ciCommitRefName),
}
if pipelines, _, err = g.client.Pipelines.ListProjectPipelines(ciProjectID, opt); err != nil {
return
}
if len(pipelines) == 0 {
return
}
commit = pipelines[0].Sha
return
}
// AcceptMR accept Merge request
func (g *Gitlab) AcceptMR(projID int, mrIID int, msg string) (state string, err error) {
var (
opt = &gitlab.AcceptMergeRequestOptions{
ShouldRemoveSourceBranch: gitlab.Bool(true),
MergeCommitMessage: gitlab.String(msg),
}
mergeRequest *gitlab.MergeRequest
)
if mergeRequest, _, err = g.client.MergeRequests.AcceptMergeRequest(projID, mrIID, opt); err != nil {
err = errors.Wrapf(err, "AcceptMergeRequest(%d,%d)", projID, mrIID)
return
}
state = mergeRequest.State
return
}
// CloseMR close merge request
func (g *Gitlab) CloseMR(projID int, mrIID int) (err error) {
var (
opt = &gitlab.UpdateMergeRequestOptions{
StateEvent: gitlab.String("close"),
}
)
if _, _, err = g.client.MergeRequests.UpdateMergeRequest(projID, mrIID, opt); err != nil {
err = errors.Wrapf(err, "CloseMR(%d,%d)", projID, mrIID)
}
return
}
// CreateMRNote create note to merge request
func (g *Gitlab) CreateMRNote(projID int, mrIID int, content string) (noteID int, err error) {
var (
opt = &gitlab.CreateMergeRequestNoteOptions{
Body: gitlab.String(content),
}
note *gitlab.Note
)
if note, _, err = g.client.Notes.CreateMergeRequestNote(projID, mrIID, opt); err != nil {
err = errors.Wrapf(err, "CreateMergeRequestNote(%d,%d)", projID, mrIID)
return
}
noteID = note.ID
return
}
// UpdateMRNote update the specified merge request note
func (g *Gitlab) UpdateMRNote(projID int, mrIID int, noteID int, content string) (err error) {
var (
opt = &gitlab.UpdateMergeRequestNoteOptions{
Body: gitlab.String(content),
}
)
if _, _, err = g.client.Notes.UpdateMergeRequestNote(projID, mrIID, noteID, opt); err != nil {
err = errors.Wrapf(err, "UpdateMergeRequestNote(%d,%d,%d)", projID, mrIID, noteID)
return
}
return
}
// DeleteMRNote delete the specified note for merge request
func (g *Gitlab) DeleteMRNote(projID int, mrIID int, noteID int) (err error) {
if _, err = g.client.Notes.DeleteMergeRequestNote(projID, mrIID, noteID); err != nil {
err = errors.Wrapf(err, "DeleteMergeRequestNote(%d,%d,%d)", projID, mrIID, noteID)
}
return
}
// UserName get user name for user id
func (g *Gitlab) UserName(userID int) (userName string, err error) {
var (
user *gitlab.User
)
if user, _, err = g.client.Users.GetUser(userID); err != nil {
err = errors.WithStack(err)
return
}
userName = user.Username
return
}
// Triggers get pipeline triggers
func (g *Gitlab) Triggers(projectID int) (triggers []*gitlab.PipelineTrigger, err error) {
var (
opt = &gitlab.ListPipelineTriggersOptions{}
)
if triggers, _, err = g.client.PipelineTriggers.ListPipelineTriggers(projectID, opt); err != nil {
err = errors.Wrapf(err, "ListPipelineTriggers (projectID: %d)", projectID)
return nil, err
}
return triggers, nil
}
// CreateTrigger create the trigger to trigger pipeline
func (g *Gitlab) CreateTrigger(projectID int) (trigger *gitlab.PipelineTrigger, err error) {
var (
opt = &gitlab.AddPipelineTriggerOptions{
Description: gitlab.String("gitlab-ci-build-on-merge-request"),
}
)
if trigger, _, err = g.client.PipelineTriggers.AddPipelineTrigger(projectID, opt); err != nil {
err = errors.Wrapf(err, "CreateTrigger (projectID: %d)", projectID)
return nil, err
}
return trigger, nil
}
// TakeOwnership take ownership of the trigger
func (g *Gitlab) TakeOwnership(projectID int, triggerID int) (trigger *gitlab.PipelineTrigger, err error) {
if trigger, _, err = g.client.PipelineTriggers.TakeOwnershipOfPipelineTrigger(projectID, triggerID); err != nil {
err = errors.Wrapf(err, "TakeOwnershipOfPipelineTrigger (projectID: %d, triggerID: %d)", projectID, triggerID)
return nil, err
}
return trigger, nil
}
// TriggerPipeline trigger the pipeline
func (g *Gitlab) TriggerPipeline(projectID int, ref string, token string) (pipeline *gitlab.Pipeline, err error) {
var (
opt = &gitlab.RunPipelineTriggerOptions{
Ref: gitlab.String(ref),
Token: gitlab.String(token),
}
)
if pipeline, _, err = g.client.PipelineTriggers.RunPipelineTrigger(projectID, opt); err != nil {
err = errors.Wrapf(err, "RunPipelineTrigger (projectID: %d, ref: %s, token: %s)", projectID, ref, token)
return nil, err
}
return pipeline, nil
}
// AwardEmojiUsernames get the username who gave the emoji
func (g *Gitlab) AwardEmojiUsernames(projID int, mrIID int) (usernames []string, err error) {
var (
opt = &gitlab.ListAwardEmojiOptions{
Page: 1,
PerPage: 100,
}
awards []*gitlab.AwardEmoji
)
if awards, _, err = g.client.AwardEmoji.ListMergeRequestAwardEmoji(projID, mrIID, opt); err != nil {
return
}
for _, a := range awards {
if a.Name == "thumbsup" {
usernames = append(usernames, a.User.Username)
}
}
return
}
// AuditProjects audit all the given projects
func (g *Gitlab) AuditProjects(repos []*model.Repo, hooks []*model.WebHook) (err error) {
var (
repo *model.Repo
projID int
)
log.Info("AuditProjects start")
for _, repo = range repos {
log.Info("AuditProjects project [%s]", repo.Config.URL)
if projID, err = g.ProjectID(repo.Config.URL); err != nil {
log.Error("Failed to get project ID [%s], err: %+v", repo.Config.URL, err)
continue
}
if err = g.AuditProjectHooks(projID, hooks); err != nil {
log.Error("Failed to get hooks [%s], err: %+v", repo.Config.URL, err)
continue
}
log.Info("AuditProjects project [%s]", repo.Config.URL)
}
log.Info("AuditProjects end")
return
}
// AuditProjectHooks check and add or edit the project webhooks
func (g *Gitlab) AuditProjectHooks(projID int, hooks []*model.WebHook) (err error) {
var (
webHooks []*gitlab.ProjectHook
h *model.WebHook
)
if webHooks, err = g.ListProjectHook(projID); err != nil {
return
}
// 配置文件中的webhook是否配置在项目中
inWebHooks := func(h *model.WebHook) (in bool, ph *gitlab.ProjectHook) {
for _, ph = range webHooks {
if h.URL == ph.URL {
in = true
return
}
}
in = false
return
}
// 找到的webhook是否和配置文件中webhook配置相等
equalHook := func(h *model.WebHook, ph *gitlab.ProjectHook) (equal bool) {
if h.PushEvents == ph.PushEvents && h.IssuesEvents == ph.IssuesEvents &&
h.ConfidentialIssuesEvents == ph.ConfidentialIssuesEvents && h.MergeRequestsEvents == ph.MergeRequestsEvents &&
h.TagPushEvents == ph.TagPushEvents && h.NoteEvents == ph.NoteEvents &&
h.JobEvents == ph.JobEvents && h.PipelineEvents == ph.PipelineEvents &&
h.WikiPageEvents == ph.WikiPageEvents && !ph.EnableSSLVerification {
return true
}
return false
}
// 配置的webhook与查询出来的对比如果存在则比较属性属性不同则修改不存在则创建
for _, h = range hooks {
if in, ph := inWebHooks(h); in {
if !equalHook(h, ph) {
if err = g.EditProjectHook(projID, ph.ID, h); err != nil {
return
}
}
} else {
if err = g.AddProjectHook(projID, h); err != nil {
return
}
}
}
return
}
// AddProjectHook add project hook for the project
func (g *Gitlab) AddProjectHook(projID int, hook *model.WebHook) (err error) {
var (
opt = &gitlab.AddProjectHookOptions{
URL: &hook.URL,
PushEvents: &hook.PushEvents,
IssuesEvents: &hook.IssuesEvents,
ConfidentialIssuesEvents: &hook.ConfidentialIssuesEvents,
MergeRequestsEvents: &hook.MergeRequestsEvents,
TagPushEvents: &hook.TagPushEvents,
NoteEvents: &hook.NoteEvents,
JobEvents: &hook.JobEvents,
PipelineEvents: &hook.PipelineEvents,
WikiPageEvents: &hook.WikiPageEvents,
EnableSSLVerification: gitlab.Bool(false),
}
)
if _, _, err = g.client.Projects.AddProjectHook(projID, opt); err != nil {
return
}
return
}
// ListProjectHook list all the webhook for the project
func (g *Gitlab) ListProjectHook(projID int) (hooks []*gitlab.ProjectHook, err error) {
var (
opt = &gitlab.ListProjectHooksOptions{
Page: 1,
PerPage: 100,
}
)
if hooks, _, err = g.client.Projects.ListProjectHooks(projID, opt); err != nil {
return
}
return
}
// EditProjectHook edit the specified webhook for the project
func (g *Gitlab) EditProjectHook(projID int, hookID int, hook *model.WebHook) (err error) {
var (
opt = &gitlab.EditProjectHookOptions{
URL: &hook.URL,
PushEvents: &hook.PushEvents,
IssuesEvents: &hook.IssuesEvents,
ConfidentialIssuesEvents: &hook.ConfidentialIssuesEvents,
MergeRequestsEvents: &hook.MergeRequestsEvents,
TagPushEvents: &hook.TagPushEvents,
NoteEvents: &hook.NoteEvents,
JobEvents: &hook.JobEvents,
PipelineEvents: &hook.PipelineEvents,
WikiPageEvents: &hook.WikiPageEvents,
EnableSSLVerification: gitlab.Bool(false),
}
)
if _, _, err = g.client.Projects.EditProjectHook(projID, hookID, opt); err != nil {
return
}
return
}
// DeletePojectHook delete the specified hook for the project
func (g *Gitlab) DeletePojectHook(projID int, hookID int) (err error) {
if _, err = g.client.Projects.DeleteProjectHook(projID, hookID); err != nil {
return
}
return
}
// ProjectID get project id by project URL-encoded name
func (g *Gitlab) ProjectID(url string) (projID int, err error) {
var (
projectName = url[strings.LastIndex(url, ":")+1 : strings.LastIndex(url, ".git")]
project *gitlab.Project
)
if project, _, err = g.client.Projects.GetProject(projectName); err != nil {
return
}
projID = project.ID
return
}
// MRPipelineStatus query PipelineState for mr
func (g *Gitlab) MRPipelineStatus(projID int, mrIID int) (id int, status string, err error) {
var pipelineList gitlab.PipelineList
if pipelineList, _, err = g.client.MergeRequests.ListMergeRequestPipelines(projID, mrIID); err != nil {
return
}
if len(pipelineList) == 0 {
status = model.PipelineSuccess
return
}
id = pipelineList[0].ID
status = pipelineList[0].Status
return
}
// LastPipeLineState query Last PipeLineState
func (g *Gitlab) LastPipeLineState(projID int, ciCommitRefName string) (state bool, err error) {
var pipelines gitlab.PipelineList
opt := &gitlab.ListProjectPipelinesOptions{
Ref: gitlab.String(ciCommitRefName),
}
state = true
if pipelines, _, err = g.client.Pipelines.ListProjectPipelines(projID, opt); err != nil {
return
}
if len(pipelines) < 2 {
return
}
state = pipelines[1].Status == model.PipelineSuccess
return
}
// MergeStatus query MergeStatus
func (g *Gitlab) MergeStatus(projID int, mrIID int) (wip bool, state string, status string, err error) {
var mergerequest *gitlab.MergeRequest
if mergerequest, _, err = g.client.MergeRequests.GetMergeRequest(projID, mrIID); err != nil {
return
}
state = mergerequest.State
status = mergerequest.MergeStatus
wip = mergerequest.WorkInProgress
return
}
// MergeLabels get Merge request labels
func (g *Gitlab) MergeLabels(projID int, mrIID int) (labels []string, err error) {
var mergerequest *gitlab.MergeRequest
if mergerequest, _, err = g.client.MergeRequests.GetMergeRequest(projID, mrIID); err != nil {
return
}
labels = mergerequest.Labels
return
}
// PlusUsernames get the username who +1
func (g *Gitlab) PlusUsernames(projID int, mrIID int) (usernames []string, err error) {
var (
opt = &gitlab.ListMergeRequestNotesOptions{
Page: 1,
PerPage: 100,
}
notes []*gitlab.Note
exist bool
)
if notes, _, err = g.client.Notes.ListMergeRequestNotes(projID, mrIID, opt); err != nil {
return
}
for _, note := range notes {
if (strings.TrimSpace(note.Body) == model.SagaCommandPlusOne) || (strings.TrimSpace(note.Body) == model.SagaCommandPlusOne1) {
exist = false
for _, user := range usernames {
if user == note.Author.Username {
exist = true
break
}
}
if !exist {
usernames = append(usernames, note.Author.Username)
}
}
}
return
}
// CompareDiff ...
func (g *Gitlab) CompareDiff(projID int, from string, to string) (files []string, err error) {
var (
opt = &gitlab.CompareOptions{
From: &from,
To: &to,
}
compare *gitlab.Compare
)
if compare, _, err = g.client.Repositories.Compare(projID, opt); err != nil {
return
}
for _, diff := range compare.Diffs {
if diff.NewFile {
files = append(files, diff.NewPath)
} else {
files = append(files, diff.OldPath)
}
}
return
}
// CommitDiff ...
func (g *Gitlab) CommitDiff(projID int, sha string) (files []string, err error) {
var (
diffs []*gitlab.Diff
opt = &gitlab.GetCommitDiffOptions{
Page: 1,
PerPage: 100,
}
)
if diffs, _, err = g.client.Commits.GetCommitDiff(projID, sha, opt); err != nil {
return
}
for _, diff := range diffs {
if diff.NewFile {
files = append(files, diff.NewPath)
} else {
files = append(files, diff.OldPath)
}
}
return
}
// MRChanges ...
func (g *Gitlab) MRChanges(projID, mrIID int) (changeFiles []string, deleteFiles []string, err error) {
var (
mr *gitlab.MergeRequest
)
if mr, _, err = g.client.MergeRequests.GetMergeRequestChanges(projID, mrIID); err != nil {
return
}
for _, c := range mr.Changes {
if c.RenamedFile {
deleteFiles = append(deleteFiles, c.OldPath)
changeFiles = append(changeFiles, c.NewPath)
} else if c.DeletedFile {
deleteFiles = append(deleteFiles, c.OldPath)
} else if c.NewFile {
changeFiles = append(changeFiles, c.NewPath)
} else {
changeFiles = append(changeFiles, c.OldPath)
}
}
return
}
// RepoRawFile ...
func (g *Gitlab) RepoRawFile(projID int, branch string, filename string) (content []byte, err error) {
var (
opt = &gitlab.GetRawFileOptions{
Ref: &branch,
}
)
if content, _, err = g.client.RepositoryFiles.GetRawFile(projID, filename, opt); err != nil {
return
}
return
}

View File

@@ -0,0 +1,141 @@
package gitlab
import (
"flag"
"testing"
"go-common/app/tool/saga/conf"
. "github.com/smartystreets/goconvey/convey"
)
var (
g *Gitlab
)
func init() {
var (
err error
)
g = New("http://gitlab.bilibili.co/api/v4", "z3nN4s4BVX5oNYXKbEPL")
flag.Set("conf", "/Users/bilibili/go/src/go-common/app/tool/saga/cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
}
// Test Function HostToken
func TestHostToken(t *testing.T) {
Convey("Test HostToken", t, func() {
var (
host string
token string
err error
)
host, token, err = g.HostToken()
So(err, ShouldBeNil)
So(host, ShouldEqual, "gitlab.bilibili.co")
So(token, ShouldEqual, "z3nN4s4BVX5oNYXKbEPL")
})
}
// TODO AutoPush
// Test MR Create/Close
// Test MRNote Create/Update/Delete
//func TestCreateMRNote(t *testing.T) {
// Convey("Test CreateMRNote", t, func() {
// var (
// err error
// noteID int
//
// mr *gitlab.MergeRequest
// //res *gitlab.Response
//
// // MR CreateMergeRequestOptions Info
// title = "test"
// description = "test"
// sourceBranch = "chy-saga-test"
// targetBranch = "saga-test-1"
// assigneeID = 15
// projectID = 23
//
// //MR Info
// state string
// status string
// )
//
// // Create CreateMergeRequestOptions Instance
// opt := new(gitlab.CreateMergeRequestOptions)
// opt.Title = &title
// opt.Description = &description
// opt.SourceBranch = &sourceBranch
// opt.TargetBranch = &targetBranch
// opt.AssigneeID = &assigneeID
// opt.TargetProjectID = &projectID
//
// // Create MergeRequest
// mr, _, err = g.client.MergeRequests.CreateMergeRequest(35, opt)
// So(err, ShouldBeNil)
//
// // Query MR Status
// state = mr.State
// status = mr.MergeStatus
// So(state, ShouldEqual, "opened")
// So(status, ShouldEqual, "cannot_be_merged")
//
// // Create MRNote
// noteID, err = g.CreateMRNote(projectID, mr.IID, "CreateMRNote: PASS!")
// So(err, ShouldBeNil)
// So(noteID, ShouldNotBeNil)
//
// // Update MRNote
// err = g.UpdateMRNote(projectID, mr.IID, noteID, "CreateMRNote: PASS!\nUpdateMRNote: PASS!")
// So(err, ShouldBeNil)
//
// // Delete MRNote
// err = g.DeleteMRNote(projectID, mr.IID, noteID)
// So(err, ShouldBeNil)
//
// // Accept MR
// err = g.AcceptMR(projectID, mr.IID, "Accept MR: PASS!")
// So(err, ShouldBeNil)
//
// //Close MR
// //err = g.CloseMR(projectID, mr.IID)
// //So(err, ShouldBeNil)
//
// // Report
// noteID, err = g.CreateMRNote(projectID, mr.IID, "- MR Create Successfully!\n"+
// "- MRNote Create Successfully\n"+
// "- MRNote Update Successfully\n"+
// "- MRNote Delete Successfully\n"+
// "- MR Accept Successfully\n"+
// "- MR Close Successfully")
// So(err, ShouldBeNil)
// })
//}
func TestProjectID(t *testing.T) {
Convey("Test ProjectID", t, func() {
var (
projID int
err error
)
projID, err = g.ProjectID("git@gitlab.bilibili.co:platform/go-common.git")
So(err, ShouldBeNil)
So(projID, ShouldEqual, 23)
})
}
func TestCommitDiff(t *testing.T) {
Convey("Test CommitDiff", t, func() {
var (
err error
files []string
)
files, err = g.CommitDiff(23, "f5c9bfa037771b7f8179db7a245a25695c90be9b")
So(err, ShouldBeNil)
So(files[0], ShouldEqual, ".gitlab-ci.yml")
})
}

View File

@@ -0,0 +1,50 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"mail.go",
"tpl.go",
],
importpath = "go-common/app/tool/saga/service/mail",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/gopkg.in/gomail.v2:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["mail_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,111 @@
package mail
import (
"bytes"
"fmt"
"text/template"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
gomail "gopkg.in/gomail.v2"
)
// SendMail2 ...
func SendMail2(addr *model.MailAddress, subject string, data string) (err error) {
var (
msg = gomail.NewMessage()
)
msg.SetAddressHeader("From", conf.Conf.Property.Mail.Address, conf.Conf.Property.Mail.Name)
msg.SetHeader("To", msg.FormatAddress(addr.Address, addr.Name))
msg.SetHeader("Subject", subject)
msg.SetBody("text/plain", data)
d := gomail.NewDialer(
conf.Conf.Property.Mail.Host,
conf.Conf.Property.Mail.Port,
conf.Conf.Property.Mail.Address,
conf.Conf.Property.Mail.Pwd,
)
if err = d.DialAndSend(msg); err != nil {
err = errors.WithMessage(err, fmt.Sprintf("Send mail (%+v,%s,%s) failed", addr, subject, data))
return
}
log.Info("Send mail (%+v,%s,%s) success", addr, subject, data)
return
}
// SendMail send mail
func SendMail(m *model.Mail, data *model.MailData) (err error) {
var (
toUsers []string
msg = gomail.NewMessage()
buf = &bytes.Buffer{}
)
msg.SetAddressHeader("From", conf.Conf.Property.Mail.Address, conf.Conf.Property.Mail.Name) // 发件人
for _, ads := range m.ToAddress {
toUsers = append(toUsers, msg.FormatAddress(ads.Address, ads.Name))
}
t := template.New("MR Mail")
if t, err = t.Parse(mailTPL); err != nil {
log.Error("tpl.Parse(%s) error(%+v)", mailTPL, errors.WithStack(err))
return
}
err = t.Execute(buf, data)
if err != nil {
log.Error("t.Execute error(%+v)", errors.WithStack(err))
return
}
msg.SetHeader("To", toUsers...)
msg.SetHeader("Subject", m.Subject) // 主题
msg.SetBody("text/html", buf.String()) // 正文
d := gomail.NewDialer(
conf.Conf.Property.Mail.Host,
conf.Conf.Property.Mail.Port,
conf.Conf.Property.Mail.Address,
conf.Conf.Property.Mail.Pwd,
)
if err = d.DialAndSend(msg); err != nil {
log.Error("Send mail Fail(%v) diff(%s)", msg, err)
return
}
return
}
// SendMail3 SendMail all parameter
func SendMail3(from string, sender string, senderPwd string, m *model.Mail, data *model.MailData) (err error) {
var (
toUsers []string
msg = gomail.NewMessage()
buf = &bytes.Buffer{}
)
msg.SetAddressHeader("From", from, sender) // 发件人
for _, ads := range m.ToAddress {
toUsers = append(toUsers, msg.FormatAddress(ads.Address, ads.Name))
}
t := template.New("MR Mail")
if t, err = t.Parse(mailTPL3); err != nil {
log.Error("tpl.Parse(%s) error(%+v)", mailTPL3, errors.WithStack(err))
return
}
err = t.Execute(buf, data)
if err != nil {
log.Error("t.Execute error(%+v)", errors.WithStack(err))
return
}
msg.SetHeader("To", toUsers...)
msg.SetHeader("Subject", m.Subject) // 主题
msg.SetBody("text/html", buf.String()) // 正文
d := gomail.NewDialer(
"smtp.exmail.qq.com",
465,
from,
senderPwd,
)
if err = d.DialAndSend(msg); err != nil {
log.Error("Send mail Fail(%v) diff(%s)", msg, err)
return
}
return
}

View File

@@ -0,0 +1,46 @@
package mail
import (
"flag"
"fmt"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
. "github.com/smartystreets/goconvey/convey"
)
func init() {
var err error
flag.Set("conf", "../../cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
}
// go test -test.v -test.run TestMail
func TestMail(t *testing.T) {
Convey("Test mail", t, func() {
m := &model.Mail{
ToAddress: []*model.MailAddress{{Name: "baihai", Address: "changhengyuan@bilibili.com"},
{Name: "muyan", Address: "changhengyuan@bilibili.com"}},
Subject: fmt.Sprintf("【Sage 提醒】%s项目发生Merge Request事件", "test-mail"),
}
mergeOut := " Merge made by the 'recursive' strategy.\n" +
"tools/saga/CHANGELOG.md | 4 ++++\n" +
"business/interface/app-show/service/rank/rank.go | 28 +++++++++++------------\n" +
"business/interface/app-show/service/show/cache.go | 6 ++---\n" +
"3 files changed, 21 insertions(+), 17 deletions(-)"
err := SendMail(m, &model.MailData{
UserName: "baihai",
SourceBranch: "featre_answer",
TargetBranch: "master",
Title: "修改变量A",
Description: "内容就是",
URL: "http://www.baidu.com",
Info: mergeOut,
})
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,130 @@
package mail
var (
mailTPL = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>saga page</title>
</head>
<body>
<form action="" method="post" class="basic-grey" style="margin-left:auto;
margin-right:auto;
max-width: 500px;
background: #F7F7F7;
padding: 25px 15px 25px 10px;
font: 12px Georgia, 'Times New Roman', Times, serif;
color: #888;
text-shadow: 1px 1px 1px #FFF;
border:1px solid #E4E4E4;">
<h1 style="font-size: 25px;
padding: 0px 0px 10px 40px;
display: block;
border-bottom:1px solid #E4E4E4;
margin: -10px -15px 30px -10px;;
color: #888;">
Saga
<span style="display: block;font-size: 11px;">
Merge Request 事件通知</span>
</h1>
<label style="display: block;margin: 0px;">
<span style="float: left;
width:100%;
text-align: left;
padding-right: 10px;
padding-left: 30px;
margin-top: 10px;
color: #888;">
申请人 : {{.UserName}}
<br />
来源分支 : {{.SourceBranch}}
<br />
目标分支 : {{.TargetBranch}}
<br />
修改标题 : {{.Title}}
<br />
修改说明 : {{.Description}}
<br />
<a href="{{.URL}}">点击查看..</a>
<br />
<br />
<br />
<h3>额外信息: </h3>
{{.Info}}
<br />
<br />
<br />
</span>
</label>
</form>
</body>
</html>`
mailTPL3 = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>saga page</title>
</head>
<body>
<form action="" method="post" class="basic-grey" style="
margin-right:auto;
max-width: 500px;
font: 12px Georgia, 'Times New Roman', Times, serif;
color: #888;
text-shadow: 1px 1px 1px #FFF;">
<h1 style="font-size: 25px;
padding: 10px 0px 10px 40px;
display: block;
border-bottom:1px solid #E4E4E4;
margin: -10px -15px 5px -10px;;
color: #03A9F4;">
Saga
<span style="display: block;font-size: 11px;">
事件通知</span>
</h1>
</form>
<label style="display: block;margin: 0px;">
<span style="float: left;
width:100%;
text-align: left;
padding-right: 10px;
padding-left: 30px;
margin-top: 10px;
color: #888;">
执行状态 :
<font class="{{.PipelineStatus}}" >
{{.PipeStatus}}
</font>
<br />
Pipeline信息:
<font class="{{.PipelineStatus}}" >
<a href="{{.URL}}">{{.URL}}</a>
</font>
<br />
来源分支 : {{.SourceBranch}}
<br />
修改说明 : {{.Description}}
<br />
额外信息: {{.Info}}
<br />
<br />
<br />
</span>
</label>
</body>
<style type="text/css">
.failed {
color: #f21303;
}
.failed a{
color: #f21303;
}
.success {
color: #1aaa55;
}
.success a{
color: #1aaa55;
}
</style>
</html>`
)

155
app/tool/saga/service/mr.go Normal file
View File

@@ -0,0 +1,155 @@
package service
import (
"context"
"fmt"
"go-common/app/tool/saga/model"
"go-common/library/log"
)
// MergeRequest ...
func (s *Service) MergeRequest(c context.Context, event *model.HookMR) (err error) {
var (
ok bool
repo *model.Repo
url = event.Project.GitSSHURL
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
sourceBranch = event.ObjectAttributes.SourceBranch
targetBranch = event.ObjectAttributes.TargetBranch
wip = event.ObjectAttributes.WorkInProgress
)
log.Info("Hook MergeRequest state: %s, projID: %d, mrid: %d", event.ObjectAttributes.State, projID, mrIID)
if ok, repo = s.findRepo(url); !ok {
return
}
if event.ObjectAttributes.State == model.MRStateOpened {
if wip {
log.Info("MergeRequest mr is wip, project ID: [%d], branch: [%s]", event.Project.ID, sourceBranch)
return
}
if !s.validTargetBranch(targetBranch, repo) {
s.gitlab.CreateMRNote(projID, mrIID, fmt.Sprintf("<pre>警告:目标分支 %s 不在SAGA白名单中此MR不会触发SAGA行为</pre>", targetBranch))
return
}
authBranch := s.cmd.GetAuthBranch(targetBranch, repo.Config.AuthBranches)
if err = s.ShowMRRoleInfo(c, authBranch, repo, event); err != nil {
return
}
} else {
if event.ObjectAttributes.State == model.MRStateMerged {
log.Info("MergeRequest.UpdateContributor:%d,%s,%s,%+v", projID, sourceBranch, targetBranch, repo.Config.AuthBranches)
if err = s.cmd.UpdateContributor(projID, mrIID, sourceBranch, targetBranch, repo.Config.AuthBranches); err != nil {
return
}
}
if err = s.d.DeleteReportStatus(c, projID, mrIID); err != nil {
return
}
}
return
}
// ShowMRRoleInfo ...
func (s *Service) ShowMRRoleInfo(c context.Context, authBranch string, repo *model.Repo, event *model.HookMR) (err error) {
var (
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
result bool
)
if result, err = s.d.ReportStatus(c, projID, mrIID); err != nil {
return
}
log.Info("MergeRequest whether create note auth info: %v", result)
if !result {
if len(repo.Config.SuperAuthUsers) > 0 {
if err = s.ReportSuperRoleInfo(c, repo.Config.SuperAuthUsers, event); err != nil {
return
}
} else {
if err = s.ReportMRRoleInfo(c, authBranch, event); err != nil {
return
}
}
if err = s.d.SetReportStatus(c, projID, mrIID, true); err != nil {
return
}
}
return
}
// ReportMRRoleInfo ...
func (s *Service) ReportMRRoleInfo(c context.Context, authBranch string, event *model.HookMR) (err error) {
var (
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
pathOwners []model.RequireReviewFolder
)
if pathOwners, err = s.cmd.GetAllPathAuth(projID, mrIID, authBranch); err != nil {
return
}
authInfo := ""
authInfo = fmt.Sprintf("<pre>SAGA权限信息提示请review: %s</pre>", event.ObjectAttributes.Title)
authInfo += "\n"
for _, os := range pathOwners {
if len(os.Owners) > 0 {
//authInfo += "----- PATH: " + os.Folder + "OWNER: "
authInfo += fmt.Sprintf("+ PATH: %sOWNER: ", os.Folder)
for _, o := range os.Owners {
if o == "all" {
authInfo += "所有人" + " 或 "
} else {
authInfo += "@" + o + " 或 "
}
}
authInfo = authInfo[:len(authInfo)-len(" 或 ")]
}
if len(os.Reviewers) > 0 {
authInfo += "REVIEWER: "
for _, o := range os.Reviewers {
if o == "all" {
authInfo += "所有人" + " 与 "
} else {
authInfo += "@" + o + " 与 "
}
}
authInfo = authInfo[:len(authInfo)-len(" 与 ")]
}
authInfo += "\n\n"
}
if _, err = s.gitlab.CreateMRNote(projID, mrIID, authInfo); err != nil {
return
}
return
}
// ReportSuperRoleInfo ...
func (s *Service) ReportSuperRoleInfo(c context.Context, superUsers []string, event *model.HookMR) (err error) {
var (
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
)
authInfo := fmt.Sprintf("<pre>SAGA权限信息提示已配置超级权限用户请review: %s</pre>", event.ObjectAttributes.Title)
authInfo += "\n"
authInfo += fmt.Sprintf("+ SUPERMAN: ")
for _, user := range superUsers {
authInfo += "@" + user + " 或 "
}
authInfo = authInfo[:len(authInfo)-len(" 或 ")]
if _, err = s.gitlab.CreateMRNote(projID, mrIID, authInfo); err != nil {
return
}
return
}

View File

@@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["notification.go"],
importpath = "go-common/app/tool/saga/service/notification",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/mail:go_default_library",
"//app/tool/saga/service/wechat:go_default_library",
"//library/log:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,202 @@
package notification
import (
"context"
"fmt"
"runtime/debug"
"strconv"
"strings"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/mail"
"go-common/app/tool/saga/service/wechat"
"go-common/library/log"
)
// WechatAuthor send wechat message to original author
func WechatAuthor(dao *dao.Dao, authorName string, url, sourceBranch, targetBranch string, comment string) (err error) {
defer func() {
if x := recover(); x != nil {
log.Error("wechatAuthor: %+v %s", x, debug.Stack())
}
}()
var (
subject = fmt.Sprintf("[SAGA] MR ( %s ) merge 通知", sourceBranch)
data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 状态 %s", url, sourceBranch, targetBranch, comment)
wct = wechat.New(dao)
ctx = context.Background()
)
return wct.PushMsg(ctx, []string{authorName}, fmt.Sprintf("%s\n\n%s", subject, data))
}
// MailPipeline ...
func MailPipeline(event *model.HookPipeline) (err error) {
var (
author = event.User.UserName
branch = event.ObjectAttributes.Ref
commit = event.ObjectAttributes.Sha
title = ""
url = ""
status = "失败"
)
if event.Commit != nil {
commitIndex := strings.LastIndex(event.Commit.URL, "commit")
url = event.Commit.URL[:commitIndex] + "pipelines/" + strconv.FormatInt(event.ObjectAttributes.ID, 10)
}
if strings.Contains(event.Commit.Message, "\n") {
title = event.Commit.Message[:strings.Index(event.Commit.Message, "\n")]
} else {
title = event.Commit.Message
}
if event.ObjectAttributes.Status == model.PipelineSuccess {
status = "成功"
}
subject := fmt.Sprintf("[SAGA] Pipeline ( %s ) %s 通知", branch, status)
data := fmt.Sprintf("Pipeline : %s \nCommit : %s\nCommitID : %s\n状态: 运行%s !", url, title, commit, status)
addr := &model.MailAddress{
Address: author + "@bilibili.com",
Name: author,
}
if err = mail.SendMail2(addr, subject, data); err != nil {
log.Error("%+v", err)
}
return
}
// WechatPipeline ...
func WechatPipeline(dao *dao.Dao, event *model.HookPipeline) (err error) {
var (
wct = wechat.New(dao)
ctx = context.Background()
author = event.User.UserName
branch = event.ObjectAttributes.Ref
commit = event.ObjectAttributes.Sha
title = ""
url = ""
status = "失败"
)
if event.Commit != nil {
commitIndex := strings.LastIndex(event.Commit.URL, "commit")
url = event.Commit.URL[:commitIndex] + "pipelines/" + strconv.FormatInt(event.ObjectAttributes.ID, 10)
}
if strings.Contains(event.Commit.Message, "\n") {
title = event.Commit.Message[:strings.Index(event.Commit.Message, "\n")]
} else {
title = event.Commit.Message
}
if event.ObjectAttributes.Status == model.PipelineSuccess {
status = "成功"
}
subject := fmt.Sprintf("[SAGA] Pipeline ( %s ) %s 通知", branch, status)
data := fmt.Sprintf("Pipeline : %s \nCommit : %s\nCommitID : %s\n状态: 运行%s !", url, title, commit, status)
if err = wct.PushMsg(ctx, []string{author}, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
log.Error("%+v", err)
}
return
}
// MailPlusOne ...
func MailPlusOne(author, reviewer, url, commit, sourceBranch, targetBranch string) (err error) {
var (
subject = fmt.Sprintf("[SAGA] MR ( %s ) review 通知", sourceBranch)
data = fmt.Sprintf("MR : %s \n ( %s -> %s ) commitID ( %s ) 已被 ( %s ) review!", url, sourceBranch, targetBranch, commit, reviewer)
addr = &model.MailAddress{
Address: author + "@bilibili.com",
Name: author,
}
)
if err = mail.SendMail2(addr, subject, data); err != nil {
log.Error("%+v", err)
}
return
}
// WechatPlusOne ...
func WechatPlusOne(dao *dao.Dao, author, reviewer, url, commit, sourceBranch, targetBranch string) (err error) {
defer func() {
if x := recover(); x != nil {
log.Error("wechatPlusOne: %+v %s", x, debug.Stack())
}
}()
var (
subject = fmt.Sprintf("[SAGA] MR ( %s ) review 通知", sourceBranch)
data = fmt.Sprintf("MR : %s \n ( %s -> %s ) commitID ( %s ) 已被 ( %s ) review!", url, sourceBranch, targetBranch, commit, reviewer)
wct = wechat.New(dao)
ctx = context.Background()
)
if err = wct.PushMsg(ctx, []string{author}, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
log.Error("%+v", err)
}
return
}
// func notifyAssign(assign string, authorName string, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) assign 通知", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 已指派给您 !", url, sourceBranch, targetBranch, authorName)
// addr = &model.MailAddress{
// Address: assign + "@bilibili.com",
// Name: assign,
// }
// )
// if err = mail.SendMail2(addr, subject, data); err != nil {
// log.Error("%+v", err)
// }
// return
// }
// func wechatAssign(dao *dao.Dao, assign string, authorName string, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) assign 通知", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 已指派给您 !", url, sourceBranch, targetBranch, authorName)
// wct = wechat.New(dao)
// ctx = context.Background()
// )
// if err = wct.PushMsg(ctx, []string{assign}, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
// log.Error("wechatAssign failed, err (%s)", err.Error())
// }
// return
// }
// func notifyReviewer(reviewers []string, authorName, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) review 请求", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 需要您的 review !\n请在 review 后留言 +1 !", url, sourceBranch, targetBranch, authorName)
// )
// for _, reviewer := range reviewers {
// addr := &model.MailAddress{
// Address: reviewer + "@bilibili.com",
// Name: reviewer,
// }
// if err = mail.SendMail2(addr, subject, data); err != nil {
// log.Error("%+v", err)
// }
// }
// return
// }
// func wechatReviewer(dao *dao.Dao, reviewers []string, authorName, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) review 请求", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 需要您的 review !\n请在 review 后留言 +1 !", url, sourceBranch, targetBranch, authorName)
// wct = wechat.New(dao)
// ctx = context.Background()
// )
// if err = wct.PushMsg(ctx, reviewers, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
// log.Error("%+v", err)
// }
// return
// }

View File

@@ -0,0 +1,73 @@
package service
import (
"context"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
)
// PipelineChanged handle pipeline changed webhook
func (s *Service) PipelineChanged(c context.Context, event *model.HookPipeline) (err error) {
var (
ok bool
repo *model.Repo
state bool
lastPipeLineState bool
wip bool
)
//get associated pipeline mr and check it's wip status, if wip is true and return
if wip, err = s.checkMrStatus(c, event.Project.ID, event.ObjectAttributes.Ref); wip {
log.Info("Pipeline associated mr is wip, project ID: [%d], branch: [%s]", event.Project.ID, event.ObjectAttributes.Ref)
return
}
if ok, repo = s.findRepo(event.Project.GitSSHURL); !ok || !repo.Config.RelatePipeline {
log.Info("PipelineChanged return repo: %s, ok: %t", event.Project.GitSSHURL, ok)
return
}
if event.ObjectKind != model.HookPipelineType {
log.Info("Pipeline hook object kind [%s] ignore", event.ObjectKind)
return
}
if (event.ObjectAttributes.Status != model.PipelineFailed) && (event.ObjectAttributes.Status != model.PipelineSuccess) && (event.ObjectAttributes.Status != model.PipelineCanceled) {
log.Info("Pipeline status [%s] ignore for project [%s]", event.ObjectAttributes.Status, event.Project.Name)
return
}
go func() {
if err = s.cmd.HookPipeline(event.Project.ID, event.ObjectAttributes.Ref, int(event.ObjectAttributes.ID)); err != nil {
log.Error("CheckPipeline: %d %s %+v", event.Project.ID, event.ObjectAttributes.Ref, err)
}
}()
// 查询上次pipeline状态状态变化才发通知
if lastPipeLineState, err = s.gitlab.LastPipeLineState(event.Project.ID, event.ObjectAttributes.Ref); err != nil {
return
}
state = event.ObjectAttributes.Status == model.PipelineSuccess
log.Info("status:%t, lastPipeLineState: %t", state, lastPipeLineState)
if lastPipeLineState != state {
go notification.MailPipeline(event)
go notification.WechatPipeline(s.d, event)
}
return
}
func (s *Service) checkMrStatus(c context.Context, projectID int, branch string) (wip bool, err error) {
var (
ok bool
mergeInfo *model.MergeInfo
)
if ok, mergeInfo, err = s.d.MergeInfo(c, projectID, branch); err != nil || !ok {
return
}
if wip, _, _, err = s.gitlab.MergeStatus(projectID, mergeInfo.MRIID); err != nil {
return
}
return
}

View File

@@ -0,0 +1,146 @@
package service
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/command"
"go-common/app/tool/saga/service/gitlab"
"go-common/library/log"
"github.com/pkg/errors"
"github.com/robfig/cron"
)
// Service biz service def.
type Service struct {
missch chan func()
d *dao.Dao
gitlab *gitlab.Gitlab
gitRepoMap map[string]*model.Repo // map[repoName]*repo
cmd *command.Command
cron *cron.Cron
}
// New a DirService and return.
func New() (s *Service) {
s = &Service{
d: dao.New(),
missch: make(chan func(), 10240),
}
// init gitlab client
s.gitlab = gitlab.New(conf.Conf.Property.Gitlab.API, conf.Conf.Property.Gitlab.Token)
s.cmd = command.New(s.d, s.gitlab)
s.cmd.Registers()
go s.cmd.ListenTask()
s.loadRepos(false)
go s.updateproc()
// start cron
s.cron = cron.New()
if err := s.cron.AddFunc(conf.Conf.Property.SyncContact.CheckCron, s.synccontactsproc); err != nil {
panic(err)
}
s.cron.Start()
//
return
}
func (s *Service) validTargetBranch(targetBranch string, gitRepo *model.Repo) bool {
for _, r := range gitRepo.Config.TargetBranchRegexes {
if r.MatchString(targetBranch) {
return true
}
}
/*for _, r := range gitRepo.Config.TargetBranches {
if r == targetBranch {
return true
}
}*/
return false
}
func (s *Service) loadRepos(reload bool) {
var (
repo *model.Repo
ok bool
)
// init code repo
if s.gitRepoMap == nil {
s.gitRepoMap = make(map[string]*model.Repo)
}
webHookRepos := make([]*model.Repo, 0)
authRepos := make([]*model.Repo, 0)
for _, r := range conf.Conf.Property.Repos {
if repo, ok = s.gitRepoMap[r.GName]; !ok {
repo = &model.Repo{
Config: r,
}
s.gitRepoMap[r.GName] = repo
webHookRepos = append(webHookRepos, repo)
authRepos = append(authRepos, repo)
} else {
if repo.AuthUpdate(r) {
authRepos = append(authRepos, repo)
}
if repo.WebHookUpdate(r) {
webHookRepos = append(webHookRepos, repo)
}
if repo.Update(r) {
s.gitRepoMap[r.GName] = repo
}
}
}
if reload {
s.BuildContributors(authRepos)
}
if err := s.gitlab.AuditProjects(webHookRepos, conf.Conf.Property.WebHooks); err != nil {
log.Error("loadRepos err (%+v)", err)
}
}
func (s *Service) findRepo(gitURL string) (ok bool, repo *model.Repo) {
for _, r := range conf.Conf.Property.Repos {
if strings.EqualFold(r.URL, gitURL) {
repo = &model.Repo{
Config: r,
}
ok = true
return
}
}
return
}
// Ping check dao health.
func (s *Service) Ping(c context.Context) (err error) {
return s.d.Ping(c)
}
// Wait wait all closed.
func (s *Service) Wait() {
}
// Close close all dao.
func (s *Service) Close() {
s.d.Close()
}
func (s *Service) updateproc() {
defer func() {
if x := recover(); x != nil {
log.Error("updateproc panic(%v)", errors.WithStack(fmt.Errorf("%v", x)))
go s.updateproc()
log.Info("updateproc recover")
}
}()
for range conf.ReloadEvents() {
log.Info("DirService reload")
s.loadRepos(true)
}
}

View File

@@ -0,0 +1,196 @@
package service
import (
"flag"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
s *Service
repoTest = &model.RepoInfo{Group: "changhengyuan", Name: "test-saga", Branch: "master"}
GitlabHookCommentTest = []byte(`{
"object_kind":"note",
"event_type":"note",
"user":{
"name":"changhengyuan",
"username":"changhengyuan",
"avatar_url":"https://www.gravatar.com/avatar/d3218d34473c6fb4d18a770f14e59a89?s=80\u0026d=identicon"
},
"project_id":35,
"project":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"},
"object_attributes":{
"id":3040,
"note":"test",
"noteable_type":"MergeRequest",
"author_id":15,
"created_at":"2018-09-26 06:55:13 UTC",
"updated_at":"2018-09-26 06:55:13 UTC",
"project_id":35,
"attachment":null,
"line_code":null,
"commit_id":"",
"noteable_id":390,
"system":false,
"st_diff":null,
"updated_by_id":null,
"type":null,
"position":null,
"original_position":null,
"resolved_at":null,
"resolved_by_id":null,
"discussion_id":"450c34e4c0f9e925bdc6a24c2ae4920d7a394ebc",
"change_position":null,
"resolved_by_push":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52#note_3040"
},
"repository":{
"name":"test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"description":"",
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga"
},
"merge_request":{
"assignee_id":null,
"author_id":15,
"created_at":"2018-09-26 06:41:55 UTC",
"description":"",
"head_pipeline_id":4510,
"id":390,
"iid":52,
"last_edited_at":null,
"last_edited_by_id":null,
"merge_commit_sha":null,
"merge_error":null,
"merge_params":{
"force_remove_source_branch":"0"
},
"merge_status":"cannot_be_merged",
"merge_user_id":null,
"merge_when_pipeline_succeeds":false,
"milestone_id":null,
"source_branch":"test-branch",
"source_project_id":35,
"state":"opened",
"target_branch":"master",
"target_project_id":35,
"time_estimate":0,
"title":"Test branch",
"updated_at":"2018-09-26 06:54:33 UTC",
"updated_by_id":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52",
"source":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"target":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"last_commit":{
"id":"51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"message":"add CONTRIBUTORS.md\n",
"timestamp":"2018-09-17T18:02:13+08:00",
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/commit/51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"author":{
"name":"哔哩哔哩",
"email":"bilibili@bilibilideMac-mini.local"
}
},
"work_in_progress":false,
"total_time_spent":0,
"human_total_time_spent":null,
"human_time_estimate":null}}`)
)
func init() {
var err error
flag.Set("conf", "../cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
}
func TestContributor(t *testing.T) {
var (
err error
repo = &model.RepoInfo{
Group: "platform",
Name: "go-common",
Branch: "master",
}
)
Convey("TEST BuildContributor", t, func() {
s = New()
err = s.cmd.BuildContributor(repo)
So(err, ShouldBeNil)
})
}
func TestMergeRequest(t *testing.T) {
var (
err error
ok bool
repo *model.Repo
url = "git@gitlab.bilibili.co:platform/go-common.git"
projID = 23
mrIID = 130
sourceBranch = "ci/test-7"
targetBranch = "master"
)
Convey("TEST ParseContributor", t, func() {
s = New()
ok, repo = s.findRepo(url)
So(ok, ShouldBeTrue)
err = s.cmd.UpdateContributor(projID, mrIID, sourceBranch, targetBranch, repo.Config.AuthBranches)
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,77 @@
package service
import (
"context"
"fmt"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/mail"
"go-common/app/tool/saga/service/wechat"
"go-common/library/log"
"github.com/pkg/errors"
)
// CollectWachatUsers send required wechat visible users stored in memcache by email
func (s *Service) CollectWachatUsers(c context.Context) (err error) {
var (
contactInfo *model.ContactInfo
userMap = make(map[string]model.RequireVisibleUser)
user string
)
if err = s.d.RequireVisibleUsers(c, &userMap); err != nil {
log.Error("get require visible user error(%v)", err)
return
}
for k, v := range userMap {
if contactInfo, err = s.d.QueryUserByID(k); err == nil {
if contactInfo.VisibleSaga {
continue
}
}
user += v.UserName + " , " + v.NickName + "\n"
}
content := fmt.Sprintf("\n\n邮箱前缀 昵称\n\n%s", user)
for _, addr := range conf.Conf.Property.ReportRequiredVisible.AlertAddrs {
if err = mail.SendMail2(addr, "需添加的企业微信名单", content); err != nil {
return
}
}
if err = s.d.DeleteRequireVisibleUsers(c); err != nil {
log.Error("Delete require visible user error(%v)", err)
return
}
return
}
// SyncContacts sync the wechat contacts
func (s *Service) SyncContacts(c context.Context) (err error) {
var (
w = wechat.New(s.d)
)
if err = w.SyncContacts(c); err != nil {
return
}
return
}
// synccontactsproc sync wechat contact procedure
func (s *Service) synccontactsproc() {
defer func() {
if x := recover(); x != nil {
log.Error("synccontactsproc panic(%v)", errors.WithStack(fmt.Errorf("%v", x)))
go s.synccontactsproc()
log.Info("synccontactsproc recover")
}
}()
var err error
if err = s.SyncContacts(context.TODO()); err != nil {
log.Error("s.SyncContacts err (%+v)", err)
}
}

View File

@@ -0,0 +1,53 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"contact.go",
"wechat.go",
],
importpath = "go-common/app/tool/saga/service/wechat",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["wechat_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,180 @@
package wechat
import (
"context"
"go-common/app/tool/saga/model"
"go-common/library/log"
)
// Changes changes structure
type Changes struct {
Adds []*model.ContactInfo
Upts []*model.ContactInfo
Dels []*model.ContactInfo
}
// SyncContacts sync the contacts from wechat work
func (w *Wechat) SyncContacts(c context.Context) (err error) {
if err = w.AnalysisContacts(c); err != nil {
return
}
/*if err = w.UpdateVisible(c); err != nil {
return
}*/
return
}
// AnalysisContacts analysis the contact difference and save them
func (w *Wechat) AnalysisContacts(c context.Context) (err error) {
var (
contactsInDB []*model.ContactInfo
wechatContacts []*model.ContactInfo
changes = &Changes{}
)
if contactsInDB, err = w.dao.ContactInfos(); err != nil {
return
}
if wechatContacts, err = w.QueryWechatContacts(c); err != nil {
return
}
if changes, err = w.diffChanges(wechatContacts, contactsInDB); err != nil {
return
}
if err = w.saveChanges(changes); err != nil {
return
}
return
}
// QueryWechatContacts query wechat contacts with access token
func (w *Wechat) QueryWechatContacts(c context.Context) (contacts []*model.ContactInfo, err error) {
var (
token string
)
if token, err = w.AccessToken(c, w.contact); err != nil {
return
}
if contacts, err = w.dao.WechatContacts(c, token); err != nil {
return
}
return
}
func (w *Wechat) saveChanges(changes *Changes) (err error) {
var (
contact *model.ContactInfo
)
log.Info("saveChanges add(%d), upt(%d), del(%d)", len(changes.Adds), len(changes.Upts), len(changes.Dels))
for _, contact = range changes.Adds {
if err = w.dao.CreateContact(contact); err != nil {
return
}
log.Info("saveChanges add: %v", contact)
}
for _, contact = range changes.Upts {
if err = w.dao.UptContact(contact); err != nil {
return
}
log.Info("saveChanges upt: %v", contact)
}
for _, contact = range changes.Dels {
if err = w.dao.DelContact(contact); err != nil {
return
}
log.Info("saveChanges del: %v", contact)
}
return
}
func (w *Wechat) diffChanges(wechatContacts, contactsInDB []*model.ContactInfo) (changes *Changes, err error) {
var (
contact *model.ContactInfo
wechatContactsMap = make(map[string]*model.ContactInfo)
contactsInDBMap = make(map[string]*model.ContactInfo)
wechatContactIDs []string
dbContactsIDs []string
userID string
)
changes = new(Changes)
for _, contact = range wechatContacts {
wechatContactsMap[contact.UserID] = contact
wechatContactIDs = append(wechatContactIDs, contact.UserID)
}
for _, contact = range contactsInDB {
contactsInDBMap[contact.UserID] = contact
dbContactsIDs = append(dbContactsIDs, contact.UserID)
}
// 分析变化
for _, userID = range wechatContactIDs {
contact = wechatContactsMap[userID]
if w.inSlice(dbContactsIDs, userID) { // 企业微信联系人ID在数据库中能找到
if !contact.AlmostEqual(contactsInDBMap[userID]) { // 但是域不同
contact.ID = contactsInDBMap[userID].ID
changes.Upts = append(changes.Upts, contact)
}
} else {
changes.Adds = append(changes.Adds, contact) // 这个联系人是新增的
}
}
for _, userID = range dbContactsIDs {
if !w.inSlice(wechatContactIDs, userID) {
changes.Dels = append(changes.Dels, contactsInDBMap[userID])
}
}
return
}
func (w *Wechat) inSlice(slice []string, target string) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// UpdateVisible update the visible property
func (w *Wechat) UpdateVisible(c context.Context) (err error) {
var (
user *model.UserInfo
users []*model.UserInfo
contact *model.ContactInfo
)
if users, err = w.querySagaVisible(c); err != nil {
return
}
for _, user = range users {
contact = &model.ContactInfo{UserID: user.UserID, VisibleSaga: true}
if err = w.dao.UptContact(contact); err != nil {
return
}
}
return
}
func (w *Wechat) querySagaVisible(c context.Context) (users []*model.UserInfo, err error) {
var (
token string
)
if token, err = w.AccessToken(c, w.saga); err != nil {
return
}
if users, err = w.dao.WechatSagaVisible(c, token, w.saga.AppID); err != nil {
return
}
return
}

View File

@@ -0,0 +1,165 @@
package wechat
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
)
// Wechat 企业微信应用
type Wechat struct {
dao *dao.Dao
saga *model.AppConfig
contact *model.AppConfig
}
// New create an new wechat work
func New(d *dao.Dao) (w *Wechat) {
w = &Wechat{
dao: d,
saga: conf.Conf.Property.Wechat,
contact: conf.Conf.Property.Contact,
}
return w
}
// NewTxtNotify create wechat format text notification
func (w *Wechat) NewTxtNotify(content string) (txtMsg *model.TxtNotification) {
return &model.TxtNotification{
Notification: model.Notification{
MsgType: "text",
AgentID: w.saga.AppID,
},
Body: model.Text{
Content: content,
},
Safe: 0,
}
}
// AccessToken get access_token from cache first, if not found, get it via wechat api.
func (w *Wechat) AccessToken(c context.Context, app *model.AppConfig) (token string, err error) {
var (
key string
expire int32
)
key = fmt.Sprintf("appid_%d", app.AppID)
if token, err = w.dao.AccessToken(c, key); err != nil {
log.Warn("AccessToken: failed to get access_token from cache, appId (%d), error (%s)", app.AppID, err.Error())
if token, expire, err = w.dao.WechatAccessToken(c, app.AppSecret); err != nil {
err = errors.Wrapf(err, "AccessToken: both mc and api can't provide access_token, appId(%d)", app.AppID)
return
}
// 通过API获取到了缓存一波
err = w.dao.SetAccessToken(c, key, token, expire)
return
}
if token == "" {
if token, expire, err = w.dao.WechatAccessToken(c, app.AppSecret); err != nil {
return
}
// 通过API获取到了缓存一波
err = w.dao.SetAccessToken(c, key, token, expire)
}
return
}
// PushMsg push text message via wechat notification api with access_token.
func (w *Wechat) PushMsg(c context.Context, userNames []string, content string) (err error) {
var (
token string
userIds string
invalidUser string
txtMsg = w.NewTxtNotify(content)
)
if token, err = w.AccessToken(c, w.saga); err != nil {
return
}
if token == "" {
err = errors.Errorf("PushMsg: get access token failed, it's empty. appid (%d), secret (%s)", w.saga.AppID, w.saga.AppSecret)
return
}
if userIds, err = w.UserIds(userNames); err != nil {
return
}
txtMsg.ToUser = userIds
if invalidUser, err = w.dao.WechatPushMsg(c, token, txtMsg); err != nil {
if err = w.addRequireVisible(c, invalidUser); err != nil {
log.Error("PushMsg add userID (%s) in cache, error(%s)", invalidUser, err.Error())
}
return
}
return
}
// UserIds query user ids for user name list
func (w *Wechat) UserIds(userNames []string) (ids string, err error) {
ids, err = w.dao.UserIds(userNames)
return
}
// addRequireVisible update wechat require visible users in memcache
func (w *Wechat) addRequireVisible(c context.Context, userIDs string) (err error) {
var (
contactInfo *model.ContactInfo
userID string
alreadyIn bool
)
users := strings.Split(userIDs, "|")
for _, userID = range users {
if alreadyIn, err = w.alreadyInCache(c, userID); err != nil || alreadyIn {
continue
}
if contactInfo, err = w.dao.QueryUserByID(userID); err != nil {
log.Error("no such userID (%s) in db, error(%s)", userID, err.Error())
return
}
if err = w.dao.SetRequireVisibleUsers(c, contactInfo); err != nil {
log.Error("failed set to cache userID (%s) username (%s), err (%s)", userID, contactInfo.UserName, err.Error())
return
}
}
return
}
// alreadyInCache check user is or not in the memcache
func (w *Wechat) alreadyInCache(c context.Context, userID string) (alreadyIn bool, err error) {
var (
userMap = make(map[string]model.RequireVisibleUser)
)
if err = w.dao.RequireVisibleUsers(c, &userMap); err != nil {
log.Error("get userID (%s) from cache error(%s)", userID, err.Error())
return
}
for k, v := range userMap {
if userID == k {
log.Info("(%s) is already exist in cache, value(%v)", k, v)
alreadyIn = true
return
}
}
return
}

View File

@@ -0,0 +1,126 @@
package wechat
import (
"context"
"flag"
"os"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
mydao *dao.Dao
wechat *Wechat
ctx = context.Background()
)
func TestMain(m *testing.M) {
var err error
flag.Set("conf", "../../cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
mydao = dao.New()
defer mydao.Close()
wechat = New(mydao)
os.Exit(m.Run())
}
func TestAddRequireVisible(t *testing.T) {
var (
err error
userMap = make(map[string]model.RequireVisibleUser)
)
Convey("TEST addRequireVisible", t, func() {
err = wechat.addRequireVisible(ctx, "000000")
So(err, ShouldNotBeNil)
err = wechat.addRequireVisible(ctx, "001134")
So(err, ShouldBeNil)
err = mydao.RequireVisibleUsers(ctx, &userMap)
So(err, ShouldBeNil)
So(userMap, ShouldContainKey, "001134")
})
}
func TestAlreadyInCache(t *testing.T) {
var (
err error
result bool
contactInfo model.ContactInfo
)
contactInfo = model.ContactInfo{
ID: "111",
UserName: "zhangsan",
UserID: "222",
NickName: "xiaolizi",
VisibleSaga: true,
}
Convey("TEST alreadyInCache", t, func() {
result, err = wechat.alreadyInCache(ctx, "000")
So(err, ShouldBeNil)
So(result, ShouldEqual, false)
So(mydao.SetRequireVisibleUsers(ctx, &contactInfo), ShouldBeNil)
result, err = wechat.alreadyInCache(ctx, "222")
So(err, ShouldBeNil)
So(result, ShouldEqual, true)
})
}
func TestSyncContacts(t *testing.T) {
var (
err error
contactInfo = &model.ContactInfo{
//UserID: "004273",
UserID: "E10021",
UserName: "eyotang",
NickName: "ben大神点C",
}
modify = &model.ContactInfo{
UserID: "000328",
UserName: "eyotang",
NickName: "ben大神点C",
VisibleSaga: false,
}
target *model.ContactInfo
almostEqual bool
)
Convey("TEST sync after add incorrect", t, func() {
err = wechat.dao.CreateContact(contactInfo)
So(err, ShouldBeNil)
target, err = wechat.dao.QueryUserByID(contactInfo.UserID)
So(err, ShouldBeNil)
almostEqual = contactInfo.AlmostEqual(target)
So(almostEqual, ShouldBeTrue)
err = wechat.SyncContacts(ctx)
So(err, ShouldBeNil)
target, err = wechat.dao.QueryUserByID(contactInfo.UserID)
So(err, ShouldNotBeNil)
})
Convey("TEST aync after change", t, func() {
contactInfo, err = wechat.dao.QueryUserByID(modify.UserID)
So(err, ShouldBeNil)
modify.ID = contactInfo.ID
err = wechat.dao.UptContact(contactInfo)
So(err, ShouldBeNil)
err = wechat.SyncContacts(ctx)
So(err, ShouldBeNil)
target, err = wechat.dao.QueryUserByID(modify.UserID)
So(err, ShouldBeNil)
//So(target.VisibleSaga, ShouldBeTrue)
So(target.UserName, ShouldNotEqual, "eyotang")
})
}