add ob configserver (#943)
This commit is contained in:
82
tools/ob-configserver/server/configserver.go
Normal file
82
tools/ob-configserver/server/configserver.go
Normal 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
|
||||
}
|
||||
128
tools/ob-configserver/server/handler.go
Normal file
128
tools/ob-configserver/server/handler.go
Normal 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)
|
||||
}
|
||||
140
tools/ob-configserver/server/http_server.go
Normal file
140
tools/ob-configserver/server/http_server.go
Normal 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()
|
||||
}
|
||||
93
tools/ob-configserver/server/http_server_test.go
Normal file
93
tools/ob-configserver/server/http_server_test.go
Normal 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)
|
||||
}
|
||||
139
tools/ob-configserver/server/obproxy_handler.go
Normal file
139
tools/ob-configserver/server/obproxy_handler.go
Normal 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
|
||||
}
|
||||
101
tools/ob-configserver/server/obproxy_handler_test.go
Normal file
101
tools/ob-configserver/server/obproxy_handler_test.go
Normal 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)
|
||||
}
|
||||
314
tools/ob-configserver/server/observer_handler.go
Normal file
314
tools/ob-configserver/server/observer_handler.go
Normal 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
|
||||
|
||||
}
|
||||
286
tools/ob-configserver/server/observer_handler_test.go
Normal file
286
tools/ob-configserver/server/observer_handler_test.go
Normal 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)
|
||||
}
|
||||
81
tools/ob-configserver/server/response.go
Normal file
81
tools/ob-configserver/server/response.go
Normal 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,
|
||||
}
|
||||
}
|
||||
32
tools/ob-configserver/server/router.go
Normal file
32
tools/ob-configserver/server/router.go
Normal 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())
|
||||
}
|
||||
Reference in New Issue
Block a user