add ob configserver (#943)

This commit is contained in:
chris-sun-star
2022-05-30 16:44:36 +08:00
committed by GitHub
parent bd06e657c6
commit e12438aa76
64 changed files with 9587 additions and 0 deletions

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
"github.com/oceanbase/configserver/config"
"github.com/oceanbase/configserver/ent"
"github.com/oceanbase/configserver/lib/trace"
"github.com/oceanbase/configserver/logger"
)
var configServer *ConfigServer
func GetConfigServer() *ConfigServer {
return configServer
}
type ConfigServer struct {
Config *config.ConfigServerConfig
Server *HttpServer
Client *ent.Client
}
func NewConfigServer(conf *config.ConfigServerConfig) *ConfigServer {
server := &ConfigServer{
Config: conf,
Server: &HttpServer{
Counter: new(Counter),
Router: gin.Default(),
Server: &http.Server{},
Address: conf.Server.Address,
},
Client: nil,
}
configServer = server
return configServer
}
func (server *ConfigServer) Run() error {
client, err := ent.Open(server.Config.Storage.DatabaseType, server.Config.Storage.ConnectionUrl)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("initialize storage client with config %v", server.Config.Storage))
}
server.Client = client
defer server.Client.Close()
if err := server.Client.Schema.Create(context.Background()); err != nil {
return errors.Wrap(err, "create configserver schema")
}
// start http server
ctx, cancel := context.WithCancel(trace.ContextWithTraceId(logger.INIT_TRACEID))
server.Server.Cancel = cancel
// register route
InitConfigServerRoutes(server.Server.Router)
// run http server
server.Server.Run(ctx)
return nil
}

View File

@ -0,0 +1,128 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/configserver/lib/codec"
"github.com/oceanbase/configserver/lib/net"
"github.com/oceanbase/configserver/lib/trace"
)
var invalidActionOnce sync.Once
var invalidActionFunc func(*gin.Context)
func getInvalidActionFunc() func(*gin.Context) {
invalidActionOnce.Do(func() {
invalidActionFunc = handlerFunctionWrapper(invalidAction)
})
return invalidActionFunc
}
func getServerIdentity() string {
ip, _ := net.GetLocalIpAddress()
return ip
}
func handlerFunctionWrapper(f func(context.Context, *gin.Context) *ApiResponse) func(*gin.Context) {
fn := func(c *gin.Context) {
tStart := time.Now()
traceId := trace.RandomTraceId()
ctxlog := trace.ContextWithTraceId(traceId)
log.WithContext(ctxlog).Infof("handle request: %s %s", c.Request.Method, c.Request.RequestURI)
response := f(ctxlog, c)
cost := time.Now().Sub(tStart).Milliseconds()
response.TraceId = traceId
response.Cost = cost
response.Server = getServerIdentity()
responseJson, err := codec.MarshalToJsonString(response)
if err != nil {
log.WithContext(ctxlog).Errorf("response: %s", "response serialization error")
c.JSON(http.StatusInternalServerError, NewErrorResponse(errors.Wrap(err, "serialize response")))
} else {
log.WithContext(ctxlog).Infof("response: %s", responseJson)
c.String(response.Code, string(responseJson))
}
}
return fn
}
func invalidAction(ctxlog context.Context, c *gin.Context) *ApiResponse {
log.WithContext(ctxlog).Error("invalid action")
return NewIllegalArgumentResponse(errors.New("invalid action"))
}
func getHandler() gin.HandlerFunc {
fn := func(c *gin.Context) {
action := c.Query("Action")
switch action {
case "ObRootServiceInfo":
getObRootServiceGetFunc()(c)
case "GetObProxyConfig":
getObProxyConfigFunc()(c)
case "GetObRootServiceInfoUrlTemplate":
getObProxyConfigWithTemplateFunc()(c)
case "ObIDCRegionInfo":
getObIdcRegionInfoFunc()(c)
default:
getInvalidActionFunc()(c)
}
}
return gin.HandlerFunc(fn)
}
func postHandler() gin.HandlerFunc {
fn := func(c *gin.Context) {
action, _ := c.GetQuery("Action")
switch action {
case "ObRootServiceInfo":
getObRootServicePostFunc()(c)
case "GetObProxyConfig":
getObProxyConfigFunc()(c)
case "GetObRootServiceInfoUrlTemplate":
getObProxyConfigWithTemplateFunc()(c)
default:
getInvalidActionFunc()(c)
}
}
return gin.HandlerFunc(fn)
}
func deleteHandler() gin.HandlerFunc {
fn := func(c *gin.Context) {
action, _ := c.GetQuery("Action")
switch action {
case "ObRootServiceInfo":
getObRootServiceDeleteFunc()(c)
default:
getInvalidActionFunc()(c)
}
}
return gin.HandlerFunc(fn)
}

