后端框架的选择面比较大,由于不涉及复杂的调度/分布式管理等场景,所以后端选用一个标准的web server即可,比如gin, iris, beego等等,因为正好最近在看iris的一些项目,所以就选用了irsi+corba的框架进行后端开发 。
通过cobra进行初始化的操作就不在赘述,这边先show一下相关的目录结构
cmd # 启动命令
common # 通用组件,如mysql, conf, log等
conf # 配置表
core/server # 核心服务,http server
dao/v1 # 数据层
docs # 文档
http # 接口层
img # readme的截图,架构图
migrate # 首次启动的数据同步
pkg # 非通用组件,如k8s客户端,websocket等
service/v1 # 业务逻辑的封装
structs/v1 # 所有结构体
main.go # 入口
封装KubeMgrServer的对象
package server
import (
"fmt"
"github.com/iris-contrib/swagger/v12"
"github.com/iris-contrib/swagger/v12/swaggerFiles"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/sessions/sessiondb/redis"
cfg "kubemgr/common/configparse"
log "kubemgr/common/formatlog"
"kubemgr/common/i18n"
"kubemgr/common/mysql"
redis_common "kubemgr/common/redis"
_ "kubemgr/docs" // docs is generated by Swag CLI, you have to import it.
"kubemgr/migrate"
"strings"
"time"
)
// session管理
const SessionCookieName = "KUBEMGR_SESS"
var SessionMgr *sessions.Sessions
type KubeMgrServer struct {
app *iris.Application
rootRoute iris.Party
configPath string
bind string
}
var km *KubeMgrServer
// 注册redis
func NewRedis() *redis.Database {
db := redis.New(redis.Config{
Network: "tcp",
Addr: cfg.GlobalConf.GetStr("common", "redisbind"),
Timeout: time.Duration(30) * time.Second,
MaxActive: cfg.GlobalConf.GetInt("common", "redismaxactive"),
Password: cfg.GlobalConf.GetStr("common", "redispasswd"),
Database: cfg.GlobalConf.GetStr("common", "redisdb"),
Prefix: cfg.GlobalConf.GetStr("common", "redisprefix"),
Driver: redis.GoRedis(), // redis.Radix() can be used instead.
})
// defer db.Close()
return db
}
// 启动服务
func Listen(route func(party iris.Party), confgPath string, serverBindHost string, serverBindPort int) error {
bind := fmt.Sprintf("%v:%v", serverBindHost, serverBindPort)
km = NewServer(confgPath, bind)
route(km.rootRoute)
return km.app.Run(iris.Addr(bind))
}
// 初始化服务对象
func NewServer(confgPath, bind string) *KubeMgrServer {
c := &KubeMgrServer{}
c.app = iris.New()
c.configPath = confgPath
c.bind = bind
return c.bootstrap()
}
// 初始化KubeMgrServer对象,初始化各个模块
func (e *KubeMgrServer) bootstrap() *KubeMgrServer {
e.setUpRootRoute()
e.setUpConfig()
e.setUpLogger()
e.setUpDB()
e.setUpSession()
e.setResultHandler()
e.setUpErrHandler()
e.setDBInitial()
return e
}
// 初始化路由,所有路由的/ 路径,均为kubemgr
func (e *KubeMgrServer) setUpRootRoute() {
e.app.Any("/", func(ctx *context.Context) {
ctx.Redirect("/kubemgr")
})
c := swagger.Config{
URL: "/kubemgr/swagger/doc.json",
}
e.app.Get("/kubemgr/swagger/{any:path}", swagger.CustomWrapHandler(&c, swaggerFiles.Handler))
e.rootRoute = e.app.Party("/kubemgr")
}
// 初始化配置文件
func (e *KubeMgrServer) setUpConfig() {
cfg.GlobalConf.CfgInit(e.configPath)
}
// 初始化日志
func (e *KubeMgrServer) setUpLogger() {
// logname := cfg.GlobalConf.GetStr("common", "logname")
loglevel := cfg.GlobalConf.GetStr("common", "loglevel")
log.InitLog(loglevel)
}
// 初始化DB
func (e *KubeMgrServer) setUpDB() {
mysql.DB.InitConn()
redis_common.DB.NewRds()
}
// 初始化session
func (e *KubeMgrServer) setUpSession() {
SessionMgr = sessions.New(sessions.Config{Cookie: SessionCookieName, AllowReclaim: true, Expires: time.Duration(cfg.GlobalConf.GetInt("common", "sessiontimeout")) * time.Hour})
db := NewRedis()
SessionMgr.UseDatabase(db)
e.rootRoute.Use(SessionMgr.Handler())
}
// 初始化数据库实例
func (e *KubeMgrServer) setDBInitial() {
DataSourceUrl := fmt.Sprintf("%s%s%s", "mysql://", cfg.GlobalConf.GetStr("mysql", "datasource"), "?multiStatements=true")
migrate.InitDB(DataSourceUrl, 5*time.Second)
}
// 初始化response
func (e *KubeMgrServer) setResultHandler() {
e.rootRoute.Use(func(ctx *context.Context) {
ctx.Next()
if ctx.GetStatusCode() >= iris.StatusOK && ctx.GetStatusCode() < iris.StatusBadRequest {
if ctx.Values().Get("token") != nil {
_, _ = ctx.Write(ctx.Values().Get("token").([]uint8))
} else {
resp := iris.Map{
"success": true,
"data": ctx.Values().Get("data"),
}
_ = ctx.JSON(resp)
}
}
})
}
// 捕获异常返回
func (e *KubeMgrServer) setUpErrHandler() {
e.rootRoute.OnAnyErrorCode(func(ctx iris.Context) {
//如果报错message为空,且errcode为404,则进行错误返回
if ctx.Values().GetString("message") == "" {
switch ctx.GetStatusCode() {
case iris.StatusNotFound:
ctx.Values().Set("message", "the server could not find the requested resource")
}
}
message := ctx.Values().Get("message")
if message == nil || message == "" {
message = ctx.Values().Get("iris.context.error")
}
// 默认接口返回错误信息为US
lang := ctx.Values().GetString("language")
if lang == "" {
lang = i18n.LanguageEnUS
}
var (
translateMessage string
err error
originMessage string
)
switch value := message.(type) {
case string:
originMessage = message.(string)
translateMessage, err = i18n.Translate(lang, value)
case []string:
originMessage = strings.Join(value, ",")
if len(value) > 0 {
translateMessage, err = i18n.Translate(lang, value[0], value[1:])
}
case context.ErrPrivate:
err := message.(context.ErrPrivate)
translateMessage = err.Error()
}
msg := translateMessage
if err != nil {
e.app.Logger().Debug(err)
msg = originMessage
}
er := iris.Map{
"success": false,
"code": ctx.GetStatusCode(),
"message": msg,
}
_ = ctx.JSON(er)
})
}
上述代码对KubeMgrServer的对象进行了初步的初始化,封装了日志/DB/缓存的/异常/返回,在完整基本的封装以后,可以进行接口的封装了,iris或者gin之类的 框架,最大的优势在于有ctx的概念,可以在请求的上下文中进行数据的修改,提取,插入等等,这就很方便了,可以利用这点就行token的提取,参数的抓取 ,甚至于请求头的改写(比如将http请求升级到websocket)。
在完成基本主体的封装后,开始编写接口,首先将接口分个类, 分成对应功能的目录 :
http
├── api
│ └── v1
│ ├── audit
│ │ ├── audit.go
│ │ └── types.go
│ ├── cluster
│ │ ├── cluster.go
│ │ ├── clusterrole.go
│ │ ├── member.go
│ │ └── types.go
│ ├── kubernetes
│ │ ├── prometheus.go
│ │ ├── proxy.go
│ │ └── types.go
│ ├── role
│ │ ├── role.go
│ │ └── types.go
│ ├── session
│ │ ├── session.go
│ │ └── types.go
│ ├── user
│ │ ├── types.go
│ │ └── user.go
│ ├── v1.go
│ └── ws
│ └── ws.go
└── route
└── route.go
上述目录将不同功能的接口基于不同目录进行了封装,类似如下代码:
package audit
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
log "kubemgr/common/formatlog"
v1AuditService "kubemgr/service/v1/audit"
)
type Handler struct {
auditService v1AuditService.Service
}
func NewHandler() *Handler {
return &Handler{
auditService: v1AuditService.NewService(),
}
}
func (h *Handler) ListPlatFormAuditRecord() iris.Handler {
return func(ctx *context.Context) {
pageNum := ctx.URLParamIntDefault("pageNum", 0)
pageSize := ctx.URLParamIntDefault("pageSize", 10)
fromTime := ctx.URLParam("fromTime")
endTime := ctx.URLParam("toTime")
total, auditLogs, err := h.auditService.ListPlatFormAuditRecord(pageNum, pageSize, fromTime, endTime)
if err != nil {
log.Errorf("获取审计日志失败: %v", err.Error())
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
return
}
var allAuditLogs ListAuditLogs
allAuditLogs.Total = total
allAuditLogs.AuditLogs = auditLogs
ctx.StatusCode(iris.StatusOK)
ctx.Values().Set("data", &allAuditLogs)
}
}
func Install(parent iris.Party) {
handler := NewHandler()
sp := parent.Party("/audit")
sp.Get("/list", handler.ListPlatFormAuditRecord())
}
然后在v1.go里面,将每个目录的接口接入root_route,并进行基础功能的封装,如日志,认证等等
func AddV1Route(app iris.Party) {
v1Party := app.Party("/v1")
session.Install(v1Party)
// 接口国际化,基于profile内的语言标识,来返回中文/英文
v1Party.Use(langHandler())
// websocket请求,将http请求升级到websocket,由于websocket获取token的方式比较特殊,所以不放入统一认证的方法,单独编写
wss.Install(v1Party)
// 基础路由的首次封装,添加认证,权限,角色的相关解析操作
authParty := v1Party.Party("")
authParty.Use(authHandler())
authParty.Use(resourceExtractHandler())
authParty.Use(roleHandler())
// 将封装好的路由导入平台用户/角色权限管理的相关接口
user.Install(authParty)
role.Install(authParty)
// 将封装好的路由导入平台审计/集群管理的相关接口
audit.Install(authParty)
cluster.Install(authParty)
kubernetes.Install(authParty)
}
至此,可以基于上面的代码看到api的请求路径
请求→v1.go(进行路由封装和分发)→分发到的明细路由处理(例如audit.go)→转发到业务实际处理逻辑,比如调用k8或者crud等
大致上的后端逻辑就是上面这样了,平台的基本操作用不到pkg,简单的crud即可。这个和其他运维或者中台管理的逻辑大致上没什么区别,主要后面要看对于k8s的操作。