Add Plugin de-graphql (#303)

This commit is contained in:
Jun
2023-04-27 18:45:19 +08:00
committed by GitHub
parent e2b4a52c9e
commit 311d5c21c2
9 changed files with 971 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
# DeGraphQL
## GraphQL
### GraphQL 端点
REST API 有多个端点GraphQL API 只有一个端点。
```shell
https://api.github.com/graphql
```
### 与 GraphQL 通信
由于 GraphQL 操作由多行 JSON 组成,可以使用 curl 或任何其他采用 HTTP 的库。
在 REST 中HTTP 谓词确定执行的操作。 在 GraphQL 中,执行查询要提供 JSON 请求体,因此 HTTP 谓词为 POST。 唯一的例外是内省查询,它是一种简单的 GET 到终结点查询。
### GraphQL POST 请求参数
标准的 GraphQL POST 请求情况如下:
- 添加 HTTP 请求头: Content-Type: application/json
- 使用 JSON 格式的请求体
- JSON 请求体包含三个字段
- query查询文档必填
- variables变量选填
- operationName操作名称选填查询文档有多个操作时必填
```json
{
"query": "{viewer{name}}",
"operationName": "",
"variables": {
"name": "value"
}
}
```
### GraphQL 基本参数类型
- 基本参数类型包含: String, Int, Float, Boolean
- [类型]代表数组,例如:[Int]代表整型数组
- GraphQL 基本参数传递
- 小括号内定义形参,注意:参数需要定义类型
- !(叹号)代表参数不能为空
```shell
query ($owner : String!, $name : String!) {
repository(owner: $owner, name: $name) {
name
forkCount
description
}
}
```
### GitHub GraphQL 测试
使用 curl 命令查询 GraphQL 用有效 JSON 请求体发出 POST 请求。 有效请求体必须包含一个名为 query 的字符串。
```shell
curl https://api.github.com/graphql -X POST \
-H "Authorization: bearer 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" \
-d "{\"query\": \"query { viewer { login }}\"}"
{
"data": {
"viewer": {
"login": "2456868764"
}
}
}
```
```shell
curl 'https://api.github.com/graphql' -X POST \
-H 'Authorization: bearer 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' \
-d '{"query":"query ($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n name\n forkCount\n description\n }\n}\n","variables":{"owner":"2456868764","name":"higress"}}'
{
"data": {
"repository": {
"name": "higress",
"forkCount": 149,
"description": "Next-generation Cloud Native Gateway | 下一代云原生网关"
}
}
}
```
## DeGraphQL 插件
### 参数配置
| 参数 | 描述 | 默认 |
|:----------------|:------------------------|:-----------|
| `gql` | graphql 查询 | 不能为空 |
| `endpoint` | graphql 查询端点 | `/graphql` |
| `timeout` | 查询连接超时,单位毫秒 | `5000` |
| `serviceSource` | 服务来源k8s, nacos,dns, ip | 不能为空 |
| `serviceName` | 服务名称 | 不能为空 |
| `servicePort` | 服务端口 | 不能为空 |
| `namespace` | 服务命名空间, 当服务来源是nacos需要配置 | |
| `domain` | 服务域名当服务来源是dns配置 | |
### 插件使用
https://github.com/alibaba/higress/issues/268
- 测试配置
```yaml
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: default
namespace: higress-system
spec:
registries:
- domain: api.github.com
name: github
port: 443
type: dns
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: github.dns
higress.io/upstream-vhost: "api.github.com"
higress.io/backend-protocol: HTTPS
name: github-api
namespace: higress-system
spec:
ingressClassName: higress
rules:
- http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /api
pathType: Prefix
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: de-graphql-github-api
namespace: higress-system
spec:
matchRules:
- ingress:
- github-api
config:
timeout: 5000
endpoint: /graphql
serviceSource: dns
serviceName: github
servicePort: 443
domain: api.github.com
gql: |
query ($owner:String! $name:String!){
repository(owner:$owner, name:$name) {
name
forkCount
description
}
}
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/de-graphql:1.0.0
```
- 测试结果
```shell
curl "http://localhost/api?owner=alibaba&name=higress" -H "Authorization: Bearer some-token"
{
"data": {
"repository": {
"description": "Next-generation Cloud Native Gateway",
"forkCount": 149,
"name": "higress"
}
}
}
```
## 参考文档
- https://github.com/graphql/graphql-spec
- https://docs.github.com/zh/graphql/guides/forming-calls-with-graphql
- https://github.com/altair-graphql/altair

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,199 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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 config
import (
"errors"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"net/url"
"regexp"
"strings"
)
const (
DefaultEndpoint string = "/graphql"
DefaultConnectionTimeout uint32 = 5000
)
var gqlVariableRegex = regexp.MustCompile(`\$(\w+)\s*:\s*(String|Float|Int|Boolean)(!?)`)
type VariableType string
const (
StringType VariableType = "String"
IntType VariableType = "Int"
FloatType VariableType = "Float"
BooleanType VariableType = "Boolean"
)
type Variable struct {
name string
typ VariableType
blank bool
value string
}
type DeGraphQLConfig struct {
client wrapper.HttpClient
gql string
endpoint string
timeout uint32
variables []Variable
}
func (d *DeGraphQLConfig) SetEndpoint(endpoint string) error {
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
d.endpoint = DefaultEndpoint
} else {
d.endpoint = endpoint
}
return nil
}
func (d *DeGraphQLConfig) GetEndpoint() string {
return d.endpoint
}
func (d *DeGraphQLConfig) GetTimeout() uint32 {
return d.timeout
}
func (d *DeGraphQLConfig) SetTimeout(timeout uint32) {
if timeout <= 0 {
d.timeout = DefaultConnectionTimeout
} else {
d.timeout = timeout
}
}
func (d *DeGraphQLConfig) SetClient(client wrapper.HttpClient) {
d.client = client
}
func (d *DeGraphQLConfig) GetClient() wrapper.HttpClient {
return d.client
}
func (d *DeGraphQLConfig) SetGql(gql string) error {
if strings.TrimSpace(gql) == "" {
return errors.New("gql can't be empty")
}
d.gql = gql
d.variables = make([]Variable, 0)
matches := gqlVariableRegex.FindAllStringSubmatch(d.gql, -1)
if len(matches) > 0 {
for _, subMatch := range matches {
variable := Variable{}
variable.name = subMatch[1]
switch subMatch[2] {
case "String":
variable.typ = StringType
case "Float":
variable.typ = FloatType
case "Int":
variable.typ = IntType
case "Boolean":
variable.typ = BooleanType
}
variable.blank = subMatch[3] != "!"
d.variables = append(d.variables, variable)
}
}
return nil
}
func (d *DeGraphQLConfig) GetGql() string {
return d.gql
}
func (d *DeGraphQLConfig) GetVersion() string {
return "1.0.0"
}
func (d *DeGraphQLConfig) ParseGqlFromUrl(requestUrl string) (string, error) {
if strings.TrimSpace(requestUrl) == "" {
return "", errors.New("request url can't be empty")
}
url, _ := url.Parse(requestUrl)
queryValues := url.Query()
values := make(map[string]string, len(queryValues))
for k, v := range queryValues {
var v1 string
if len(v) > 1 {
v1 = strings.Join(v, ",")
} else {
v1 = v[0]
}
values[k] = v1
}
variables := make([]Variable, 0, len(d.variables))
for _, variable := range d.variables {
val, ok := values[variable.name]
// TODO validate variable type and blank
if ok {
variables = append(variables, Variable{
name: variable.name,
typ: variable.typ,
blank: variable.blank,
value: val,
})
}
}
var build strings.Builder
// write query
build.WriteString("{\"query\":")
build.WriteString("\"")
build.WriteString(getJsonStr(d.gql))
build.WriteString("\"")
// write varialbes
if len(variables) > 0 {
index := 0
build.WriteString(",")
build.WriteString("\"variables\":{")
for _, variable := range variables {
build.WriteString("\"")
build.WriteString(variable.name)
build.WriteString("\":")
if variable.typ == StringType {
build.WriteString("\"")
build.WriteString(getJsonStr(variable.value))
build.WriteString("\"")
} else {
build.WriteString(variable.value)
}
if index < len(variables)-1 {
build.WriteString(",")
}
index++
}
build.WriteString("}")
}
build.WriteString("}")
return build.String(), nil
}
func getJsonStr(str string) string {
d := strings.ReplaceAll(str, "\"", "\\\"")
return strings.ReplaceAll(d, "\n", "\\n")
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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 config
import (
"errors"
"github.com/stretchr/testify/assert"
"testing"
)
func TestDeGraphQLConfig_SetGql(t *testing.T) {
tests := []struct {
name string
gql string
wantVariables []Variable
wantErr error
}{
{
name: "empty gql",
gql: "",
wantErr: errors.New("gql can't be empty"),
},
{
name: "no params",
gql: "query",
wantVariables: []Variable{},
wantErr: nil,
},
{
name: "four params",
gql: "query ($owner:String $num:Float! $int : Int! $boolean : Boolean )",
wantErr: nil,
wantVariables: []Variable{
{
name: "owner",
typ: StringType,
blank: true,
},
{
name: "num",
typ: FloatType,
blank: false,
},
{
name: "int",
typ: IntType,
blank: false,
},
{
name: "boolean",
typ: BooleanType,
blank: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &DeGraphQLConfig{}
err := d.SetGql(tt.gql)
assert.Equal(t, tt.wantErr, err)
if err != nil {
return
}
assert.Equal(t, tt.wantVariables, d.variables)
})
}
}
func TestDeGraphQLConfig_ParseGqlFromUrl(t *testing.T) {
tests := []struct {
name string
gql string
url string
want string
wantErr error
}{
{
name: "empty url",
gql: "query ($owner:String! $name:String!)",
url: "",
want: "",
wantErr: errors.New("request url can't be empty"),
},
{
name: "no params",
gql: "query HeroNameQuery {\n hero {\n name\n }\n}",
url: "/api?owner=a",
want: "{\"query\":\"query HeroNameQuery {\\n hero {\\n name\\n }\\n}\"}",
wantErr: nil,
},
{
name: "one string variable",
gql: "query FetchSomeIDQuery($someId: String!) {\n human(id: $someId) {\n name\n }\n}",
url: "/api?someId=a",
want: "{\"query\":\"query FetchSomeIDQuery($someId: String!) {\\n human(id: $someId) {\\n name\\n }\\n}\",\"variables\":{\"someId\":\"a\"}}",
wantErr: nil,
},
{
name: "multi variables",
gql: "query FetchSomeIDQuery($someId: String! $num: Int $price: Float! $need:Boolean!) {\n human(id: $someId) {\n name\n }\n}",
url: "/api?someId=a&num=10&price=12.0&need=false&hee=1",
want: "{\"query\":\"query FetchSomeIDQuery($someId: String! $num: Int $price: Float! $need:Boolean!) {\\n human(id: $someId) {\\n name\\n }\\n}\",\"variables\":{\"someId\":\"a\",\"num\":10,\"price\":12.0,\"need\":false}}",
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &DeGraphQLConfig{}
d.SetGql(tt.gql)
body, err := d.ParseGqlFromUrl(tt.url)
assert.Equal(t, tt.wantErr, err)
if err != nil {
return
}
assert.Equal(t, tt.want, body)
})
}
}
func TestDeGraphQLConfig_SetEndpoint(t *testing.T) {
tests := []struct {
name string
endPoint string
wantErr error
want string
}{
{
name: "empty endpoint",
endPoint: "",
wantErr: nil,
want: "/graphql",
},
{
name: "empty endpoint with blank",
endPoint: " ",
wantErr: nil,
want: "/graphql",
},
{
name: "with value",
endPoint: " /graphql2 ",
wantErr: nil,
want: "/graphql2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &DeGraphQLConfig{}
err := d.SetEndpoint(tt.endPoint)
assert.Equal(t, tt.wantErr, err)
if err != nil {
return
}
assert.Equal(t, tt.want, d.endpoint)
})
}
}

