From 3e29c941d55f7354766f9860deacf85275e3a008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 10 Oct 2024 10:50:02 +0800 Subject: [PATCH 01/10] e2e: retry to solve timeout (#759) Signed-off-by: spacewander --- e2e/go.mod | 1 + e2e/go.sum | 2 ++ e2e/tests/consumer.go | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/e2e/go.mod b/e2e/go.mod index 414d2a20..ab7b8d8e 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -17,6 +17,7 @@ module mosn.io/htnn/e2e go 1.21.5 require ( + github.com/avast/retry-go v3.0.0+incompatible github.com/stretchr/testify v1.9.0 istio.io/client-go v1.21.2 k8s.io/api v0.29.3 diff --git a/e2e/go.sum b/e2e/go.sum index 9d09ec2c..2a9c7759 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -8,6 +8,8 @@ github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRB github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/e2e/tests/consumer.go b/e2e/tests/consumer.go index ee68d68d..531cdcf8 100644 --- a/e2e/tests/consumer.go +++ b/e2e/tests/consumer.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/avast/retry-go" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -42,7 +43,20 @@ func init() { suite.Register(suite.Test{ Manifests: []string{"base/httproute.yml"}, Run: func(t *testing.T, suite *suite.Suite) { - rsp, err := suite.Get("/echo", hdrWithKey("rick")) + var rsp *http.Response + var err error + err = retry.Do( + func() error { + rsp, err = suite.Get("/echo", hdrWithKey("rick")) + return err + }, + retry.RetryIf(func(err error) bool { + return true + }), + retry.Attempts(3), + // backoff delay + retry.Delay(500*time.Millisecond), + ) require.NoError(t, err) require.Equal(t, 200, rsp.StatusCode) req, _, err := suite.Capture(rsp) From 146a1651a743b015c5f69e86d66cceef66d4caf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 11 Oct 2024 13:53:23 +0800 Subject: [PATCH 02/10] test: add ability to specify extra cluster (#764) Signed-off-by: spacewander --- .../tests/integration/dataplane/bootstrap.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api/plugins/tests/integration/dataplane/bootstrap.go b/api/plugins/tests/integration/dataplane/bootstrap.go index 19726878..105d48e2 100644 --- a/api/plugins/tests/integration/dataplane/bootstrap.go +++ b/api/plugins/tests/integration/dataplane/bootstrap.go @@ -33,12 +33,14 @@ type bootstrap struct { consumers map[string]map[string]interface{} httpFilterGolang map[string]interface{} accessLogFormat string + clusters []map[string]interface{} } func Bootstrap() *bootstrap { return &bootstrap{ backendRoutes: []map[string]interface{}{}, consumers: map[string]map[string]interface{}{}, + clusters: []map[string]interface{}{}, } } @@ -81,6 +83,16 @@ func (b *bootstrap) SetAccessLogFormat(fmt string) *bootstrap { return b } +func (b *bootstrap) AddCluster(s string) *bootstrap { + var n map[string]interface{} + err := yaml.Unmarshal([]byte(s), &n) + if err != nil { + panic(err) + } + b.clusters = append(b.clusters, n) + return b +} + func (b *bootstrap) buildConfiguration() (map[string]interface{}, error) { var root map[string]interface{} // check if the input is valid yaml @@ -135,6 +147,13 @@ func (b *bootstrap) buildConfiguration() (map[string]interface{}, error) { } } + staticResources := root["static_resources"].(map[string]interface{}) + clusters := staticResources["clusters"].([]interface{}) + newClusters := []interface{}{} + for _, c := range b.clusters { + newClusters = append(newClusters, c) + } + staticResources["clusters"] = append(clusters, newClusters...) return root, nil } From 93569d8f14a48d7c2a0d1cc5a05f97c08e318f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 14 Oct 2024 10:02:57 +0800 Subject: [PATCH 03/10] doc: fix plugin_integration_test_framework.md (#766) Signed-off-by: spacewander --- .../developer-guide/plugin_integration_test_framework.md | 6 ++++-- .../developer-guide/plugin_integration_test_framework.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/site/content/en/docs/developer-guide/plugin_integration_test_framework.md b/site/content/en/docs/developer-guide/plugin_integration_test_framework.md index b36a8ad1..8fbce53c 100644 --- a/site/content/en/docs/developer-guide/plugin_integration_test_framework.md +++ b/site/content/en/docs/developer-guide/plugin_integration_test_framework.md @@ -4,7 +4,7 @@ title: Plugin Integration Test Framework ## How to run test -Assumed you are at the `./plugins`: +Assumed you are at the `./plugins` or `./api` directory of this project: 1. Run `make build-test-so` to build the Go plugins. 2. Run `go test -v ./tests/integration -run TestPluginXX` to run the selected tests. @@ -14,7 +14,9 @@ The `$test_dir` is where the test files locate, which is `./tests/integration` i Some tests require third-party services. You can start them by running `docker compose up $service` under `./tests/integration/testdata/services`. -By default, the test framework starts Envoy using the image `envoyproxy/envoy`. You can specify a different image by setting the `PROXY_IMAGE` environment variable. For example, `PROXY_IMAGE=envoyproxy/envoy:contrib-v1.29.4 go test -v ./tests/integration/ -run TestLimitCountRedis` will use the image `envoyproxy/envoy:contrib-v1.29.4`. +By default, the test framework starts Envoy using the image `envoyproxy/envoy:contrib-$latest`. You can specify a different image by setting the `PROXY_IMAGE` environment variable. For example, `PROXY_IMAGE=envoyproxy/envoy:contrib-v1.29.4 go test -tags envoy1.29 -v ./tests/integration/ -run TestLimitCountRedis` will use the image `envoyproxy/envoy:contrib-v1.29.4`. + +You may have noticed that when executing `go test`, we added `-tags envoy1.29`. This is because there are interface differences across different versions of Envoy. In this case, we specified the label for Envoy version 1.29. See [HTNN's Envoy multi-version support](./dataplane_support.md) for details. Note that the version of Envoy being run, the `-tags` parameter in the `go test` command, and the version of the Envoy interface that is depended upon when running `make build-test-so` should be consistent. ## Port usage diff --git a/site/content/zh-hans/docs/developer-guide/plugin_integration_test_framework.md b/site/content/zh-hans/docs/developer-guide/plugin_integration_test_framework.md index 64491be2..f67b1b6d 100644 --- a/site/content/zh-hans/docs/developer-guide/plugin_integration_test_framework.md +++ b/site/content/zh-hans/docs/developer-guide/plugin_integration_test_framework.md @@ -4,7 +4,7 @@ title: 插件集成测试框架 ## 如何运行测试 -假设您位于 `./plugins`: +假设您位于本项目的 `./plugins` 或 `./api` 目录下: 1. 运行 `make build-test-so` 构建 Go 插件。 2. 运行 `go test -v ./tests/integration -run TestPluginXX` 来运行选定的测试。 @@ -14,7 +14,9 @@ title: 插件集成测试框架 一些测试需要第三方服务。您可以通过在 `./tests/integration/testdata/services` 下运行 `docker compose up $service` 来启动它们。 -默认情况下,测试框架通过镜像 `envoyproxy/envoy` 启动 Envoy。你可以通过设置环境变量 `PROXY_IMAGE` 来指定其他镜像。例如,`PROXY_IMAGE=envoyproxy/envoy:contrib-v1.29.4 go test -v ./tests/integration/ -run TestLimitCountRedis` 将使用 `envoyproxy/envoy:contrib-v1.29.4` 镜像。 +默认情况下,测试框架通过镜像 `envoyproxy/envoy:contrib-$latest` 启动 Envoy。你可以通过设置环境变量 `PROXY_IMAGE` 来指定其他镜像。例如,`PROXY_IMAGE=envoyproxy/envoy:contrib-v1.29.4 go test -tags envoy1.29 -v ./tests/integration/ -run TestLimitCountRedis` 将使用 `envoyproxy/envoy:contrib-v1.29.4` 镜像。 + +您可能已经注意到,在执行 `go test` 时,我们添加了 `-tags envoy1.29`。这是因为不同版本 Envoy 接口存在差异。在这种情况下,我们指定了 Envoy 1.29 版本的标签。具体见 [HTNN 的 Envoy 多版本支持](./dataplane_support.md)。注意运行的 Envoy 版本,以及 `go test` 命令中的 `-tags` 参数,和 `make build-test-so` 时依赖的 Envoy 接口版本应该保持一致。 ## 端口使用 From 112dadbd4cb1c5d59f508920b1d07c52b132bd0a Mon Sep 17 00:00:00 2001 From: am6737 <91421697+am6737@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:05:37 +0800 Subject: [PATCH 04/10] feat(makefile, workflows): add YAML linter to workflow and Makefile (#767) - Add `lint-yaml` target in Makefile to lint YAML files, ignoring Helm template YAML files - Update GitHub Actions workflow to include YAML lint step Signed-off-by: am6737 <1359816810@qq.com> --- .github/workflows/lint.yml | 5 ++++- Makefile | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b3404baf..3ee734b4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -121,4 +121,7 @@ jobs: run: npm install -g markdownlint-cli - name: lint markdown - run: make lint-markdown \ No newline at end of file + run: make lint-markdown + + - name: lint yaml + run: make lint-yaml \ No newline at end of file diff --git a/Makefile b/Makefile index b93d84e0..0d8c1b0e 100644 --- a/Makefile +++ b/Makefile @@ -190,13 +190,23 @@ lint-markdown: @# ignore markdown under 'external/istio' markdownlint '{*.md,site/**/*.md}' --disable MD012 MD013 MD029 MD033 MD034 MD036 MD041 +# We don’t use if ! command -v yamllint because some environments might have a pre-installed Python version. +# Checking the specific path ensures we're using the Node.js version to avoid conflicts. +.PHONY: lint-yaml +lint-yaml: + YAML_LINT="$$(npm config get prefix)/bin/yamllint"; \ + if ! test -x "$${YAML_LINT}"; then \ + npm install -g yaml-lint; \ + fi; \ + "$${YAML_LINT}" "**/*.(yaml|yml)" --ignore="manifests/charts/*/templates/**/*.(yaml|yml)" --ignore="manifests/charts/*/files/**/*.(yaml|yml)" + .PHONY: lint-remain lint-remain: grep '>>>>>>' $(shell git ls-files .) | grep -v 'Makefile:' && exit 1 || true cd tools && go run cmd/linter/main.go .PHONY: lint -lint: lint-go lint-proto lint-license lint-spell lint-editorconfig lint-cjk lint-remain lint-markdown +lint: lint-go lint-proto lint-license lint-spell lint-editorconfig lint-cjk lint-remain lint-markdown lint-yaml .PHONY: fmt fmt: fmt-go fmt-proto fix-spell fix-cjk From 331d72b44c55cb031ea58cf1262ba1723aec4814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 15 Oct 2024 11:45:46 +0800 Subject: [PATCH 05/10] add basic trailer processing (#762) Signed-off-by: spacewander --- .github/workflows/test.yml | 4 + .gitignore | 1 + .ignore_words | 1 + Makefile | 2 +- api/Makefile | 9 + api/pkg/filtermanager/api/api.go | 14 +- api/pkg/filtermanager/filtermanager.go | 361 +++++++++++++----- api/pkg/filtermanager/filtermanager_dev.go | 4 +- api/pkg/filtermanager/filtermanager_latest.go | 2 +- api/pkg/filtermanager/filtermanager_test.go | 62 ++- .../tests/integration/dataplane/bootstrap.yml | 40 ++ .../tests/integration/dataplane/data_plane.go | 37 ++ api/plugins/tests/pkg/envoy/capi.go | 12 + api/tests/integration/config.pb.go | 90 +++-- api/tests/integration/config.pb.validate.go | 6 +- api/tests/integration/config.proto | 6 +- .../integration/filtermanager_dev_test.go | 60 +++ .../integration/filtermanager_latest_test.go | 300 +++++++++++++++ api/tests/integration/filtermanager_test.go | 96 ++--- api/tests/integration/http_filter_test.go | 12 +- api/tests/integration/test_plugins.go | 100 ++++- .../testdata/services/docker-compose.yml | 12 + .../testdata/services/grpc/Dockerfile | 6 + .../integration/testdata/services/grpc/go.mod | 29 ++ .../integration/testdata/services/grpc/go.sum | 14 + .../testdata/services/grpc/main.go | 49 +++ .../testdata/services/grpc/sample.pb.go | 202 ++++++++++ .../services/grpc/sample.pb.validate.go | 239 ++++++++++++ .../testdata/services/grpc/sample.proto | 21 + .../testdata/services/grpc/sample_grpc.pb.go | 160 ++++++++ .../en/docs/developer-guide/get_involved.md | 16 +- .../developer-guide/plugin_development.md | 17 +- .../docs/developer-guide/get_involved.md | 10 +- .../developer-guide/plugin_development.md | 17 +- 34 files changed, 1772 insertions(+), 239 deletions(-) create mode 100644 api/tests/integration/filtermanager_dev_test.go create mode 100644 api/tests/integration/filtermanager_latest_test.go create mode 100644 api/tests/integration/testdata/services/docker-compose.yml create mode 100644 api/tests/integration/testdata/services/grpc/Dockerfile create mode 100644 api/tests/integration/testdata/services/grpc/go.mod create mode 100644 api/tests/integration/testdata/services/grpc/go.sum create mode 100644 api/tests/integration/testdata/services/grpc/main.go create mode 100644 api/tests/integration/testdata/services/grpc/sample.pb.go create mode 100644 api/tests/integration/testdata/services/grpc/sample.pb.validate.go create mode 100644 api/tests/integration/testdata/services/grpc/sample.proto create mode 100644 api/tests/integration/testdata/services/grpc/sample_grpc.pb.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3539f93f..cff46244 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,6 +66,10 @@ jobs: - name: Unit test run: make unit-test + - name: Set up services + run: | + make start-service + - name: Build run: make build-test-so - name: Integration test diff --git a/.gitignore b/.gitignore index 647db897..46ad4f50 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ site/static/images site/.hugo_build.lock site/tmp # dev files +api/tests/integration/testdata/services/grpc/grpc controller/**/cache controller/**/log !controller/internal/log diff --git a/.ignore_words b/.ignore_words index 56a2a5ae..ed86371f 100644 --- a/.ignore_words +++ b/.ignore_words @@ -1 +1,2 @@ fo +te diff --git a/Makefile b/Makefile index 0d8c1b0e..4d7540c8 100644 --- a/Makefile +++ b/Makefile @@ -144,7 +144,7 @@ lint-spell: dev-tools ${DEV_TOOLS_IMAGE} \ make lint-spell-local -CODESPELL = codespell --skip 'test-envoy,go.mod,go.sum,*.svg,*.patch,./site/public/**,external,.git,.idea,go.work.sum' --check-filenames --check-hidden --ignore-words ./.ignore_words . +CODESPELL = codespell --skip '.ignore_words,test-envoy,go.mod,go.sum,*.svg,*.patch,./site/public/**,external,.git,.idea,go.work.sum' --check-filenames --check-hidden --ignore-words ./.ignore_words . .PHONY: lint-spell-local lint-spell-local: $(CODESPELL) diff --git a/api/Makefile b/api/Makefile index 3552c4fe..0e0c86c4 100644 --- a/api/Makefile +++ b/api/Makefile @@ -46,6 +46,7 @@ build-test-so: # The data plane image used in the integration test can be controlled via env var PROXY_IMAGE .PHONY: integration-test integration-test: + if ! command -v grpcurl >/dev/null 2>&1; then go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest; fi test -d /tmp/htnn_coverage && rm -rf /tmp/htnn_coverage || true $(foreach PKG, $(shell go list ./tests/integration/...), \ go test -tags envoy${ENVOY_API_VERSION} -v ${PKG} || exit 1; \ @@ -55,3 +56,11 @@ integration-test: .PHONY: benchmark benchmark: go test -tags benchmark,envoy${ENVOY_API_VERSION} -v ./tests/integration/ -run TestBenchmark + +.PHONY: start-service +start-service: + cd ./tests/integration/testdata/services && docker compose up -d --build + +.PHONY: stop-service +stop-service: + cd ./tests/integration/testdata/services && docker compose down diff --git a/api/pkg/filtermanager/api/api.go b/api/pkg/filtermanager/api/api.go index ccfad7fb..7861838b 100644 --- a/api/pkg/filtermanager/api/api.go +++ b/api/pkg/filtermanager/api/api.go @@ -24,17 +24,17 @@ import ( type DecodeWholeRequestFilter interface { // DecodeRequest processes the whole request once when WaitAllData is returned from DecodeHeaders - // headers: the request header + // headers: the request headers // data: the whole request body, nil if the request doesn't have body - // trailers: TODO, just a placeholder + // trailers: the request trailers, nil if the request doesn't have trailers DecodeRequest(headers RequestHeaderMap, data BufferInstance, trailers RequestTrailerMap) ResultAction } type EncodeWholeResponseFilter interface { // EncodeResponse processes the whole response once when WaitAllData is returned from EncodeHeaders - // headers: the response header + // headers: the response headers // data: the whole response body, nil if the response doesn't have body - // trailers: TODO, just a placeholder + // trailers: the response trailers, current it's nil because of a bug in Envoy EncodeResponse(headers ResponseHeaderMap, data BufferInstance, trailers ResponseTrailerMap) ResultAction } @@ -51,7 +51,7 @@ type Filter interface { // DecodeData might be called multiple times during handling the request body. // The endStream is true when handling the last piece of the body. DecodeData(data BufferInstance, endStream bool) ResultAction - // TODO, just a placeholder. DecodeTrailers is not called yet + // DecodeTrailers processes request trailers. It doesn't fully work on Envoy < 1.31. DecodeTrailers(trailers RequestTrailerMap) ResultAction DecodeWholeRequestFilter @@ -62,12 +62,12 @@ type Filter interface { // EncodeData might be called multiple times during handling the response body. // The endStream is true when handling the last piece of the body. EncodeData(data BufferInstance, endStream bool) ResultAction - // TODO, just a placeholder. EncodeTrailers is not called yet + // EncodeTrailers processes response trailers. It doesn't fully work on Envoy < 1.31. EncodeTrailers(trailers ResponseTrailerMap) ResultAction EncodeWholeResponseFilter // OnLog is called when the HTTP stream is ended on HTTP Connection Manager filter. - // TODO: the trailers here are just placeholders. + // The trailers here are always nil on Envoy < 1.32. OnLog(reqHeaders RequestHeaderMap, reqTrailers RequestTrailerMap, respHeaders ResponseHeaderMap, respTrailers ResponseTrailerMap) } diff --git a/api/pkg/filtermanager/filtermanager.go b/api/pkg/filtermanager/filtermanager.go index 2ad122aa..17a9134e 100644 --- a/api/pkg/filtermanager/filtermanager.go +++ b/api/pkg/filtermanager/filtermanager.go @@ -38,21 +38,25 @@ type filterManager struct { decodeRequestNeeded bool decodeIdx int reqHdr api.RequestHeaderMap // don't access it in Encode phases + reqBuf capi.BufferInstance // don't access it in Encode phases encodeResponseNeeded bool encodeIdx int rspHdr api.ResponseHeaderMap + rspBuf capi.BufferInstance runningInGoThread atomic.Int32 hdrLock sync.Mutex // use a group of bools instead of map to avoid lookup - canSkipDecodeHeaders bool - canSkipDecodeData bool - canSkipEncodeHeaders bool - canSkipEncodeData bool - canSkipOnLog bool - canSkipMethod map[string]bool + canSkipDecodeHeaders bool + canSkipDecodeData bool + canSkipDecodeTrailers bool + canSkipEncodeHeaders bool + canSkipEncodeData bool + canSkipEncodeTrailers bool + canSkipOnLog bool + canSkipMethod map[string]bool callbacks *filterManagerCallbackHandler config *filterManagerConfig @@ -66,17 +70,21 @@ func (m *filterManager) Reset() { m.decodeRequestNeeded = false m.decodeIdx = -1 m.reqHdr = nil + m.reqBuf = nil m.encodeResponseNeeded = false m.encodeIdx = -1 m.rspHdr = nil + m.rspBuf = nil m.runningInGoThread.Store(0) // defence in depth m.canSkipDecodeHeaders = false m.canSkipDecodeData = false + m.canSkipDecodeTrailers = false m.canSkipEncodeHeaders = false m.canSkipEncodeData = false + m.canSkipEncodeTrailers = false m.canSkipOnLog = false m.callbacks.Reset() @@ -116,9 +124,11 @@ func newSkipMethodsMap() map[string]bool { "DecodeHeaders": true, "DecodeData": true, "DecodeRequest": true, + "DecodeTrailers": true, "EncodeHeaders": true, "EncodeData": true, "EncodeResponse": true, + "EncodeTrailers": true, "OnLog": true, } } @@ -220,8 +230,10 @@ func FilterManagerFactory(c interface{}, cb capi.FilterCallbackHandler) (streamF // even it is not called, DecodeData will not be skipped. Same as EncodeResponse. fm.canSkipDecodeHeaders = fm.canSkipMethod["DecodeHeaders"] && fm.canSkipMethod["DecodeRequest"] && fm.config.initOnce == nil fm.canSkipDecodeData = fm.canSkipMethod["DecodeData"] && fm.canSkipMethod["DecodeRequest"] + fm.canSkipDecodeTrailers = fm.canSkipMethod["DecodeTrailers"] && fm.canSkipMethod["DecodeRequest"] fm.canSkipEncodeHeaders = fm.canSkipMethod["EncodeHeaders"] fm.canSkipEncodeData = fm.canSkipMethod["EncodeData"] && fm.canSkipMethod["EncodeResponse"] + fm.canSkipEncodeTrailers = fm.canSkipMethod["EncodeTrailers"] && fm.canSkipMethod["EncodeResponse"] fm.canSkipOnLog = fm.canSkipMethod["OnLog"] return wrapFilterManager(fm) @@ -443,8 +455,10 @@ func (m *filterManager) DecodeHeaders(headers capi.RequestHeaderMap, endStream b canSkipMethod := c.CanSkipMethod m.canSkipDecodeData = m.canSkipDecodeData && canSkipMethod["DecodeData"] && canSkipMethod["DecodeRequest"] + m.canSkipDecodeTrailers = m.canSkipDecodeTrailers && canSkipMethod["DecodeTrailers"] && canSkipMethod["DecodeRequest"] m.canSkipEncodeHeaders = m.canSkipEncodeData && canSkipMethod["EncodeHeaders"] m.canSkipEncodeData = m.canSkipEncodeData && canSkipMethod["EncodeData"] && canSkipMethod["EncodeResponse"] + m.canSkipEncodeTrailers = m.canSkipEncodeTrailers && canSkipMethod["EncodeTrailers"] && canSkipMethod["EncodeResponse"] m.canSkipOnLog = m.canSkipOnLog && canSkipMethod["OnLog"] // TODO: add field to control if merging is allowed @@ -495,7 +509,7 @@ func (m *filterManager) DecodeHeaders(headers capi.RequestHeaderMap, endStream b return } - // no body + // no body and no trailers res = f.DecodeRequest(m.reqHdr, nil, nil) if m.handleAction(res, phaseDecodeRequest, f) { return @@ -509,6 +523,94 @@ func (m *filterManager) DecodeHeaders(headers capi.RequestHeaderMap, endStream b return capi.Running } +func (m *filterManager) DecodeRequest(headers api.RequestHeaderMap, buf capi.BufferInstance, trailers capi.RequestTrailerMap) bool { + // for readable + endStreamInBody := trailers == nil + hasBody := buf != nil + hasTrailers := trailers != nil + + var res api.ResultAction + if hasBody { + for i := 0; i < m.decodeIdx; i++ { + f := m.filters[i] + res = f.DecodeData(buf, endStreamInBody) + if m.handleAction(res, phaseDecodeData, f) { + return false + } + } + } + + // run DecodeTrailers as well after processing all the data + if hasTrailers { + for i := 0; i < m.decodeIdx; i++ { + f := m.filters[i] + res = f.DecodeTrailers(trailers) + if m.handleAction(res, phaseDecodeTrailers, f) { + return false + } + } + } + + f := m.filters[m.decodeIdx] + res = f.DecodeRequest(headers, buf, trailers) + if m.handleAction(res, phaseDecodeRequest, f) { + return false + } + + n := len(m.filters) + i := m.decodeIdx + 1 + for i < n { + for ; i < n; i++ { + f := m.filters[i] + // The endStream in DecodeHeaders indicates whether there is a body. + // The body always exists when we hit this path. + res = f.DecodeHeaders(headers, false) + if m.handleAction(res, phaseDecodeHeaders, f) { + return false + } + if m.decodeRequestNeeded { + // decodeRequestNeeded will be set to false below + break + } + } + + // When there are multiple filters want to decode the whole req, + // run part of the DecodeData which is before them + if hasBody { + for j := m.decodeIdx + 1; j < i; j++ { + f := m.filters[j] + res = f.DecodeData(buf, endStreamInBody) + if m.handleAction(res, phaseDecodeData, f) { + return false + } + } + } + + if hasTrailers { + for j := m.decodeIdx + 1; j < i; j++ { + f := m.filters[j] + res = f.DecodeTrailers(trailers) + if m.handleAction(res, phaseDecodeTrailers, f) { + return false + } + } + } + + if m.decodeRequestNeeded { + m.decodeRequestNeeded = false + m.decodeIdx = i + f := m.filters[m.decodeIdx] + res = f.DecodeRequest(headers, buf, trailers) + if m.handleAction(res, phaseDecodeRequest, f) { + return false + } + i++ + } + } + + return true +} + func (m *filterManager) DecodeData(buf capi.BufferInstance, endStream bool) capi.StatusType { if m.canSkipDecodeData { return capi.Continue @@ -534,6 +636,7 @@ func (m *filterManager) DecodeData(buf capi.BufferInstance, endStream bool) capi // Otherwise, streaming processing is used. It's simple and already satisfies our // most demand, so we choose this way for now. + status := capi.Continue n := len(m.filters) if m.decodeIdx == -1 { // every filter doesn't need buffered body @@ -544,63 +647,49 @@ func (m *filterManager) DecodeData(buf capi.BufferInstance, endStream bool) capi return } } - m.callbacks.Continue(capi.Continue, true) - - } else { - for i := 0; i < m.decodeIdx; i++ { - f := m.filters[i] - res = f.DecodeData(buf, endStream) - if m.handleAction(res, phaseDecodeData, f) { - return - } - } - - f := m.filters[m.decodeIdx] - res = f.DecodeRequest(m.reqHdr, buf, nil) - if m.handleAction(res, phaseDecodeRequest, f) { + } else if endStream { + conti := m.DecodeRequest(m.reqHdr, buf, nil) + if !conti { return } + } else { + m.reqBuf = buf + status = capi.StopAndBuffer + } - i := m.decodeIdx + 1 - for i < n { - for ; i < n; i++ { - f := m.filters[i] - // The endStream in DecodeHeaders indicates whether there is a body. - // The body always exists when we hit this path. - res = f.DecodeHeaders(m.reqHdr, false) - if m.handleAction(res, phaseDecodeHeaders, f) { - return - } - if m.decodeRequestNeeded { - // decodeRequestNeeded will be set to false below - break - } - } + m.callbacks.Continue(status, true) + }() - // When there are multiple filters want to decode the whole req, - // run part of the DecodeData which is before them - for j := m.decodeIdx + 1; j < i; j++ { - f := m.filters[j] - res = f.DecodeData(buf, endStream) - if m.handleAction(res, phaseDecodeData, f) { - return - } - } + return capi.Running +} - if m.decodeRequestNeeded { - m.decodeRequestNeeded = false - m.decodeIdx = i - f := m.filters[m.decodeIdx] - res = f.DecodeRequest(m.reqHdr, buf, nil) - if m.handleAction(res, phaseDecodeRequest, f) { - return - } - i++ +func (m *filterManager) DecodeTrailers(trailers capi.RequestTrailerMap) capi.StatusType { + if m.canSkipDecodeTrailers { + return capi.Continue + } + + m.MarkRunningInGoThread(true) + + go func() { + defer m.MarkRunningInGoThread(false) + defer m.callbacks.DecoderFilterCallbacks().RecoverPanic() + var res api.ResultAction + + if m.decodeIdx == -1 { + for _, f := range m.filters { + res = f.DecodeTrailers(trailers) + if m.handleAction(res, phaseDecodeTrailers, f) { + return } } - - m.callbacks.Continue(capi.Continue, true) + } else { + conti := m.DecodeRequest(m.reqHdr, m.reqBuf, trailers) + if !conti { + return + } } + + m.callbacks.Continue(capi.Continue, true) }() return capi.Running @@ -657,6 +746,88 @@ func (m *filterManager) EncodeHeaders(headers capi.ResponseHeaderMap, endStream return capi.Running } +func (m *filterManager) EncodeResponse(headers api.ResponseHeaderMap, buf capi.BufferInstance, trailers capi.ResponseTrailerMap) bool { + endStreamInBody := trailers == nil + hasBody := buf != nil + hasTrailers := trailers != nil + + var res api.ResultAction + n := len(m.filters) + if hasBody { + for i := n - 1; i > m.encodeIdx; i-- { + f := m.filters[i] + res = f.EncodeData(buf, endStreamInBody) + if m.handleAction(res, phaseEncodeData, f) { + return false + } + } + } + + if hasTrailers { + for i := n - 1; i > m.encodeIdx; i-- { + f := m.filters[i] + res = f.EncodeTrailers(trailers) + if m.handleAction(res, phaseEncodeTrailers, f) { + return false + } + } + } + + f := m.filters[m.encodeIdx] + res = f.EncodeResponse(m.rspHdr, buf, nil) + if m.handleAction(res, phaseEncodeResponse, f) { + return false + } + + i := m.encodeIdx - 1 + for i >= 0 { + for ; i >= 0; i-- { + f := m.filters[i] + res = f.EncodeHeaders(m.rspHdr, false) + if m.handleAction(res, phaseEncodeHeaders, f) { + return false + } + if m.encodeResponseNeeded { + // encodeResponseNeeded will be set to false below + break + } + } + + if hasBody { + for j := m.encodeIdx - 1; j > i; j-- { + f := m.filters[j] + res = f.EncodeData(buf, endStreamInBody) + if m.handleAction(res, phaseEncodeData, f) { + return false + } + } + } + + if hasTrailers { + for j := m.encodeIdx - 1; j > i; j-- { + f := m.filters[j] + res = f.EncodeTrailers(trailers) + if m.handleAction(res, phaseEncodeTrailers, f) { + return false + } + } + } + + if m.encodeResponseNeeded { + m.encodeResponseNeeded = false + m.encodeIdx = i + f := m.filters[m.encodeIdx] + res = f.EncodeResponse(m.rspHdr, buf, nil) + if m.handleAction(res, phaseEncodeResponse, f) { + return false + } + i-- + } + } + + return true +} + func (m *filterManager) EncodeData(buf capi.BufferInstance, endStream bool) capi.StatusType { if m.canSkipEncodeData { return capi.Continue @@ -669,6 +840,7 @@ func (m *filterManager) EncodeData(buf capi.BufferInstance, endStream bool) capi defer m.callbacks.EncoderFilterCallbacks().RecoverPanic() var res api.ResultAction + status := capi.Continue n := len(m.filters) if m.encodeIdx == -1 { // every filter doesn't need buffered body @@ -679,73 +851,56 @@ func (m *filterManager) EncodeData(buf capi.BufferInstance, endStream bool) capi return } } - m.callbacks.Continue(capi.Continue, false) - } else { - for i := n - 1; i > m.encodeIdx; i-- { - f := m.filters[i] - res = f.EncodeData(buf, endStream) - if m.handleAction(res, phaseEncodeData, f) { - return - } - } - - f := m.filters[m.encodeIdx] - res = f.EncodeResponse(m.rspHdr, buf, nil) - if m.handleAction(res, phaseEncodeResponse, f) { + // FIXME: we should implement like the decode part here, but it will cause server closed the stream without sending trailers + conti := m.EncodeResponse(m.rspHdr, buf, nil) + if !conti { return } + } - i := m.encodeIdx - 1 - for i >= 0 { - for ; i >= 0; i-- { - f := m.filters[i] - res = f.EncodeHeaders(m.rspHdr, false) - if m.handleAction(res, phaseEncodeHeaders, f) { - return - } - if m.encodeResponseNeeded { - // encodeResponseNeeded will be set to false below - break - } - } + m.callbacks.Continue(status, false) + }() - for j := m.encodeIdx - 1; j > i; j-- { - f := m.filters[j] - res = f.EncodeData(buf, endStream) - if m.handleAction(res, phaseEncodeData, f) { - return - } - } + return capi.Running +} - if m.encodeResponseNeeded { - m.encodeResponseNeeded = false - m.encodeIdx = i - f := m.filters[m.encodeIdx] - res = f.EncodeResponse(m.rspHdr, buf, nil) - if m.handleAction(res, phaseEncodeResponse, f) { - return - } - i-- +func (m *filterManager) EncodeTrailers(trailers capi.ResponseTrailerMap) capi.StatusType { + if m.canSkipEncodeTrailers { + return capi.Continue + } + + m.MarkRunningInGoThread(true) + + go func() { + defer m.MarkRunningInGoThread(false) + defer m.callbacks.EncoderFilterCallbacks().RecoverPanic() + var res api.ResultAction + + if m.encodeIdx == -1 { + for _, f := range m.filters { + res = f.EncodeTrailers(trailers) + if m.handleAction(res, phaseEncodeTrailers, f) { + return } } - - m.callbacks.Continue(capi.Continue, false) } + + m.callbacks.Continue(capi.Continue, false) }() return capi.Running } -// TODO: handle trailers +func (m *filterManager) runOnLogPhase(reqHdr api.RequestHeaderMap, reqTrailer api.RequestTrailerMap, + rspHdr api.ResponseHeaderMap, rspTrailer api.ResponseTrailerMap) { -func (m *filterManager) runOnLogPhase(reqHdr api.RequestHeaderMap, rspHdr api.ResponseHeaderMap) { // It is unsafe to access the f.callbacks in the goroutine, as the underlying request // may be destroyed when the goroutine is running. So if people want to do some IO jobs, // they need to copy the used data from the request to the Go side before kicking off // the goroutine. for _, f := range m.filters { - f.OnLog(reqHdr, nil, rspHdr, nil) + f.OnLog(reqHdr, reqTrailer, rspHdr, rspTrailer) } if m.IsRunningInGoThread() { diff --git a/api/pkg/filtermanager/filtermanager_dev.go b/api/pkg/filtermanager/filtermanager_dev.go index b5e1f795..f577e113 100644 --- a/api/pkg/filtermanager/filtermanager_dev.go +++ b/api/pkg/filtermanager/filtermanager_dev.go @@ -24,7 +24,7 @@ const ( supportGettingHeadersOnLog = true ) -func (m *filterManager) OnLog(reqHdr capi.RequestHeaderMap, _ capi.RequestTrailerMap, rspHdr capi.ResponseHeaderMap, _ capi.ResponseTrailerMap) { +func (m *filterManager) OnLog(reqHdr capi.RequestHeaderMap, reqTrailer capi.RequestTrailerMap, rspHdr capi.ResponseHeaderMap, rspTrailer capi.ResponseTrailerMap) { if m.canSkipOnLog { return } @@ -41,7 +41,7 @@ func (m *filterManager) OnLog(reqHdr capi.RequestHeaderMap, _ capi.RequestTraile h.RequestHeaderMap = reqHdr } m.hdrLock.Unlock() - m.runOnLogPhase(m.reqHdr, rspHdr) + m.runOnLogPhase(m.reqHdr, reqTrailer, rspHdr, rspTrailer) } func wrapFilterManager(fm *filterManager) capi.StreamFilter { diff --git a/api/pkg/filtermanager/filtermanager_latest.go b/api/pkg/filtermanager/filtermanager_latest.go index 977f55a9..8c2eb1ec 100644 --- a/api/pkg/filtermanager/filtermanager_latest.go +++ b/api/pkg/filtermanager/filtermanager_latest.go @@ -40,7 +40,7 @@ func (m *filterManager) OnLog(_ capi.RequestHeaderMap, _ capi.RequestTrailerMap, rspHdr = m.rspHdr m.hdrLock.Unlock() - m.runOnLogPhase(reqHdr, rspHdr) + m.runOnLogPhase(reqHdr, nil, rspHdr, nil) } type filterManagerWrapper struct { diff --git a/api/pkg/filtermanager/filtermanager_test.go b/api/pkg/filtermanager/filtermanager_test.go index f066fe2e..07450341 100644 --- a/api/pkg/filtermanager/filtermanager_test.go +++ b/api/pkg/filtermanager/filtermanager_test.go @@ -46,13 +46,21 @@ func TestPassThrough(t *testing.T) { m.DecodeHeaders(hdr, false) cb.WaitContinued() buf := envoy.NewBufferInstance([]byte{}) - m.DecodeData(buf, true) + m.DecodeData(buf, false) cb.WaitContinued() + trailer := envoy.NewRequestTrailerMap(http.Header{}) + m.DecodeTrailers(trailer) + cb.WaitContinued() + respHdr := envoy.NewResponseHeaderMap(http.Header{}) m.EncodeHeaders(respHdr, false) cb.WaitContinued() - m.EncodeData(buf, true) + m.EncodeData(buf, false) + cb.WaitContinued() + respTrailer := envoy.NewResponseTrailerMap(http.Header{}) + m.EncodeTrailers(respTrailer) cb.WaitContinued() + m.OnLog(hdr, nil, respHdr, nil) } } @@ -352,6 +360,11 @@ func (f *addReqFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream boo return api.Continue } +func (f *addReqFilter) DecodeTrailers(trailers api.RequestTrailerMap) api.ResultAction { + trailers.Set(f.conf.hdrName, "htnn") + return api.Continue +} + func TestSkipMethodWhenThereAreMultiFilters(t *testing.T) { cb := envoy.NewCAPIFilterCallbackHandler() config := initFilterManagerConfig("ns") @@ -374,6 +387,8 @@ func TestSkipMethodWhenThereAreMultiFilters(t *testing.T) { assert.Equal(t, false, m.canSkipOnLog) assert.Equal(t, false, m.canSkipDecodeHeaders) assert.Equal(t, true, m.canSkipDecodeData) + assert.Equal(t, false, m.canSkipDecodeTrailers) + assert.Equal(t, true, m.canSkipEncodeTrailers) } } @@ -571,6 +586,26 @@ func TestDoNotRecycleInUsedFilterManager(t *testing.T) { } wg.Wait() + // DecodeTrailers + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + cb := envoy.NewCAPIFilterCallbackHandler() + m := unwrapFilterManager(FilterManagerFactory(config, cb)) + h := http.Header{} + hdr := envoy.NewRequestHeaderMap(h) + m.DecodeHeaders(hdr, false) + cb.WaitContinued() + m.DecodeData(nil, true) + cb.WaitContinued() + trailer := envoy.NewRequestTrailerMap(h) + m.DecodeTrailers(trailer) + m.OnLog(hdr, nil, nil, nil) + wg.Done() + }(i) + } + wg.Wait() + // EncodeHeaders wg.Add(n) for i := 0; i < n; i++ { @@ -608,4 +643,27 @@ func TestDoNotRecycleInUsedFilterManager(t *testing.T) { }(i) } wg.Wait() + + // EncodeTrailers + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + cb := envoy.NewCAPIFilterCallbackHandler() + m := unwrapFilterManager(FilterManagerFactory(config, cb)) + h := http.Header{} + hdr := envoy.NewRequestHeaderMap(h) + m.DecodeHeaders(hdr, true) + cb.WaitContinued() + hdr2 := envoy.NewResponseHeaderMap(h) + m.EncodeHeaders(hdr2, true) + cb.WaitContinued() + m.EncodeData(nil, true) + cb.WaitContinued() + trailer := envoy.NewRequestTrailerMap(h) + m.EncodeTrailers(trailer) + m.OnLog(hdr, nil, hdr2, trailer) + wg.Done() + }(i) + } + wg.Wait() } diff --git a/api/plugins/tests/integration/dataplane/bootstrap.yml b/api/plugins/tests/integration/dataplane/bootstrap.yml index 007e32a5..8e82e532 100644 --- a/api/plugins/tests/integration/dataplane/bootstrap.yml +++ b/api/plugins/tests/integration/dataplane/bootstrap.yml @@ -25,6 +25,8 @@ static_resources: - name: envoy.access_loggers.stdout typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + http_protocol_options: + enable_trailers: true http_filters: - name: htnn-consumer disabled: true @@ -77,6 +79,8 @@ static_resources: - name: envoy.access_loggers.stdout typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + http_protocol_options: + enable_trailers: true http_filters: - name: htnn.filters.http.bandwidth_limit typed_config: @@ -110,6 +114,7 @@ static_resources: source_code: inline_string: | function envoy_on_request(handle) + handle:logInfo("upstream receives request") local headers = handle:headers() local resp_headers = {[":status"] = "200"} for key, value in pairs(headers) do @@ -135,6 +140,23 @@ static_resources: data = body:getBytes(0, size) end + local trailers = handle:trailers() + if trailers ~= nil then + for key, value in pairs(trailers) do + handle:logInfo("upstream receives trailer: " .. key .. " = " .. value) + -- Lua doesn't support setting trailler, use headers instead + local k = "echo-trailer-" .. key + local v = resp_headers[k] + if v ~= nil then + table.insert(v, value) + value = v + else + value = {value} + end + resp_headers[k] = value + end + end + handle:respond( resp_headers, data @@ -175,6 +197,10 @@ static_resources: end function envoy_on_response(handle) end + - match: + prefix: /api.tests. + route: + cluster: grpc_backend - name: dynamic_config internal_listener: {} filter_chains: @@ -210,6 +236,7 @@ static_resources: - name: backend type: strict_dns lb_policy: round_robin + http2_protocol_options: {} load_assignment: cluster_name: backend endpoints: @@ -219,6 +246,19 @@ static_resources: socket_address: address: 127.0.0.1 port_value: 10001 + - name: grpc_backend + type: strict_dns + lb_policy: round_robin + http2_protocol_options: {} + load_assignment: + cluster_name: grpc_backend + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: grpc + port_value: 50051 - name: config_server connect_timeout: 0.25s type: strict_dns diff --git a/api/plugins/tests/integration/dataplane/data_plane.go b/api/plugins/tests/integration/dataplane/data_plane.go index 21d3fdd7..5d3a79b3 100644 --- a/api/plugins/tests/integration/dataplane/data_plane.go +++ b/api/plugins/tests/integration/dataplane/data_plane.go @@ -368,6 +368,10 @@ func (dp *DataPlane) Post(path string, header http.Header, body io.Reader) (*htt return dp.do("POST", path, header, body) } +func (dp *DataPlane) PostWithTrailer(path string, header http.Header, body io.Reader, trailer http.Header) (*http.Response, error) { + return dp.doWithTrailer("POST", path, header, body, trailer) +} + func (dp *DataPlane) Put(path string, header http.Header, body io.Reader) (*http.Response, error) { return dp.do("PUT", path, header, body) } @@ -419,6 +423,39 @@ func (dp *DataPlane) do(method string, path string, header http.Header, body io. return resp, err } +func (dp *DataPlane) doWithTrailer(method string, path string, header http.Header, body io.Reader, trailer http.Header) (*http.Response, error) { + req, err := http.NewRequest(method, "http://localhost:10000"+path, body) + if err != nil { + return nil, err + } + req.Header = header + req.Header.Add("TE", "trailers") + req.Trailer = trailer + req.TransferEncoding = []string{"chunked"} + tr := &http.Transport{ + DialContext: func(ctx context.Context, proto, addr string) (conn net.Conn, err error) { + return net.DialTimeout("tcp", ":10000", 1*time.Second) + }, + } + + client := &http.Client{Transport: tr, + Timeout: 10 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + resp, err := client.Do(req) + return resp, err +} + +// Use grpcurl so that the caller can specify the proto file without building the Go code. +// TODO: we can rewrite this in Go. +func (dp *DataPlane) Grpcurl(importPath, protoFile, fullMethodName, req string) ([]byte, error) { + cmd := exec.Command("grpcurl", "-v", "-format-error", "-import-path", importPath, "-proto", protoFile, "-plaintext", "-d", req, ":10000", fullMethodName) + dp.t.Logf("run grpcurl command: %s", cmd.String()) + return cmd.CombinedOutput() +} + func (dp *DataPlane) Configured() bool { // TODO: this is fine for the first init of the envoy configuration. // But it may be misleading when updating the configuration. diff --git a/api/plugins/tests/pkg/envoy/capi.go b/api/plugins/tests/pkg/envoy/capi.go index 58d55527..89b9f710 100644 --- a/api/plugins/tests/pkg/envoy/capi.go +++ b/api/plugins/tests/pkg/envoy/capi.go @@ -330,6 +330,18 @@ func (bi *BufferInstance) AppendString(s string) error { return bi.Append([]byte(s)) } +func NewRequestTrailerMap(hdr http.Header) api.RequestTrailerMap { + return &HeaderMap{ + Header: hdr, + } +} + +func NewResponseTrailerMap(hdr http.Header) api.ResponseTrailerMap { + return &HeaderMap{ + Header: hdr, + } +} + type DynamicMetadata struct { store map[string]map[string]interface{} } diff --git a/api/tests/integration/config.pb.go b/api/tests/integration/config.pb.go index 0c469501..f61503d8 100644 --- a/api/tests/integration/config.pb.go +++ b/api/tests/integration/config.pb.go @@ -40,12 +40,14 @@ type Config struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Need bool `protobuf:"varint,1,opt,name=need,proto3" json:"need,omitempty"` - Decode bool `protobuf:"varint,2,opt,name=decode,proto3" json:"decode,omitempty"` - Encode bool `protobuf:"varint,3,opt,name=encode,proto3" json:"encode,omitempty"` - Headers bool `protobuf:"varint,4,opt,name=headers,proto3" json:"headers,omitempty"` - Data bool `protobuf:"varint,5,opt,name=data,proto3" json:"data,omitempty"` - ReplyMsg string `protobuf:"bytes,6,opt,name=reply_msg,json=replyMsg,proto3" json:"reply_msg,omitempty"` + NeedBuffer bool `protobuf:"varint,1,opt,name=need_buffer,json=needBuffer,proto3" json:"need_buffer,omitempty"` + Decode bool `protobuf:"varint,2,opt,name=decode,proto3" json:"decode,omitempty"` + Encode bool `protobuf:"varint,3,opt,name=encode,proto3" json:"encode,omitempty"` + Headers bool `protobuf:"varint,4,opt,name=headers,proto3" json:"headers,omitempty"` + Data bool `protobuf:"varint,5,opt,name=data,proto3" json:"data,omitempty"` + Trailers bool `protobuf:"varint,6,opt,name=trailers,proto3" json:"trailers,omitempty"` + ReplyMsg string `protobuf:"bytes,7,opt,name=reply_msg,json=replyMsg,proto3" json:"reply_msg,omitempty"` + InGrpcMode bool `protobuf:"varint,8,opt,name=in_grpc_mode,json=inGrpcMode,proto3" json:"in_grpc_mode,omitempty"` } func (x *Config) Reset() { @@ -80,9 +82,9 @@ func (*Config) Descriptor() ([]byte, []int) { return file_api_tests_integration_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetNeed() bool { +func (x *Config) GetNeedBuffer() bool { if x != nil { - return x.Need + return x.NeedBuffer } return false } @@ -115,6 +117,13 @@ func (x *Config) GetData() bool { return false } +func (x *Config) GetTrailers() bool { + if x != nil { + return x.Trailers + } + return false +} + func (x *Config) GetReplyMsg() string { if x != nil { return x.ReplyMsg @@ -122,6 +131,13 @@ func (x *Config) GetReplyMsg() string { return "" } +func (x *Config) GetInGrpcMode() bool { + if x != nil { + return x.InGrpcMode + } + return false +} + type BadPluginConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -246,33 +262,37 @@ var file_api_tests_integration_config_proto_rawDesc = []byte{ 0x0a, 0x22, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x97, 0x01, 0x0a, 0x06, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x65, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6e, 0x65, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x65, 0x63, 0x6f, - 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x06, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x65, 0x61, - 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6c, - 0x79, 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, - 0x6c, 0x79, 0x4d, 0x73, 0x67, 0x22, 0xa9, 0x01, 0x0a, 0x0f, 0x42, 0x61, 0x64, 0x50, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x61, 0x6e, - 0x69, 0x63, 0x5f, 0x69, 0x6e, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0e, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x49, 0x6e, 0x46, 0x61, 0x63, 0x74, - 0x6f, 0x72, 0x79, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x5f, 0x69, 0x6e, 0x5f, - 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x61, 0x6e, - 0x69, 0x63, 0x49, 0x6e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x5f, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x22, 0x0a, - 0x0d, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x5f, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x49, 0x6e, 0x49, 0x6e, 0x69, - 0x74, 0x22, 0x24, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x24, 0x5a, 0x22, 0x6d, 0x6f, 0x73, 0x6e, 0x2e, - 0x69, 0x6f, 0x2f, 0x68, 0x74, 0x6e, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x65, 0x73, 0x74, - 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xe2, 0x01, 0x0a, 0x06, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x65, 0x65, 0x64, 0x5f, 0x62, + 0x75, 0x66, 0x66, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x65, 0x65, + 0x64, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, + 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x4d, 0x73, 0x67, 0x12, 0x20, + 0x0a, 0x0c, 0x69, 0x6e, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x6e, 0x47, 0x72, 0x70, 0x63, 0x4d, 0x6f, 0x64, 0x65, + 0x22, 0xa9, 0x01, 0x0a, 0x0f, 0x42, 0x61, 0x64, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x5f, 0x69, 0x6e, + 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, + 0x70, 0x61, 0x6e, 0x69, 0x63, 0x49, 0x6e, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x24, + 0x0a, 0x0e, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x5f, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x49, 0x6e, 0x50, + 0x61, 0x72, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x69, 0x6e, + 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x49, 0x6e, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x61, 0x6e, 0x69, + 0x63, 0x5f, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0b, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x49, 0x6e, 0x49, 0x6e, 0x69, 0x74, 0x22, 0x24, 0x0a, 0x0e, + 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x42, 0x24, 0x5a, 0x22, 0x6d, 0x6f, 0x73, 0x6e, 0x2e, 0x69, 0x6f, 0x2f, 0x68, 0x74, + 0x6e, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/api/tests/integration/config.pb.validate.go b/api/tests/integration/config.pb.validate.go index eabe5e50..650c3698 100644 --- a/api/tests/integration/config.pb.validate.go +++ b/api/tests/integration/config.pb.validate.go @@ -56,7 +56,7 @@ func (m *Config) validate(all bool) error { var errors []error - // no validation rules for Need + // no validation rules for NeedBuffer // no validation rules for Decode @@ -66,8 +66,12 @@ func (m *Config) validate(all bool) error { // no validation rules for Data + // no validation rules for Trailers + // no validation rules for ReplyMsg + // no validation rules for InGrpcMode + if len(errors) > 0 { return ConfigMultiError(errors) } diff --git a/api/tests/integration/config.proto b/api/tests/integration/config.proto index e8e1b523..6d32d058 100644 --- a/api/tests/integration/config.proto +++ b/api/tests/integration/config.proto @@ -19,12 +19,14 @@ package api.tests.integration; option go_package = "mosn.io/htnn/api/tests/integration"; message Config { - bool need = 1; + bool need_buffer = 1; bool decode = 2; bool encode = 3; bool headers = 4; bool data = 5; - string reply_msg = 6; + bool trailers = 6; + string reply_msg = 7; + bool in_grpc_mode = 8; } message BadPluginConfig { diff --git a/api/tests/integration/filtermanager_dev_test.go b/api/tests/integration/filtermanager_dev_test.go new file mode 100644 index 00000000..dcb01b69 --- /dev/null +++ b/api/tests/integration/filtermanager_dev_test.go @@ -0,0 +1,60 @@ +// Copyright The HTNN Authors. +// +// 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. + +//go:build envoydev + +package integration + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "mosn.io/htnn/api/pkg/filtermanager" + "mosn.io/htnn/api/pkg/filtermanager/model" + "mosn.io/htnn/api/plugins/tests/integration/dataplane" +) + +func TestFilterManagerLogWithTrailers(t *testing.T) { + dp, err := dataplane.StartDataPlane(t, &dataplane.Option{ + ExpectLogPattern: []string{ + `receive request trailers: .*expires:Wed, 21 Oct 2015 07:28:00 GMT.*`, + }, + }) + if err != nil { + t.Fatalf("failed to start data plane: %v", err) + return + } + defer dp.Stop() + + lp := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "onLog", + Config: &Config{}, + }, + }, + } + + controlPlane.UseGoPluginConfig(t, lp, dp) + hdr := http.Header{} + trailer := http.Header{} + trailer.Add("Expires", "Wed, 21 Oct 2015 07:28:00 GMT") + resp, err := dp.PostWithTrailer("/echo", hdr, bytes.NewReader([]byte("test")), trailer) + require.Nil(t, err) + assert.Equal(t, 200, resp.StatusCode) +} diff --git a/api/tests/integration/filtermanager_latest_test.go b/api/tests/integration/filtermanager_latest_test.go new file mode 100644 index 00000000..9ecf369a --- /dev/null +++ b/api/tests/integration/filtermanager_latest_test.go @@ -0,0 +1,300 @@ +// Copyright The HTNN Authors. +// +// 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. + +//go:build !envoy1.29 + +package integration + +import ( + "bytes" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "mosn.io/htnn/api/pkg/filtermanager" + "mosn.io/htnn/api/pkg/filtermanager/model" + "mosn.io/htnn/api/plugins/tests/integration/dataplane" + "mosn.io/htnn/api/plugins/tests/integration/helper" +) + +func TestFilterManagerTrailers(t *testing.T) { + dp, err := dataplane.StartDataPlane(t, &dataplane.Option{}) + if err != nil { + t.Fatalf("failed to start data plane: %v", err) + return + } + defer dp.Stop() + + s := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "stream", + Config: &Config{ + Decode: true, + Encode: true, + Trailers: true, + }, + }, + }, + } + lr := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "localReply", + Config: &Config{ + Decode: true, + Trailers: true, + }, + }, + }, + } + + tests := []struct { + name string + config *filtermanager.FilterManagerConfig + expectWithoutBody func(t *testing.T, resp *http.Response) + expectWithBody func(t *testing.T, resp *http.Response) + }{ + { + name: "DecodeTrailers", + config: s, + expectWithoutBody: func(t *testing.T, resp *http.Response) { + assert.Equal(t, []string{"stream"}, resp.Header.Values("Echo-Trailer-Run")) + }, + }, + { + name: "localReply", + config: lr, + expectWithoutBody: func(t *testing.T, resp *http.Response) { + assert.Equal(t, 206, resp.StatusCode) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controlPlane.UseGoPluginConfig(t, tt.config, dp) + hdr := http.Header{} + trailer := http.Header{} + trailer.Add("Expires", "Wed, 21 Oct 2015 07:28:00 GMT") + resp, err := dp.PostWithTrailer("/echo", hdr, bytes.NewReader([]byte("test")), trailer) + require.Nil(t, err) + tt.expectWithoutBody(t, resp) + }) + } +} + +func TestFilterManagerBufferingWithTrailers(t *testing.T) { + dp, err := dataplane.StartDataPlane(t, &dataplane.Option{ + LogLevel: "debug", + }) + if err != nil { + t.Fatalf("failed to start data plane: %v", err) + return + } + defer dp.Stop() + + b := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "buffer", + Config: &Config{ + Decode: true, + NeedBuffer: true, + }, + }, + }, + } + bThenb := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "buffer", + Config: &Config{ + Decode: true, + NeedBuffer: true, + }, + }, + { + Name: "buffer", + Config: &Config{ + Decode: true, + NeedBuffer: true, + }, + }, + }, + } + sThenbThennbThenb := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "stream", + Config: &Config{ + Decode: true, + NeedBuffer: true, + }, + }, + { + Name: "buffer", + Config: &Config{ + Decode: true, + NeedBuffer: true, + }, + }, + { + Name: "buffer", + Config: &Config{ + Decode: true, + }, + }, + { + Name: "buffer", + Config: &Config{ + Decode: true, + NeedBuffer: true, + }, + }, + }, + } + + tests := []struct { + name string + config *filtermanager.FilterManagerConfig + expectWithBody func(t *testing.T, resp *http.Response) + }{ + { + name: "DecodeRequest", + config: b, + expectWithBody: func(t *testing.T, resp *http.Response) { + assert.Equal(t, []string{"buffer"}, resp.Header.Values("Echo-Trailer-Run")) + assert.Equal(t, []string{"buffer"}, resp.Header.Values("Echo-Run")) + assertBody(t, "testbuffer\n", resp) + }, + }, + { + name: "DecodeRequest, then DecodeRequest", + config: bThenb, + expectWithBody: func(t *testing.T, resp *http.Response) { + assert.Equal(t, []string{"buffer", "buffer"}, resp.Header.Values("Echo-Trailer-Run")) + assert.Equal(t, []string{"buffer", "buffer"}, resp.Header.Values("Echo-Run")) + }, + }, + { + name: "DecodeTrailers, DecodeRequest, DecodeTrailers, then DecodeRequest", + config: sThenbThennbThenb, + expectWithBody: func(t *testing.T, resp *http.Response) { + assert.Equal(t, []string{"stream", "buffer", "no buffer", "buffer"}, resp.Header.Values("Echo-Trailer-Run")) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controlPlane.UseGoPluginConfig(t, tt.config, dp) + hdr := http.Header{} + trailer := http.Header{} + trailer.Add("Expires", "Wed, 21 Oct 2015 07:28:00 GMT") + resp, err := dp.PostWithTrailer("/echo", hdr, bytes.NewReader([]byte("test")), trailer) + require.Nil(t, err) + defer resp.Body.Close() + tt.expectWithBody(t, resp) + }) + } +} + +func grpcurl(dp *dataplane.DataPlane, fullMethodName, req string) ([]byte, error) { + prefix := "api.tests.integration.testdata.services.grpc." + pwd, _ := os.Getwd() + return dp.Grpcurl(filepath.Join(pwd, "testdata/services/grpc"), "sample.proto", prefix+fullMethodName, req) +} + +func TestFilterManagerTrailersWithGrpcBackend(t *testing.T) { + dp, err := dataplane.StartDataPlane(t, &dataplane.Option{ + LogLevel: "debug", + }) + if err != nil { + t.Fatalf("failed to start data plane: %v", err) + return + } + defer dp.Stop() + + helper.WaitServiceUp(t, ":50051", "grpc") + + s := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "stream", + Config: &Config{}, + }, + }, + } + + b := &filtermanager.FilterManagerConfig{ + Plugins: []*model.FilterConfig{ + { + Name: "buffer", + Config: &Config{ + NeedBuffer: true, + InGrpcMode: true, + Encode: true, + }, + }, + }, + } + + tests := []struct { + name string + config *filtermanager.FilterManagerConfig + expect func(t *testing.T, resp []byte) + }{ + { + name: "EncodeTrailers", + config: s, + expect: func(t *testing.T, resp []byte) { + exp := `Response contents: +{ + "message": "Hello Jordan" +} + +Response trailers received: +run: stream` + assert.Contains(t, string(resp), exp, "response: %s", string(resp)) + }, + }, + { + name: "EncodeResponse", + config: b, + expect: func(t *testing.T, resp []byte) { + exp := `Response contents: +{ + "message": "Hello Jordan" +} + +Response trailers received: +(empty)` + assert.Contains(t, string(resp), exp, "response: %s", string(resp)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controlPlane.UseGoPluginConfig(t, tt.config, dp) + resp, _ := grpcurl(dp, "Sample.SayHello", `{"name":"Jordan"}`) + tt.expect(t, resp) + }) + } +} diff --git a/api/tests/integration/filtermanager_test.go b/api/tests/integration/filtermanager_test.go index bfb612b6..17bd1097 100644 --- a/api/tests/integration/filtermanager_test.go +++ b/api/tests/integration/filtermanager_test.go @@ -50,8 +50,8 @@ func TestFilterManagerDecode(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, }, @@ -67,8 +67,8 @@ func TestFilterManagerDecode(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, }, @@ -84,8 +84,8 @@ func TestFilterManagerDecode(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -107,8 +107,8 @@ func TestFilterManagerDecode(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -120,8 +120,8 @@ func TestFilterManagerDecode(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, }, @@ -147,8 +147,8 @@ func TestFilterManagerDecode(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -278,8 +278,8 @@ func TestFilterManagerEncode(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, }, @@ -289,8 +289,8 @@ func TestFilterManagerEncode(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, { @@ -312,8 +312,8 @@ func TestFilterManagerEncode(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, { @@ -329,8 +329,8 @@ func TestFilterManagerEncode(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, { @@ -342,8 +342,8 @@ func TestFilterManagerEncode(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, { @@ -381,8 +381,8 @@ func TestFilterManagerEncode(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, }, @@ -520,8 +520,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, }, @@ -531,8 +531,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "localReply", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, }, @@ -542,8 +542,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -560,8 +560,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -596,8 +596,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -621,8 +621,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -636,8 +636,8 @@ func TestFilterManagerDecodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Decode: true, - Need: true, + Decode: true, + NeedBuffer: true, }, }, { @@ -752,8 +752,8 @@ func TestFilterManagerEncodeLocalReply(t *testing.T) { { Name: "localReply", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, }, @@ -763,8 +763,8 @@ func TestFilterManagerEncodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, { @@ -788,8 +788,8 @@ func TestFilterManagerEncodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, }, @@ -806,8 +806,8 @@ func TestFilterManagerEncodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, }, @@ -830,8 +830,8 @@ func TestFilterManagerEncodeLocalReply(t *testing.T) { { Name: "buffer", Config: &Config{ - Encode: true, - Need: true, + Encode: true, + NeedBuffer: true, }, }, }, diff --git a/api/tests/integration/http_filter_test.go b/api/tests/integration/http_filter_test.go index a3c0ab22..c320c042 100644 --- a/api/tests/integration/http_filter_test.go +++ b/api/tests/integration/http_filter_test.go @@ -32,8 +32,8 @@ func TestFilterPlugin(t *testing.T) { map[string]interface{}{ "name": "buffer", "config": map[string]interface{}{ - "decode": true, - "need": true, + "decode": true, + "need_buffer": true, }, }, }, @@ -76,8 +76,8 @@ func TestFilterMergeIntoRoute(t *testing.T) { map[string]interface{}{ "name": "buffer", "config": map[string]interface{}{ - "decode": true, - "need": false, + "decode": true, + "need_buffer": false, }, }, map[string]interface{}{ @@ -132,8 +132,8 @@ func TestFilterMergeIntoRoute(t *testing.T) { { name: "override", config: controlplane.NewSinglePluginConfig("buffer", map[string]interface{}{ - "decode": true, - "need": true, + "decode": true, + "need_buffer": true, }), run: func(t *testing.T) { resp, _ := dp.Get("/echo", nil) diff --git a/api/tests/integration/test_plugins.go b/api/tests/integration/test_plugins.go index c7208a64..9fe9359d 100644 --- a/api/tests/integration/test_plugins.go +++ b/api/tests/integration/test_plugins.go @@ -65,6 +65,12 @@ func (f *streamFilter) DecodeData(data api.BufferInstance, endStream bool) api.R return api.Continue } +func (f *streamFilter) DecodeTrailers(trailers api.RequestTrailerMap) api.ResultAction { + api.LogInfof("traceback: %s", string(debug.Stack())) + trailers.Add("run", "stream") + return api.Continue +} + func (f *streamFilter) EncodeHeaders(headers api.ResponseHeaderMap, endStream bool) api.ResultAction { api.LogInfof("traceback: %s", string(debug.Stack())) headers.Add("run", "stream") @@ -80,6 +86,12 @@ func (f *streamFilter) EncodeData(data api.BufferInstance, endStream bool) api.R return api.Continue } +func (f *streamFilter) EncodeTrailers(trailers api.ResponseTrailerMap) api.ResultAction { + api.LogInfof("traceback: %s", string(debug.Stack())) + trailers.Add("run", "stream") + return api.Continue +} + func (p *streamPlugin) Factory() api.FilterFactory { return streamFactory } @@ -103,12 +115,15 @@ type bufferFilter struct { config *Config } -func (f *bufferFilter) DecodeRequest(headers api.RequestHeaderMap, buf api.BufferInstance, trailer api.RequestTrailerMap) api.ResultAction { +func (f *bufferFilter) DecodeRequest(headers api.RequestHeaderMap, buf api.BufferInstance, trailers api.RequestTrailerMap) api.ResultAction { api.LogInfof("traceback: %s", string(debug.Stack())) headers.Add("run", "buffer") if buf != nil && f.config.Decode { buf.AppendString("buffer\n") } + if trailers != nil && f.config.Decode { + trailers.Add("run", "buffer") + } return api.Continue } @@ -116,16 +131,19 @@ func (f *bufferFilter) EncodeResponse(headers api.ResponseHeaderMap, buf api.Buf api.LogInfof("traceback: %s", string(debug.Stack())) headers.Add("run", "buffer") headers.Del("content-length") - if buf != nil && f.config.Encode { + if buf != nil && f.config.Encode && !f.config.InGrpcMode { buf.AppendString("buffer\n") } + if trailers != nil && f.config.Encode { + trailers.Add("run", "buffer") + } return api.Continue } func (f *bufferFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.ResultAction { api.LogInfof("traceback: %s", string(debug.Stack())) _, ok := headers.Get("stream") - if !ok && f.config.Need { + if !ok && f.config.NeedBuffer { return api.WaitAllData } headers.Add("run", "no buffer") @@ -140,10 +158,18 @@ func (f *bufferFilter) DecodeData(data api.BufferInstance, endStream bool) api.R return api.Continue } +func (f *bufferFilter) DecodeTrailers(trailers api.RequestTrailerMap) api.ResultAction { + api.LogInfof("traceback: %s", string(debug.Stack())) + if f.config.Decode { + trailers.Add("run", "no buffer") + } + return api.Continue +} + func (f *bufferFilter) EncodeHeaders(headers api.ResponseHeaderMap, endStream bool) api.ResultAction { api.LogInfof("traceback: %s", string(debug.Stack())) _, ok := headers.Get("stream") - if !ok && f.config.Need { + if !ok && f.config.NeedBuffer { return api.WaitAllData } headers.Del("content-length") @@ -159,6 +185,14 @@ func (f *bufferFilter) EncodeData(data api.BufferInstance, endStream bool) api.R return api.Continue } +func (f *bufferFilter) EncodeTrailers(trailers api.ResponseTrailerMap) api.ResultAction { + api.LogInfof("traceback: %s", string(debug.Stack())) + if f.config.Encode { + trailers.Add("run", "no buffer") + } + return api.Continue +} + func (p *bufferPlugin) Factory() api.FilterFactory { return bufferFactory } @@ -224,7 +258,7 @@ func (f *localReplyFilter) EncodeResponse(headers api.ResponseHeaderMap, buf api func (f *localReplyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.ResultAction { api.LogInfof("traceback: %s", string(debug.Stack())) - if f.config.Need { + if f.config.NeedBuffer { return api.WaitAllData } f.reqHdr = headers @@ -243,9 +277,17 @@ func (f *localReplyFilter) DecodeData(data api.BufferInstance, endStream bool) a return api.Continue } +func (f *localReplyFilter) DecodeTrailers(trailers api.RequestTrailerMap) api.ResultAction { + api.LogInfof("traceback: %s", string(debug.Stack())) + if f.config.Decode && f.config.Trailers { + return f.NewLocalResponse("reply", true) + } + return api.Continue +} + func (f *localReplyFilter) EncodeHeaders(headers api.ResponseHeaderMap, endStream bool) api.ResultAction { api.LogInfof("traceback: %s", string(debug.Stack())) - if f.config.Need { + if f.config.NeedBuffer { return api.WaitAllData } if f.config.Encode && f.config.Headers { @@ -490,6 +532,51 @@ func (f *beforeConsumerAndHasOtherMethodFilter) EncodeHeaders(headers api.Respon return api.Continue } +type onLogPlugin struct { + plugins.PluginMethodDefaultImpl +} + +func (p *onLogPlugin) Order() plugins.PluginOrder { + return plugins.PluginOrder{ + Position: plugins.OrderPositionAccess, + } +} + +func (p *onLogPlugin) Config() api.PluginConfig { + return &Config{} +} + +func (p *onLogPlugin) Factory() api.FilterFactory { + return onLogFactory +} + +func onLogFactory(c interface{}, callbacks api.FilterCallbackHandler) api.Filter { + return &onLogFilter{ + callbacks: callbacks, + config: c.(*Config), + } +} + +type onLogFilter struct { + api.PassThroughFilter + + callbacks api.FilterCallbackHandler + config *Config +} + +func (f *onLogFilter) OnLog(reqHeaders api.RequestHeaderMap, reqTrailers api.RequestTrailerMap, + respHeaders api.ResponseHeaderMap, respTrailers api.ResponseTrailerMap) { + + trailers := map[string]string{} + if reqTrailers != nil { + reqTrailers.Range(func(k, v string) bool { + trailers[k] = v + return true + }) + } + api.LogWarnf("receive request trailers: %+v", trailers) +} + func init() { plugins.RegisterPlugin("stream", &streamPlugin{}) plugins.RegisterPlugin("buffer", &bufferPlugin{}) @@ -500,4 +587,5 @@ func init() { plugins.RegisterPlugin("benchmark", &benchmarkPlugin{}) plugins.RegisterPlugin("benchmark2", &benchmarkPlugin{}) plugins.RegisterPlugin("beforeConsumerAndHasOtherMethod", &beforeConsumerAndHasOtherMethodPlugin{}) + plugins.RegisterPlugin("onLog", &onLogPlugin{}) } diff --git a/api/tests/integration/testdata/services/docker-compose.yml b/api/tests/integration/testdata/services/docker-compose.yml new file mode 100644 index 00000000..7aa8088d --- /dev/null +++ b/api/tests/integration/testdata/services/docker-compose.yml @@ -0,0 +1,12 @@ +services: + # names in alphabetical order + grpc: + build: ./grpc + ports: + - "50051:50051" + networks: + service: + + +networks: + service: diff --git a/api/tests/integration/testdata/services/grpc/Dockerfile b/api/tests/integration/testdata/services/grpc/Dockerfile new file mode 100644 index 00000000..b7cb4e0c --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/Dockerfile @@ -0,0 +1,6 @@ +FROM m.daocloud.io/docker.io/library/golang:1.21-bullseye + +WORKDIR /app +COPY . /app +RUN go build +CMD ["./grpc"] diff --git a/api/tests/integration/testdata/services/grpc/go.mod b/api/tests/integration/testdata/services/grpc/go.mod new file mode 100644 index 00000000..82a78ac3 --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/go.mod @@ -0,0 +1,29 @@ +// Copyright The HTNN Authors. +// +// 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. + +module mosn.io/htnn/api/tests/integration/testdata/services/grpc + +go 1.21 + +require ( + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 +) + +require ( + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect +) diff --git a/api/tests/integration/testdata/services/grpc/go.sum b/api/tests/integration/testdata/services/grpc/go.sum new file mode 100644 index 00000000..a8432dc8 --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/go.sum @@ -0,0 +1,14 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/api/tests/integration/testdata/services/grpc/main.go b/api/tests/integration/testdata/services/grpc/main.go new file mode 100644 index 00000000..86084d3f --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/main.go @@ -0,0 +1,49 @@ +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative sample.proto +package main + +import ( + "context" + "fmt" + "log" + "net" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Define a sample gRPC service +type sampleServer struct { + UnimplementedSampleServer +} + +// Define the service method +func (s *sampleServer) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) { + return &HelloResponse{Message: "Hello " + req.Name}, nil +} + +func (s *sampleServer) Ouch(ctx context.Context, req *HelloRequest) (*HelloResponse, error) { + // Return an error + err := status.Error(codes.Internal, "An internal error occurred") + return nil, err +} + +func main() { + // Create a TCP listener + lis, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + + // Create a gRPC server + s := grpc.NewServer() + + // Register the service with the server + RegisterSampleServer(s, &sampleServer{}) + + // Start the server + fmt.Println("Server started. Listening on port :50051") + if err := s.Serve(lis); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} diff --git a/api/tests/integration/testdata/services/grpc/sample.pb.go b/api/tests/integration/testdata/services/grpc/sample.pb.go new file mode 100644 index 00000000..1d87c9b1 --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/sample.pb.go @@ -0,0 +1,202 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v4.24.4 +// source: sample.proto + +package main + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + mi := &file_sample_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_sample_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_sample_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type HelloResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *HelloResponse) Reset() { + *x = HelloResponse{} + mi := &file_sample_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HelloResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloResponse) ProtoMessage() {} + +func (x *HelloResponse) ProtoReflect() protoreflect.Message { + mi := &file_sample_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead. +func (*HelloResponse) Descriptor() ([]byte, []int) { + return file_sample_proto_rawDescGZIP(), []int{1} +} + +func (x *HelloResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_sample_proto protoreflect.FileDescriptor + +var file_sample_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2c, + 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x22, 0x22, 0x0a, 0x0c, + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x22, 0x29, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x94, 0x02, 0x0a, 0x06, + 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x12, 0x3a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, + 0x64, 0x61, 0x74, 0x61, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x3b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x48, + 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, + 0x01, 0x0a, 0x04, 0x4f, 0x75, 0x63, 0x68, 0x12, 0x3a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, + 0x73, 0x74, 0x73, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, + 0x64, 0x61, 0x74, 0x61, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x42, 0x40, 0x5a, 0x3e, 0x6d, 0x6f, 0x73, 0x6e, 0x2e, 0x69, 0x6f, 0x2f, 0x68, 0x74, + 0x6e, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, + 0x61, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, + 0x6d, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sample_proto_rawDescOnce sync.Once + file_sample_proto_rawDescData = file_sample_proto_rawDesc +) + +func file_sample_proto_rawDescGZIP() []byte { + file_sample_proto_rawDescOnce.Do(func() { + file_sample_proto_rawDescData = protoimpl.X.CompressGZIP(file_sample_proto_rawDescData) + }) + return file_sample_proto_rawDescData +} + +var file_sample_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_sample_proto_goTypes = []any{ + (*HelloRequest)(nil), // 0: api.tests.integration.testdata.services.grpc.HelloRequest + (*HelloResponse)(nil), // 1: api.tests.integration.testdata.services.grpc.HelloResponse +} +var file_sample_proto_depIdxs = []int32{ + 0, // 0: api.tests.integration.testdata.services.grpc.Sample.SayHello:input_type -> api.tests.integration.testdata.services.grpc.HelloRequest + 0, // 1: api.tests.integration.testdata.services.grpc.Sample.Ouch:input_type -> api.tests.integration.testdata.services.grpc.HelloRequest + 1, // 2: api.tests.integration.testdata.services.grpc.Sample.SayHello:output_type -> api.tests.integration.testdata.services.grpc.HelloResponse + 1, // 3: api.tests.integration.testdata.services.grpc.Sample.Ouch:output_type -> api.tests.integration.testdata.services.grpc.HelloResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sample_proto_init() } +func file_sample_proto_init() { + if File_sample_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sample_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sample_proto_goTypes, + DependencyIndexes: file_sample_proto_depIdxs, + MessageInfos: file_sample_proto_msgTypes, + }.Build() + File_sample_proto = out.File + file_sample_proto_rawDesc = nil + file_sample_proto_goTypes = nil + file_sample_proto_depIdxs = nil +} diff --git a/api/tests/integration/testdata/services/grpc/sample.pb.validate.go b/api/tests/integration/testdata/services/grpc/sample.pb.validate.go new file mode 100644 index 00000000..faaf2b9a --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/sample.pb.validate.go @@ -0,0 +1,239 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: api/tests/integration/testdata/services/grpc/sample.proto + +package main + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on HelloRequest with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *HelloRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on HelloRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in HelloRequestMultiError, or +// nil if none found. +func (m *HelloRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *HelloRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + if len(errors) > 0 { + return HelloRequestMultiError(errors) + } + + return nil +} + +// HelloRequestMultiError is an error wrapping multiple validation errors +// returned by HelloRequest.ValidateAll() if the designated constraints aren't met. +type HelloRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m HelloRequestMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m HelloRequestMultiError) AllErrors() []error { return m } + +// HelloRequestValidationError is the validation error returned by +// HelloRequest.Validate if the designated constraints aren't met. +type HelloRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e HelloRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e HelloRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e HelloRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e HelloRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e HelloRequestValidationError) ErrorName() string { return "HelloRequestValidationError" } + +// Error satisfies the builtin error interface +func (e HelloRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sHelloRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = HelloRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = HelloRequestValidationError{} + +// Validate checks the field values on HelloResponse with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *HelloResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on HelloResponse with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in HelloResponseMultiError, or +// nil if none found. +func (m *HelloResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *HelloResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Message + + if len(errors) > 0 { + return HelloResponseMultiError(errors) + } + + return nil +} + +// HelloResponseMultiError is an error wrapping multiple validation errors +// returned by HelloResponse.ValidateAll() if the designated constraints +// aren't met. +type HelloResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m HelloResponseMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m HelloResponseMultiError) AllErrors() []error { return m } + +// HelloResponseValidationError is the validation error returned by +// HelloResponse.Validate if the designated constraints aren't met. +type HelloResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e HelloResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e HelloResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e HelloResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e HelloResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e HelloResponseValidationError) ErrorName() string { return "HelloResponseValidationError" } + +// Error satisfies the builtin error interface +func (e HelloResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sHelloResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = HelloResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = HelloResponseValidationError{} diff --git a/api/tests/integration/testdata/services/grpc/sample.proto b/api/tests/integration/testdata/services/grpc/sample.proto new file mode 100644 index 00000000..9532e341 --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/sample.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package api.tests.integration.testdata.services.grpc; +option go_package = "mosn.io/htnn/api/tests/integration/testdata/services/grpc/main"; + +// Run `go generate` once we change this file + +service Sample { + rpc SayHello(HelloRequest) returns (HelloResponse) { + } + rpc Ouch(HelloRequest) returns (HelloResponse) { + } +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + string message = 1; +} diff --git a/api/tests/integration/testdata/services/grpc/sample_grpc.pb.go b/api/tests/integration/testdata/services/grpc/sample_grpc.pb.go new file mode 100644 index 00000000..dd237c0c --- /dev/null +++ b/api/tests/integration/testdata/services/grpc/sample_grpc.pb.go @@ -0,0 +1,160 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v4.24.4 +// source: sample.proto + +package main + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Sample_SayHello_FullMethodName = "/api.tests.integration.testdata.services.grpc.Sample/SayHello" + Sample_Ouch_FullMethodName = "/api.tests.integration.testdata.services.grpc.Sample/Ouch" +) + +// SampleClient is the client API for Sample service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SampleClient interface { + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) + Ouch(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) +} + +type sampleClient struct { + cc grpc.ClientConnInterface +} + +func NewSampleClient(cc grpc.ClientConnInterface) SampleClient { + return &sampleClient{cc} +} + +func (c *sampleClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HelloResponse) + err := c.cc.Invoke(ctx, Sample_SayHello_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *sampleClient) Ouch(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HelloResponse) + err := c.cc.Invoke(ctx, Sample_Ouch_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SampleServer is the server API for Sample service. +// All implementations must embed UnimplementedSampleServer +// for forward compatibility. +type SampleServer interface { + SayHello(context.Context, *HelloRequest) (*HelloResponse, error) + Ouch(context.Context, *HelloRequest) (*HelloResponse, error) + mustEmbedUnimplementedSampleServer() +} + +// UnimplementedSampleServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSampleServer struct{} + +func (UnimplementedSampleServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} +func (UnimplementedSampleServer) Ouch(context.Context, *HelloRequest) (*HelloResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Ouch not implemented") +} +func (UnimplementedSampleServer) mustEmbedUnimplementedSampleServer() {} +func (UnimplementedSampleServer) testEmbeddedByValue() {} + +// UnsafeSampleServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SampleServer will +// result in compilation errors. +type UnsafeSampleServer interface { + mustEmbedUnimplementedSampleServer() +} + +func RegisterSampleServer(s grpc.ServiceRegistrar, srv SampleServer) { + // If the following call pancis, it indicates UnimplementedSampleServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Sample_ServiceDesc, srv) +} + +func _Sample_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SampleServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Sample_SayHello_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SampleServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Sample_Ouch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SampleServer).Ouch(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Sample_Ouch_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SampleServer).Ouch(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Sample_ServiceDesc is the grpc.ServiceDesc for Sample service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Sample_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.tests.integration.testdata.services.grpc.Sample", + HandlerType: (*SampleServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Sample_SayHello_Handler, + }, + { + MethodName: "Ouch", + Handler: _Sample_Ouch_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sample.proto", +} diff --git a/site/content/en/docs/developer-guide/get_involved.md b/site/content/en/docs/developer-guide/get_involved.md index 9d2b4c57..c679db20 100644 --- a/site/content/en/docs/developer-guide/get_involved.md +++ b/site/content/en/docs/developer-guide/get_involved.md @@ -112,15 +112,17 @@ In plugin development, features related to each request should be placed in `fil `filter` mainly defines the following methods: 1. DecodeHeaders -2. DecodeData -3. EncodeHeaders -4. EncodeData -5. OnLog +2. DecodeData (if request body exists) +3. DecodeTrailers (if request trailers exists) +4. EncodeHeaders +5. EncodeData (if response body exists) +6. EncodeTrailers (if response trailers exists) +7. OnLog Normally, the above methods are executed from top to bottom. However, there are exceptions: -1. If there is no body, the corresponding DecodeData and EncodeData methods will not be executed. -2. Since the OnLog operation is triggered by the client interrupting the request, OnLog may execute concurrently with other methods if the client interrupts prematurely. -3. In requests like bidirectional streams, it is possible to handle the request body and upstream response at the same time, so DecodeData may execute concurrently with EncodeHeaders or EncodeData. +* If there is no body, the corresponding DecodeData and EncodeData methods will not be executed. The same as trailers and DecodeTrailers/EncodeTrailers. +* Since the OnLog operation is triggered by the client interrupting the request, OnLog may execute concurrently with other methods if the client interrupts prematurely. +* In requests like bidirectional streams, it is possible to handle the request body and upstream response at the same time, so DecodeData may execute concurrently with EncodeHeaders or EncodeData. So, when reading and writing `filter`, there is a risk of concurrent access and lock consideration is necessary. diff --git a/site/content/en/docs/developer-guide/plugin_development.md b/site/content/en/docs/developer-guide/plugin_development.md index fbf6f988..c064321d 100644 --- a/site/content/en/docs/developer-guide/plugin_development.md +++ b/site/content/en/docs/developer-guide/plugin_development.md @@ -86,10 +86,12 @@ Assumed we have three plugins called `A`, `B` and `C`. For each plugin, the calling order of callbacks is: 1. DecodeHeaders -2. DecodeData -3. EncodeHeaders -4. EncodeData -5. OnLog +2. DecodeData (if request body exists) +3. DecodeTrailers (if request trailers exists) +4. EncodeHeaders +5. EncodeData (if response body exists) +6. EncodeTrailers (if response trailers exists) +7. OnLog Between plugins, the order of invocation is determined by the order of the plugins. Suppose plugin `A` is in the `Authn` group, `B` is in `Authz`, and `C` is in `Traffic`. @@ -108,6 +110,7 @@ When logging requests, the call order is `Authn -> Authz -> Traffic`. Note that this picture shows the main path. The execution path may have slight differences. For example, * If the request doesn't have body, the `DecodeData` won't be called. +* If the request contains trailers, the `DecodeTrailers` will be called after the body is handled. * If the request is replied by Envoy before being sent to the upstream, we will leave the Decode path and enter the Encode path. For example, if the plugin B rejects the request with some custom headers, the Decode path is `A -> B` and the Encode path is `C -> B -> A`. The custom headers will be rewritten by the plugins. This behavior is equal to Envoy. @@ -128,17 +131,17 @@ Therefore, we introduce a group of new types: If `WaitAllData` is returned from `DecodeHeaders`, we will: 1. buffer the whole body -2. execute the `DecodeData` of previous plugins +2. execute the `DecodeData` and `DecodeTrailers` of previous plugins 3. execute the `DecodeRequest` of this plugin 4. back to the original path, continue to execute the `DecodeHeaders` of the next plugin ![filter manager, with DecodeWholeRequestFilter, buffer the whole request](/images/filtermanager_sub_path.jpg) -Note: `DecodeRequest` is only executed if `DecodeHeaders` returns `WaitAllData`. So if `DecodeRequest` is defined, `DecodeHeaders` must be defined as well. When both `DecodeRequest` and `DecodeData` are defined in the plugin: if `DecodeHeaders` returns `WaitAllData`, only `DecodeRequest` is executed, otherwise, only `DecodeData` is executed. +Note: `DecodeRequest` is only executed if `DecodeHeaders` returns `WaitAllData`. So if `DecodeRequest` is defined, `DecodeHeaders` must be defined as well. When both `DecodeRequest` and `DecodeData/DecodeTrailers` are defined in the plugin: if `DecodeHeaders` returns `WaitAllData`, only `DecodeRequest` is executed, otherwise, only `DecodeData/DecodeTrailers` is executed. The same process applies to the Encode path in a reverse order, and the method is slightly different. This time it requires `EncodeHeaders` to return `WaitAllData` to invoke `EncodeResponse`. -Note: `EncodeResponse` is only executed if `EncodeHeaders` returns `WaitAllData`. So if `EncodeResponse` is defined, `EncodeHeaders` must be defined as well. When both `EncodeResponse` and `EncodeData` are defined in the plugin: if `EncodeHeaders` returns `WaitAllData`, only `EncodeResponse` is executed, otherwise, only `EncodeData` is executed. +Note: `EncodeResponse` is only executed if `EncodeHeaders` returns `WaitAllData`. So if `EncodeResponse` is defined, `EncodeHeaders` must be defined as well. When both `EncodeResponse` and `EncodeData/EncodeTrailers` are defined in the plugin: if `EncodeHeaders` returns `WaitAllData`, only `EncodeResponse` is executed, otherwise, only `EncodeData/EncodeTrailers` is executed. Currently, `DecodeRequest` is not supported by plugins whose order is `Access` or `Authn`. diff --git a/site/content/zh-hans/docs/developer-guide/get_involved.md b/site/content/zh-hans/docs/developer-guide/get_involved.md index a84f6efd..671ea6ca 100644 --- a/site/content/zh-hans/docs/developer-guide/get_involved.md +++ b/site/content/zh-hans/docs/developer-guide/get_involved.md @@ -112,10 +112,12 @@ spec: `filter` 里主要定义下面的方法: 1. DecodeHeaders -2. DecodeData -3. EncodeHeaders -4. EncodeData -5. OnLog +2. DecodeData(如果存在 request body) +3. DecodeTrailers(如果存在 request trailer) +4. EncodeHeaders +5. EncodeData(如果存在 response body) +6. EncodeTrailers(如果存在 response trailer) +7. OnLog 正常情况下,会从上到下执行上述方法。但存在以下特例: diff --git a/site/content/zh-hans/docs/developer-guide/plugin_development.md b/site/content/zh-hans/docs/developer-guide/plugin_development.md index 331f1582..75787ec5 100644 --- a/site/content/zh-hans/docs/developer-guide/plugin_development.md +++ b/site/content/zh-hans/docs/developer-guide/plugin_development.md @@ -85,10 +85,12 @@ filter manager 实现了以下特性: 对于每个插件,回调的调用顺序是: 1. DecodeHeaders -2. DecodeData -3. EncodeHeaders -4. EncodeData -5. OnLog +2. DecodeData(如果存在 request body) +3. DecodeTrailers(如果存在 request trailer) +4. EncodeHeaders +5. EncodeData(如果存在 response body) +6. EncodeTrailers(如果存在 response trailer) +7. OnLog 在插件之间,调用顺序由插件顺序决定。假设 `A` 插件在 `Authn` 组,`B` 在 `Authz`,`C` 在 `Traffic`。 处理请求时(Decode 路径),调用顺序是 `A -> B -> C`。 @@ -105,6 +107,7 @@ filter manager 实现了以下特性: 请注意,这张图片显示的是主路径。实际执行路径可能有细微差别。例如, * 如果请求没有 body,将不会调用 `DecodeData`。 +* 如果请求中有 trailers,则会在处理完 body 后调用 `DecodeTrailers` 处理 trailers。 * 如果 Envoy 在发送给上游之前回复了请求,我们将离开 Decode 路径并进入 Encode 路径。例如,如果插件 B 用一些自定义头拒绝了请求,Decode 路径是 `A -> B`,Encode 路径是 `C -> B -> A`。自定义头将被该路径上的插件重写。这种行为和 Envoy 的处理方式一致。 在某些情况下,我们需要中止 header filter 的执行,直到收到整个 body。例如, @@ -123,17 +126,17 @@ filter manager 实现了以下特性: 如果 `DecodeHeaders` 返回 `WaitAllData`,我们将: 1. 缓冲整个 body -2. 执行之前插件的 `DecodeData` +2. 执行之前插件的 `DecodeData/DecodeTrailers` 3. 执行此插件的 `DecodeRequest` 4. 回到原始路径,继续执行下一个插件的 `DecodeHeaders` ![过滤器管理器,带有 DecodeWholeRequestFilter,缓冲整个请求](/images/filtermanager_sub_path.jpg) -注意:`DecodeRequest` 仅在 `DecodeHeaders` 返回 `WaitAllData` 时才被执行。所以如果定义了 `DecodeRequest`,一定要定义 `DecodeHeaders`。如果插件里同时定义了 `DecodeRequest` 和 `DecodeData`,执行哪一个方法取决于 `DecodeHeaders` 是否返回 `WaitAllData`:如果 `DecodeHeaders` 返回 `WaitAllData`,只有 `DecodeRequest` 会运行,否则只有 `DecodeData` 会运行。 +注意:`DecodeRequest` 仅在 `DecodeHeaders` 返回 `WaitAllData` 时才被执行。所以如果定义了 `DecodeRequest`,一定要定义 `DecodeHeaders`。如果插件里同时定义了 `DecodeRequest` 和 `DecodeData/DecodeTrailers`,执行哪一个方法取决于 `DecodeHeaders` 是否返回 `WaitAllData`:如果 `DecodeHeaders` 返回 `WaitAllData`,只有 `DecodeRequest` 会运行,否则只有 `DecodeData/DecodeTrailers` 会运行。 同样的过程适用于方向相反的 Encode 路径,且方式略有不同。此时需要由 `EncodeHeaders` 返回 `WaitAllData`,调用方法 `EncodeResponse`。 -注意:`EncodeResponse` 仅在 `EncodeHeaders` 返回 `WaitAllData` 时才被执行。所以如果定义了 `EncodeResponse`,一定要定义 `EncodeHeaders`。当插件里同时定义了 `EncodeResponse` 和 `EncodeData`:如果 `EncodeHeaders` 返回 `WaitAllData`,只有 `EncodeResponse` 会运行,否则只有 `EncodeData` 会运行。 +注意:`EncodeResponse` 仅在 `EncodeHeaders` 返回 `WaitAllData` 时才被执行。所以如果定义了 `EncodeResponse`,一定要定义 `EncodeHeaders`。当插件里同时定义了 `EncodeResponse` 和 `EncodeData/EncodeTrailers`:如果 `EncodeHeaders` 返回 `WaitAllData`,只有 `EncodeResponse` 会运行,否则只有 `EncodeData/EncodeTrailers` 会运行。 目前顺序为 `Access` 或 `Authn` 的插件不支持 `DecodeRequest` 方法。 From 8f54616556fd19fad48751e8a38a311c5580a10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 16 Oct 2024 10:19:57 +0800 Subject: [PATCH 06/10] test: unite the task name (#765) Signed-off-by: spacewander --- .github/workflows/test.yml | 2 +- controller/Makefile | 8 ++++---- controller/tests/integration/helper/helper.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cff46244..9b459c98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -215,7 +215,7 @@ jobs: - name: Set up services run: | - make start-controller-service + make start-service - name: Ensure benchmark is runnable run: | diff --git a/controller/Makefile b/controller/Makefile index bb73d1ef..f0aa85e2 100644 --- a/controller/Makefile +++ b/controller/Makefile @@ -108,10 +108,10 @@ envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. $(ENVTEST): $(LOCALBIN) test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest -.PHONY: start-controller-service -start-controller-service: +.PHONY: start-service +start-service: cd ./tests/testdata/services && docker compose up -d -.PHONY: stop-controller-service -stop-controller-service: +.PHONY: stop-service +stop-service: cd ./tests/testdata/services && docker compose down diff --git a/controller/tests/integration/helper/helper.go b/controller/tests/integration/helper/helper.go index 01da977c..a301337b 100644 --- a/controller/tests/integration/helper/helper.go +++ b/controller/tests/integration/helper/helper.go @@ -45,5 +45,5 @@ func WaitServiceUp(port string, service string) { c.Close() return true }, 10*time.Second, 50*time.Millisecond, - fmt.Sprintf("%s is unavailable. Please run `make start-controller-service` in ./controller to make it up.", service)) + fmt.Sprintf("%s is unavailable. Please run `make start-service` in ./controller to make it up.", service)) } From 88fb1167cee0c89649db9358a0a262044454b6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 16 Oct 2024 10:25:37 +0800 Subject: [PATCH 07/10] ci: improve linter & avoid crash with garbage input (#761) Signed-off-by: spacewander --- tools/cmd/linter/main.go | 24 ++++++++++++++++++------ types/plugins/plugins_test.go | 6 +++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tools/cmd/linter/main.go b/tools/cmd/linter/main.go index 76294803..06fa7e24 100644 --- a/tools/cmd/linter/main.go +++ b/tools/cmd/linter/main.go @@ -556,9 +556,11 @@ func getFeatureMaturityLevel(category string) ([]maturityLevel, error) { // the third row is the status scanner.Scan() ss := strings.Split(scanner.Text(), "|") - if len(ss) > 2 { - status = strings.ToLower(strings.TrimSpace(ss[2])) + if len(ss) < 3 { + return fmt.Errorf("status is missing in the `## Attribute` table of %s", path) } + + status = strings.ToLower(strings.TrimSpace(ss[2])) break } } @@ -614,22 +616,32 @@ func lintFeatureMaturityLevel() error { for category, recs := range records { for _, record := range recs { - if record.Status == "experimental" && record.ExperimentalSince == "" { - return fmt.Errorf("experimental_since of %s %s is missing in %s", category, record.Name, recordFile) + if record.Status == "experimental" { + if record.ExperimentalSince == "" { + return fmt.Errorf("experimental_since of %s %s is missing in %s", category, record.Name, recordFile) + } + } else if record.Status == "stable" { + if record.StableSince == "" { + return fmt.Errorf("stable_since of %s %s is missing in %s", category, record.Name, recordFile) + } + } else { + return fmt.Errorf("status '%s' of %s %s is invalid in %s", record.Status, category, record.Name, recordFile) } + found := false for i, r := range actualRecords[category] { if r.Name == record.Name { found = true if r.Status != record.Status { - return fmt.Errorf("status of %s %s is mismatched between %s and the documentation", category, record.Name, recordFile) + return fmt.Errorf("status of %s %s is mismatched between %s and the documentation. Please update the record in %s.", + category, record.Name, recordFile, recordFile) } actualRecords[category] = slices.Delete(actualRecords[category], i, i+1) break } } if !found { - return fmt.Errorf("%s %s is missing in the documentation", category, record.Name) + return fmt.Errorf("feature maturity record of %s %s is missing in the documentation", category, record.Name) } } } diff --git a/types/plugins/plugins_test.go b/types/plugins/plugins_test.go index 1e45b915..19ab05b2 100644 --- a/types/plugins/plugins_test.go +++ b/types/plugins/plugins_test.go @@ -40,7 +40,11 @@ func snakeToCamel(s string) string { } func getSecondColumn(line string) string { - return strings.TrimSpace(strings.Split(line, "|")[2]) + cols := strings.Split(line, "|") + if len(cols) < 3 { + return "" + } + return strings.TrimSpace(cols[2]) } func TestCheckPluginAttributes(t *testing.T) { From 8ded9c5688fb93b3905a61504419071b73ba7409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 17 Oct 2024 10:00:27 +0800 Subject: [PATCH 08/10] add routePatch plugin (#769) Signed-off-by: spacewander --- controller/internal/istio/envoyfilter.go | 28 ++++++-- controller/internal/model/model.go | 15 ++-- .../internal/translation/merged_state.go | 42 ++++++----- controller/plugins/plugins.go | 1 + controller/plugins/routepatch/config.go | 32 +++++++++ .../plugins/testdata/http/route_patch.in.yml | 15 ++++ .../plugins/testdata/http/route_patch.out.yml | 26 +++++++ e2e/Makefile | 2 + e2e/base/nacos.yml | 2 +- e2e/pkg/suite/suite.go | 1 + e2e/tests/route_patch.go | 47 +++++++++++++ e2e/tests/route_patch.yml | 69 +++++++++++++++++++ manifests/Makefile | 4 ++ manifests/images/cp/Dockerfile | 4 +- manifests/images/dp/Dockerfile | 4 +- types/plugins/plugins.go | 1 + types/plugins/routepatch/config.go | 58 ++++++++++++++++ 17 files changed, 323 insertions(+), 28 deletions(-) create mode 100644 controller/plugins/routepatch/config.go create mode 100644 controller/plugins/testdata/http/route_patch.in.yml create mode 100644 controller/plugins/testdata/http/route_patch.out.yml create mode 100644 e2e/tests/route_patch.go create mode 100644 e2e/tests/route_patch.yml create mode 100644 types/plugins/routepatch/config.go diff --git a/controller/internal/istio/envoyfilter.go b/controller/internal/istio/envoyfilter.go index a9fd57ab..e478ae2e 100644 --- a/controller/internal/istio/envoyfilter.go +++ b/controller/internal/istio/envoyfilter.go @@ -184,6 +184,28 @@ func GenerateRouteFilter(host *model.VirtualHost, route string, config map[strin }, } + routeConfig := map[string]interface{}{} + routeFilters, _ := config[model.CategoryRouteFilter].(map[string]*fmModel.FilterConfig) + extraRouteConfig, _ := config[model.CategoryRoute].(map[string]*fmModel.FilterConfig) + if routeFilters == nil || extraRouteConfig == nil { + // bug in the code + panic("route filter and route config must be provided") + } + + if len(routeFilters) > 0 { + plainCfg := map[string]interface{}{} + for k, v := range routeFilters { + plainCfg[k] = v.Config + } + routeConfig["typed_per_filter_config"] = plainCfg + } + for _, filter := range extraRouteConfig { + fields, _ := filter.Config.(map[string]interface{}) + for k, v := range fields { + routeConfig[k] = v + } + } + return &istiov1a3.EnvoyFilter{ // We don't set ObjectMeta here because this EnvoyFilter will be merged later Spec: istioapi.EnvoyFilter{ @@ -199,9 +221,7 @@ func GenerateRouteFilter(host *model.VirtualHost, route string, config map[strin }, Patch: &istioapi.EnvoyFilter_Patch{ Operation: istioapi.EnvoyFilter_Patch_MERGE, - Value: MustNewStruct(map[string]interface{}{ - "typed_per_filter_config": config, - }), + Value: MustNewStruct(routeConfig), }, }, }, @@ -334,7 +354,7 @@ func GenerateLDSFilter(key string, ldsName string, hasHCM bool, config map[strin if cfg == nil { cfg = map[string]interface{}{} } - ecdsName := key + "-" + model.CategoryGolangPlugins + ecdsName := key + "-" + model.ECDSGolangPlugins ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ ApplyTo: istioapi.EnvoyFilter_HTTP_FILTER, diff --git a/controller/internal/model/model.go b/controller/internal/model/model.go index e53da5f6..c87ce7ca 100644 --- a/controller/internal/model/model.go +++ b/controller/internal/model/model.go @@ -47,9 +47,14 @@ type VirtualHost struct { } const ( - CategoryECDSGolang = "ecds_golang" - CategoryECDSListener = "ecds_listener" - CategoryECDSNetwork = "ecds_network" - CategoryListener = "listener" - CategoryGolangPlugins = "golang-filter" + CategoryECDSGolang = "ecds_golang" + CategoryECDSListener = "ecds_listener" + CategoryECDSNetwork = "ecds_network" + CategoryListener = "listener" + + CategoryRoute = "route" + CategoryRouteFilter = "route_filter" + + // This constant is used in the resource name which doesn't support '_' in the name + ECDSGolangPlugins = "golang-filter" ) diff --git a/controller/internal/translation/merged_state.go b/controller/internal/translation/merged_state.go index 2f91771f..46f3dfc7 100644 --- a/controller/internal/translation/merged_state.go +++ b/controller/internal/translation/merged_state.go @@ -92,9 +92,11 @@ const ( func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerConfig, nsName *types.NamespacedName, virtualHost *model.VirtualHost) map[string]interface{} { - config := map[string]interface{}{} + nativeFilters := map[string]map[string]*fmModel.FilterConfig{ + model.CategoryRoute: {}, + model.CategoryRouteFilter: {}, + } - nativeFilters := []*fmModel.FilterConfig{} goFilterManager := &filtermanager.FilterManagerConfig{ Plugins: []*fmModel.FilterConfig{}, } @@ -143,9 +145,14 @@ func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerC m = wrapper.ToRouteConfig(m) } - m["@type"] = url plugin.Config = m - nativeFilters = append(nativeFilters, plugin) + if url != "" { + filterName := fmt.Sprintf("htnn.filters.http.%s", plugin.Name) + m["@type"] = url + nativeFilters[model.CategoryRouteFilter][filterName] = plugin + } else { + nativeFilters[model.CategoryRoute][name] = plugin + } } _, ok = p.(plugins.ConsumerPlugin) @@ -173,26 +180,29 @@ func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerC golangFilterName := "htnn.filters.http.golang" if ctrlcfg.EnableLDSPluginViaECDS() { - golangFilterName = virtualHost.ECDSResourceName + "-" + model.CategoryGolangPlugins + golangFilterName = virtualHost.ECDSResourceName + "-" + model.ECDSGolangPlugins } - config[golangFilterName] = map[string]interface{}{ - "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute", - "plugins_config": map[string]interface{}{ - "fm": map[string]interface{}{ - "config": map[string]interface{}{ - "@type": "type.googleapis.com/xds.type.v3.TypedStruct", - "value": v, + golangFilterPlugin := &fmModel.FilterConfig{ + Config: map[string]interface{}{ + "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute", + "plugins_config": map[string]interface{}{ + "fm": map[string]interface{}{ + "config": map[string]interface{}{ + "@type": "type.googleapis.com/xds.type.v3.TypedStruct", + "value": v, + }, }, }, }, } + nativeFilters[model.CategoryRouteFilter][golangFilterName] = golangFilterPlugin } - for _, filter := range nativeFilters { - name := fmt.Sprintf("htnn.filters.http.%s", filter.Name) - config[name] = filter.Config + // satisfy the requirement of the returned type + config := map[string]interface{}{} + for k, v := range nativeFilters { + config[k] = v } - return config } diff --git a/controller/plugins/plugins.go b/controller/plugins/plugins.go index 4e5c4d35..e80d1b30 100644 --- a/controller/plugins/plugins.go +++ b/controller/plugins/plugins.go @@ -24,6 +24,7 @@ import ( _ "mosn.io/htnn/controller/plugins/localratelimit" _ "mosn.io/htnn/controller/plugins/lua" _ "mosn.io/htnn/controller/plugins/networkrbac" + _ "mosn.io/htnn/controller/plugins/routepatch" _ "mosn.io/htnn/controller/plugins/tlsinspector" _ "mosn.io/htnn/types/plugins" ) diff --git a/controller/plugins/routepatch/config.go b/controller/plugins/routepatch/config.go new file mode 100644 index 00000000..8c655a3f --- /dev/null +++ b/controller/plugins/routepatch/config.go @@ -0,0 +1,32 @@ +// Copyright The HTNN Authors. +// +// 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 routepatch + +import ( + "mosn.io/htnn/api/pkg/plugins" + "mosn.io/htnn/types/plugins/routepatch" +) + +func init() { + plugins.RegisterPlugin(routepatch.Name, &plugin{}) +} + +type plugin struct { + routepatch.Plugin +} + +func (p *plugin) ConfigTypeURL() string { + return "" +} diff --git a/controller/plugins/testdata/http/route_patch.in.yml b/controller/plugins/testdata/http/route_patch.in.yml new file mode 100644 index 00000000..05d3c831 --- /dev/null +++ b/controller/plugins/testdata/http/route_patch.in.yml @@ -0,0 +1,15 @@ +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy + namespace: default +spec: + targetRef: + group: networking.istio.io + kind: VirtualService + name: default + filters: + routePatch: + config: + route: + cluster_header: "Cluster-Name" diff --git a/controller/plugins/testdata/http/route_patch.out.yml b/controller/plugins/testdata/http/route_patch.out.yml new file mode 100644 index 00000000..eae2aa2f --- /dev/null +++ b/controller/plugins/testdata/http/route_patch.out.yml @@ -0,0 +1,26 @@ +- metadata: + creationTimestamp: null + name: htnn-h-default.local + namespace: default + spec: + configPatches: + - applyTo: HTTP_ROUTE + match: + routeConfiguration: + vhost: + name: default.local:80 + route: + name: default/default + patch: + operation: MERGE + value: + route: + cluster_header: Cluster-Name + status: {} +- metadata: + creationTimestamp: null + name: htnn-http-filter + namespace: istio-system + spec: + priority: -10 + status: {} diff --git a/e2e/Makefile b/e2e/Makefile index 898fc5ab..8cc358c6 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -45,12 +45,14 @@ delete-cluster: kind .PHONY: e2e-prepare-controller-image e2e-prepare-controller-image: kind cd ../manifests/ && CONTROLLER_IMAGE=htnn/controller:e2e CONTROLLER_BASE_IMAGE=$(DOCKER_MIRROR)docker.io/istio/pilot:$(ISTIO_VERSION) \ + GO_BUILD_BASE_IMAGE=$(DOCKER_MIRROR)docker.io/golang:1.21 \ make build-controller-image $(KIND) load docker-image -n htnn htnn/controller:e2e .PHONY: e2e-prepare-data-plane-image e2e-prepare-data-plane-image: kind cd ../manifests/ && PROXY_BASE_IMAGE=$(DOCKER_MIRROR)docker.io/istio/proxyv2:$(ISTIO_VERSION) \ + GO_BUILD_BASE_IMAGE=$(DOCKER_MIRROR)docker.io/golang:1.21 \ PROXY_IMAGE=htnn/gateway:e2e make build-proxy-image $(KIND) load docker-image htnn/gateway:e2e --name htnn diff --git a/e2e/base/nacos.yml b/e2e/base/nacos.yml index f70c4d86..7134296a 100644 --- a/e2e/base/nacos.yml +++ b/e2e/base/nacos.yml @@ -29,7 +29,7 @@ spec: spec: containers: - name: nacos - image: nacos/nacos-server:v1.4.6-slim + image: m.daocloud.io/docker.io/nacos/nacos-server:v1.4.6-slim ports: - containerPort: 8848 env: diff --git a/e2e/pkg/suite/suite.go b/e2e/pkg/suite/suite.go index d4dadf58..4385de2f 100644 --- a/e2e/pkg/suite/suite.go +++ b/e2e/pkg/suite/suite.go @@ -136,6 +136,7 @@ func (suite *Suite) waitDeployments(t *testing.T) { } { cmdline := fmt.Sprintf("kubectl wait --timeout=5m -n %s deployment/%s --for=condition=Available", cond.ns, cond.name) + t.Logf("start waiting for deployment %s in namespace %s, cmd: %s", cond.name, cond.ns, cmdline) cmd := strings.Fields(cmdline) wait := exec.Command(cmd[0], cmd[1:]...) err := wait.Run() diff --git a/e2e/tests/route_patch.go b/e2e/tests/route_patch.go new file mode 100644 index 00000000..95a55d2b --- /dev/null +++ b/e2e/tests/route_patch.go @@ -0,0 +1,47 @@ +// Copyright The HTNN Authors. +// +// 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 ( + "context" + "net" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "mosn.io/htnn/e2e/pkg/suite" +) + +func init() { + suite.Register(suite.Test{ + Run: func(t *testing.T, suite *suite.Suite) { + tr := &http.Transport{DialContext: func(ctx context.Context, proto, addr string) (conn net.Conn, err error) { + return net.DialTimeout("tcp", ":18000", 1*time.Second) + }} + client := &http.Client{Transport: tr, Timeout: 10 * time.Second} + rsp, err := client.Get("http://default.local:18000/echo") + require.NoError(t, err) + require.Equal(t, 403, rsp.StatusCode) + rsp, err = client.Get("http://default.local:18000/echo2") + require.NoError(t, err) + require.Equal(t, 403, rsp.StatusCode) + rsp, err = client.Get("http://default.local:18000/") + require.NoError(t, err) + require.Equal(t, 405, rsp.StatusCode) + }, + }) +} diff --git a/e2e/tests/route_patch.yml b/e2e/tests/route_patch.yml new file mode 100644 index 00000000..d25eb749 --- /dev/null +++ b/e2e/tests/route_patch.yml @@ -0,0 +1,69 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: vs + namespace: istio-system +spec: + gateways: + - default + hosts: + - "default.local" + http: + - match: + - uri: + prefix: /echo + route: + - destination: + host: backend + port: + number: 8080 + - match: + - uri: + prefix: /echo2 + route: + - destination: + host: backend + port: + number: 8080 + - match: + - uri: + prefix: / + name: last + route: + - destination: + host: backend + port: + number: 8080 +--- +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy + namespace: istio-system +spec: + targetRef: + group: networking.istio.io + kind: VirtualService + name: vs + filters: + routePatch: + config: + directResponse: + status: 403 +--- +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy2 + namespace: istio-system +spec: + targetRef: + group: networking.istio.io + kind: VirtualService + name: vs + sectionName: last + filters: + routePatch: + config: + directResponse: + status: 405 diff --git a/manifests/Makefile b/manifests/Makefile index d7fcc197..760d2519 100644 --- a/manifests/Makefile +++ b/manifests/Makefile @@ -24,10 +24,12 @@ PROXY_IMAGE ?= htnn/proxy:latest PROXY_BASE_IMAGE ?= istio/proxyv2:$(ISTIO_VERSION) CONTROLLER_IMAGE ?= htnn/controller:latest CONTROLLER_BASE_IMAGE ?= docker.io/istio/pilot:$(ISTIO_VERSION) +GO_BUILD_BASE_IMAGE ?= golang:1.21 .PHONY: build-proxy-image build-proxy-image: cd .. && $(CONTAINER_TOOL) build -t ${PROXY_IMAGE} --build-arg GOPROXY=${GOPROXY} --build-arg PROXY_BASE_IMAGE=${PROXY_BASE_IMAGE} \ + --build-arg GO_BUILD_BASE_IMAGE=${GO_BUILD_BASE_IMAGE} \ -f manifests/images/dp/Dockerfile . # If you wish to build the controller image targeting other platforms you can use the --platform flag. @@ -37,6 +39,7 @@ build-proxy-image: build-controller-image: cd .. && $(CONTAINER_TOOL) build -t ${CONTROLLER_IMAGE} \ --build-arg GOPROXY=${GOPROXY} --build-arg CONTROLLER_BASE_IMAGE=${CONTROLLER_BASE_IMAGE} \ + --build-arg GO_BUILD_BASE_IMAGE=${GO_BUILD_BASE_IMAGE} \ -f manifests/images/cp/Dockerfile . # PLATFORMS defines the target platforms for the image be built to provide support to multiple @@ -54,6 +57,7 @@ docker-buildx: ## Build and push docker image for cross-platform support $(CONTAINER_TOOL) buildx use project-v3-builder && \ $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${CONTROLLER_IMAGE} \ --build-arg GOPROXY=${GOPROXY} --build-arg CONTROLLER_BASE_IMAGE=${CONTROLLER_BASE_IMAGE} \ + --build-arg GO_BUILD_BASE_IMAGE=${GO_BUILD_BASE_IMAGE} \ -f /tmp/Dockerfile.cross . ; \ $(CONTAINER_TOOL) buildx rm project-v3-builder rm /tmp/Dockerfile.cross diff --git a/manifests/images/cp/Dockerfile b/manifests/images/cp/Dockerfile index 7e9f635a..0fdc49eb 100644 --- a/manifests/images/cp/Dockerfile +++ b/manifests/images/cp/Dockerfile @@ -14,8 +14,10 @@ # Dockerfile has specific requirement to put this ARG at the beginning: # https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact +ARG GO_BUILD_BASE_IMAGE ARG CONTROLLER_BASE_IMAGE -FROM golang:1.21 as builder +# hadolint ignore=DL3006 +FROM ${GO_BUILD_BASE_IMAGE} as builder ARG TARGETOS ARG TARGETARCH ARG GOPROXY diff --git a/manifests/images/dp/Dockerfile b/manifests/images/dp/Dockerfile index eb2a9886..7717a902 100644 --- a/manifests/images/dp/Dockerfile +++ b/manifests/images/dp/Dockerfile @@ -14,8 +14,10 @@ # Dockerfile has specific requirement to put this ARG at the beginning: # https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact +ARG GO_BUILD_BASE_IMAGE ARG PROXY_BASE_IMAGE -FROM golang:1.21 as builder +# hadolint ignore=DL3006 +FROM ${GO_BUILD_BASE_IMAGE} as builder ARG TARGETOS ARG TARGETARCH ARG GOPROXY diff --git a/types/plugins/plugins.go b/types/plugins/plugins.go index 318dda4a..169d0fd6 100644 --- a/types/plugins/plugins.go +++ b/types/plugins/plugins.go @@ -37,5 +37,6 @@ import ( _ "mosn.io/htnn/types/plugins/networkrbac" _ "mosn.io/htnn/types/plugins/oidc" _ "mosn.io/htnn/types/plugins/opa" + _ "mosn.io/htnn/types/plugins/routepatch" _ "mosn.io/htnn/types/plugins/tlsinspector" ) diff --git a/types/plugins/routepatch/config.go b/types/plugins/routepatch/config.go new file mode 100644 index 00000000..48f0ce8b --- /dev/null +++ b/types/plugins/routepatch/config.go @@ -0,0 +1,58 @@ +// Copyright The HTNN Authors. +// +// 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 routepatch + +import ( + "github.com/envoyproxy/go-control-plane/envoy/api/v2/route" + + "mosn.io/htnn/api/pkg/filtermanager/api" + "mosn.io/htnn/api/pkg/plugins" +) + +const ( + Name = "routePatch" +) + +func init() { + plugins.RegisterPluginType(Name, &Plugin{}) +} + +type Plugin struct { + plugins.PluginMethodDefaultImpl +} + +func (p *Plugin) Order() plugins.PluginOrder { + return plugins.PluginOrder{ + Position: plugins.OrderPositionInner, + } +} + +func (p *Plugin) Type() plugins.PluginType { + return plugins.TypeGeneral +} + +func (p *Plugin) Config() api.PluginConfig { + return &CustomConfig{} +} + +type CustomConfig struct { + route.Route +} + +func (conf *CustomConfig) Validate() error { + // We can't use the default validation because the route is not a complete route. + // Skip the validation for now. + return nil +} From ec9ff43ae39e27a987fdd8f9f65c1e7954342e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 18 Oct 2024 12:24:46 +0800 Subject: [PATCH 09/10] fix: the plugin order sent from controller may be wrong (#774) Signed-off-by: spacewander --- api/pkg/plugins/plugins.go | 4 ++-- .../virtualservice_subpolicies.out.yml | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/pkg/plugins/plugins.go b/api/pkg/plugins/plugins.go index 74666c28..4e8df0d9 100644 --- a/api/pkg/plugins/plugins.go +++ b/api/pkg/plugins/plugins.go @@ -202,8 +202,8 @@ func ComparePluginOrder(a, b string) bool { } func ComparePluginOrderInt(a, b string) int { - pa := plugins[a] - pb := plugins[b] + pa := pluginTypes[a] + pb := pluginTypes[b] if pa == nil || pb == nil { // The caller should guarantee the a, b are valid plugin name, so this case only happens // in test. diff --git a/controller/internal/translation/testdata/translation/virtualservice_subpolicies.out.yml b/controller/internal/translation/testdata/translation/virtualservice_subpolicies.out.yml index eacdaaa1..f16fb490 100644 --- a/controller/internal/translation/testdata/translation/virtualservice_subpolicies.out.yml +++ b/controller/internal/translation/testdata/translation/virtualservice_subpolicies.out.yml @@ -27,15 +27,15 @@ '@type': type.googleapis.com/xds.type.v3.TypedStruct value: plugins: + - config: + average: 1 + name: limitReq - config: pet: goldfish name: animal - config: hostName: John name: demo - - config: - average: 1 - name: limitReq - applyTo: HTTP_ROUTE match: routeConfiguration: @@ -56,12 +56,6 @@ value: namespace: default plugins: - - config: - pet: fish - name: animal - - config: - hostName: John - name: demo - config: keys: - name: Authorization @@ -69,4 +63,10 @@ - config: average: 1 name: limitReq + - config: + pet: fish + name: animal + - config: + hostName: John + name: demo status: {} From 23ad00dcfd73666a1864cb47f48b243d6c037f16 Mon Sep 17 00:00:00 2001 From: WeixinX <49450531+WeixinX@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:05:06 +0800 Subject: [PATCH 10/10] fix: getting headers for OnLog (#770) --- api/pkg/filtermanager/filtermanager.go | 4 ++-- plugins/tests/integration/debug_mode_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pkg/filtermanager/filtermanager.go b/api/pkg/filtermanager/filtermanager.go index 17a9134e..f9feb707 100644 --- a/api/pkg/filtermanager/filtermanager.go +++ b/api/pkg/filtermanager/filtermanager.go @@ -334,7 +334,7 @@ func (m *filterManager) localReply(v *api.LocalResponse, decoding bool) { } func (m *filterManager) DecodeHeaders(headers capi.RequestHeaderMap, endStream bool) capi.StatusType { - if !supportGettingHeadersOnLog && m.DebugModeEnabled() { + if !supportGettingHeadersOnLog { // Ensure the headers are cached on the Go side. headers.Get("test") headers := &filterManagerRequestHeaderMap{ @@ -696,7 +696,7 @@ func (m *filterManager) DecodeTrailers(trailers capi.RequestTrailerMap) capi.Sta } func (m *filterManager) EncodeHeaders(headers capi.ResponseHeaderMap, endStream bool) capi.StatusType { - if !supportGettingHeadersOnLog && m.DebugModeEnabled() { + if !supportGettingHeadersOnLog { // Ensure the headers are cached on the Go side. headers.Get("test") m.rspHdr = headers diff --git a/plugins/tests/integration/debug_mode_test.go b/plugins/tests/integration/debug_mode_test.go index e729255f..2bf15e16 100644 --- a/plugins/tests/integration/debug_mode_test.go +++ b/plugins/tests/integration/debug_mode_test.go @@ -68,7 +68,7 @@ func TestDebugModeSlowLog(t *testing.T) { pass := time.Since(now) assert.Equal(t, 200, resp.StatusCode) // delay time plus the req time - assert.True(t, pass < 55*time.Millisecond, pass) + assert.True(t, pass < 60*time.Millisecond, pass) } func TestDebugModeSlowLogNoPlugin(t *testing.T) { @@ -243,7 +243,7 @@ func TestDebugModeSlowLogWithFiltersFromConsumer(t *testing.T) { pass := time.Since(now) assert.Equal(t, 200, resp.StatusCode) // delay time plus the req time - assert.True(t, pass < 55*time.Millisecond, pass) + assert.True(t, pass < 60*time.Millisecond, pass) }, }, }