You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/02/09 11:53:27 UTC

[apisix] branch master updated: feat(grpc-transcode): support .pb file (#6264)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6558ba5  feat(grpc-transcode): support .pb file (#6264)
6558ba5 is described below

commit 6558ba519c304324b84a6364ac4f5fd6be79ffa9
Author: 罗泽轩 <sp...@gmail.com>
AuthorDate: Wed Feb 9 19:53:19 2022 +0800

    feat(grpc-transcode): support .pb file (#6264)
---
 apisix/plugins/grpc-transcode/proto.lua  |  70 +++++++++++---
 apisix/plugins/grpc-transcode/util.lua   |  13 ++-
 docs/en/latest/plugins/grpc-transcode.md |  63 ++++++++++++-
 docs/zh/latest/plugins/grpc-transcode.md |  63 ++++++++++++-
 rockspec/apisix-master-0.rockspec        |   2 +-
 t/grpc_server_example                    |   2 +-
 t/plugin/grpc-transcode2.t               | 152 +++++++++++++++++++++++++++++++
 7 files changed, 343 insertions(+), 22 deletions(-)

diff --git a/apisix/plugins/grpc-transcode/proto.lua b/apisix/plugins/grpc-transcode/proto.lua
index de19be2..e997b55 100644
--- a/apisix/plugins/grpc-transcode/proto.lua
+++ b/apisix/plugins/grpc-transcode/proto.lua
@@ -14,25 +14,23 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-local core        = require("apisix.core")
-local config_util = require("apisix.core.config_util")
-local pb          = require("pb")
-local protoc      = require("protoc")
-local pcall       = pcall
-local ipairs      = ipairs
-local protos
+local core          = require("apisix.core")
+local config_util   = require("apisix.core.config_util")
+local pb            = require("pb")
+local protoc        = require("protoc")
+local pcall         = pcall
+local ipairs        = ipairs
+local decode_base64 = ngx.decode_base64
 
 
+local protos
 local lrucache_proto = core.lrucache.new({
     ttl = 300, count = 100
 })
 
 local proto_fake_file = "filename for loaded"
 
-local function compile_proto(content)
-    -- clear pb state
-    pb.state(nil)
-
+local function compile_proto_text(content)
     protoc.reload()
     local _p  = protoc.new()
     -- the loaded proto won't appears in _p.loaded without a file name after lua-protobuf=0.3.2,
@@ -50,8 +48,6 @@ local function compile_proto(content)
     end
 
     local compiled = _p.loaded
-    -- fetch pb state
-    compiled.pb_state = pb.state(nil)
 
     local index = {}
     for _, s in ipairs(compiled[proto_fake_file].service or {}) do
@@ -69,6 +65,54 @@ local function compile_proto(content)
 end
 
 
+local function compile_proto_bin(content)
+    content = decode_base64(content)
+    if not content then
+        return nil
+    end
+
+    -- pb.load doesn't return err
+    local ok = pb.load(content)
+    if not ok then
+        return nil
+    end
+
+    local index = {}
+    for name, _, methods in pb.services() do
+        local method_index = {}
+        for _, m in ipairs(methods) do
+            method_index[m.name] = m
+        end
+        -- remove the prefix '.'
+        index[name:sub(2)] = method_index
+    end
+
+    local compiled = {}
+    compiled[proto_fake_file] = {}
+    compiled[proto_fake_file].index = index
+
+    return compiled
+end
+
+
+local function compile_proto(content)
+    -- clear pb state
+    pb.state(nil)
+
+    local compiled, err = compile_proto_text(content)
+    if not compiled then
+        compiled = compile_proto_bin(content)
+        if not compiled then
+            return nil, err
+        end
+    end
+
+    -- fetch pb state
+    compiled.pb_state = pb.state(nil)
+    return compiled
+end
+
+
 local _M = {
     version = 0.1,
     compile_proto = compile_proto,
diff --git a/apisix/plugins/grpc-transcode/util.lua b/apisix/plugins/grpc-transcode/util.lua
index 18c9f78..2a7bfb6 100644
--- a/apisix/plugins/grpc-transcode/util.lua
+++ b/apisix/plugins/grpc-transcode/util.lua
@@ -27,23 +27,26 @@ local type              = type
 local _M = {version = 0.1}
 
 
-function _M.find_method(protos, service, method)
-    local loaded = protos[proto_fake_file]
-    if not loaded or type(loaded) ~= "table" then
+function _M.find_method(proto, service, method)
+    local loaded = proto[proto_fake_file]
+    if type(loaded) ~= "table" then
+        core.log.error("compiled proto not found")
         return nil
     end
 
-    if not loaded.index[service] or type(loaded.index[service]) ~= "table" then
+    if type(loaded.index[service]) ~= "table" then
+        core.log.error("compiled proto service not found")
         return nil
     end
 
     local res = loaded.index[service][method]
     if not res then
+        core.log.error("compiled proto method not found")
         return nil
     end
 
     -- restore pb state
-    pb.state(protos.pb_state)
+    pb.state(proto.pb_state)
     return res
 end
 
diff --git a/docs/en/latest/plugins/grpc-transcode.md b/docs/en/latest/plugins/grpc-transcode.md
index 2f576ef..206169e 100644
--- a/docs/en/latest/plugins/grpc-transcode.md
+++ b/docs/en/latest/plugins/grpc-transcode.md
@@ -29,7 +29,7 @@ HTTP(s) -> APISIX -> gRPC server
 
 #### Attributes
 
-* `content`: `.proto` file's content.
+* `content`: `.proto` or `.pb` file's content.
 
 #### Add a proto
 
@@ -52,6 +52,67 @@ curl http://127.0.0.1:9080/apisix/admin/proto/1 -H 'X-API-KEY: edd1c9f034335f136
 }'
 ```
 
+If your `.proto` file contains imports, or you want to combine multiple `.proto` files into a proto,
+you can use `.pb` file to create the proto.
+
+Assumed we have a `.proto` called `proto/helloworld.proto`, which imports another proto file:
+
+```proto
+syntax = "proto3";
+
+package helloworld;
+import "proto/import.proto";
+...
+```
+
+First of all, let's create a `.pb` file from `.proto` files:
+
+```shell
+protoc --include_imports --descriptor_set_out=proto.pb proto/helloworld.proto
+```
+
+The output binary file `proto.pb` will contain both `helloworld.proto` and `import.proto`.
+
+Then we can submit the content of `proto.pb` as the `content` field of the proto.
+
+As the content is binary, we need to encode it in base64 first. Here we use a Python script to do it:
+
+```python
+#!/usr/bin/env python
+# coding: utf-8
+# save this file as upload_pb.py
+import base64
+import sys
+# sudo pip install requests
+import requests
+
+if len(sys.argv) <= 1:
+    print("bad argument")
+    sys.exit(1)
+with open(sys.argv[1], 'rb') as f:
+    content = base64.b64encode(f.read())
+id = sys.argv[2]
+api_key = "edd1c9f034335f136f87ad84b625c8f1" # Change it
+
+reqParam = {
+    "content": content,
+}
+resp = requests.put("http://127.0.0.1:9080/apisix/admin/proto/" + id, json=reqParam, headers={
+    "X-API-KEY": api_key,
+})
+print(resp.status_code)
+print(resp.text)
+```
+
+Create proto:
+
+```bash
+chmod +x ./upload_pb.pb
+./upload_pb.py proto.pb 1
+# 200
+# {"node":{"value":{"create_time":1643879753,"update_time":1643883085,"content":"CmgKEnByb3RvL2ltcG9ydC5wcm90bxIDcGtnIhoKBFVzZXISEgoEbmFtZRgBIAEoCVIEbmFtZSIeCghSZXNwb25zZRISCgRib2R5GAEgASgJUgRib2R5QglaBy4vcHJvdG9iBnByb3RvMwq9AQoPcHJvdG8vc3JjLnByb3RvEgpoZWxsb3dvcmxkGhJwcm90by9pbXBvcnQucHJvdG8iPAoHUmVxdWVzdBIdCgR1c2VyGAEgASgLMgkucGtnLlVzZXJSBHVzZXISEgoEYm9keRgCIAEoCVIEYm9keTI5CgpUZXN0SW1wb3J0EisKA1J1bhITLmhlbGxvd29ybGQuUmVxdWVzdBoNLnBrZy5SZXNwb25zZSIAQglaBy4vcHJvdG9iBnByb3RvMw=="},"key":"\ [...]
+```
+
 ## Attribute List
 
 | Name      | Type                                                                           | Requirement | Default | Valid | Description                      |
diff --git a/docs/zh/latest/plugins/grpc-transcode.md b/docs/zh/latest/plugins/grpc-transcode.md
index d031943..842072e 100644
--- a/docs/zh/latest/plugins/grpc-transcode.md
+++ b/docs/zh/latest/plugins/grpc-transcode.md
@@ -27,7 +27,7 @@ HTTP(s) -> APISIX -> gRPC server
 
 ### 参数
 
-* `content`: `.proto` 文件的内容
+* `content`: `.proto` 或 `.pb` 文件的内容
 
 ### 添加proto
 
@@ -50,6 +50,67 @@ curl http://127.0.0.1:9080/apisix/admin/proto/1 -H 'X-API-KEY: edd1c9f034335f136
 }'
 ```
 
+如果你的 `.proto` 文件包含 import,或者你想把多个 `.proto` 文件合并成一个 proto。
+你可以使用 `.pb` 文件来创建 proto。
+
+假设我们有一个 `.proto` 叫 `proto/helloworld.proto`,它导入了另一个 proto 文件:
+
+```proto
+syntax = "proto3";
+
+package helloworld;
+import "proto/import.proto";
+...
+```
+
+首先,让我们从 `.proto`文件创建一个`.pb`文件。
+
+```shell
+protoc --include_imports --descriptor_set_out=proto.pb proto/helloworld.proto
+```
+
+输出的二进制文件 `proto.pb` 将同时包含 `helloworld.proto` 和 `import.proto`。
+
+然后我们可以将 `proto.pb` 的内容作为 proto 的 `content` 字段提交。
+
+由于内容是二进制的,我们需要先对其进行 base64 编码。这里我们用一个 Python 脚本来做。
+
+```python
+#!/usr/bin/env python
+# coding: utf-8
+# save this file as upload_pb.py
+import base64
+import sys
+# sudo pip install requests
+import requests
+
+if len(sys.argv) <= 1:
+    print("bad argument")
+    sys.exit(1)
+with open(sys.argv[1], 'rb') as f:
+    content = base64.b64encode(f.read())
+id = sys.argv[2]
+api_key = "edd1c9f034335f136f87ad84b625c8f1" # Change it
+
+reqParam = {
+    "content": content,
+}
+resp = requests.put("http://127.0.0.1:9080/apisix/admin/proto/" + id, json=reqParam, headers={
+    "X-API-KEY": api_key,
+})
+print(resp.status_code)
+print(resp.text)
+```
+
+创建proto:
+
+```bash
+chmod +x ./upload_pb.pb
+./upload_pb.py proto.pb 1
+# 200
+# {"node":{"value":{"create_time":1643879753,"update_time":1643883085,"content":"CmgKEnByb3RvL2ltcG9ydC5wcm90bxIDcGtnIhoKBFVzZXISEgoEbmFtZRgBIAEoCVIEbmFtZSIeCghSZXNwb25zZRISCgRib2R5GAEgASgJUgRib2R5QglaBy4vcHJvdG9iBnByb3RvMwq9AQoPcHJvdG8vc3JjLnByb3RvEgpoZWxsb3dvcmxkGhJwcm90by9pbXBvcnQucHJvdG8iPAoHUmVxdWVzdBIdCgR1c2VyGAEgASgLMgkucGtnLlVzZXJSBHVzZXISEgoEYm9keRgCIAEoCVIEYm9keTI5CgpUZXN0SW1wb3J0EisKA1J1bhITLmhlbGxvd29ybGQuUmVxdWVzdBoNLnBrZy5SZXNwb25zZSIAQglaBy4vcHJvdG9iBnByb3RvMw=="},"key":"\ [...]
+```
+
 ## 参数列表
 
 | 名称      | 类型                                                                       | 必选项 | 默认值 | 有效值 | 描述                       |
diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec
index 3bc8121..a6f3411 100644
--- a/rockspec/apisix-master-0.rockspec
+++ b/rockspec/apisix-master-0.rockspec
@@ -46,7 +46,7 @@ dependencies = {
     "lua-resty-session = 2.24",
     "opentracing-openresty = 0.1",
     "lua-resty-radixtree = 2.8.1",
-    "lua-protobuf = 0.3.3",
+    "api7-lua-protobuf = 0.1.0",
     "lua-resty-openidc = 1.7.2-1",
     "luafilesystem = 1.7.0-2",
     "api7-lua-tinyyaml = 0.4.2",
diff --git a/t/grpc_server_example b/t/grpc_server_example
index f7ee318..5e74be6 160000
--- a/t/grpc_server_example
+++ b/t/grpc_server_example
@@ -1 +1 @@
-Subproject commit f7ee318f701e04bf21bf000baab539f5a8bc7eaa
+Subproject commit 5e74be697f24151648be1712fce0ab2fdd0ec964
diff --git a/t/plugin/grpc-transcode2.t b/t/plugin/grpc-transcode2.t
index 7a48077..93b84b2 100644
--- a/t/plugin/grpc-transcode2.t
+++ b/t/plugin/grpc-transcode2.t
@@ -230,3 +230,155 @@ location /t {
 {"message":"Hello world, name: John"}
 --- error_log
 failed to encode request data to protobuf
+
+
+
+=== TEST 6: set binary rule
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin")
+            local json = require("toolkit.json")
+
+            local content = t.read_file("t/grpc_server_example/proto.pb")
+            local data = {content = ngx.encode_base64(content)}
+            local code, body = t.test('/apisix/admin/proto/1',
+                 ngx.HTTP_PUT,
+                json.encode(data)
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code, body = t.test('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/grpctest",
+                    "plugins": {
+                        "grpc-transcode": {
+                            "proto_id": "1",
+                            "service": "helloworld.TestImport",
+                            "method": "Run"
+                        }
+                    },
+                    "upstream": {
+                        "scheme": "grpc",
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:50051": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 7: hit route
+--- request
+POST /grpctest
+{"body":"world","user":{"name":"Hello"}}
+--- more_headers
+Content-Type: application/json
+--- response_body chomp
+{"body":"Hello world"}
+
+
+
+=== TEST 8: service/method not found
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/service_not_found",
+                    "plugins": {
+                        "grpc-transcode": {
+                            "proto_id": "1",
+                            "service": "helloworld.TestImportx",
+                            "method": "Run"
+                        }
+                    },
+                    "upstream": {
+                        "scheme": "grpc",
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:50051": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code, body = t('/apisix/admin/routes/2',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/method_not_found",
+                    "plugins": {
+                        "grpc-transcode": {
+                            "proto_id": "1",
+                            "service": "helloworld.TestImport",
+                            "method": "Runx"
+                        }
+                    },
+                    "upstream": {
+                        "scheme": "grpc",
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:50051": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 9: hit route
+--- request
+POST /service_not_found
+{"body":"world","user":{"name":"Hello"}}
+--- more_headers
+Content-Type: application/json
+--- error_log
+Undefined service method
+--- error_code: 503
+
+
+
+=== TEST 10: hit route
+--- request
+POST /method_not_found
+{"body":"world","user":{"name":"Hello"}}
+--- more_headers
+Content-Type: application/json
+--- error_log
+Undefined service method
+--- error_code: 503