View File

@@ -0,0 +1,125 @@
static_resources:
listeners:
- name: main
address:
socket_address:
address: 0.0.0.0
port_value: 18000
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/api"
route:
cluster: mock_service
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: |-
{
"gql": "query ($owner:String! $name:String!){\n repository(owner:$owner, name:$name) {\n name\n forkCount\n description\n}\n}",
"serviceSource": "dns",
"serviceName": "github",
"servicePort": 443,
"domain": "api.github.com",
"endpoint": "/graphql",
"timeout": 2000
}
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "./main.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: staticreply
address:
socket_address:
address: 127.0.0.1
port_value: 8099
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/"
direct_response:
status: 200
body:
inline_string: "example body\n"
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: mock_service
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: mock_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8099
- name: outbound|443||github.dns
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
dns_refresh_rate: 5s
dns_lookup_family: V4_ONLY
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
load_assignment:
cluster_name: outbound|443||github.dns
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: api.github.com
port_value: 443
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001

View File

@@ -0,0 +1,20 @@
module de-graphql
go 1.19
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230410091208-df60dd43079c
github.com/stretchr/testify v1.8.0
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tetratelabs/wazero v1.0.0-rc.1 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,29 @@
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230410091208-df60dd43079c h1:W1QzLx6pefqDWi4peW2HKcZY0rgEy11+JCuWtssp1Ew=
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230410091208-df60dd43079c/go.mod h1:AzSnkuon5c26nIePTiJQIAFsKdhkNdncLcTuahpGtQs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
github.com/tetratelabs/wazero v1.0.0-rc.1 h1:ytecMV5Ue0BwezjKh/cM5yv1Mo49ep2R2snSsQUyToc=
github.com/tetratelabs/wazero v1.0.0-rc.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,61 @@
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: default
namespace: higress-system
spec:
registries:
- domain: api.github.com
name: github
port: 443
type: dns
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: github.dns
higress.io/upstream-vhost: "api.github.com"
higress.io/backend-protocol: HTTPS
name: github-api
namespace: higress-system
spec:
ingressClassName: higress
rules:
- http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: default
path: /api
pathType: Prefix
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: de-graphql-github-api
namespace: higress-system
spec:
defaultConfigDisable: true
matchRules:
- config:
domain: api.github.com
endpoint: /graphql
gql: |-
query ($owner:String! $name:String!){
repository(owner:$owner, name:$name) {
name
forkCount
description
}
}
serviceName: github
servicePort: 443
serviceSource: dns
timeout: 5000
configDisable: false
ingress:
- github-api
url: oci://docker.io/2456868764/de-graphql:1.0.0

