You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/09/07 09:27:57 UTC

[apisix-dashboard] branch master updated: Feat: manage api support route group (#432)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 316633b  Feat: manage api  support route group (#432)
316633b is described below

commit 316633b76638192709daf7e17de59b090fc43515
Author: liuxiran <be...@126.com>
AuthorDate: Mon Sep 7 17:27:47 2020 +0800

    Feat: manage api  support route group (#432)
    
    * feat(manage-api): add new module route group
    
    * feat(manager-api): add route suport routeGroup
    
    * fix: update some code and lint errors
    
    * fix: support to create new routeGroup while creating route
    
    Co-authored-by: liuxiran <yo...@googl.com>
---
 api/errno/error.go            |   8 ++
 api/route/base.go             |  15 +--
 api/route/route.go            |  51 +++++++++-
 api/route/route_group.go      | 217 ++++++++++++++++++++++++++++++++++++++++++
 api/route/route_group_test.go |  45 +++++++++
 api/script/db/schema.sql      |  14 ++-
 api/service/route.go          |  58 +++++++----
 api/service/route_group.go    | 137 ++++++++++++++++++++++++++
 8 files changed, 517 insertions(+), 28 deletions(-)

diff --git a/api/errno/error.go b/api/errno/error.go
index 93180d8..68f313d 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -86,6 +86,14 @@ var (
 	ApisixConsumerDeleteError = Message{"010704", "APISIX Consumer delete failed", 500}
 	DuplicateUserName         = Message{"010705", "Duplicate consumer username", 400}
 
+	// 08 routeGroup
+	RouteGroupRequestError      = Message{"010801", "RouteGroup request parameters exception: %s", 400}
+	DBRouteGroupError           = Message{"010802", "RouteGroup storage failure: %s", 500}
+	DBRouteGroupDeleteError     = Message{"010803", "RouteGroup storage delete failed: %s", 500}
+	RouteGroupHasRoutesError    = Message{"010804", "Route exist in this route group ", 500}
+	RouteGroupSelectRoutesError = Message{"010805", "RouteGroup select routes failed : %s", 500}
+	DuplicateRouteGroupName     = Message{"010806", "RouteGroup name is duplicate : %s", 500}
+
 	// 99 authentication
 	AuthenticationUserError = Message{"019901", "username or password error", 401}
 )
diff --git a/api/route/base.go b/api/route/base.go
index 1b48d4a..41f5a1c 100644
--- a/api/route/base.go
+++ b/api/route/base.go
@@ -17,13 +17,13 @@
 package route
 
 import (
-  "github.com/gin-contrib/pprof"
-  "github.com/gin-contrib/sessions"
-  "github.com/gin-contrib/sessions/cookie"
-  "github.com/gin-gonic/gin"
+	"github.com/gin-contrib/pprof"
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-contrib/sessions/cookie"
+	"github.com/gin-gonic/gin"
 
-  "github.com/apisix/manager-api/conf"
-  "github.com/apisix/manager-api/filter"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/filter"
 )
 
 func SetUpRouter() *gin.Engine {
@@ -35,7 +35,7 @@ func SetUpRouter() *gin.Engine {
 	r := gin.New()
 	store := cookie.NewStore([]byte("secret"))
 	r.Use(sessions.Sessions("session", store))
-	r.Use(filter.CORS(), filter.Authentication(),filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+	r.Use(filter.CORS(), filter.Authentication(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
 
 	AppendHealthCheck(r)
 	AppendAuthentication(r)
@@ -44,6 +44,7 @@ func SetUpRouter() *gin.Engine {
 	AppendPlugin(r)
 	AppendUpstream(r)
 	AppendConsumer(r)
+	AppendRouteGroup(r)
 
 	pprof.Register(r)
 
diff --git a/api/route/route.go b/api/route/route.go
index c86b437..b6ace35 100644
--- a/api/route/route.go
+++ b/api/route/route.go
@@ -20,6 +20,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/apisix/manager-api/conf"
 	"github.com/apisix/manager-api/errno"
@@ -101,11 +102,19 @@ func listRoute(c *gin.Context) {
 		db = db.Where("upstream_nodes like ? ", "%"+ip+"%")
 		isSearch = false
 	}
+	if rgid, exist := c.GetQuery("route_group_id"); exist {
+		db = db.Where("route_group_id = ?", rgid)
+		isSearch = false
+	}
+	if rgName, exist := c.GetQuery("route_group_name"); exist {
+		db = db.Where("route_group_name like ?", "%"+rgName+"%")
+		isSearch = false
+	}
 	// search
 	if isSearch {
 		if search, exist := c.GetQuery("search"); exist {
 			s := "%" + search + "%"
-			db = db.Where("name like ? or description like ? or hosts like ? or uris like ? or upstream_nodes like ? ", s, s, s, s, s)
+			db = db.Where("name like ? or description like ? or hosts like ? or uris like ? or upstream_nodes like ? or route_group_id = ? or route_group_name like ?", s, s, s, s, s, search, s)
 		}
 	}
 	// mysql
@@ -190,6 +199,15 @@ func updateRoute(c *gin.Context) {
 		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
 		return
 	}
+	routeGroup := &service.RouteGroupDao{}
+	isCreateGroup := false
+	if len(strings.Trim(routeRequest.RouteGroupId, "")) == 0 {
+		isCreateGroup = true
+		routeGroup.ID = uuid.NewV4()
+		// create route group
+		routeGroup.Name = routeRequest.RouteGroupName
+		routeRequest.RouteGroupId = routeGroup.ID.String()
+	}
 	logger.Info(routeRequest.Plugins)
 	db := conf.DB()
 	arr := service.ToApisixRequest(routeRequest)
@@ -205,6 +223,15 @@ func updateRoute(c *gin.Context) {
 			}
 		}()
 		logger.Info(rd)
+		if isCreateGroup {
+			if err := tx.Model(&service.RouteGroupDao{}).Create(routeGroup).Error; err != nil {
+				tx.Rollback()
+				e := errno.FromMessage(errno.DuplicateRouteGroupName, routeGroup.Name)
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+				return
+			}
+		}
 		if err := tx.Model(&service.Route{}).Update(rd).Error; err != nil {
 			// rollback
 			tx.Rollback()
@@ -288,6 +315,8 @@ func findRoute(c *gin.Context) {
 			}
 			result.Script = script
 
+			result.RouteGroupId = route.RouteGroupId
+			result.RouteGroupName = route.RouteGroupName
 			resp, _ := json.Marshal(result)
 			c.Data(http.StatusOK, service.ContentType, resp)
 		}
@@ -311,6 +340,15 @@ func createRoute(c *gin.Context) {
 		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
 		return
 	}
+	routeGroup := &service.RouteGroupDao{}
+	isCreateGroup := false
+	if len(strings.Trim(routeRequest.RouteGroupId, "")) == 0 {
+		isCreateGroup = true
+		routeGroup.ID = uuid.NewV4()
+		// create route group
+		routeGroup.Name = routeRequest.RouteGroupName
+		routeRequest.RouteGroupId = routeGroup.ID.String()
+	}
 	logger.Info(routeRequest.Plugins)
 	db := conf.DB()
 	arr := service.ToApisixRequest(routeRequest)
@@ -326,7 +364,16 @@ func createRoute(c *gin.Context) {
 			}
 		}()
 		logger.Info(rd)
-		if err := tx.Create(rd).Error; err != nil {
+		if isCreateGroup {
+			if err := tx.Model(&service.RouteGroupDao{}).Create(routeGroup).Error; err != nil {
+				tx.Rollback()
+				e := errno.FromMessage(errno.DuplicateRouteGroupName, routeGroup.Name)
+				logger.Error(e.Msg)
+				c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+				return
+			}
+		}
+		if err := tx.Model(&service.Route{}).Create(rd).Error; err != nil {
 			// rollback
 			tx.Rollback()
 			e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
diff --git a/api/route/route_group.go b/api/route/route_group.go
new file mode 100644
index 0000000..bfe138a
--- /dev/null
+++ b/api/route/route_group.go
@@ -0,0 +1,217 @@
+package route
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/apisix/manager-api/service"
+	"github.com/gin-gonic/gin"
+	uuid "github.com/satori/go.uuid"
+)
+
+func AppendRouteGroup(r *gin.Engine) *gin.Engine {
+	r.POST("/apisix/admin/routegroups", createRouteGroup)
+	r.GET("/apisix/admin/routegroups/:gid", findRouteGroup)
+	r.GET("/apisix/admin/routegroups", listRouteGroup)
+	r.GET("/apisix/admin/names/routegroups", listRouteGroupName)
+	r.PUT("/apisix/admin/routegroups/:gid", updateRouteGroup)
+	r.DELETE("/apisix/admin/routegroups/:gid", deleteRouteGroup)
+	r.GET("/apisix/admin/notexist/routegroups", isRouteGroupExist)
+	return r
+}
+
+func isRouteGroupExist(c *gin.Context) {
+	if name, exist := c.GetQuery("name"); exist {
+		db := conf.DB()
+		db = db.Table("route_group")
+		exclude, exist := c.GetQuery("exclude")
+		if exist {
+			db = db.Where("name=? and id<>?", name, exclude)
+		} else {
+			db = db.Where("name=?", name)
+		}
+		var count int
+		if err := db.Count(&count).Error; err != nil {
+			e := errno.FromMessage(errno.RouteGroupRequestError, err.Error())
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+			return
+		} else {
+			if count == 0 {
+				c.Data(http.StatusOK, service.ContentType, errno.Success())
+				return
+			} else {
+				e := errno.FromMessage(errno.DuplicateRouteGroupName, name)
+				c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+				return
+			}
+		}
+	} else {
+		e := errno.FromMessage(errno.RouteGroupRequestError, "name is needed")
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+}
+
+func createRouteGroup(c *gin.Context) {
+	u4 := uuid.NewV4()
+	gid := u4.String()
+	// todo 参数校验
+	param, exist := c.Get("requestBody")
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route_group create with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	// trans params
+	rr := &service.RouteGroupRequest{}
+	if err := rr.Parse(param); err != nil {
+		e := errno.FromMessage(errno.RouteGroupRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	rr.Id = gid
+	fmt.Println(rr)
+	// mysql
+	if rd, err := service.Trans2RouteGroupDao(rr); err != nil {
+		c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+		return
+	} else {
+		if err := rd.CreateRouteGroup(); err != nil {
+			e := errno.FromMessage(errno.DBRouteGroupError, err.Error())
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+			return
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+
+func findRouteGroup(c *gin.Context) {
+	gid := c.Param("gid")
+	routeGroup := &service.RouteGroupDao{}
+	if err, count := routeGroup.FindRouteGroup(gid); err != nil {
+		e := errno.FromMessage(errno.RouteGroupRequestError, err.Error()+"route_group ID:"+gid)
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	} else {
+		if count < 1 {
+			e := errno.FromMessage(errno.RouteRequestError, " route_group ID: "+gid+" not exist")
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(e.Status, e.Response())
+			return
+		}
+	}
+	resp, _ := json.Marshal(routeGroup)
+	c.Data(http.StatusOK, service.ContentType, resp)
+}
+
+func listRouteGroup(c *gin.Context) {
+	size, _ := strconv.Atoi(c.Query("size"))
+	page, _ := strconv.Atoi(c.Query("page"))
+	if size == 0 {
+		size = 10
+	}
+	var s string
+
+	rd := &service.RouteGroupDao{}
+	routeGroupList := []service.RouteGroupDao{}
+	if search, exist := c.GetQuery("search"); exist && len(search) > 0 {
+		s = "%" + search + "%"
+	}
+	if err, count := rd.GetRouteGroupList(&routeGroupList, s, page, size); err != nil {
+		e := errno.FromMessage(errno.RouteGroupRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		result := &service.ListResponse{Count: count, Data: routeGroupList}
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
+
+func listRouteGroupName(c *gin.Context) {
+	db := conf.DB()
+	routeGroupList := []service.RouteGroupDao{}
+	var count int
+	if err := db.Order("name").Table("route_group").Find(&routeGroupList).Count(&count).Error; err != nil {
+		e := errno.FromMessage(errno.RouteGroupRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	} else {
+		responseList := make([]*service.RouteGroupNameResponse, 0)
+		for _, r := range routeGroupList {
+			response, err := r.Parse2NameResponse()
+			if err == nil {
+				responseList = append(responseList, response)
+			}
+		}
+		result := &service.ListResponse{Count: count, Data: responseList}
+		resp, _ := json.Marshal(result)
+		c.Data(http.StatusOK, service.ContentType, resp)
+	}
+}
+
+func updateRouteGroup(c *gin.Context) {
+	// get params
+	gid := c.Param("gid")
+	param, exist := c.Get("requestBody")
+	if !exist || len(param.([]byte)) < 1 {
+		e := errno.FromMessage(errno.RouteRequestError, "route_group update with no post data")
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	// trans params
+	rr := &service.RouteGroupRequest{}
+	if err := rr.Parse(param); err != nil {
+		e := errno.FromMessage(errno.RouteGroupRequestError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	rr.Id = gid
+	if ud, err := service.Trans2RouteGroupDao(rr); err != nil {
+		c.AbortWithStatusJSON(http.StatusInternalServerError, err.Response())
+		return
+	} else {
+		// mysql
+		if err2 := ud.UpdateRouteGroup(); err2 != nil {
+			e := errno.FromMessage(errno.DBRouteGroupError, err2.Error())
+			logger.Error(e.Msg)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+			return
+		}
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
+
+func deleteRouteGroup(c *gin.Context) {
+	gid := c.Param("gid")
+	// 参数校验
+	routeGroup := &service.RouteGroupDao{}
+	if err := conf.DB().Table("route_group").Where("id=?", gid).First(&routeGroup).Error; err != nil {
+		e := errno.FromMessage(errno.RouteGroupRequestError, err.Error()+" route_group ID: "+gid)
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+		return
+	}
+	// delete from mysql
+	routeGroup.ID = uuid.FromStringOrNil(gid)
+	if err := routeGroup.DeleteRouteGroup(); err != nil {
+		e := errno.FromMessage(errno.DBRouteGroupDeleteError, err.Error())
+		logger.Error(e.Msg)
+		c.AbortWithStatusJSON(http.StatusInternalServerError, e.Response())
+		return
+	}
+	c.Data(http.StatusOK, service.ContentType, errno.Success())
+}
diff --git a/api/route/route_group_test.go b/api/route/route_group_test.go
new file mode 100644
index 0000000..0500c8f
--- /dev/null
+++ b/api/route/route_group_test.go
@@ -0,0 +1,45 @@
+package route
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestRouteGroupCurd(t *testing.T) {
+	// create ok
+	handler.
+		Post(uriPrefix+"/routegroups").
+		Header("Authorization", token).
+		JSON(`{
+			"name": "routegroup_test",
+			"description": "test description"
+		}`).
+		Expect(t).
+		Status(http.StatusOK).
+		End()
+
+	//c1, _ := service.GetConsumerByUserName("e2e_test_consumer1")
+	id := "8954a39b-330e-4b85-89f5-d1bbfd785b5b"
+	//update ok
+	handler.
+		Put(uriPrefix+"/routegroups/"+id).
+		Header("Authorization", token).
+		JSON(`{
+			"name": "routegroup_test2",
+			"description": "test description"
+		}`).
+		Expect(t).
+		Status(http.StatusOK).
+		End()
+	// duplicate username
+	handler.
+		Post(uriPrefix+"/routegroups").
+		Header("Authorization", token).
+		JSON(`{
+			"name": "routegroup_test",
+			"description": "test description"
+		}`).
+		Expect(t).
+		Status(http.StatusInternalServerError).
+		End()
+}
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
index b1db5a0..af83929 100644
--- a/api/script/db/schema.sql
+++ b/api/script/db/schema.sql
@@ -16,6 +16,8 @@ CREATE TABLE `routes` (
   `content_admin_api` text,
   `create_time` bigint(20),
   `update_time` bigint(20),
+  `route_group_id` varchar(64) NOT NULL,
+  `route_group_name` varchar(64) NOT NULL,
 
   PRIMARY KEY (`id`)
 ) DEFAULT CHARSET=utf8;
@@ -56,4 +58,14 @@ CREATE TABLE `consumers` (
   `update_time` int(10) unsigned NOT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `uni_username` (`username`)
-) DEFAULT CHARSET=utf8;
\ No newline at end of file
+) DEFAULT CHARSET=utf8;
+-- route_group
+CREATE TABLE `route_group` (
+  `id` varchar(64) NOT NULL unique,
+  `name` varchar(200) NOT NULL unique,
+  `description` varchar(200) DEFAULT NULL,
+  `create_time` bigint(20),
+  `update_time` bigint(20),
+
+  PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
diff --git a/api/service/route.go b/api/service/route.go
index 88cc16b..5a88af9 100644
--- a/api/service/route.go
+++ b/api/service/route.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os/exec"
+	"strings"
 	"time"
 
 	"github.com/apisix/manager-api/conf"
@@ -53,6 +54,13 @@ func (r *RouteRequest) Parse(body interface{}) error {
 		if r.Uris == nil || len(r.Uris) < 1 {
 			r.Uris = []string{"/*"}
 		}
+		if len(strings.Trim(r.RouteGroupId, "")) > 0 {
+			routeGroup := &RouteGroupDao{}
+			if err, _ := routeGroup.FindRouteGroup(r.RouteGroupId); err != nil {
+				return err
+			}
+			r.RouteGroupName = routeGroup.Name
+		}
 	}
 	return nil
 }
@@ -72,6 +80,8 @@ func (rd *Route) Parse(r *RouteRequest, arr *ApisixRouteRequest) error {
 	//rd.Name = arr.Name
 	rd.Description = arr.Desc
 	rd.UpstreamId = r.UpstreamId
+	rd.RouteGroupId = r.RouteGroupId
+	rd.RouteGroupName = r.RouteGroupName
 	if content, err := json.Marshal(r); err != nil {
 		return err
 	} else {
@@ -180,6 +190,8 @@ type RouteRequest struct {
 	UpstreamHeader   map[string]string      `json:"upstream_header,omitempty"`
 	Plugins          map[string]interface{} `json:"plugins"`
 	Script           map[string]interface{} `json:"script"`
+	RouteGroupId     string                 `json:"route_group_id"`
+	RouteGroupName   string                 `json:"route_group_name"`
 }
 
 func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
@@ -277,6 +289,7 @@ func (r *ApisixRouteResponse) Parse() (*RouteRequest, error) {
 		Redirect:         redirect,
 		Upstream:         o.Upstream,
 		UpstreamId:       o.UpstreamId,
+		RouteGroupId:     o.RouteGroupId,
 		UpstreamProtocol: upstreamProtocol,
 		UpstreamPath:     upstreamPath,
 		UpstreamHeader:   upstreamHeader,
@@ -388,17 +401,19 @@ type Node struct {
 }
 
 type Value struct {
-	Id         string                 `json:"id"`
-	Name       string                 `json:"name"`
-	Desc       string                 `json:"desc,omitempty"`
-	Priority   int64                  `json:"priority"`
-	Methods    []string               `json:"methods"`
-	Uris       []string               `json:"uris"`
-	Hosts      []string               `json:"hosts"`
-	Vars       [][]string             `json:"vars"`
-	Upstream   *Upstream              `json:"upstream,omitempty"`
-	UpstreamId string                 `json:"upstream_id,omitempty"`
-	Plugins    map[string]interface{} `json:"plugins"`
+	Id             string                 `json:"id"`
+	Name           string                 `json:"name"`
+	Desc           string                 `json:"desc,omitempty"`
+	Priority       int64                  `json:"priority"`
+	Methods        []string               `json:"methods"`
+	Uris           []string               `json:"uris"`
+	Hosts          []string               `json:"hosts"`
+	Vars           [][]string             `json:"vars"`
+	Upstream       *Upstream              `json:"upstream,omitempty"`
+	UpstreamId     string                 `json:"upstream_id,omitempty"`
+	Plugins        map[string]interface{} `json:"plugins"`
+	RouteGroupId   string                 `json:"route_group_id"`
+	RouteGroupName string                 `json:"route_group_name"`
 }
 
 type Route struct {
@@ -413,17 +428,21 @@ type Route struct {
 	Content         string `json:"content"`
 	Script          string `json:"script"`
 	ContentAdminApi string `json:"content_admin_api"`
+	RouteGroupId    string `json:"route_group_id"`
+	RouteGroupName  string `json:"route_group_name"`
 }
 
 type RouteResponse struct {
 	Base
-	Name        string    `json:"name"`
-	Description string    `json:"description,omitempty"`
-	Hosts       []string  `json:"hosts,omitempty"`
-	Uris        []string  `json:"uris,omitempty"`
-	Upstream    *Upstream `json:"upstream,omitempty"`
-	UpstreamId  string    `json:"upstream_id,omitempty"`
-	Priority    int64     `json:"priority"`
+	Name           string    `json:"name"`
+	Description    string    `json:"description,omitempty"`
+	Hosts          []string  `json:"hosts,omitempty"`
+	Uris           []string  `json:"uris,omitempty"`
+	Upstream       *Upstream `json:"upstream,omitempty"`
+	UpstreamId     string    `json:"upstream_id,omitempty"`
+	Priority       int64     `json:"priority"`
+	RouteGroupId   string    `json:"route_group_id"`
+	RouteGroupName string    `json:"route_group_name"`
 }
 
 type ListResponse struct {
@@ -437,6 +456,8 @@ func (rr *RouteResponse) Parse(r *Route) {
 	rr.Description = r.Description
 	rr.UpstreamId = r.UpstreamId
 	rr.Priority = r.Priority
+	rr.RouteGroupId = r.RouteGroupId
+	rr.RouteGroupName = r.RouteGroupName
 	// hosts
 	if len(r.Hosts) > 0 {
 		var hosts []string
@@ -575,6 +596,7 @@ func ToRoute(routeRequest *RouteRequest,
 	rd.ID = u4
 	// content_admin_api
 	if resp != nil {
+		resp.Node.Value.RouteGroupId = rd.RouteGroupId
 		if respStr, err := json.Marshal(resp); err != nil {
 			e := errno.FromMessage(errno.DBRouteCreateError, err.Error())
 			return nil, e
diff --git a/api/service/route_group.go b/api/service/route_group.go
new file mode 100644
index 0000000..b016883
--- /dev/null
+++ b/api/service/route_group.go
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package service
+
+import (
+	"encoding/json"
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	uuid "github.com/satori/go.uuid"
+)
+
+type RouteGroupDao struct {
+	Base
+	Name        string `json:"name"`
+	Description string `json:"description,omitempty"`
+}
+
+func (RouteGroupDao) TableName() string {
+	return "route_group"
+}
+
+func (rd *RouteGroupDao) CreateRouteGroup() error {
+	return conf.DB().Create(&rd).Error
+}
+
+func (rd *RouteGroupDao) FindRouteGroup(id string) (error, int) {
+	var count int
+	if err := conf.DB().Table("route_group").Where("id=?", id).Count(&count).Error; err != nil {
+		return err, 0
+	}
+	conf.DB().Table("route_group").Where("id=?", id).First(&rd)
+	return nil, count
+}
+
+func (rd *RouteGroupDao) GetRouteGroupList(routeGroupList *[]RouteGroupDao, search string, page, size int) (error, int) {
+	db := conf.DB()
+	db = db.Table(rd.TableName())
+	if len(search) != 0 {
+		db = db.Where("name like ? or description like ? ", search, search)
+	}
+	var count int
+	if err := db.Order("update_time desc").Offset((page - 1) * size).Limit(size).Find(&routeGroupList).Error; err != nil {
+		return err, 0
+	} else {
+		if err := db.Count(&count).Error; err != nil {
+			return err, 0
+		}
+		return nil, count
+	}
+}
+
+func (rd *RouteGroupDao) UpdateRouteGroup() error {
+	db := conf.DB()
+	return db.Model(&RouteGroupDao{}).Update(rd).Error
+}
+
+func (rd *RouteGroupDao) DeleteRouteGroup() error {
+	if err, count := rd.FindRoute(); err != nil {
+		e := errno.FromMessage(errno.RouteGroupSelectRoutesError, err.Error())
+		logger.Error(e.Msg)
+		return e
+	} else {
+		if count > 0 {
+			e := errno.FromMessage(errno.RouteGroupHasRoutesError)
+			logger.Error(e.Msg)
+			return e
+		}
+	}
+	return conf.DB().Delete(&rd).Error
+}
+
+type RouteGroupNameResponse struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+}
+
+func (u *RouteGroupDao) Parse2NameResponse() (*RouteGroupNameResponse, error) {
+	// routeGroup
+	unr := &RouteGroupNameResponse{
+		ID:   u.ID.String(),
+		Name: u.Name,
+	}
+	return unr, nil
+}
+
+type RouteGroupRequest struct {
+	Id          string `json:"id,omitempty"`
+	Name        string `json:"name"`
+	Description string `json:"description"`
+}
+
+func (u *RouteGroupRequest) toJson() []byte {
+	res, _ := json.Marshal(&u)
+	return res
+}
+
+func (r *RouteGroupRequest) Parse(body interface{}) error {
+	if err := json.Unmarshal(body.([]byte), r); err != nil {
+		r = nil
+		return err
+	}
+	return nil
+}
+
+func Trans2RouteGroupDao(r *RouteGroupRequest) (*RouteGroupDao, *errno.ManagerError) {
+
+	u := &RouteGroupDao{
+		Name:        r.Name,
+		Description: r.Description,
+	}
+	// id
+	u.ID = uuid.FromStringOrNil(r.Id)
+	return u, nil
+}
+
+func (r *RouteGroupDao) FindRoute() (error, int) {
+	var count int
+	if err := conf.DB().Table("routes").Where("route_group_id=?", r.ID).Count(&count).Error; err != nil {
+		return err, 0
+	}
+	return nil, count
+}