View File

@ -0,0 +1,140 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
libhttp "github.com/oceanbase/configserver/lib/http"
)
type HttpServer struct {
// server will be stopped, new request will be rejected
Stopping int32
// current session count, concurrent safely
Counter *Counter
// http routers
Router *gin.Engine
// address
Address string
// http server, call its Run, Shutdown methods
Server *http.Server
// stop the http.Server by calling cancel method
Cancel context.CancelFunc
}
// UseCounter use counter middleware
func (server *HttpServer) UseCounter() {
server.Router.Use(
server.counterPreHandlerFunc,
server.counterPostHandlerFunc,
)
}
// Run start a httpServer
// when ctx is cancelled, call shutdown to stop the httpServer
func (server *HttpServer) Run(ctx context.Context) {
server.Server.Handler = server.Router
if server.Address != "" {
log.WithContext(ctx).Infof("listen on address: %s", server.Address)
tcpListener, err := libhttp.NewTcpListener(server.Address)
if err != nil {
log.WithError(err).
Errorf("create tcp listener on address '%s' failed %v", server.Address, err)
return
}
go func() {
if err := server.Server.Serve(tcpListener); err != nil {
log.WithError(err).
Info("tcp server exited")
}
}()
}
for {
select {
case <-ctx.Done():
if err := server.Shutdown(ctx); err != nil {
log.WithContext(ctx).
WithError(err).
Error("server shutdown failed!")
// in a for loop, sleep 100ms
time.Sleep(time.Millisecond * 100)
} else {
log.WithContext(ctx).Info("server shutdown successfully.")
return
}
}
}
}
// shutdown httpServer can shutdown if sessionCount is 0,
// otherwise, return an error
func (server *HttpServer) Shutdown(ctx context.Context) error {
atomic.StoreInt32(&(server.Stopping), 1)
sessionCount := atomic.LoadInt32(&server.Counter.sessionCount)
if sessionCount > 0 {
return errors.Errorf("server shutdown failed, cur-session count:%d, shutdown will be success when wait session-count is 0.", sessionCount)
}
return server.Server.Close()
}
// counterPreHandlerFunc middleware for httpServer session count, before process a request
func (server *HttpServer) counterPreHandlerFunc(c *gin.Context) {
if atomic.LoadInt32(&(server.Stopping)) == 1 {
c.Abort()
c.JSON(http.StatusServiceUnavailable, "server is shutdowning now.")
return
}
server.Counter.incr()
c.Next()
}
// counterPostHandlerFunc middleware for httpServer session count, after process a request
func (server *HttpServer) counterPostHandlerFunc(c *gin.Context) {
c.Next()
server.Counter.decr()
}
// counter session counter
// when server receive a request, sessionCount +1,
// when the request returns a response, sessionCount -1.
type Counter struct {
sessionCount int32
sync.Mutex
}
// incr sessionCount +1 concurrent safely
func (c *Counter) incr() {
c.Lock()
c.sessionCount++
defer c.Unlock()
}
// decr sessionCount -1 concurrent safely
func (c *Counter) decr() {
c.Lock()
c.sessionCount--
defer c.Unlock()
}

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
. "github.com/smartystreets/goconvey/convey"
)
func TestCounter(t *testing.T) {
count := new(Counter)
Convey("counter after init", t, func() {
So(count.sessionCount, ShouldEqual, 0)
})
count.incr()
Convey("counter after incr", t, func() {
So(count.sessionCount, ShouldEqual, 1)
})
count.decr()
Convey("counter after decr", t, func() {
So(count.sessionCount, ShouldEqual, 0)
})
}
func TestHttpServer(t *testing.T) {
server := &HttpServer{
Counter: new(Counter),
Router: gin.Default(),
Server: &http.Server{
Addr: ":0",
},
}
w := httptest.NewRecorder()
server.UseCounter()
server.Router.GET("/foo", fooHandler)
end := make(chan bool, 1)
handler := func(w http.ResponseWriter, r *http.Request) {
server.Router.ServeHTTP(w, r)
time.Sleep(time.Second)
}
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
go func() {
handler(w, req)
end <- true
}()
time.Sleep(10 * time.Millisecond)
t.Run("handle a 1 second request", func(t *testing.T) {
Convey("session count should be 1", t, func() {
So(server.Counter.sessionCount, ShouldEqual, 1)
})
err := server.Shutdown(context.Background())
Convey("server shutdown should fail", t, func() {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, "server shutdown failed")
})
})
<-end
t.Run("handle request end", func(t *testing.T) {
Convey("session count should be 0", t, func() {
So(server.Counter.sessionCount, ShouldEqual, 0)
})
err := server.Shutdown(context.Background())
Convey("server shutdown should success", t, func() {
So(err, ShouldBeNil)
})
})
}
func fooHandler(c *gin.Context) {
time.Sleep(time.Second)
}

