mirror of
https://github.com/alibaba/higress.git
synced 2026-03-04 08:30:48 +08:00
feat: wasm support opa (Open Policy Agent) (#760)
This commit is contained in:
101
plugins/wasm-go/extensions/opa/README.md
Normal file
101
plugins/wasm-go/extensions/opa/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 功能说明
|
||||
|
||||
该插件实现了 `OPA` 策略控制
|
||||
|
||||
# 该教程使用k8s,[k8s配置文件](../../../../test/e2e/conformance/tests/go-wasm-opa.yaml)
|
||||
|
||||
支持client `k8s,nacos,ip,route` 策略去访问
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 字段 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|---------------|--------|------|-----|--------------------------------------|
|
||||
| policy | string | 必填 | - | opa 策略 |
|
||||
| timeout | string | 必填 | - | 访问超时时间设置 |
|
||||
| serviceSource | string | 必填 | - | k8s,nacos,ip,route |
|
||||
| host | string | 非必填 | - | 服务主机(serviceSource为`ip`必填) |
|
||||
| serviceName | string | 非必填 | - | 服务名称(serviceSource为`k8s,nacos,ip`必填) |
|
||||
| servicePort | string | 非必填 | - | 服务端口(serviceSource为`k8s,nacos,ip`必填) |
|
||||
| namespace | string | 非必填 | - | 服务端口(serviceSource为`k8s,nacos`必填) |
|
||||
|
||||
这是一个用于OPA认证配置的表格,确保在提供所有必要的信息时遵循上述指导。
|
||||
|
||||
## 配置示例
|
||||
|
||||
```yaml
|
||||
serviceSource: k8s
|
||||
serviceName: opa
|
||||
servicePort: 8181
|
||||
namespace: higress-backend
|
||||
policy: example1
|
||||
timeout: 5s
|
||||
```
|
||||
|
||||
# 在宿主机上执行OPA的流程
|
||||
|
||||
## 启动opa服务
|
||||
|
||||
```shell
|
||||
docker run -d --name opa -p 8181:8181 openpolicyagent/opa:0.35.0 run -s
|
||||
```
|
||||
|
||||
## 创建opa策略
|
||||
|
||||
```shell
|
||||
curl -X PUT '127.0.0.1:8181/v1/policies/example1' \
|
||||
-H 'Content-Type: text/plain' \
|
||||
-d 'package example1
|
||||
|
||||
import input.request
|
||||
|
||||
default allow = false
|
||||
|
||||
allow {
|
||||
# HTTP method must GET
|
||||
request.method == "GET"
|
||||
}'
|
||||
```
|
||||
|
||||
## 查询策略
|
||||
|
||||
```shell
|
||||
curl -X POST '127.0.0.1:8181/v1/data/example1/allow' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"input":{"request":{"method":"GET"}}}'
|
||||
```
|
||||
|
||||
# 测试插件
|
||||
|
||||
## 打包 WASM 插件
|
||||
|
||||
> 在 `wasm-go` 目录下把Dockerfile文件改成`PLUGIN_NAME=opa`,然后执行以下命令
|
||||
|
||||
```shell
|
||||
docker build -t build-wasm-opa --build-arg GOPROXY=https://goproxy.cn,direct --platform=linux/amd64 .
|
||||
```
|
||||
|
||||
## 拷贝插件
|
||||
|
||||
> 在当前的目录执行以下命令,将插件拷贝当前的目录
|
||||
|
||||
```shell
|
||||
docker cp wasm-opa:/plugin.wasm .
|
||||
```
|
||||
|
||||
## 运行插件
|
||||
|
||||
> 运行前修改envoy.yaml 这两个字段 `OPA_SERVER` `OPA_PORT` 替换宿主机上的IP和端口
|
||||
|
||||
```shell
|
||||
docker compose up
|
||||
```
|
||||
|
||||
## 使用curl测试插件
|
||||
|
||||
```shell
|
||||
curl http://127.0.0.1:10000/get -X GET -v
|
||||
```
|
||||
|
||||
```shell
|
||||
curl http://127.0.0.1:10000/get -X POST -v
|
||||
```
|
||||
1
plugins/wasm-go/extensions/opa/VERSION
Normal file
1
plugins/wasm-go/extensions/opa/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
82
plugins/wasm-go/extensions/opa/config.go
Normal file
82
plugins/wasm-go/extensions/opa/config.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type OpaConfig struct {
|
||||
policy string
|
||||
timeout uint32
|
||||
|
||||
client wrapper.HttpClient
|
||||
}
|
||||
|
||||
func Client(json gjson.Result) (wrapper.HttpClient, error) {
|
||||
serviceSource := strings.TrimSpace(json.Get("serviceSource").String())
|
||||
serviceName := strings.TrimSpace(json.Get("serviceName").String())
|
||||
servicePort := json.Get("servicePort").Int()
|
||||
|
||||
host := strings.TrimSpace(json.Get("host").String())
|
||||
if host == "" {
|
||||
if serviceName == "" || servicePort == 0 {
|
||||
return nil, errors.New("invalid service config")
|
||||
}
|
||||
}
|
||||
|
||||
var namespace string
|
||||
if serviceSource == "k8s" || serviceSource == "nacos" {
|
||||
if namespace = strings.TrimSpace(json.Get("namespace").String()); namespace == "" {
|
||||
return nil, errors.New("namespace not allow empty")
|
||||
}
|
||||
}
|
||||
|
||||
switch serviceSource {
|
||||
case "k8s":
|
||||
return wrapper.NewClusterClient(wrapper.K8sCluster{
|
||||
ServiceName: serviceName,
|
||||
Namespace: namespace,
|
||||
Port: servicePort,
|
||||
}), nil
|
||||
case "nacos":
|
||||
return wrapper.NewClusterClient(wrapper.NacosCluster{
|
||||
ServiceName: serviceName,
|
||||
NamespaceID: namespace,
|
||||
Port: servicePort,
|
||||
}), nil
|
||||
case "ip":
|
||||
return wrapper.NewClusterClient(wrapper.StaticIpCluster{
|
||||
ServiceName: serviceName,
|
||||
Host: host,
|
||||
Port: servicePort,
|
||||
}), nil
|
||||
case "dns":
|
||||
return wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
ServiceName: serviceName,
|
||||
Port: servicePort,
|
||||
Domain: json.Get("domain").String(),
|
||||
}), nil
|
||||
case "route":
|
||||
return wrapper.NewClusterClient(wrapper.RouteCluster{
|
||||
Host: host,
|
||||
}), nil
|
||||
}
|
||||
return nil, errors.New("unknown service source: " + serviceSource)
|
||||
}
|
||||
50
plugins/wasm-go/extensions/opa/config_test.go
Normal file
50
plugins/wasm-go/extensions/opa/config_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
json := gjson.Result{Type: gjson.JSON, Raw: `{"serviceSource": "k8s","serviceName": "opa","servicePort": 8181,"namespace": "example1","policy": "example1","timeout": "5s"}`}
|
||||
config := &OpaConfig{}
|
||||
assert.NoError(t, parseConfig(json, config, wrapper.Log{}))
|
||||
assert.Equal(t, config.policy, "example1")
|
||||
assert.Equal(t, config.timeout, uint32(5000))
|
||||
assert.NotNil(t, config.client)
|
||||
|
||||
type tt struct {
|
||||
raw string
|
||||
result bool
|
||||
}
|
||||
|
||||
tests := []tt{
|
||||
{raw: `{}`, result: false},
|
||||
{raw: `{"policy": "example1","timeout": "5s"}`, result: false},
|
||||
{raw: `{"serviceSource": "route","host": "example.com","policy": "example1","timeout": "5s"}`, result: true},
|
||||
{raw: `{"serviceSource": "nacos","serviceName": "opa","servicePort": 8181,"policy": "example1","timeout": "5s"}`, result: false},
|
||||
{raw: `{"serviceSource": "nacos","serviceName": "opa","servicePort": 8181,"namespace": "example1","policy": "example1","timeout": "5s"}`, result: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
json = gjson.Result{Type: gjson.JSON, Raw: test.raw}
|
||||
assert.Equal(t, parseConfig(json, config, wrapper.Log{}) == nil, test.result)
|
||||
}
|
||||
}
|
||||
16
plugins/wasm-go/extensions/opa/docker-compose.yaml
Normal file
16
plugins/wasm-go/extensions/opa/docker-compose.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:1.3.1
|
||||
entrypoint: /usr/local/bin/envoy
|
||||
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
volumes:
|
||||
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./plugin.wasm:/etc/envoy/plugin.wasm
|
||||
|
||||
networks:
|
||||
wasmtest: { }
|
||||
69
plugins/wasm-go/extensions/opa/envoy.yaml
Normal file
69
plugins/wasm-go/extensions/opa/envoy.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 10000
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
stat_prefix: ingress_http
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: [ "*" ]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: opa-server
|
||||
http_filters:
|
||||
- name: wasmdemo
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
name: wasmdemo
|
||||
vm_config:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: /etc/envoy/plugin.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
{
|
||||
"serviceSource": "route",
|
||||
"host": "OPA_SERVER:OPA_PORT",
|
||||
"policy": "example1",
|
||||
"timeout": "5s"
|
||||
}
|
||||
- name: envoy.filters.http.router
|
||||
clusters:
|
||||
- name: opa-server
|
||||
connect_timeout: 0.5s
|
||||
type: STRICT_DNS
|
||||
lb_policy: ROUND_ROBIN
|
||||
dns_refresh_rate: 5s
|
||||
dns_lookup_family: V4_ONLY
|
||||
load_assignment:
|
||||
cluster_name: opa-server
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: OPA_SERVER # opa server Host IP
|
||||
port_value: OPA_PORT # opa server Host PORT
|
||||
23
plugins/wasm-go/extensions/opa/go.mod
Normal file
23
plugins/wasm-go/extensions/opa/go.mod
Normal file
@@ -0,0 +1,23 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/opa
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // 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
|
||||
)
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
24
plugins/wasm-go/extensions/opa/go.sum
Normal file
24
plugins/wasm-go/extensions/opa/go.sum
Normal file
@@ -0,0 +1,24 @@
|
||||
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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
134
plugins/wasm-go/extensions/opa/main.go
Normal file
134
plugins/wasm-go/extensions/opa/main.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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(
|
||||
"opa",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
)
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Input map[string]interface{} `json:"input"`
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *OpaConfig, log wrapper.Log) error {
|
||||
policy := json.Get("policy").String()
|
||||
if strings.TrimSpace(policy) == "" {
|
||||
return errors.New("policy not allow empty")
|
||||
}
|
||||
|
||||
timeout := json.Get("timeout").String()
|
||||
if strings.TrimSpace(timeout) == "" {
|
||||
return errors.New("timeout not allow empty")
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
return errors.New("timeout parse fail: " + err.Error())
|
||||
}
|
||||
|
||||
var uint32Duration uint32
|
||||
|
||||
if duration.Milliseconds() > int64(^uint32(0)) {
|
||||
} else {
|
||||
uint32Duration = uint32(duration.Milliseconds())
|
||||
}
|
||||
config.timeout = uint32Duration
|
||||
|
||||
client, err := Client(json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.client = client
|
||||
config.policy = policy
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config OpaConfig, log wrapper.Log) types.Action {
|
||||
return opaCall(ctx, config, nil, log)
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config OpaConfig, body []byte, log wrapper.Log) types.Action {
|
||||
return opaCall(ctx, config, body, log)
|
||||
}
|
||||
|
||||
func opaCall(ctx wrapper.HttpContext, config OpaConfig, body []byte, log wrapper.Log) types.Action {
|
||||
request := make(map[string]interface{}, 6)
|
||||
headers, _ := proxywasm.GetHttpRequestHeaders()
|
||||
|
||||
request["method"] = ctx.Method()
|
||||
request["scheme"] = ctx.Scheme()
|
||||
request["path"] = ctx.Path()
|
||||
request["headers"] = headers
|
||||
if len(body) != 0 {
|
||||
request["body"] = body
|
||||
}
|
||||
parse, _ := url.Parse(ctx.Path())
|
||||
query, _ := url.ParseQuery(parse.RawQuery)
|
||||
request["query"] = query
|
||||
|
||||
data, _ := json.Marshal(Metadata{Input: map[string]interface{}{"request": request}})
|
||||
if err := config.client.Post(fmt.Sprintf("/v1/data/%s/allow", config.policy),
|
||||
[][2]string{{"Content-Type", "application/json"}},
|
||||
data, rspCall, config.timeout); err != nil {
|
||||
log.Errorf("client opa fail %v", err)
|
||||
return types.ActionPause
|
||||
}
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func rspCall(statusCode int, _ http.Header, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
proxywasm.SendHttpResponse(uint32(statusCode), nil, []byte("opa state not is 200"), -1)
|
||||
return
|
||||
}
|
||||
var rsp map[string]interface{}
|
||||
if err := json.Unmarshal(responseBody, &rsp); err != nil {
|
||||
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil, []byte(fmt.Sprintf("opa parse rsp fail %+v", err)), -1)
|
||||
return
|
||||
}
|
||||
|
||||
result, ok := rsp["result"].(bool)
|
||||
if !ok {
|
||||
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil, []byte("rsp type conversion fail"), -1)
|
||||
return
|
||||
}
|
||||
|
||||
if !result {
|
||||
proxywasm.SendHttpResponse(http.StatusUnauthorized, nil, []byte("opa server not allowed"), -1)
|
||||
return
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
73
test/e2e/conformance/base/opa.yaml
Normal file
73
test/e2e/conformance/base/opa.yaml
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: opa
|
||||
namespace: higress-conformance-app-backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: opa
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: opa
|
||||
spec:
|
||||
containers:
|
||||
- name: opa
|
||||
image: openpolicyagent/opa:latest
|
||||
ports:
|
||||
- containerPort: 8181
|
||||
command: [ "opa", "run", "-s" ]
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: opa
|
||||
namespace: higress-conformance-app-backend
|
||||
spec:
|
||||
selector:
|
||||
app: opa
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8181
|
||||
targetPort: 8181
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: curl-opa
|
||||
namespace: higress-conformance-app-backend
|
||||
spec:
|
||||
containers:
|
||||
- name: opa-test
|
||||
image: curlimages/curl:latest
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
curl -X PUT 'http://opa:8181/v1/policies/example1' \
|
||||
-H 'Content-Type: text/plain' \
|
||||
-d 'package example1
|
||||
import input.request
|
||||
default allow = false
|
||||
allow {
|
||||
# HTTP method must GET
|
||||
request.method == "GET"
|
||||
}'
|
||||
restartPolicy: OnFailure
|
||||
83
test/e2e/conformance/tests/go-wasm-opa.go
Normal file
83
test/e2e/conformance/tests/go-wasm-opa.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
stdHttp "net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(WasmPluginsOpa)
|
||||
}
|
||||
|
||||
var WasmPluginsOpa = suite.ConformanceTest{
|
||||
ShortName: "WasmPluginsOpa",
|
||||
Description: "The Ingress in the higress-conformance-infra namespace test the opa wasm plugins.",
|
||||
Manifests: []string{"tests/go-wasm-opa.yaml"},
|
||||
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
|
||||
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||
testcases := []http.Assertion{
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Method: "GET",
|
||||
Host: "foo.com",
|
||||
Path: "/",
|
||||
UnfollowRedirect: true,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: stdHttp.StatusOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
CompareTarget: http.CompareTargetResponse,
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Method: "POST",
|
||||
Host: "foo.com",
|
||||
Path: "/",
|
||||
UnfollowRedirect: true,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: stdHttp.StatusUnauthorized,
|
||||
Body: []byte("opa server not allowed"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("WasmPlugins opa", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
48
test/e2e/conformance/tests/go-wasm-opa.yaml
Normal file
48
test/e2e/conformance/tests/go-wasm-opa.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-opa
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "foo.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: opa
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
serviceSource: k8s
|
||||
namespace: higress-conformance-app-backend
|
||||
serviceName: opa
|
||||
servicePort: 8181
|
||||
policy: example1
|
||||
timeout: 5s
|
||||
url: file:///opt/plugins/wasm-go/extensions/opa/plugin.wasm
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -205,7 +206,7 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
|
||||
expected.Request.ActualRequest.Method = strings.ToUpper(expected.Request.ActualRequest.Method)
|
||||
|
||||
if expected.Response.ExpectedResponse.StatusCode == 0 {
|
||||
expected.Response.ExpectedResponse.StatusCode = 200
|
||||
expected.Response.ExpectedResponse.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
t.Logf("Making %s request to %s://%s%s", expected.Request.ActualRequest.Method, scheme, gwAddr, expected.Request.ActualRequest.Path)
|
||||
@@ -233,7 +234,7 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
|
||||
}
|
||||
}
|
||||
|
||||
backendSetHeaders := []string{}
|
||||
backendSetHeaders := make([]string, 0, len(expected.Response.AdditionalResponseHeaders))
|
||||
for name, val := range expected.Response.AdditionalResponseHeaders {
|
||||
backendSetHeaders = append(backendSetHeaders, name+":"+val)
|
||||
}
|
||||
@@ -298,12 +299,12 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro
|
||||
return false
|
||||
}
|
||||
|
||||
if cRes.StatusCode == 200 && !expected.Response.ExpectedResponseNoRequest && cReq.Host == "" && cReq.Path == "" && cReq.Headers == nil && cReq.Body == nil {
|
||||
if cRes.StatusCode == http.StatusOK && !expected.Response.ExpectedResponseNoRequest && cReq.Host == "" && cReq.Path == "" && cReq.Headers == nil && cReq.Body == nil {
|
||||
t.Logf(`decoding client's response failed. Maybe you have chosen a wrong backend.
|
||||
Choose echo-server if you want to check expected request header&body instead of response header&body.`)
|
||||
return false
|
||||
}
|
||||
if err := CompareRequest(&req, cReq, cRes, expected); err != nil {
|
||||
if err = CompareRequest(&req, cReq, cRes, expected); err != nil {
|
||||
t.Logf("request expectation failed for actual request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||
return false
|
||||
}
|
||||
@@ -313,12 +314,12 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro
|
||||
You can only choose one to compare between Response and Request.`)
|
||||
return false
|
||||
}
|
||||
if err := CompareResponse(cRes, expected); err != nil {
|
||||
if err = CompareResponse(cRes, expected); err != nil {
|
||||
t.Logf("Response expectation failed for actual request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
t.Logf("invalid CompareTarget: %v please set it CompareTargetRequest or CompareTargetResponse", expected.Meta.CompareTarget)
|
||||
t.Logf("invalid CompareTarget: %v please set it CompareTargetRequest or CompareTargetResponse", expected.Meta.CompareTarget)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -331,7 +332,7 @@ func CompareRequest(req *roundtripper.Request, cReq *roundtripper.CapturedReques
|
||||
if expected.Response.ExpectedResponse.StatusCode != cRes.StatusCode {
|
||||
return fmt.Errorf("expected status code to be %d, got %d", expected.Response.ExpectedResponse.StatusCode, cRes.StatusCode)
|
||||
}
|
||||
if cRes.StatusCode == 200 && !expected.Response.ExpectedResponseNoRequest {
|
||||
if cRes.StatusCode == http.StatusOK && !expected.Response.ExpectedResponseNoRequest {
|
||||
// The request expected to arrive at the backend is
|
||||
// the same as the request made, unless otherwise
|
||||
// specified.
|
||||
|
||||
@@ -15,11 +15,12 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sigs.k8s.io/yaml"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -141,10 +142,5 @@ func ApplyConfigmapDataWithYaml(t *testing.T, c client.Client, namespace string,
|
||||
cm.Data[key] = data
|
||||
|
||||
t.Logf("🏗 Updating %s %s", name, namespace)
|
||||
|
||||
if err := c.Update(ctx, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.Update(ctx, cm)
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ func New(s Options) *ConformanceTestSuite {
|
||||
"base/eureka.yaml",
|
||||
"base/nacos.yaml",
|
||||
"base/dubbo.yaml",
|
||||
"base/opa.yaml",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ else
|
||||
for file in `ls $EXTENSIONS_DIR`
|
||||
do
|
||||
# TODO: adjust waf build
|
||||
if [ "$file" == "waf" ]; then
|
||||
if [ "$file" == "waf" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ -d $EXTENSIONS_DIR$file ]; then
|
||||
|
||||
Reference in New Issue
Block a user