View File

@@ -0,0 +1,157 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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 main
import (
"errors"
"fmt"
"net/http"
"de-graphql/config"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {
wrapper.SetCtx(
"de-graphql",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
)
}
func parseConfig(json gjson.Result, config *config.DeGraphQLConfig, log wrapper.Log) error {
log.Debug("parseConfig()")
gql := json.Get("gql").String()
endpoint := json.Get("endpoint").String()
timeout := json.Get("timeout").Int()
log.Debugf("gql:%s endpoint:%s timeout:%d", gql, endpoint, timeout)
err := config.SetGql(gql)
if err != nil {
return err
}
err = config.SetEndpoint(endpoint)
if err != nil {
return err
}
config.SetTimeout(uint32(timeout))
serviceSource := json.Get("serviceSource").String()
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
log.Debugf("serviceSource:%s serviceName:%s servicePort:%d", serviceSource, serviceName, servicePort)
if serviceName == "" || servicePort == 0 {
return errors.New("invalid service config")
}
switch serviceSource {
case "k8s":
namespace := json.Get("namespace").String()
config.SetClient(wrapper.NewClusterClient(wrapper.K8sCluster{
ServiceName: serviceName,
Namespace: namespace,
Port: servicePort,
}))
return nil
case "nacos":
namespace := json.Get("namespace").String()
config.SetClient(wrapper.NewClusterClient(wrapper.NacosCluster{
ServiceName: serviceName,
NamespaceID: namespace,
Port: servicePort,
}))
return nil
case "ip":
config.SetClient(wrapper.NewClusterClient(wrapper.StaticIpCluster{
ServiceName: serviceName,
Port: servicePort,
}))
return nil
case "dns":
domain := json.Get("domain").String()
config.SetClient(wrapper.NewClusterClient(wrapper.DnsCluster{
ServiceName: serviceName,
Port: servicePort,
Domain: domain,
}))
return nil
default:
return errors.New("unknown service source: " + serviceSource)
}
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config config.DeGraphQLConfig, log wrapper.Log) types.Action {
log.Debug("onHttpRequestHeaders()")
log.Debugf("schema:%s host:%s path:%s", ctx.Scheme(), ctx.Host(), ctx.Path())
requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")
method, _ := proxywasm.GetHttpRequestHeader(":method")
log.Debugf("method:%s, request:%s", method, requestUrl)
if err := proxywasm.RemoveHttpRequestHeader("content-length"); err != nil {
log.Debug("can not reset content-length")
}
replaceBody, err := config.ParseGqlFromUrl(requestUrl)
if err != nil {
log.Warnf("failed to parse request url %s : %v", requestUrl, err)
}
log.Debugf("replace body:%s", replaceBody)
// Pass headers to upstream cluster
headers, _ := proxywasm.GetHttpRequestHeaders()
for i := len(headers) - 1; i >= 0; i-- {
key := headers[i][0]
if key == ":method" || key == ":path" || key == ":authority" {
headers = append(headers[:i], headers[i+1:]...)
}
}
// Add header Content-Type: application/json
headers = append(headers, [2]string{"Content-Type", "application/json"})
// Call upstream graphql endpoint
config.GetClient().Post(config.GetEndpoint(), headers, []byte(replaceBody),
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
// Pass response headers and body to client
headers := make([][2]string, 0, len(responseHeaders)+3)
for headK, headV := range responseHeaders {
headers = append(headers, [2]string{headK, headV[0]})
}
// Add debug headers
headers = append(headers, [2]string{"x-degraphql-endpoint", config.GetEndpoint()})
headers = append(headers, [2]string{"x-degraphql-timeout", fmt.Sprintf("%d", config.GetTimeout())})
headers = append(headers, [2]string{"x-degraphql-version", config.GetVersion()})
proxywasm.SendHttpResponse(uint32(statusCode), headers, responseBody, -1)
return
}, config.GetTimeout())
return types.ActionPause
}
func onHttpRequestBody(ctx wrapper.HttpContext, config config.DeGraphQLConfig, body []byte, log wrapper.Log) types.Action {
log.Debug("onHttpRequestBody()")
return types.ActionContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config config.DeGraphQLConfig, log wrapper.Log) types.Action {
log.Debug("onHttpResponseHeaders()")
return types.ActionContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, config config.DeGraphQLConfig, body []byte, log wrapper.Log) types.Action {
log.Debug("onHttpResponseBody()")
return types.ActionContinue
}