View File

@ -0,0 +1,139 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"fmt"
"strconv"
"sync"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
// log "github.com/sirupsen/logrus"
"github.com/oceanbase/configserver/model"
)
const (
CONFIG_URL_FORMAT = "%s/services?Action=ObRootServiceInfo&ObCluster=%s"
)
var obProxyConfigOnce sync.Once
var obProxyConfigFunc func(*gin.Context)
var obProxyConfigWithTemplateOnce sync.Once
var obProxyConfigWithTemplateFunc func(*gin.Context)
func getObProxyConfigFunc() func(*gin.Context) {
obProxyConfigOnce.Do(func() {
obProxyConfigFunc = handlerFunctionWrapper(getObProxyConfig)
})
return obProxyConfigFunc
}
func getObProxyConfigWithTemplateFunc() func(*gin.Context) {
obProxyConfigWithTemplateOnce.Do(func() {
obProxyConfigWithTemplateFunc = handlerFunctionWrapper(getObProxyConfigWithTemplate)
})
return obProxyConfigWithTemplateFunc
}
func getServiceAddress() string {
return fmt.Sprintf("http://%s:%d", GetConfigServer().Config.Vip.Address, GetConfigServer().Config.Vip.Port)
}
func isVersionOnly(c *gin.Context) (bool, error) {
ret := false
var err error
versionOnly, ok := c.GetQuery("VersionOnly")
if ok {
ret, err = strconv.ParseBool(versionOnly)
}
return ret, err
}
func getObProxyConfig(ctxlog context.Context, c *gin.Context) *ApiResponse {
var response *ApiResponse
client := GetConfigServer().Client
versionOnly, err := isVersionOnly(c)
if err != nil {
return NewIllegalArgumentResponse(errors.Wrap(err, "invalid parameter, failed to parse versiononly"))
}
rootServiceInfoUrlMap := make(map[string]*model.RootServiceInfoUrl)
clusters, err := client.ObCluster.Query().All(context.Background())
if err != nil {
return NewErrorResponse(errors.Wrap(err, "query ob clusters"))
}
for _, cluster := range clusters {
rootServiceInfoUrlMap[cluster.Name] = &model.RootServiceInfoUrl{
ObCluster: cluster.Name,
Url: fmt.Sprintf(CONFIG_URL_FORMAT, getServiceAddress(), cluster.Name),
}
}
rootServiceInfoUrls := make([]*model.RootServiceInfoUrl, 0, len(rootServiceInfoUrlMap))
for _, info := range rootServiceInfoUrlMap {
rootServiceInfoUrls = append(rootServiceInfoUrls, info)
}
obProxyConfig, err := model.NewObProxyConfig(getServiceAddress(), rootServiceInfoUrls)
if err != nil {
response = NewErrorResponse(errors.Wrap(err, "generate obproxy config"))
} else {
if versionOnly {
response = NewSuccessResponse(model.NewObProxyConfigVersionOnly(obProxyConfig.Version))
} else {
response = NewSuccessResponse(obProxyConfig)
}
}
return response
}
func getObProxyConfigWithTemplate(ctxlog context.Context, c *gin.Context) *ApiResponse {
var response *ApiResponse
client := GetConfigServer().Client
versionOnly, err := isVersionOnly(c)
if err != nil {
return NewIllegalArgumentResponse(errors.Wrap(err, "invalid parameter, failed to parse versiononly"))
}
clusterMap := make(map[string]interface{})
clusters, err := client.ObCluster.Query().All(context.Background())
if err != nil {
return NewErrorResponse(errors.Wrap(err, "query ob clusters"))
}
for _, cluster := range clusters {
clusterMap[cluster.Name] = nil
}
clusterNames := make([]string, 0, len(clusterMap))
for clusterName := range clusterMap {
clusterNames = append(clusterNames, clusterName)
}
obProxyConfigWithTemplate, err := model.NewObProxyConfigWithTemplate(getServiceAddress(), clusterNames)
if err != nil {
response = NewErrorResponse(errors.Wrap(err, "generate obproxy config with template"))
} else {
if versionOnly {
response = NewSuccessResponse(model.NewObProxyConfigVersionOnly(obProxyConfigWithTemplate.Version))
} else {
response = NewSuccessResponse(obProxyConfigWithTemplate)
}
}
return response
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/oceanbase/configserver/config"
"github.com/oceanbase/configserver/ent"
)
func TestParseVersionOnlyNormal(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=GetObproxyConfig&VersionOnly=true", nil)
versionOnly, err := isVersionOnly(c)
require.True(t, versionOnly)
require.True(t, err == nil)
}
func TestParseVersionOnlyError(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=GetObproxyConfig&VersionOnly=abc", nil)
_, err := isVersionOnly(c)
require.True(t, err != nil)
}
func TestParseVersionOnlyNotExists(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=GetObproxyConfig", nil)
c.Params = []gin.Param{{Key: "Version", Value: "abc"}}
versionOnly, err := isVersionOnly(c)
require.False(t, versionOnly)
require.True(t, err == nil)
}
func TestGetObProxyConfig(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=GetObproxyConfig", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
response := getObProxyConfig(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}
func TestGetObproxyConfigWithTemplate(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=GetObproxyConfig", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
response := getObProxyConfigWithTemplate(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}

View File

@ -0,0 +1,314 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"context"
"encoding/json"
"fmt"
"strconv"
"sync"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/configserver/ent"
"github.com/oceanbase/configserver/ent/obcluster"
"github.com/oceanbase/configserver/model"
)
var obRootServiceGetOnce sync.Once
var obRootServiceGetFunc func(*gin.Context)
var obRootServicePostOnce sync.Once
var obRootServicePostFunc func(*gin.Context)
var obRootServiceDeleteOnce sync.Once
var obRootServiceDeleteFunc func(*gin.Context)
var obIdcRegionInfoOnce sync.Once
var obIdcRegionInfoFunc func(*gin.Context)
func getObIdcRegionInfoFunc() func(c *gin.Context) {
obIdcRegionInfoOnce.Do(func() {
obIdcRegionInfoFunc = handlerFunctionWrapper(getObIdcRegionInfo)
})
return obIdcRegionInfoFunc
}
func getObRootServiceGetFunc() func(*gin.Context) {
obRootServiceGetOnce.Do(func() {
obRootServiceGetFunc = handlerFunctionWrapper(getObRootServiceInfo)
})
return obRootServiceGetFunc
}
func getObRootServicePostFunc() func(*gin.Context) {
obRootServicePostOnce.Do(func() {
obRootServicePostFunc = handlerFunctionWrapper(createOrUpdateObRootServiceInfo)
})
return obRootServicePostFunc
}
func getObRootServiceDeleteFunc() func(*gin.Context) {
obRootServiceDeleteOnce.Do(func() {
obRootServiceDeleteFunc = handlerFunctionWrapper(deleteObRootServiceInfo)
})
return obRootServiceDeleteFunc
}
type RootServiceInfoParam struct {
ObCluster string
ObClusterId int64
Version int
}
func getCommonParam(c *gin.Context) (*RootServiceInfoParam, error) {
var err error
name := ""
obCluster, obClusterOk := c.GetQuery("ObCluster")
obRegion, obRegionOk := c.GetQuery("ObRegion")
if obClusterOk {
name = obCluster
}
if obRegionOk {
name = obRegion
}
if len(name) == 0 {
return nil, errors.New("no obcluster or obregion")
}
obClusterId, obClusterIdOk := c.GetQuery("ObClusterId")
obRegionId, obRegionIdOk := c.GetQuery("ObRegionId")
var clusterId int64
var clusterIdStr string
if obClusterIdOk {
clusterIdStr = obClusterId
}
if obRegionIdOk {
clusterIdStr = obRegionId
}
if clusterIdStr != "" {
clusterId, err = strconv.ParseInt(clusterIdStr, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "parse ob cluster id")
}
}
version := 0
versionStr, versionOk := c.GetQuery("version")
if versionOk {
version, err = strconv.Atoi(versionStr)
if err != nil {
return nil, errors.Wrap(err, "parse version")
}
}
return &RootServiceInfoParam{
ObCluster: name,
ObClusterId: clusterId,
Version: version,
}, nil
}
func selectPrimaryCluster(clusters []*model.ObRootServiceInfo) *model.ObRootServiceInfo {
var primaryCluster *model.ObRootServiceInfo
for _, cluster := range clusters {
if primaryCluster == nil {
primaryCluster = cluster
} else {
if primaryCluster.Type != "PRIMARY" {
if cluster.Type == "PRIMARY" || cluster.TimeStamp > primaryCluster.TimeStamp {
primaryCluster = cluster
}
} else {
if cluster.Type == "PRIMARY" && cluster.TimeStamp > primaryCluster.TimeStamp {
primaryCluster = cluster
}
}
}
}
return primaryCluster
}
func getObIdcRegionInfo(ctxlog context.Context, c *gin.Context) *ApiResponse {
// return empty idc list
param, err := getCommonParam(c)
if err != nil {
return NewErrorResponse(errors.Wrap(err, "parse ob idc region info query parameter"))
}
rootServiceInfoList, err := getRootServiceInfoList(ctxlog, param.ObCluster, param.ObClusterId)
if err != nil {
if rootServiceInfoList != nil && len(rootServiceInfoList) == 0 {
return NewNotFoundResponse(errors.New(fmt.Sprintf("no obcluster found with query param %v", param)))
} else {
return NewErrorResponse(errors.Wrap(err, fmt.Sprintf("get all rootservice info for cluster %s:%d", param.ObCluster, param.ObClusterId)))
}
}
idcList := make([]*model.IdcRegionInfo, 0, 0)
if param.Version < 2 || param.ObClusterId > 0 {
primaryCluster := selectPrimaryCluster(rootServiceInfoList)
obClusterIdcRegionInfo := &model.ObClusterIdcRegionInfo{
Cluster: primaryCluster.ObCluster,
ClusterId: primaryCluster.ObClusterId,
IdcList: idcList,
ReadonlyRsList: "",
}
return NewSuccessResponse(obClusterIdcRegionInfo)
} else {
obClusterIdcRegionInfoList := make([]*model.ObClusterIdcRegionInfo, 0, 4)
for _, cluster := range rootServiceInfoList {
obClusterIdcRegionInfo := &model.ObClusterIdcRegionInfo{
Cluster: cluster.ObCluster,
ClusterId: cluster.ObClusterId,
IdcList: idcList,
ReadonlyRsList: "",
}
obClusterIdcRegionInfoList = append(obClusterIdcRegionInfoList, obClusterIdcRegionInfo)
}
return NewSuccessResponse(obClusterIdcRegionInfoList)
}
}
func getRootServiceInfoList(ctxlog context.Context, obCluster string, obClusterId int64) ([]*model.ObRootServiceInfo, error) {
var clusters []*ent.ObCluster
var err error
rootServiceInfoList := make([]*model.ObRootServiceInfo, 0, 4)
client := GetConfigServer().Client
if obClusterId != 0 {
log.WithContext(ctxlog).Infof("query ob clusters with obcluster %s and obcluster_id %d", obCluster, obClusterId)
clusters, err = client.ObCluster.Query().Where(obcluster.Name(obCluster), obcluster.ObClusterID(obClusterId)).All(context.Background())
} else {
log.WithContext(ctxlog).Infof("query ob clusters with obcluster %s", obCluster)
clusters, err = client.ObCluster.Query().Where(obcluster.Name(obCluster)).All(context.Background())
}
if err != nil {
return nil, errors.Wrap(err, "query ob clusters from db")
}
if len(clusters) == 0 {
return rootServiceInfoList, errors.New(fmt.Sprintf("no root service info found with obcluster %s, obcluster id %d", obCluster, obClusterId))
}
for _, cluster := range clusters {
var rootServiceInfo model.ObRootServiceInfo
err = json.Unmarshal([]byte(cluster.RootserviceJSON), &rootServiceInfo)
if err != nil {
return nil, errors.Wrap(err, "deserialize root service info")
}
rootServiceInfo.Fill()
rootServiceInfoList = append(rootServiceInfoList, &rootServiceInfo)
}
return rootServiceInfoList, nil
}
func getObRootServiceInfo(ctxlog context.Context, c *gin.Context) *ApiResponse {
var response *ApiResponse
param, err := getCommonParam(c)
if err != nil {
return NewErrorResponse(errors.Wrap(err, "parse rootservice query parameter"))
}
rootServiceInfoList, err := getRootServiceInfoList(ctxlog, param.ObCluster, param.ObClusterId)
if err != nil {
if rootServiceInfoList != nil && len(rootServiceInfoList) == 0 {
return NewNotFoundResponse(errors.New(fmt.Sprintf("no obcluster found with query param %v", param)))
} else {
return NewErrorResponse(errors.Wrap(err, fmt.Sprintf("get all rootservice info for cluster %s:%d", param.ObCluster, param.ObClusterId)))
}
}
if param.Version < 2 || param.ObClusterId > 0 {
log.WithContext(ctxlog).Infof("return primary ob cluster")
response = NewSuccessResponse(selectPrimaryCluster(rootServiceInfoList))
} else {
log.WithContext(ctxlog).Infof("return all ob clusters")
response = NewSuccessResponse(rootServiceInfoList)
}
return response
}
func createOrUpdateObRootServiceInfo(ctxlog context.Context, c *gin.Context) *ApiResponse {
var response *ApiResponse
client := GetConfigServer().Client
obRootServiceInfo := new(model.ObRootServiceInfo)
err := c.ShouldBindJSON(obRootServiceInfo)
if err != nil {
return NewErrorResponse(errors.Wrap(err, "bind rootservice query parameter"))
}
obRootServiceInfo.Fill()
param, err := getCommonParam(c)
if err != nil {
return NewErrorResponse(errors.Wrap(err, "parse rootservice query parameter"))
}
if len(obRootServiceInfo.ObCluster) == 0 {
return NewIllegalArgumentResponse(errors.New("ob cluster name is required"))
}
if param.Version > 1 {
if len(obRootServiceInfo.Type) == 0 {
return NewIllegalArgumentResponse(errors.New("ob cluster type is required when version > 1"))
}
}
rsBytes, err := json.Marshal(obRootServiceInfo)
if err != nil {
response = NewErrorResponse(errors.Wrap(err, "serialize ob rootservice info"))
} else {
rootServiceInfoJson := string(rsBytes)
log.WithContext(ctxlog).Infof("store rootservice info %s", rootServiceInfoJson)
err := client.ObCluster.
Create().
SetName(obRootServiceInfo.ObCluster).
SetObClusterID(obRootServiceInfo.ObClusterId).
SetType(obRootServiceInfo.Type).
SetRootserviceJSON(rootServiceInfoJson).
OnConflict().
SetRootserviceJSON(rootServiceInfoJson).
Exec(context.Background())
if err != nil {
response = NewErrorResponse(errors.Wrap(err, "save ob rootservice info"))
} else {
response = NewSuccessResponse("successful")
}
}
return response
}
func deleteObRootServiceInfo(ctxlog context.Context, c *gin.Context) *ApiResponse {
var response *ApiResponse
client := GetConfigServer().Client
param, err := getCommonParam(c)
if err != nil {
return NewErrorResponse(errors.Wrap(err, "parse rootservice query parameter"))
}
if param.Version < 2 {
response = NewIllegalArgumentResponse(errors.New("delete obcluster rs info is only supported when version >= 2"))
} else if param.ObClusterId == 0 {
response = NewIllegalArgumentResponse(errors.New("delete obcluster rs info is only supported with obcluster id"))
} else {
affected, err := client.ObCluster.
Delete().
Where(obcluster.Name(param.ObCluster), obcluster.ObClusterID(param.ObClusterId)).
Exec(context.Background())
if err != nil {
response = NewErrorResponse(errors.Wrap(err, fmt.Sprintf("delete obcluster %s with ob cluster id %d in db", param.ObCluster, param.ObClusterId)))
} else {
log.WithContext(ctxlog).Infof("delete obcluster %s with ob cluster id %d in db, affected rows %d", param.ObCluster, param.ObClusterId, affected)
response = NewSuccessResponse("success")
}
}
return response
}

