diff --git a/plugins/wasm-go/extensions/opa/README.md b/plugins/wasm-go/extensions/opa/README.md new file mode 100644 index 0000000000..80428338ee --- /dev/null +++ b/plugins/wasm-go/extensions/opa/README.md @@ -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 +``` \ No newline at end of file diff --git a/plugins/wasm-go/extensions/opa/VERSION b/plugins/wasm-go/extensions/opa/VERSION new file mode 100644 index 0000000000..afaf360d37 --- /dev/null +++ b/plugins/wasm-go/extensions/opa/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/opa/config.go b/plugins/wasm-go/extensions/opa/config.go new file mode 100644 index 0000000000..fffb1fe85e --- /dev/null +++ b/plugins/wasm-go/extensions/opa/config.go @@ -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) +} diff --git a/plugins/wasm-go/extensions/opa/config_test.go b/plugins/wasm-go/extensions/opa/config_test.go new file mode 100644 index 0000000000..87ab9d2ada --- /dev/null +++ b/plugins/wasm-go/extensions/opa/config_test.go @@ -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) + } +} diff --git a/plugins/wasm-go/extensions/opa/docker-compose.yaml b/plugins/wasm-go/extensions/opa/docker-compose.yaml new file mode 100644 index 0000000000..48b039532f --- /dev/null +++ b/plugins/wasm-go/extensions/opa/docker-compose.yaml @@ -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: { } diff --git a/plugins/wasm-go/extensions/opa/envoy.yaml b/plugins/wasm-go/extensions/opa/envoy.yaml new file mode 100644 index 0000000000..e62b86fbe1 --- /dev/null +++ b/plugins/wasm-go/extensions/opa/envoy.yaml @@ -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 diff --git a/plugins/wasm-go/extensions/opa/go.mod b/plugins/wasm-go/extensions/opa/go.mod new file mode 100644 index 0000000000..1fec9b2d1a --- /dev/null +++ b/plugins/wasm-go/extensions/opa/go.mod @@ -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 => ../.. diff --git a/plugins/wasm-go/extensions/opa/go.sum b/plugins/wasm-go/extensions/opa/go.sum new file mode 100644 index 0000000000..e35b8a38aa --- /dev/null +++ b/plugins/wasm-go/extensions/opa/go.sum @@ -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= diff --git a/plugins/wasm-go/extensions/opa/main.go b/plugins/wasm-go/extensions/opa/main.go new file mode 100644 index 0000000000..a87a430d20 --- /dev/null +++ b/plugins/wasm-go/extensions/opa/main.go @@ -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() +} diff --git a/test/e2e/conformance/base/opa.yaml b/test/e2e/conformance/base/opa.yaml new file mode 100644 index 0000000000..1b73341500 --- /dev/null +++ b/test/e2e/conformance/base/opa.yaml @@ -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 diff --git a/test/e2e/conformance/tests/go-wasm-opa.go b/test/e2e/conformance/tests/go-wasm-opa.go new file mode 100644 index 0000000000..b97a547fae --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-opa.go @@ -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) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/go-wasm-opa.yaml b/test/e2e/conformance/tests/go-wasm-opa.yaml new file mode 100644 index 0000000000..b2d90c55d6 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-opa.yaml @@ -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 \ No newline at end of file diff --git a/test/e2e/conformance/utils/http/http.go b/test/e2e/conformance/utils/http/http.go index 38e8b9a2fb..2cc582a9bd 100644 --- a/test/e2e/conformance/utils/http/http.go +++ b/test/e2e/conformance/utils/http/http.go @@ -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. diff --git a/test/e2e/conformance/utils/kubernetes/helpers.go b/test/e2e/conformance/utils/kubernetes/helpers.go index ce002be242..f423e1d680 100644 --- a/test/e2e/conformance/utils/kubernetes/helpers.go +++ b/test/e2e/conformance/utils/kubernetes/helpers.go @@ -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) } diff --git a/test/e2e/conformance/utils/suite/suite.go b/test/e2e/conformance/utils/suite/suite.go index 9f18da8de0..f958084a4e 100644 --- a/test/e2e/conformance/utils/suite/suite.go +++ b/test/e2e/conformance/utils/suite/suite.go @@ -130,6 +130,7 @@ func New(s Options) *ConformanceTestSuite { "base/eureka.yaml", "base/nacos.yaml", "base/dubbo.yaml", + "base/opa.yaml", } } diff --git a/tools/hack/build-wasm-plugins.sh b/tools/hack/build-wasm-plugins.sh index a6797e2224..713b6666b5 100755 --- a/tools/hack/build-wasm-plugins.sh +++ b/tools/hack/build-wasm-plugins.sh @@ -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