mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 04:37:31 +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"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -205,7 +206,7 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
|
|||||||
expected.Request.ActualRequest.Method = strings.ToUpper(expected.Request.ActualRequest.Method)
|
expected.Request.ActualRequest.Method = strings.ToUpper(expected.Request.ActualRequest.Method)
|
||||||
|
|
||||||
if expected.Response.ExpectedResponse.StatusCode == 0 {
|
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)
|
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 {
|
for name, val := range expected.Response.AdditionalResponseHeaders {
|
||||||
backendSetHeaders = append(backendSetHeaders, name+":"+val)
|
backendSetHeaders = append(backendSetHeaders, name+":"+val)
|
||||||
}
|
}
|
||||||
@@ -298,12 +299,12 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro
|
|||||||
return false
|
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.
|
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.`)
|
Choose echo-server if you want to check expected request header&body instead of response header&body.`)
|
||||||
return false
|
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)
|
t.Logf("request expectation failed for actual request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||||
return false
|
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.`)
|
You can only choose one to compare between Response and Request.`)
|
||||||
return false
|
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)
|
t.Logf("Response expectation failed for actual request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +332,7 @@ func CompareRequest(req *roundtripper.Request, cReq *roundtripper.CapturedReques
|
|||||||
if expected.Response.ExpectedResponse.StatusCode != cRes.StatusCode {
|
if expected.Response.ExpectedResponse.StatusCode != cRes.StatusCode {
|
||||||
return fmt.Errorf("expected status code to be %d, got %d", 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 request expected to arrive at the backend is
|
||||||
// the same as the request made, unless otherwise
|
// the same as the request made, unless otherwise
|
||||||
// specified.
|
// specified.
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ package kubernetes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sigs.k8s.io/yaml"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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
|
cm.Data[key] = data
|
||||||
|
|
||||||
t.Logf("🏗 Updating %s %s", name, namespace)
|
t.Logf("🏗 Updating %s %s", name, namespace)
|
||||||
|
return c.Update(ctx, cm)
|
||||||
if err := c.Update(ctx, cm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ func New(s Options) *ConformanceTestSuite {
|
|||||||
"base/eureka.yaml",
|
"base/eureka.yaml",
|
||||||
"base/nacos.yaml",
|
"base/nacos.yaml",
|
||||||
"base/dubbo.yaml",
|
"base/dubbo.yaml",
|
||||||
|
"base/opa.yaml",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ else
|
|||||||
for file in `ls $EXTENSIONS_DIR`
|
for file in `ls $EXTENSIONS_DIR`
|
||||||
do
|
do
|
||||||
# TODO: adjust waf build
|
# TODO: adjust waf build
|
||||||
if [ "$file" == "waf" ]; then
|
if [ "$file" == "waf" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if [ -d $EXTENSIONS_DIR$file ]; then
|
if [ -d $EXTENSIONS_DIR$file ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user