You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by bz...@apache.org on 2021/10/19 05:32:18 UTC

[apisix-dashboard] branch master updated: feat: support proto API (#2099)

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

bzp2010 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 9f17637  feat: support proto API (#2099)
9f17637 is described below

commit 9f17637090627e971d244be33c72629f6dd051a3
Author: bzp2010 <bz...@apache.org>
AuthorDate: Tue Oct 19 00:32:13 2021 -0500

    feat: support proto API (#2099)
---
 .github/workflows/backend-e2e-test.yml    |  14 ++
 api/internal/handler/proto/proto.go       | 260 ++++++++++++++++++++++++++++++
 api/internal/handler/proto/proto_test.go  |  47 ++++++
 api/internal/route.go                     |   2 +
 api/test/docker/docker-compose.yaml       |  10 ++
 api/test/e2enew/base/base.go              |   1 +
 api/test/e2enew/proto/proto_suite_test.go |  40 +++++
 api/test/e2enew/proto/proto_test.go       | 255 +++++++++++++++++++++++++++++
 8 files changed, 629 insertions(+)

diff --git a/.github/workflows/backend-e2e-test.yml b/.github/workflows/backend-e2e-test.yml
index ba8d43d..f870cfa 100644
--- a/.github/workflows/backend-e2e-test.yml
+++ b/.github/workflows/backend-e2e-test.yml
@@ -70,6 +70,13 @@ jobs:
           --set *.cache-from=type=local,src=/tmp/.buildx-cache \
           --set *.cache-to=type=local,dest=/tmp/.buildx-cache
 
+      - name: build and start grpc_server_example
+        working-directory: ./api/test/docker
+        run: |
+          wget https://github.com/api7/grpc_server_example/archive/refs/tags/20210819.tar.gz
+          tar -xzvf 20210819.tar.gz && cd grpc_server_example-20210819
+          docker build -t grpc_server_example:latest .
+
       - name: run docker compose
         working-directory: ./api/test/docker
         run: |
@@ -171,6 +178,13 @@ jobs:
           --set *.cache-from=type=local,src=/tmp/.buildx-cache \
           --set *.cache-to=type=local,dest=/tmp/.buildx-cache
 
+      - name: build and start grpc_server_example
+        working-directory: ./api/test/docker
+        run: |
+          wget https://github.com/api7/grpc_server_example/archive/refs/tags/20210819.tar.gz
+          tar -xzvf 20210819.tar.gz && cd grpc_server_example-20210819
+          docker build -t grpc_server_example:latest .
+
       - name: run docker compose
         working-directory: ./api/test/docker
         run: |
diff --git a/api/internal/handler/proto/proto.go b/api/internal/handler/proto/proto.go
new file mode 100644
index 0000000..6c815e7
--- /dev/null
+++ b/api/internal/handler/proto/proto.go
@@ -0,0 +1,260 @@
+/*
+ * 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 proto
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+	"github.com/shiningrush/droplet"
+	"github.com/shiningrush/droplet/data"
+	"github.com/shiningrush/droplet/wrapper"
+	wgin "github.com/shiningrush/droplet/wrapper/gin"
+
+	"github.com/apisix/manager-api/internal/core/entity"
+	"github.com/apisix/manager-api/internal/core/store"
+	"github.com/apisix/manager-api/internal/handler"
+	"github.com/apisix/manager-api/internal/utils"
+)
+
+type Handler struct {
+	routeStore        store.Interface
+	serviceStore      store.Interface
+	consumerStore     store.Interface
+	pluginConfigStore store.Interface
+	globalRuleStore   store.Interface
+	protoStore        store.Interface
+}
+
+func NewHandler() (handler.RouteRegister, error) {
+	return &Handler{
+		routeStore:        store.GetStore(store.HubKeyRoute),
+		serviceStore:      store.GetStore(store.HubKeyService),
+		consumerStore:     store.GetStore(store.HubKeyConsumer),
+		pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
+		globalRuleStore:   store.GetStore(store.HubKeyGlobalRule),
+		protoStore:        store.GetStore(store.HubKeyProto),
+	}, nil
+}
+
+func (h *Handler) ApplyRoute(r *gin.Engine) {
+	r.GET("/apisix/admin/proto/:id", wgin.Wraps(h.Get,
+		wrapper.InputType(reflect.TypeOf(GetInput{}))))
+	r.GET("/apisix/admin/proto", wgin.Wraps(h.List,
+		wrapper.InputType(reflect.TypeOf(ListInput{}))))
+	r.POST("/apisix/admin/proto", wgin.Wraps(h.Create,
+		wrapper.InputType(reflect.TypeOf(entity.Proto{}))))
+	r.PUT("/apisix/admin/proto", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
+	r.PUT("/apisix/admin/proto/:id", wgin.Wraps(h.Update,
+		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
+	r.PATCH("/apisix/admin/proto/:id", wgin.Wraps(h.Patch,
+		wrapper.InputType(reflect.TypeOf(PatchInput{}))))
+	r.PATCH("/apisix/admin/proto/:id/*path", wgin.Wraps(h.Patch,
+		wrapper.InputType(reflect.TypeOf(PatchInput{}))))
+	r.DELETE("/apisix/admin/proto/:ids", wgin.Wraps(h.BatchDelete,
+		wrapper.InputType(reflect.TypeOf(BatchDeleteInput{}))))
+}
+
+var plugins = []string{"grpc-transcode"}
+
+type GetInput struct {
+	ID string `auto_read:"id,path" validate:"required"`
+}
+
+func (h *Handler) Get(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*GetInput)
+
+	r, err := h.protoStore.Get(c.Context(), input.ID)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return r, nil
+}
+
+type ListInput struct {
+	Desc string `auto_read:"desc,query"`
+	store.Pagination
+}
+
+func (h *Handler) List(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*ListInput)
+
+	ret, err := h.protoStore.List(c.Context(), store.ListInput{
+		Predicate: func(obj interface{}) bool {
+			if input.Desc != "" {
+				return strings.Contains(obj.(*entity.Proto).Desc, input.Desc)
+			}
+			return true
+		},
+		PageSize:   input.PageSize,
+		PageNumber: input.PageNumber,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return ret, nil
+}
+
+func (h *Handler) Create(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*entity.Proto)
+
+	// check proto id exist
+	if input.ID != nil {
+		protoID := utils.InterfaceToString(input.ID)
+		ret, err := h.protoStore.Get(c.Context(), protoID)
+		if err != nil && err != data.ErrNotFound {
+			return handler.SpecCodeResponse(err), err
+		}
+		if ret != nil {
+			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, errors.New("proto id exists")
+		}
+	}
+
+	// create
+	ret, err := h.protoStore.Create(c.Context(), input)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return ret, nil
+}
+
+type UpdateInput struct {
+	ID string `auto_read:"id,path"`
+	entity.Proto
+}
+
+func (h *Handler) Update(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*UpdateInput)
+
+	// check if ID in body is equal ID in path
+	if err := handler.IDCompare(input.ID, input.Proto.ID); err != nil {
+		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
+	}
+
+	if input.ID != "" {
+		input.Proto.ID = input.ID
+	}
+
+	res, err := h.protoStore.Update(c.Context(), &input.Proto, true)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return res, nil
+}
+
+type PatchInput struct {
+	ID      string `auto_read:"id,path"`
+	SubPath string `auto_read:"path,path"`
+	Body    []byte `auto_read:"@body"`
+}
+
+func (h *Handler) Patch(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*PatchInput)
+	reqBody := input.Body
+	id := input.ID
+	subPath := input.SubPath
+
+	stored, err := h.protoStore.Get(c.Context(), id)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	res, err := utils.MergePatch(stored, subPath, reqBody)
+
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	var proto entity.Proto
+	if err := json.Unmarshal(res, &proto); err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	ret, err := h.protoStore.Update(c.Context(), &proto, false)
+	if err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return ret, nil
+}
+
+type BatchDeleteInput struct {
+	IDs string `auto_read:"ids,path"`
+}
+
+func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
+	input := c.Input().(*BatchDeleteInput)
+
+	ids := strings.Split(input.IDs, ",")
+	checklist := []store.Interface{h.routeStore, h.consumerStore, h.serviceStore, h.pluginConfigStore, h.globalRuleStore}
+
+	for _, id := range ids {
+		for _, store := range checklist {
+			if err := h.checkProtoUsed(c.Context(), store, id); err != nil {
+				return handler.SpecCodeResponse(err), err
+			}
+		}
+	}
+
+	if err := h.protoStore.BatchDelete(c.Context(), ids); err != nil {
+		return handler.SpecCodeResponse(err), err
+	}
+
+	return nil, nil
+}
+
+func (h *Handler) checkProtoUsed(ctx context.Context, storeInterface store.Interface, key string) error {
+	ret, err := storeInterface.List(ctx, store.ListInput{
+		Predicate: func(obj interface{}) bool {
+			record := obj.(entity.GetPlugins)
+			for _, plugin := range plugins {
+				if _, ok := record.GetPlugins()[plugin]; ok {
+					configs := record.GetPlugins()[plugin].(map[string]interface{})
+					protoId := utils.InterfaceToString(configs["proto_id"])
+					if protoId == key {
+						return true
+					}
+				}
+			}
+			return false
+		},
+		Format: func(obj interface{}) interface{} {
+			return obj.(entity.GetPlugins)
+		},
+		PageSize:   0,
+		PageNumber: 0,
+	})
+	if err != nil {
+		return err
+	}
+	if ret.TotalSize > 0 {
+		return fmt.Errorf("proto used check invalid: %s: %s is using this proto", storeInterface.Type(), ret.Rows[0].(entity.GetBaseInfo).GetBaseInfo().ID)
+	}
+	return nil
+
+}
diff --git a/api/internal/handler/proto/proto_test.go b/api/internal/handler/proto/proto_test.go
new file mode 100644
index 0000000..942dacd
--- /dev/null
+++ b/api/internal/handler/proto/proto_test.go
@@ -0,0 +1,47 @@
+/*
+ * 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 proto
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/apisix/manager-api/internal/core/entity"
+)
+
+func TestStructUnmarshal(t *testing.T) {
+	// define and parse data
+	jsonStr := `{
+    "id": 1,
+    "create_time": 1700000000,
+    "update_time": 1700000000,
+    "desc": "desc",
+    "content": "content"
+}`
+	proto := entity.Proto{}
+	err := json.Unmarshal([]byte(jsonStr), &proto)
+
+	// asserts
+	assert.Nil(t, err)
+	assert.Equal(t, proto.ID, float64(1))
+	assert.Equal(t, proto.CreateTime, int64(1700000000))
+	assert.Equal(t, proto.UpdateTime, int64(1700000000))
+	assert.Equal(t, proto.Desc, "desc")
+	assert.Equal(t, proto.Content, "content")
+}
diff --git a/api/internal/route.go b/api/internal/route.go
index 06aa690..6a507cc 100644
--- a/api/internal/route.go
+++ b/api/internal/route.go
@@ -35,6 +35,7 @@ import (
 	"github.com/apisix/manager-api/internal/handler/label"
 	"github.com/apisix/manager-api/internal/handler/migrate"
 	"github.com/apisix/manager-api/internal/handler/plugin_config"
+	"github.com/apisix/manager-api/internal/handler/proto"
 	"github.com/apisix/manager-api/internal/handler/route"
 	"github.com/apisix/manager-api/internal/handler/schema"
 	"github.com/apisix/manager-api/internal/handler/server_info"
@@ -77,6 +78,7 @@ func SetUpRouter() *gin.Engine {
 		tool.NewHandler,
 		plugin_config.NewHandler,
 		migrate.NewHandler,
+		proto.NewHandler,
 	}
 
 	for i := range factories {
diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml
index 4782d0e..ce4fe03 100644
--- a/api/test/docker/docker-compose.yaml
+++ b/api/test/docker/docker-compose.yaml
@@ -125,6 +125,16 @@ services:
       apisix_dashboard_e2e:
         ipv4_address: 172.16.238.20
 
+  upstream_grpc:
+    image: grpc_server_example
+    restart: always
+    ports:
+      - '50051:50051'
+      - '50052:50052'
+    networks:
+      apisix_dashboard_e2e:
+        ipv4_address: 172.16.238.21
+
   apisix:
     hostname: apisix_server1
     image: apache/apisix:2.10.0-alpine
diff --git a/api/test/e2enew/base/base.go b/api/test/e2enew/base/base.go
index 32253ff..ced3bf8 100644
--- a/api/test/e2enew/base/base.go
+++ b/api/test/e2enew/base/base.go
@@ -37,6 +37,7 @@ var (
 	token string
 
 	UpstreamIp             = "172.16.238.20"
+	UpstreamGrpcIp         = "172.16.238.21"
 	APISIXHost             = "http://127.0.0.1:9080"
 	APISIXInternalUrl      = "http://172.16.238.30:9080"
 	APISIXSingleWorkerHost = "http://127.0.0.1:9081"
diff --git a/api/test/e2enew/proto/proto_suite_test.go b/api/test/e2enew/proto/proto_suite_test.go
new file mode 100644
index 0000000..ab0b826
--- /dev/null
+++ b/api/test/e2enew/proto/proto_suite_test.go
@@ -0,0 +1,40 @@
+/*
+ * 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 proto
+
+import (
+	"testing"
+	"time"
+
+	"github.com/onsi/ginkgo"
+	"github.com/onsi/gomega"
+
+	"github.com/apisix/manager-api/test/e2enew/base"
+)
+
+func TestRoute(t *testing.T) {
+	gomega.RegisterFailHandler(ginkgo.Fail)
+	ginkgo.RunSpecs(t, "proto suite")
+}
+
+var _ = ginkgo.AfterSuite(func() {
+	base.CleanResource("routes")
+	base.CleanResource("upstreams")
+	base.CleanResource("consumers")
+	base.CleanResource("proto")
+	time.Sleep(base.SleepTime)
+})
diff --git a/api/test/e2enew/proto/proto_test.go b/api/test/e2enew/proto/proto_test.go
new file mode 100644
index 0000000..0b16980
--- /dev/null
+++ b/api/test/e2enew/proto/proto_test.go
@@ -0,0 +1,255 @@
+/*
+ * 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 proto
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/onsi/ginkgo"
+	"github.com/onsi/gomega"
+
+	"github.com/apisix/manager-api/test/e2enew/base"
+)
+
+var correctProtobuf = `syntax = "proto3";
+    package helloworld;
+    service Greeter {
+        rpc SayHello (HelloRequest) returns (HelloReply) {}
+    }
+    message HelloRequest {
+        string name = 1;
+    }
+    message HelloReply {
+        string message = 1;
+    }`
+
+var _ = ginkgo.Describe("Proto", func() {
+	ginkgo.It("create proto success", func() {
+		createProtoBody := make(map[string]interface{})
+		createProtoBody["id"] = 1
+		createProtoBody["desc"] = "test_proto1"
+		createProtoBody["content"] = correctProtobuf
+
+		_createProtoBody, err := json.Marshal(createProtoBody)
+		gomega.Expect(err).To(gomega.BeNil())
+
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodPost,
+			Path:         "/apisix/admin/proto",
+			Body:         string(_createProtoBody),
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("create proto failed, id existed", func() {
+		createProtoBody := make(map[string]interface{})
+		createProtoBody["id"] = 1
+		createProtoBody["desc"] = "test_proto1"
+		createProtoBody["content"] = correctProtobuf
+
+		_createProtoBody, err := json.Marshal(createProtoBody)
+		gomega.Expect(err).To(gomega.BeNil())
+
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodPost,
+			Path:         "/apisix/admin/proto",
+			Body:         string(_createProtoBody),
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "proto id exists",
+			ExpectStatus: http.StatusBadRequest,
+		})
+	})
+	ginkgo.It("update proto success", func() {
+		updateProtoBody := make(map[string]interface{})
+		updateProtoBody["id"] = 1
+		updateProtoBody["desc"] = "test_proto1_modify"
+		updateProtoBody["content"] = correctProtobuf
+
+		_updateProtoBody, err := json.Marshal(updateProtoBody)
+		gomega.Expect(err).To(gomega.BeNil())
+
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodPut,
+			Path:         "/apisix/admin/proto",
+			Body:         string(_updateProtoBody),
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "test_proto1_modify",
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("list proto", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/proto",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "test_proto1_modify",
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("get proto", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodGet,
+			Path:         "/apisix/admin/proto/1",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "test_proto1_modify",
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("delete not existed proto", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/proto/not-exist",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusNotFound,
+		})
+	})
+	ginkgo.It("delete proto", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/proto/1",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		})
+	})
+})
+
+var _ = ginkgo.Describe("Proto with grpc-transcode plugin", func() {
+	ginkgo.It("create proto success", func() {
+		createProtoBody := make(map[string]interface{})
+		createProtoBody["id"] = 1
+		createProtoBody["desc"] = "test_proto1"
+		createProtoBody["content"] = correctProtobuf
+
+		_createProtoBody, err := json.Marshal(createProtoBody)
+		gomega.Expect(err).To(gomega.BeNil())
+
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodPost,
+			Path:         "/apisix/admin/proto",
+			Body:         string(_createProtoBody),
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("create route with grpc-transcode", func() {
+		createRouteBody := make(map[string]interface{})
+		createRouteBody["id"] = 1
+		createRouteBody["name"] = "test_route"
+		createRouteBody["uri"] = "/grpc_test"
+		createRouteBody["methods"] = []string{"GET", "POST"}
+		createRouteBody["upstream"] = map[string]interface{}{
+			"nodes": []map[string]interface{}{
+				{
+					"host":   base.UpstreamGrpcIp,
+					"port":   50051,
+					"weight": 1,
+				},
+			},
+			"type":   "roundrobin",
+			"scheme": "grpc",
+		}
+		createRouteBody["plugins"] = map[string]interface{}{
+			"grpc-transcode": map[string]interface{}{
+				"method":   "SayHello",
+				"proto_id": "1",
+				"service":  "helloworld.Greeter",
+			},
+		}
+
+		_createRouteBody, err := json.Marshal(createRouteBody)
+		gomega.Expect(err).To(gomega.BeNil())
+
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodPost,
+			Path:         "/apisix/admin/routes",
+			Body:         string(_createRouteBody),
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("hit GET route for grpc-transcode test", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.APISIXExpect(),
+			Method:       http.MethodGet,
+			Path:         "/grpc_test",
+			Query:        "name=world",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "{\"message\":\"Hello world\"}",
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("hit POST route for grpc-transcode test", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.APISIXExpect(),
+			Method:       http.MethodPost,
+			Path:         "/grpc_test",
+			Body:         "name=world",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "{\"message\":\"Hello world\"}",
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("hit JSON POST route for grpc-transcode test", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.APISIXExpect(),
+			Method:       http.MethodPost,
+			Path:         "/grpc_test",
+			Body:         "{\"name\": \"world\"}",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "{\"message\":\"Hello world\"}",
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("delete route used proto", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/proto/1",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectBody:   "proto used check invalid: route",
+			ExpectStatus: http.StatusBadRequest,
+		})
+	})
+	ginkgo.It("delete conflict route", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/routes/1",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		})
+	})
+	ginkgo.It("delete proto again", func() {
+		base.RunTestCase(base.HttpTestCase{
+			Object:       base.ManagerApiExpect(),
+			Method:       http.MethodDelete,
+			Path:         "/apisix/admin/proto/1",
+			Headers:      map[string]string{"Authorization": base.GetToken()},
+			ExpectStatus: http.StatusOK,
+		})
+	})
+})