View File

@ -0,0 +1,286 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/oceanbase/configserver/config"
"github.com/oceanbase/configserver/ent"
)
const testRootServiceJson = "{\"Type\":\"PRIMARY\",\"ObClusterId\":1,\"ObRegionId\":1,\"ObCluster\":\"c1\",\"ObRegion\":\"c1\",\"ReadonlyRsList\":[],\"RsList\":[{\"address\":\"1.1.1.1:2882\",\"role\":\"LEADER\",\"sql_port\":2881}],\"timestamp\":1649435362283000}"
func TestGetRootServiceInfoParamOldVersion(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObRegion=c1&ObRegionId=1", nil)
obRootServiceInfoParam, err := getCommonParam(c)
require.Equal(t, "c1", obRootServiceInfoParam.ObCluster)
require.Equal(t, int64(1), obRootServiceInfoParam.ObClusterId)
require.Equal(t, 0, obRootServiceInfoParam.Version)
require.True(t, err == nil)
}
func TestGetRootServiceInfoParamVersion2(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&ObClusterId=1&version=2", nil)
obRootServiceInfoParam, err := getCommonParam(c)
require.Equal(t, "c1", obRootServiceInfoParam.ObCluster)
require.Equal(t, int64(1), obRootServiceInfoParam.ObClusterId)
require.Equal(t, 2, obRootServiceInfoParam.Version)
require.True(t, err == nil)
}
func TestGetObRootServiceInfo(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
client.ObCluster.
Create().
SetName("c1").
SetObClusterID(1).
SetType("PRIMARY").
SetRootserviceJSON(testRootServiceJson).
OnConflict().
SetRootserviceJSON(testRootServiceJson).
Exec(context.Background())
response := getObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}
func TestGetObRootServiceInfoV2(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&version=2", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
client.ObCluster.
Create().
SetName("c1").
SetObClusterID(1).
SetType("PRIMARY").
SetRootserviceJSON(testRootServiceJson).
OnConflict().
SetRootserviceJSON(testRootServiceJson).
Exec(context.Background())
response := getObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}
func TestGetObRootServiceInfoV2WithObClusterId(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&ObClusterId=1&version=2", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
client.ObCluster.
Create().
SetName("c1").
SetObClusterID(1).
SetType("PRIMARY").
SetRootserviceJSON(testRootServiceJson).
OnConflict().
SetRootserviceJSON(testRootServiceJson).
Exec(context.Background())
response := getObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}
func TestGetObRootServiceInfoNoResult(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c2&ObClusterId=2&version=2", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
response := getObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusNotFound, response.Code)
}
func TestCreateOrUpdateObRootServiceInfo(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&ObClusterId=1&version=2", bytes.NewBuffer([]byte(testRootServiceJson)))
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
response := createOrUpdateObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}
func TestDeleteObRootServiceInfo(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("DELETE", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&ObClusterId=1&version=2", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
client.ObCluster.
Create().
SetName("c1").
SetObClusterID(1).
SetType("PRIMARY").
SetRootserviceJSON(testRootServiceJson).
OnConflict().
SetRootserviceJSON(testRootServiceJson).
Exec(context.Background())
response := deleteObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusOK, response.Code)
}
func TestDeleteObRootServiceInfoVersion1(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("DELETE", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&ObClusterId=1", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
client.ObCluster.
Create().
SetName("c1").
SetObClusterID(1).
SetType("PRIMARY").
SetRootserviceJSON(testRootServiceJson).
OnConflict().
SetRootserviceJSON(testRootServiceJson).
Exec(context.Background())
response := deleteObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusBadRequest, response.Code)
}
func TestDeleteObRootServiceInfoWithoutClusterId(t *testing.T) {
// test gin
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("DELETE", "http://1.1.1.1:8080/services?Action=ObRootServiceInfo&ObCluster=c1&version=2", nil)
// mock db client
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
client.Schema.Create(context.Background())
configServerConfig, _ := config.ParseConfigServerConfig("../etc/config.yaml")
configServer = &ConfigServer{
Config: configServerConfig,
Client: client,
}
client.ObCluster.
Create().
SetName("c1").
SetObClusterID(1).
SetType("PRIMARY").
SetRootserviceJSON(testRootServiceJson).
OnConflict().
SetRootserviceJSON(testRootServiceJson).
Exec(context.Background())
response := deleteObRootServiceInfo(context.Background(), c)
require.Equal(t, http.StatusBadRequest, response.Code)
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"fmt"
"net/http"
)
type ApiResponse struct {
Code int `json:"Code"`
Message string `json:"Message"`
Successful bool `json:"Success"`
Data interface{} `json:"Data"`
TraceId string `json:"Trace"`
Server string `json:"Server"`
Cost int64 `json:"Cost"`
}
type IterableData struct {
Contents interface{} `json:"Contents"`
}
func NewSuccessResponse(data interface{}) *ApiResponse {
return &ApiResponse{
Code: http.StatusOK,
Message: "successful",
Successful: true,
Data: data,
}
}
func NewBadRequestResponse(err error) *ApiResponse {
return &ApiResponse{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("bad request: %v", err),
Successful: false,
}
}
func NewIllegalArgumentResponse(err error) *ApiResponse {
return &ApiResponse{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("illegal argument: %v", err),
Successful: false,
}
}
func NewNotFoundResponse(err error) *ApiResponse {
return &ApiResponse{
Code: http.StatusNotFound,
Message: fmt.Sprintf("resource not found: %v", err),
Successful: false,
}
}
func NewNotImplementedResponse(err error) *ApiResponse {
return &ApiResponse{
Code: http.StatusNotImplemented,
Message: fmt.Sprintf("request not implemented: %v", err),
Successful: false,
}
}
func NewErrorResponse(err error) *ApiResponse {
return &ApiResponse{
Code: http.StatusInternalServerError,
Message: fmt.Sprintf("got internal error: %v", err),
Successful: false,
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OceanBase
* OceanBase CE is licensed under Mulan PubL v2.
* You can use this software according to the terms and conditions of the Mulan PubL v2.
* You may obtain a copy of Mulan PubL v2 at:
* http://license.coscl.org.cn/MulanPubL-2.0
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PubL v2 for more details.
*/
package server
import (
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
)
func InitConfigServerRoutes(r *gin.Engine) {
r.Use(
gin.Recovery(), // gin's crash-free middleware
)
// register pprof for debug
pprof.Register(r, "debug/pprof")
// register route
r.GET("/services", getHandler())
r.POST("/services", postHandler())
r.DELETE("/services", deleteHandler())
}