diff --git a/.bazelrc b/.bazelrc index 20ea75c04d65f..8241adbbf8113 100644 --- a/.bazelrc +++ b/.bazelrc @@ -174,6 +174,7 @@ build:asan --linkopt='-L/opt/llvm/lib/clang/18/lib/x86_64-unknown-linux-gnu' build:macos --action_env=PATH=/opt/homebrew/bin:/opt/local/bin:/usr/local/bin:/usr/bin:/bin build:macos --host_action_env=PATH=/opt/homebrew/bin:/opt/local/bin:/usr/local/bin:/usr/bin:/bin build:macos --define tcmalloc=disabled +build:macos --cxxopt=-Wno-nullability-completeness # macOS ASAN/UBSAN build:macos-asan --config=asan @@ -275,7 +276,7 @@ build:fuzz-coverage --test_tag_filters=-nocoverage # resources required to build and run the tests. build:fuzz-coverage --define=wasm=disabled build:fuzz-coverage --config=fuzz-coverage-config -build:fuzz-coverage-config --//tools/coverage:config=//test:fuzz_coverage_config +build:fuzz-coverage-config --//tools/coverage:config=@envoy//test:fuzz_coverage_config build:cache-local --remote_cache=grpc://localhost:9092 @@ -539,6 +540,7 @@ common:common-envoy-engflow --google_default_credentials=false common:common-envoy-engflow --credential_helper=*.engflow.com=%workspace%/bazel/engflow-bazel-credential-helper.sh common:common-envoy-engflow --grpc_keepalive_time=60s common:common-envoy-engflow --grpc_keepalive_timeout=30s +common:common-envoy-engflow --remote_cache_compression common:cache-envoy-engflow --remote_cache=grpcs://mordenite.cluster.engflow.com common:cache-envoy-engflow --remote_timeout=3600s @@ -561,7 +563,7 @@ common:remote-cache-envoy-engflow --config=common-envoy-engflow common:remote-cache-envoy-engflow --config=cache-envoy-engflow # Specifies the rustfmt.toml for all rustfmt_test targets. -build --@rules_rust//rust/settings:rustfmt.toml=//:rustfmt.toml +build --@rules_rust//rust/settings:rustfmt.toml=@envoy//:rustfmt.toml ############################################################################# # debug: Various Bazel debugging flags diff --git a/.bazelversion b/.bazelversion index 93c8ddab9fef3..e8be68404bcb3 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.6.0 +7.6.1 diff --git a/.clang-tidy b/.clang-tidy index fba82af03640d..a9f97bc6c75c4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -71,8 +71,7 @@ CheckOptions: |^value_or$| |^Ip6(ntohl|htonl)$| |^get_$| - |^HeaderHasValue(Ref)?$| - |^HeaderValueOf$| + |^ContainsHeader$| |^Is(Superset|Subset)OfHeaders$| |^LLVMFuzzerInitialize$| |^LLVMFuzzerTestOneInput$| diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 5367105d7c717..b6e7a08639b32 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -35,3 +35,314 @@ change this to `minimal` or `all` depending on where you're running the containe Docker for Mac/Windows is known to have disk performance issue, this makes formatting all files in the container very slow. [Update the mount consistency to 'delegated'](https://code.visualstudio.com/docs/remote/containers-advanced#_update-the-mount-consistency-to-delegated-for-macos) is recommended. + +## Detailed Setup Guide for Local Development + +This section provides an exhaustive walkthrough for setting up Envoy source code locally for development and debugging. This guide has been tested on both Apple M1 Max and Intel Core i9 machines running macOS Sequoia (version 15.5). + +For developers who are not C++ experts or are more familiar with CMake/Make build systems, Envoy's Bazel-based build system and dev container support makes local development much more accessible. + +### Prerequisites + +- macOS Sequoia (version 15.5) or compatible system +- VSCode or Cursor IDE +- Docker Desktop with sufficient resources +- Dev Containers extension + +### Step-by-Step Setup Process + +#### 1. Repository Setup + +1. Clone the Envoy repository from GitHub or create a fork: + ```bash + git clone https://github.com/envoyproxy/envoy.git + # OR + git clone https://github.com/YOUR_USERNAME/envoy.git + ``` + +2. Open the downloaded folder in VSCode or Cursor + +3. The IDE will detect the `.devcontainer` folder and prompt to reopen in Dev Container mode. Accept this prompt. + + You should see a blue label at the bottom-left of the window indicating "Container" or "Dev Container" status, confirming you're running in container mode. + + ![Container Status](./images/container-status.png) + +#### 2. Initialize Development Environment + +1. Once in dev container mode, open a new terminal (**Terminal** → **New Terminal**) + +2. Run the compilation database refresh script: + ```bash + ci/do_ci.sh refresh_compdb + ``` + + This script refreshes the compilation database and generates dependencies for code completion, including protobuf generated codes and external dependencies. This process may take 30-60 minutes depending on your machine performance. + + > **Note**: Re-run this script whenever you change proto definitions or modify bazel structure to maintain proper code completion. + +#### 3. Troubleshooting Build Issues + +If you encounter "no space left on device" errors during the compilation process, you'll need to adjust Docker Desktop settings: + +![Docker Space Error](./images/docker-space-error.png) + +**Resolution Steps:** + +1. Open Docker Desktop +2. Navigate to **Settings** → **Resources** +3. Increase **Memory Limit** to at least 8GB (recommended: 16GB+) +4. Increase **Disk usage limit** to at least 100GB + + ![Docker Settings](./images/docker-settings.png) + +5. If settings are grayed out, perform a factory reset: + - Go to **Troubleshoot** → **Reset to factory defaults** + - Restart Docker Desktop + - Reconfigure the resource limits + - Click **Apply and restart** + +**Successful Build Verification:** + +Look for a completion message similar to (the total actions might vary): +``` +Build completed successfully, 11415 total actions +``` + +![Build Success](./images/build-success.png) + +#### 4. Debug Configuration Setup + +##### Create Envoy Configuration File + +Before generating debug configuration, create an `envoy.yaml` file in the repository root (you can choose any name of your liking).This is a standard envoy configuration, you should be able to change it according to your need. Here's a sample configuration for debugging OpenTelemetry tracing: + +```yaml +admin: + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 9902 + +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + # Enable tracing + tracing: + provider: + name: envoy.tracers.opentelemetry + typed_config: + "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + grpc_service: + envoy_grpc: + cluster_name: opentelemetry_collector + timeout: 100s + service_name: envoy-proxy + random_sampling: + value: 100 + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + host_rewrite_literal: www.envoyproxy.io + cluster: service_envoyproxy_io + + clusters: + - name: service_envoyproxy_io + type: LOGICAL_DNS + # Comment out the following line to test on v6 networks + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service_envoyproxy_io + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: www.envoyproxy.io + port_value: 443 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: www.envoyproxy.io + + # OpenTelemetry Collector cluster + - name: opentelemetry_collector + type: STRICT_DNS + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: opentelemetry_collector + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: localhost + port_value: 5002 +``` + +##### Generate Debug Configuration + +Run the debug configuration generator: + +```bash +tools/vscode/generate_debug_config.py //source/exe:envoy-static --args "-c envoy.yaml" +``` + +**Advanced Configuration Options:** + +The script now supports automatic compiler configuration detection and manual override options: + +- **Auto-detection**: The script automatically detects the optimal compiler configuration based on your platform: + - On ARM64/aarch64 architectures (Apple Silicon Macs): Automatically uses `clang` for better C++20 concepts compatibility + - On other architectures: Uses Bazel's default configuration + +- **Manual override**: You can explicitly specify a build configuration using the `--config` flag: + ```bash + # Force use of clang configuration + tools/vscode/generate_debug_config.py //source/exe:envoy-static --args "-c envoy.yaml" --config clang + + # Force use of gcc configuration + tools/vscode/generate_debug_config.py //source/exe:envoy-static --args "-c envoy.yaml" --config gcc + ``` + +- **Additional options**: + ```bash + # Use LLDB debugger instead of GDB (recommended for macOS) + tools/vscode/generate_debug_config.py //source/exe:envoy-static --args "-c envoy.yaml" --debugger lldb + + # Overwrite existing configuration completely + tools/vscode/generate_debug_config.py //source/exe:envoy-static --args "-c envoy.yaml" --overwrite + ``` + +![Debug Configuration Run](./images/debug-configuration-run.png) + +> **Important**: This command may take over an hour to complete. You'll need to re-run this script whenever you make code changes. + +> **ARM64/Apple Silicon Note**: If you're using an ARM64 system (like Apple Silicon Macs) and encounter GCC-related compilation errors with C++20 concepts compatibility, the script will automatically use clang configuration to resolve these issues. This addresses protobuf dependency compatibility problems that can occur with GCC on ARM64 architectures. + +There are two types of debuggers envoy community recommends to use, GDB (GNU Debugger) , a widely used and powerful debugger for various languages, including C++ and LLDB, the LLVM project's debugger, often used with Clang-compiled code (Clang is one of the many compilers for C++). + +Now click F5 to start debugging, this should have generated the `launch.json` as shown below. The generated `launch.json` will use GDB by default. + +![GDB Launch JSON](./images/GDB-launch-json.png) + +However, if you encounter an error with GDB (which can happen with some Mac Machines), try using the LLDB debugger as shown in the next section. + +![GDB Error](./images/gdb-error.png) + +##### Configure LLDB Debugger (Recommended for macOS) + +For better compatibility on macOS, modify the generated `launch.json` to use LLDB: + +1. Open `launch.json` (Cmd+P → type "launch.json") + + ![Launch JSON](./images/GDB-launch-json.png) + +2. Replace the GDB configuration with LLDB: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "LLDB //source/exe:envoy-static", + "type": "lldb", + "request": "launch", + "program": "/build/.cache/bazel/_bazel_vscode/2d35de14639eaad1ac7060a4dd7e3351/execroot/envoy/bazel-out/k8-dbg/bin/source/exe/envoy-static", + "args": ["-c", "envoy.yaml"], + "cwd": "${workspaceFolder}", + "stopOnEntry": false, + "sourceMap": { + "/proc/self/cwd": "${workspaceFolder}" + } + } + ] +} +``` + +> **Note**: Update the `program` path with the actual hash from your generated GDB configuration. Ensure the CodeLLDB extension is installed in your dev container. + +![CodeLLDB Extension](./images/code-lldb-extension.png) + +#### 5. Setting Up Breakpoints + +##### Main Entry Point + +Add a breakpoint in the main function located at `source/exe/main.cc`: + +![Main Breakpoint](./images/main-breakpoint.png) + +##### Custom Debugging Points + +You can add additional debug points, For example, for tracing debugging, add breakpoints in specific areas like `source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc`: + +![Tracer Breakpoint](./images/tracer-breakpoint.png) + +#### 6. Starting Debug Session + +1. Start debugging by pressing **F5** or going to **Run** → **Start Debugging** + +2. Allow 1-2 minutes for the debugger to attach to the process (Envoy is a large codebase) + +3. Most of the Envoy source code is located in the `source` folder at the root of the repository.The breakpoint in `source/exe/main.cc` should activate + + ![Debug Active](./images/debug-active.png) + +4. Continue execution (**Run** → **Continue**) to see Envoy start up and begin emitting logs + +#### 7. Verifying Setup + +Once Envoy starts successfully, verify the setup by accessing. The ports are based on the configuration we have used earlier: + +- **Admin Interface**: http://localhost:9902 + + ![Admin Interface](./images/admin-interface.png) + +- **Proxy Endpoint**: Try reaching to the URL http://localhost:10000 and you should be redirected to envoyproxy.io based on the configuration file we gave above. + + ![Proxy Endpoint](./images/proxy-endpoint.png) + +- **Admin Endpoints**: +Try accessing different admin endpoints from your local machine. + - Clusters: http://localhost:9902/clusters + - Config Dump: http://localhost:9902/config_dump + - Stats: http://localhost:9902/stats + + ![Cluster Endpoints](./images/cluster-endpoints.png) + ![Config Dump](./images/config-dump.png) + ![Stats Endpoints](./images/stats-endpoints.png) + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f24d57d22ede2..eb34fc304c06c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,6 +15,9 @@ ], "containerEnv": { "ENVOY_SRCDIR": "${containerWorkspaceFolder}", + "HTTPS_PROXY": "${env:VSCODE_CONTAINER_HTTPS_PROXY}", + "HTTP_PROXY": "${env:VSCODE_CONTAINER_HTTP_PROXY}", + "NO_PROXY": "${env:VSCODE_CONTAINER_NO_PROXY}" }, "remoteUser": "vscode", "containerUser": "vscode", diff --git a/.devcontainer/images/GDB-launch-json.png b/.devcontainer/images/GDB-launch-json.png new file mode 100644 index 0000000000000..a96eedd646668 Binary files /dev/null and b/.devcontainer/images/GDB-launch-json.png differ diff --git a/.devcontainer/images/admin-interface.png b/.devcontainer/images/admin-interface.png new file mode 100644 index 0000000000000..cd59e07344b08 Binary files /dev/null and b/.devcontainer/images/admin-interface.png differ diff --git a/.devcontainer/images/build-success.png b/.devcontainer/images/build-success.png new file mode 100644 index 0000000000000..5b1df30719b2b Binary files /dev/null and b/.devcontainer/images/build-success.png differ diff --git a/.devcontainer/images/cluster-endpoints.png b/.devcontainer/images/cluster-endpoints.png new file mode 100644 index 0000000000000..0f29e7945c349 Binary files /dev/null and b/.devcontainer/images/cluster-endpoints.png differ diff --git a/.devcontainer/images/code-lldb-extension.png b/.devcontainer/images/code-lldb-extension.png new file mode 100644 index 0000000000000..8a458ae4a2d6b Binary files /dev/null and b/.devcontainer/images/code-lldb-extension.png differ diff --git a/.devcontainer/images/config-dump.png b/.devcontainer/images/config-dump.png new file mode 100644 index 0000000000000..80b36d11e9da7 Binary files /dev/null and b/.devcontainer/images/config-dump.png differ diff --git a/.devcontainer/images/container-status.png b/.devcontainer/images/container-status.png new file mode 100644 index 0000000000000..9e574e85cccb3 Binary files /dev/null and b/.devcontainer/images/container-status.png differ diff --git a/.devcontainer/images/debug-active.png b/.devcontainer/images/debug-active.png new file mode 100644 index 0000000000000..7e2b88c83a3eb Binary files /dev/null and b/.devcontainer/images/debug-active.png differ diff --git a/.devcontainer/images/debug-configuration-run.png b/.devcontainer/images/debug-configuration-run.png new file mode 100644 index 0000000000000..a5724cdbd3aad Binary files /dev/null and b/.devcontainer/images/debug-configuration-run.png differ diff --git a/.devcontainer/images/docker-settings.png b/.devcontainer/images/docker-settings.png new file mode 100644 index 0000000000000..7b5c91fb75ded Binary files /dev/null and b/.devcontainer/images/docker-settings.png differ diff --git a/.devcontainer/images/docker-space-error.png b/.devcontainer/images/docker-space-error.png new file mode 100644 index 0000000000000..212c1c6b136a4 Binary files /dev/null and b/.devcontainer/images/docker-space-error.png differ diff --git a/.devcontainer/images/gdb-error.png b/.devcontainer/images/gdb-error.png new file mode 100644 index 0000000000000..657165c73bbec Binary files /dev/null and b/.devcontainer/images/gdb-error.png differ diff --git a/.devcontainer/images/main-breakpoint.png b/.devcontainer/images/main-breakpoint.png new file mode 100644 index 0000000000000..39dc6804a88f2 Binary files /dev/null and b/.devcontainer/images/main-breakpoint.png differ diff --git a/.devcontainer/images/proxy-endpoint.png b/.devcontainer/images/proxy-endpoint.png new file mode 100644 index 0000000000000..21e28150bafbf Binary files /dev/null and b/.devcontainer/images/proxy-endpoint.png differ diff --git a/.devcontainer/images/stats-endpoints.png b/.devcontainer/images/stats-endpoints.png new file mode 100644 index 0000000000000..5200ddcdd6bf2 Binary files /dev/null and b/.devcontainer/images/stats-endpoints.png differ diff --git a/.devcontainer/images/tracer-breakpoint.png b/.devcontainer/images/tracer-breakpoint.png new file mode 100644 index 0000000000000..f91141eb6645a Binary files /dev/null and b/.devcontainer/images/tracer-breakpoint.png differ diff --git a/.dockerignore b/.dockerignore index 935fd5f24e3f4..66f2e6b90aa0a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,12 @@ /* !/VERSION.txt !/build_envoy +!/build_envoy_debug !/ci +!/distribution/docker !/configs/google-vrp !/configs/*yaml +!/linux/amd64/build_envoy_debug !/linux/amd64/release.tar.zst !/linux/amd64/schema_validator_tool !/linux/amd64/router_check_tool diff --git a/.gitattributes b/.gitattributes index 74e5a411fb82d..68991c2b92819 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,5 @@ /test/common/tls/test_data/aes_128_key binary /test/common/tls/test_data/ticket_key_* binary /test/**/*_corpus/* linguist-generated=true -requirements.txt binary package.lock binary yarn.lock binary diff --git a/.github/config.yml b/.github/config.yml index 9f1678d853475..72f06fcbc0c8d 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -146,7 +146,7 @@ checks: name: >- Envoy/Publish and verify on-run: - - publish + - release - verify required: true @@ -336,7 +336,7 @@ run: precheck-publish: paths: - "**/*" - publish: + release: paths: - .bazelrc - .bazelversion @@ -345,6 +345,7 @@ run: - bazel/**/* - ci/**/* - contrib/**/* + - distribution/**/* - envoy/**/* - examples/**/* - source/**/* @@ -359,6 +360,7 @@ run: - bazel/**/* - ci/**/* - contrib/**/* + - distribution/**/* - envoy/**/* - examples/**/* - source/**/* diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3f35da513ec80..5783a8b191b96 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -34,6 +34,12 @@ updates: interval: daily time: "06:00" +- package-ecosystem: "docker" + directory: "/distribution/docker" + schedule: + interval: daily + time: "06:00" + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/_check_coverage.yml b/.github/workflows/_check_coverage.yml index 756ff79bda3ac..127c703c27053 100644 --- a/.github/workflows/_check_coverage.yml +++ b/.github/workflows/_check_coverage.yml @@ -53,7 +53,7 @@ jobs: request: ${{ inputs.request }} runs-on: ${{ fromJSON(inputs.request).config.ci.agent-ubuntu }} steps-post: | - - uses: envoyproxy/toolshed/gh-actions/gcs/artefact/sync@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcs/artefact/sync@actions-v0.3.25 with: bucket: ${{ inputs.trusted && vars.GCS_ARTIFACT_BUCKET_POST || vars.GCS_ARTIFACT_BUCKET_PRE }} path: generated/${{ matrix.target }}/html diff --git a/.github/workflows/_finish.yml b/.github/workflows/_finish.yml index 8340b7e3fd2fa..4640b778b94bd 100644 --- a/.github/workflows/_finish.yml +++ b/.github/workflows/_finish.yml @@ -36,7 +36,7 @@ jobs: actions: read contents: read steps: - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 name: Incoming data id: needs with: @@ -87,7 +87,7 @@ jobs: summary: "Check has finished", text: $text}}}} - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 name: Print summary with: input: ${{ toJSON(steps.needs.outputs.value).summary-title }} @@ -95,13 +95,13 @@ jobs: "## \(.)" options: -Rr output-path: GITHUB_STEP_SUMMARY - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 name: Appauth id: appauth with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/gh-actions/github/checks@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checks@actions-v0.3.25 name: Update check with: action: update diff --git a/.github/workflows/_load.yml b/.github/workflows/_load.yml index 6b211c323a502..628475c545fbf 100644 --- a/.github/workflows/_load.yml +++ b/.github/workflows/_load.yml @@ -100,7 +100,7 @@ jobs: # Handle any failure in triggering job # Remove any `checks` we dont care about # Prepare a check request - - uses: envoyproxy/toolshed/gh-actions/github/env/load@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/env/load@actions-v0.3.25 name: Load env id: data with: @@ -111,13 +111,13 @@ jobs: GH_TOKEN: ${{ github.token }} # Update the check - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 name: Appauth id: appauth with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/gh-actions/github/checks@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checks@actions-v0.3.25 name: Update check if: ${{ fromJSON(steps.data.outputs.data).data.check.action == 'RUN' }} with: @@ -125,7 +125,7 @@ jobs: checks: ${{ toJSON(fromJSON(steps.data.outputs.data).checks) }} token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 name: Print request summary with: input: | @@ -145,7 +145,7 @@ jobs: | $summary.summary as $summary | "${{ inputs.template-request-summary }}" - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: request-output name: Load request with: diff --git a/.github/workflows/_load_env.yml b/.github/workflows/_load_env.yml index 32a7f3f04132c..caaec276c52c5 100644 --- a/.github/workflows/_load_env.yml +++ b/.github/workflows/_load_env.yml @@ -63,18 +63,18 @@ jobs: request: ${{ steps.env.outputs.data }} trusted: true steps: - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: started name: Create timestamp with: options: -r filter: | now - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout name: Checkout Envoy repository - name: Generate environment variables - uses: envoyproxy/toolshed/gh-actions/envoy/ci/env@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/envoy/ci/env@actions-v0.3.25 id: env with: branch-name: ${{ inputs.branch-name }} @@ -86,7 +86,7 @@ jobs: - name: Request summary id: summary - uses: envoyproxy/toolshed/gh-actions/github/env/summary@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/env/summary@actions-v0.3.25 with: actor: ${{ toJSON(fromJSON(steps.env.outputs.data).request.actor) }} base-sha: ${{ fromJSON(steps.env.outputs.data).request.base-sha }} diff --git a/.github/workflows/_precheck_deps.yml b/.github/workflows/_precheck_deps.yml index a816809832bd6..b5909cf8ffb5b 100644 --- a/.github/workflows/_precheck_deps.yml +++ b/.github/workflows/_precheck_deps.yml @@ -66,4 +66,4 @@ jobs: ref: ${{ fromJSON(inputs.request).request.sha }} persist-credentials: false - name: Dependency Review - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 diff --git a/.github/workflows/_precheck_publish.yml b/.github/workflows/_precheck_publish.yml index c12dffc1c92b6..62b700bf22d4c 100644 --- a/.github/workflows/_precheck_publish.yml +++ b/.github/workflows/_precheck_publish.yml @@ -78,7 +78,7 @@ jobs: --config=docs-ci rbe: true steps-post: | - - uses: envoyproxy/toolshed/gh-actions/gcs/artefact/sync@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcs/artefact/sync@actions-v0.3.25 with: bucket: ${{ inputs.trusted && vars.GCS_ARTIFACT_BUCKET_POST || vars.GCS_ARTIFACT_BUCKET_PRE }} path: generated/docs diff --git a/.github/workflows/_publish_build.yml b/.github/workflows/_publish_build.yml index 42438e49f603e..562025c33a249 100644 --- a/.github/workflows/_publish_build.yml +++ b/.github/workflows/_publish_build.yml @@ -6,8 +6,6 @@ permissions: on: workflow_call: secrets: - dockerhub-password: - required: false gcs-cache-key: required: true gpg-key: @@ -15,6 +13,9 @@ on: gpg-key-password: required: true inputs: + arch: + type: string + required: true gcs-cache-bucket: type: string required: true @@ -31,7 +32,7 @@ concurrency: ${{ github.actor != 'trigger-release-envoy[bot]' && github.event.inputs.head_ref || github.run_id - }}-${{ github.event.workflow.id }}-publish + }}-${{ inputs.arch }}-${{ github.event.workflow.id }}-publish cancel-in-progress: true @@ -42,117 +43,59 @@ jobs: permissions: contents: read packages: read - name: ${{ matrix.name || matrix.target }} + name: Binary uses: ./.github/workflows/_run.yml with: - arch: ${{ matrix.arch }} - bazel-extra: ${{ matrix.bazel-extra }} - target: ${{ matrix.target }} - target-suffix: ${{ matrix.arch }} - cache-build-image: ${{ fromJSON(inputs.request).request.build-image.default }} - cache-build-image-key-suffix: ${{ matrix.arch == 'arm64' && format('-{0}', matrix.arch) || '' }} - concurrency-suffix: -${{ matrix.arch }} - gcs-cache-bucket: ${{ inputs.gcs-cache-bucket }} - rbe: ${{ matrix.rbe }} - request: ${{ inputs.request }} - runs-on: ${{ matrix.runs-on }} - timeout-minutes: 120 - trusted: ${{ inputs.trusted }} - upload-name: release.${{ matrix.arch }} - upload-path: envoy/${{ matrix.arch }}/bin/ - strategy: - fail-fast: false - matrix: - include: - - target: release.server_only - name: Release (x64) - arch: x64 - bazel-extra: >- - --config=remote-envoy-engflow - rbe: true - - target: release.server_only - name: Release (arm64) - arch: arm64 - bazel-extra: >- - --config=remote-envoy-engflow - rbe: true - runs-on: ${{ vars.ENVOY_ARM_VM || 'ubuntu-24.04-arm' }} - - distribution: - permissions: - contents: read - packages: read - secrets: - gcs-cache-key: ${{ secrets.gcs-cache-key }} - gpg-key: ${{ secrets.gpg-key }} - gpg-key-password: ${{ secrets.gpg-key-password }} - name: ${{ matrix.name || matrix.target }} - needs: - - binary - uses: ./.github/workflows/_run.yml - with: - arch: ${{ matrix.arch }} + arch: ${{ inputs.arch }} bazel-extra: >- - --config=remote-cache-envoy-engflow - downloads: | - release.${{ matrix.arch }}: release/${{ matrix.arch }}/bin/ - target: ${{ matrix.target }} - target-suffix: ${{ matrix.arch }} + --config=remote-envoy-engflow + target: release.server_only + target-suffix: ${{ inputs.arch }} cache-build-image: ${{ fromJSON(inputs.request).request.build-image.default }} - cache-build-image-key-suffix: ${{ matrix.cache-build-image-key-suffix }} - concurrency-suffix: -${{ matrix.arch }} + cache-build-image-key-suffix: ${{ inputs.arch == 'arm64' && '-arm64' || '' }} + concurrency-suffix: -${{ inputs.arch }} gcs-cache-bucket: ${{ inputs.gcs-cache-bucket }} - import-gpg: true - rbe: false + rbe: true request: ${{ inputs.request }} - runs-on: ${{ matrix.runs-on }} + runs-on: ${{ inputs.arch == 'arm64' && (vars.ENVOY_ARM_VM || 'ubuntu-24.04-arm') || null }} + timeout-minutes: 120 trusted: ${{ inputs.trusted }} - upload-name: packages.${{ matrix.arch }} - upload-path: envoy/${{ matrix.arch }} - strategy: - fail-fast: false - matrix: - include: - - target: distribution - name: Package debs (x64) - arch: x64 - - target: distribution - name: Package debs (arm64) - arch: arm64 - cache-build-image-key-suffix: -arm64 - runs-on: ${{ vars.ENVOY_ARM_VM || 'ubuntu-24.04-arm' }} + upload-name: release.${{ inputs.arch }} + upload-path: envoy/${{ inputs.arch }}/bin/ docker: permissions: contents: read packages: read - secrets: - dockerhub-password: ${{ secrets.dockerhub-password }} - name: ${{ matrix.name || matrix.target }} + name: Docker OCI needs: - binary uses: ./.github/workflows/_run.yml with: - target: ${{ matrix.target }} + arch: ${{ inputs.arch }} + target: docker + target-suffix: ${{ inputs.arch }} cache-build-image: ${{ fromJSON(inputs.request).request.build-image.default }} + cache-build-image-key-suffix: ${{ inputs.arch == 'arm64' && '-arm64' || '' }} + concurrency-suffix: -${{ inputs.arch }} downloads: | - release.arm64: envoy/arm64/bin/ - release.x64: envoy/x64/bin/ + release.${{ inputs.arch }}: envoy/${{ inputs.arch }}/bin/ request: ${{ inputs.request }} source: | export NO_BUILD_SETUP=1 export ENVOY_DOCKER_IN_DOCKER=1 + export ENVOY_DOCKER_SAVE_IMAGE=true + export ENVOY_OCI_DIR=build_images + + # export DOCKER_BUILD_PLATFORM=${{ inputs.arch == 'x64' && 'linux/amd64' || 'linux/arm64' }} + # export DOCKER_LOAD_IMAGES=true + # export DOCKER_FORCE_OCI_OUTPUT=true trusted: ${{ inputs.trusted }} - upload-name: docker - upload-path: build_images - strategy: - fail-fast: false - matrix: - include: - - target: docker - name: Docker (Linux multiarch) + upload-name: oci.${{ inputs.arch }} + upload-path: envoy/${{ inputs.arch }}/build_images + runs-on: ${{ inputs.arch == 'arm64' && (vars.ENVOY_ARM_VM || 'ubuntu-24.04-arm') || null }} - sign: + distribution: permissions: contents: read packages: read @@ -160,34 +103,26 @@ jobs: gcs-cache-key: ${{ secrets.gcs-cache-key }} gpg-key: ${{ secrets.gpg-key }} gpg-key-password: ${{ secrets.gpg-key-password }} - name: ${{ matrix.name || matrix.target }} + name: Packages needs: - - distribution + - binary uses: ./.github/workflows/_run.yml with: - target: release.signed + arch: ${{ inputs.arch }} bazel-extra: >- - --//distribution:x64-packages=//distribution:custom/x64/packages.x64.tar.gz - --//distribution:arm64-packages=//distribution:custom/arm64/packages.arm64.tar.gz - --//distribution:x64-release=//distribution:custom/x64/bin/release.tar.zst - --//distribution:arm64-release=//distribution:custom/arm64/bin/release.tar.zst - cache-build-image: ${{ fromJSON(inputs.request).request.build-image.default }} - diskspace-hack: true + --config=remote-cache-envoy-engflow downloads: | - packages.arm64: envoy/arm64/ - packages.x64: envoy/x64/ - release.arm64: envoy/arm64/bin/ - release.x64: envoy/x64/bin/ + release.${{ inputs.arch }}: release/${{ inputs.arch }}/bin/ + target: distribution + target-suffix: ${{ inputs.arch }} + cache-build-image: ${{ fromJSON(inputs.request).request.build-image.default }} + cache-build-image-key-suffix: ${{ inputs.arch == 'arm64' && '-arm64' || '' }} + concurrency-suffix: -${{ inputs.arch }} gcs-cache-bucket: ${{ inputs.gcs-cache-bucket }} import-gpg: true + rbe: false request: ${{ inputs.request }} - source: | - export NO_BUILD_SETUP=1 + runs-on: ${{ inputs.arch == 'arm64' && (vars.ENVOY_ARM_VM || 'ubuntu-24.04-arm') || null }} trusted: ${{ inputs.trusted }} - upload-name: release.signed - upload-path: envoy/release.signed.tar.zst - steps-pre: | - - run: | - mkdir distribution/custom - cp -a %{{ runner.temp }}/envoy/x64 %{{ runner.temp }}/envoy/arm64 distribution/custom - shell: bash + upload-name: packages.${{ inputs.arch }} + upload-path: envoy/${{ inputs.arch }} diff --git a/.github/workflows/_publish_publish.yml b/.github/workflows/_publish_release.yml similarity index 54% rename from .github/workflows/_publish_publish.yml rename to .github/workflows/_publish_release.yml index 01179c0af456a..e8da8c7eb9fe0 100644 --- a/.github/workflows/_publish_publish.yml +++ b/.github/workflows/_publish_release.yml @@ -6,12 +6,18 @@ permissions: on: workflow_call: secrets: + dockerhub-password: + dockerhub-username: ENVOY_CI_SYNC_APP_ID: ENVOY_CI_SYNC_APP_KEY: ENVOY_CI_PUBLISH_APP_ID: ENVOY_CI_PUBLISH_APP_KEY: gcs-cache-key: required: true + gpg-key: + required: true + gpg-key-password: + required: true inputs: gcs-cache-bucket: type: string @@ -33,7 +39,65 @@ concurrency: jobs: - publish: + sign: + permissions: + contents: read + packages: read + secrets: + gcs-cache-key: ${{ secrets.gcs-cache-key }} + gpg-key: ${{ secrets.gpg-key }} + gpg-key-password: ${{ secrets.gpg-key-password }} + if: ${{ vars.ENVOY_CI_RELEASE || github.repository == 'envoyproxy/envoy' }} + name: Sign packages + uses: ./.github/workflows/_run.yml + with: + target: release.signed + bazel-extra: >- + --//distribution:x64-packages=//distribution:custom/x64/packages.x64.tar.gz + --//distribution:arm64-packages=//distribution:custom/arm64/packages.arm64.tar.gz + --//distribution:x64-release=//distribution:custom/x64/bin/release.tar.zst + --//distribution:arm64-release=//distribution:custom/arm64/bin/release.tar.zst + cache-build-image: ${{ fromJSON(inputs.request).request.build-image.default }} + diskspace-hack: true + downloads: | + packages.arm64: envoy/arm64/ + packages.x64: envoy/x64/ + release.arm64: envoy/arm64/bin/ + release.x64: envoy/x64/bin/ + gcs-cache-bucket: ${{ inputs.gcs-cache-bucket }} + import-gpg: true + request: ${{ inputs.request }} + source: | + export NO_BUILD_SETUP=1 + trusted: ${{ inputs.trusted }} + upload-name: release.signed + upload-path: envoy/release.signed.tar.zst + steps-pre: | + - run: | + mkdir distribution/custom + cp -a %{{ runner.temp }}/envoy/x64 %{{ runner.temp }}/envoy/arm64 distribution/custom + shell: bash + + container: + secrets: + dockerhub-username: ${{ secrets.dockerhub-username }} + dockerhub-password: ${{ secrets.dockerhub-password }} + permissions: + contents: read + packages: read + name: Publish container images + uses: ./.github/workflows/_publish_release_container.yml + with: + dockerhub-repo: ${{ vars.DOCKERHUB_REPO || 'envoy' }} + dev: ${{ fromJSON(inputs.request).request.version.dev }} + sha: ${{ fromJSON(inputs.request).request.sha }} + target-branch: ${{ fromJSON(inputs.request).request.target-branch }} + trusted: ${{ inputs.trusted }} + version-major: ${{ fromJSON(inputs.request).request.version.major }} + version-minor: ${{ fromJSON(inputs.request).request.version.minor }} + version-patch: ${{ fromJSON(inputs.request).request.version.patch }} + + release: secrets: app-id: ${{ inputs.trusted && secrets.ENVOY_CI_PUBLISH_APP_ID || '' }} app-key: ${{ inputs.trusted && secrets.ENVOY_CI_PUBLISH_APP_KEY || '' }} @@ -41,6 +105,9 @@ jobs: permissions: contents: read packages: read + needs: + - container + - sign name: ${{ matrix.name || matrix.target }} uses: ./.github/workflows/_run.yml with: @@ -65,7 +132,7 @@ jobs: export ENVOY_REPO=${{ github.repository }} export ENVOY_PUBLISH_DRY_RUN=${{ (fromJSON(inputs.request).request.version.dev || ! inputs.trusted) && 1 || '' }} - publish_docs: + docs: # For normal commits to Envoy main this will trigger an update in the website repo, # which will update its envoy dep shas, and rebuild the website for the latest docs # @@ -76,14 +143,14 @@ jobs: if: ${{ inputs.trusted && github.repository == 'envoyproxy/envoy' }} runs-on: ${{ fromJSON(inputs.request).config.ci.agent-ubuntu }} needs: - - publish + - release steps: - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 id: appauth with: app_id: ${{ secrets.ENVOY_CI_SYNC_APP_ID }} key: ${{ secrets.ENVOY_CI_SYNC_APP_KEY }} - - uses: envoyproxy/toolshed/gh-actions/dispatch@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/dispatch@actions-v0.3.25 with: ref: main repository: ${{ fromJSON(inputs.request).request.version.dev && 'envoyproxy/envoy-website' || 'envoyproxy/archive' }} diff --git a/.github/workflows/_publish_release_container.yml b/.github/workflows/_publish_release_container.yml new file mode 100644 index 0000000000000..a417aa404804a --- /dev/null +++ b/.github/workflows/_publish_release_container.yml @@ -0,0 +1,207 @@ +name: Publish (containers) + +permissions: + contents: read + +on: + workflow_call: + secrets: + dockerhub-password: + dockerhub-username: + inputs: + dev: + required: true + type: boolean + default: true + dockerhub-repo: + required: true + default: envoy + type: string + sha: + required: true + type: string + target-branch: + required: true + type: string + trusted: + required: true + type: boolean + version-major: + required: false + type: number + version-minor: + required: false + type: number + version-patch: + required: false + type: number + +concurrency: + group: >- + ${{ github.actor != 'trigger-release-envoy[bot]' + && github.event.inputs.head_ref + || github.run_id + }}-${{ github.event.workflow.id }}-publish-release-container + cancel-in-progress: true + + +jobs: + push-manifests: + name: Create manifests (${{ inputs.trustred && 'dry run' || 'push' }}) + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: read + steps: + - name: Generate manifest configuration (dev) + id: dev-config + if: ${{ inputs.dev && inputs.target-branch == 'main' }} + uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 + with: + input-format: yaml + filter: >- + {manifests: .} + input: | + - name: ${{ inputs.dockerhub-repo }} + tag: dev + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy.{arch}.tar + additional-tags: + - dev-${{ github.sha }} + - name: ${{ inputs.dockerhub-repo }} + tag: contrib-dev + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-contrib.{arch}.tar + additional-tags: + - contrib-dev-${{ github.sha }} + - name: ${{ inputs.dockerhub-repo }} + tag: contrib-debug-dev + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-contrib-debug.{arch}.tar + additional-tags: + - contrib-debug-dev-${{ github.sha }} + - name: ${{ inputs.dockerhub-repo }} + tag: debug-dev + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-debug.{arch}.tar + additional-tags: + - debug-dev-${{ github.sha }} + - name: ${{ inputs.dockerhub-repo }} + tag: distroless-dev + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-distroless.{arch}.tar + additional-tags: + - distroless-dev-${{ github.sha }} + - name: ${{ inputs.dockerhub-repo }} + tag: google-vrp-dev + registry: docker.io/envoyproxy + architectures: + - amd64 + artifact-pattern: envoy-google-vrp.{arch}.tar + additional-tags: + - google-vrp-dev-${{ github.sha }} + - name: ${{ inputs.dockerhub-repo }} + tag: tools-dev + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-tools.{arch}.tar + additional-tags: + - tools-dev-${{ github.sha }} + + - name: Generate manifest configuration (release) + uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 + id: release-config + if: ${{ ! inputs.dev || ! inputs.target-branch != 'main' }} + with: + input-format: yaml + filter: >- + {manifests: .} + input: | + - name: ${{ inputs.dockerhub-repo }} + tag: v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy.{arch}.tar + additional-tags: + - v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + - name: ${{ inputs.dockerhub-repo }} + tag: contrib-v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-contrib.{arch}.tar + additional-tags: + - contrib-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + - name: ${{ inputs.dockerhub-repo }} + tag: contrib-debug-v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-contrib-debug.{arch}.tar + additional-tags: + - contrib-debug-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + - name: ${{ inputs.dockerhub-repo }} + tag: debug-v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-debug.{arch}.tar + additional-tags: + - debug-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + - name: ${{ inputs.dockerhub-repo }} + tag: distroless-v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-distroless.{arch}.tar + additional-tags: + - distroless-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + - name: ${{ inputs.dockerhub-repo }} + tag: google-vrp-v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + artifact-pattern: envoy-google-vrp.{arch}.tar + additional-tags: + - google-vrp-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + - name: ${{ inputs.dockerhub-repo }} + tag: tools-v${{ inputs.version-major }}.${{ inputs.version-minor }}.${{ inputs.version-patch }} + registry: docker.io/envoyproxy + architectures: + - amd64 + - arm64 + artifact-pattern: envoy-tools.{arch}.tar + additional-tags: + - tools-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest + + - name: Collect and push OCI artifacts + uses: envoyproxy/toolshed/gh-actions/oci/collector@actions-v0.3.25 + with: + artifacts-pattern: oci.* + manifest-config: ${{ steps.dev-config.outputs.value || steps.release-config.outputs.value }} + dry-run: ${{ ! inputs.trusted || (inputs.target-branch != 'main' && inputs.dev) }} + dockerhub-username: ${{ inputs.trusted && secrets.dockerhub-username || '' }} + dockerhub-password: ${{ inputs.trusted && secrets.dockerhub-password || '' }} diff --git a/.github/workflows/_publish_verify.yml b/.github/workflows/_publish_verify.yml index 381df02220162..1f236fcf8020f 100644 --- a/.github/workflows/_publish_verify.yml +++ b/.github/workflows/_publish_verify.yml @@ -55,7 +55,8 @@ jobs: - name: examples target: verify_examples downloads: | - docker: build_images + oci.arm64: build_images + oci.x64: build_images rbe: false source: | export NO_BUILD_SETUP=1 @@ -74,7 +75,7 @@ jobs: for image in "${IMAGES[@]}"; do src_name="$(echo ${image} | cut -d: -f1)" dest_name="$(echo ${image} | cut -d: -f2)" - src="oci-archive:%{{ runner.temp }}/build_images/${src_name}.tar" + src="oci-archive:%{{ runner.temp }}/build_images/${src_name}.amd64.tar" dest="docker-daemon:envoyproxy/envoy:${dest_name}" echo "Copy image: ${src} ${dest}" skopeo copy -q "${src}" "${dest}" @@ -109,7 +110,7 @@ jobs: - name: distroless target: verify-distroless downloads: | - docker: build_images + oci.x64: build_images rbe: false source: | export NO_BUILD_SETUP=1 @@ -120,7 +121,7 @@ jobs: for image in "${IMAGES[@]}"; do src_name="$(echo ${image} | cut -d: -f1)" dest_name="$(echo ${image} | cut -d: -f2)" - src="oci-archive:%{{ runner.temp }}/build_images/${src_name}.tar" + src="oci-archive:%{{ runner.temp }}/build_images/${src_name}.amd64.tar" dest="docker-daemon:envoyproxy/envoy:${dest_name}" echo "Copy image: ${src} ${dest}" skopeo copy -q "${src}" "${dest}" diff --git a/.github/workflows/_request.yml b/.github/workflows/_request.yml index 3094c75babc5c..7f75f9877ff98 100644 --- a/.github/workflows/_request.yml +++ b/.github/workflows/_request.yml @@ -56,14 +56,14 @@ jobs: caches: ${{ steps.caches.outputs.value }} config: ${{ steps.config.outputs.config }} steps: - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: started name: Create timestamp with: options: -r filter: | now - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout name: Checkout Envoy repository (requested) with: @@ -77,7 +77,7 @@ jobs: # *ALL* variables collected should be treated as untrusted and should be sanitized before # use - name: Generate environment variables from commit - uses: envoyproxy/toolshed/gh-actions/envoy/ci/request@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/envoy/ci/request@actions-v0.3.25 id: env with: branch-name: ${{ steps.checkout.outputs.branch-name }} @@ -88,7 +88,7 @@ jobs: vars: ${{ toJSON(vars) }} working-directory: requested - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout-target name: Checkout Envoy repository (target branch) with: @@ -96,7 +96,7 @@ jobs: config: | fetch-depth: 1 path: target - - uses: envoyproxy/toolshed/gh-actions/hashfiles@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/hashfiles@actions-v0.3.25 id: bazel-cache-hash name: Bazel cache hash with: @@ -105,7 +105,7 @@ jobs: - name: Request summary id: summary - uses: envoyproxy/toolshed/gh-actions/github/env/summary@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/env/summary@actions-v0.3.25 with: actor: ${{ toJSON(fromJSON(steps.env.outputs.data).request.actor) }} base-sha: ${{ fromJSON(steps.env.outputs.data).request.base-sha }} @@ -121,7 +121,7 @@ jobs: target-branch: ${{ fromJSON(steps.env.outputs.data).request.target-branch }} - name: Environment data - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: data with: input: | @@ -151,31 +151,31 @@ jobs: # TODO(phlax): shift this to ci/request action above - name: Check Docker cache (x64) id: cache-exists-docker-x64 - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: lookup-only: true path: /tmp/cache key: ${{ fromJSON(steps.data.outputs.value).request.build-image.default }} - name: Check Docker cache (arm64) id: cache-exists-docker-arm64 - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: lookup-only: true path: /tmp/cache key: ${{ fromJSON(steps.data.outputs.value).request.build-image.default }}-arm64 - - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.25 name: Setup GCP with: key: ${{ secrets.gcs-cache-key }} - - uses: envoyproxy/toolshed/gh-actions/gcs/cache/exists@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcs/cache/exists@actions-v0.3.25 name: Check GCS bucket cache (x64) id: cache-exists-bazel-x64 with: bucket: ${{ inputs.gcs-cache-bucket }} key: ${{ fromJSON(steps.data.outputs.value).config.ci.cache.bazel }}-x64 - - uses: envoyproxy/toolshed/gh-actions/gcs/cache/exists@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcs/cache/exists@actions-v0.3.25 name: Check GCS bucket cache (arm64) id: cache-exists-bazel-arm64 with: @@ -183,7 +183,7 @@ jobs: key: ${{ fromJSON(steps.data.outputs.value).config.ci.cache.bazel }}-arm64 - name: Caches - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: caches with: input-format: yaml diff --git a/.github/workflows/_request_cache_bazel.yml b/.github/workflows/_request_cache_bazel.yml index c69a5549b380c..4ace4bfad264d 100644 --- a/.github/workflows/_request_cache_bazel.yml +++ b/.github/workflows/_request_cache_bazel.yml @@ -51,7 +51,7 @@ jobs: name: "[${{ inputs.arch }}] Prime Bazel cache" if: ${{ ! fromJSON(inputs.caches).bazel[inputs.arch] }} steps: - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout-target name: Checkout Envoy repository (target branch) with: @@ -59,14 +59,14 @@ jobs: config: | fetch-depth: 1 - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 id: appauth name: Appauth (mutex lock) with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.25 name: Setup GCP with: key: ${{ secrets.gcs-cache-key }} @@ -76,7 +76,7 @@ jobs: sudo mkdir /build sudo chown runner:docker /build echo "GITHUB_TOKEN=${{ github.token }}" >> $GITHUB_ENV - - uses: envoyproxy/toolshed/gh-actions/cache/prime@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/cache/prime@actions-v0.3.25 id: bazel-cache name: Prime Bazel cache with: diff --git a/.github/workflows/_request_cache_docker.yml b/.github/workflows/_request_cache_docker.yml index 4cff18840480f..8be701708e46f 100644 --- a/.github/workflows/_request_cache_docker.yml +++ b/.github/workflows/_request_cache_docker.yml @@ -39,7 +39,7 @@ on: # For a job that does, you can restore with something like: # # steps: -# - uses: envoyproxy/toolshed/gh-actions/docker/cache/restore@actions-v0.3.22 +# - uses: envoyproxy/toolshed/gh-actions/docker/cache/restore@actions-v0.3.25 # with: # key: "${{ needs.env.outputs.build-image }}" # @@ -51,13 +51,13 @@ jobs: name: "[${{ inputs.arch }}] Prime Docker cache" if: ${{ ! fromJSON(inputs.caches).docker[inputs.arch] }} steps: - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 id: appauth name: Appauth (mutex lock) with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/gh-actions/docker/cache/prime@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/docker/cache/prime@actions-v0.3.25 id: docker name: Prime Docker cache (${{ inputs.image-tag }}${{ inputs.cache-suffix }}) with: @@ -65,7 +65,7 @@ jobs: key-suffix: ${{ inputs.cache-suffix }} lock-token: ${{ steps.appauth.outputs.token }} lock-repository: ${{ inputs.lock-repository }} - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: data name: Cache data with: @@ -73,7 +73,7 @@ jobs: input: | cached: ${{ steps.docker.outputs.cached }} key: ${{ inputs.image-tag }}${{ inputs.cache-suffix }} - - uses: envoyproxy/toolshed/gh-actions/json/table@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/json/table@actions-v0.3.25 name: Summary with: json: ${{ steps.data.outputs.value }} diff --git a/.github/workflows/_request_checks.yml b/.github/workflows/_request_checks.yml index 3b412dcb4c9fa..91952e9acdb26 100644 --- a/.github/workflows/_request_checks.yml +++ b/.github/workflows/_request_checks.yml @@ -55,7 +55,7 @@ jobs: runs-on: ${{ fromJSON(inputs.env).config.ci.agent-ubuntu }} name: Start checks steps: - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: check-config name: Prepare check data with: @@ -78,13 +78,13 @@ jobs: | .skipped.output.summary = "${{ inputs.skipped-summary }}" | .skipped.output.text = "" - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 name: Appauth id: appauth with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/gh-actions/github/checks@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checks@actions-v0.3.25 name: Start checks id: checks with: @@ -95,7 +95,7 @@ jobs: ${{ fromJSON(inputs.env).summary.summary }} token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/gh-actions/json/table@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/json/table@actions-v0.3.25 name: Summary with: collapse-open: true @@ -119,7 +119,7 @@ jobs: output-path: GITHUB_STEP_SUMMARY title: Checks started/skipped - - uses: envoyproxy/toolshed/gh-actions/github/env/save@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/env/save@actions-v0.3.25 name: Save env id: data with: diff --git a/.github/workflows/_run.yml b/.github/workflows/_run.yml index d0ac0c8fdd4a6..4fa34b68da667 100644 --- a/.github/workflows/_run.yml +++ b/.github/workflows/_run.yml @@ -155,7 +155,7 @@ on: summary-post: type: string default: | - - uses: envoyproxy/toolshed/gh-actions/envoy/run/summary@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/run/summary@actions-v0.3.25 with: context: %{{ inputs.context }} steps-pre: @@ -217,7 +217,7 @@ jobs: name: ${{ inputs.target-suffix && format('[{0}] ', inputs.target-suffix) || '' }}${{ inputs.command }} ${{ inputs.target }} timeout-minutes: ${{ inputs.timeout-minutes }} steps: - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 id: started name: Create timestamp with: @@ -225,7 +225,7 @@ jobs: filter: | now # This controls which input vars are exposed to the run action (and related steps) - - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/jq@actions-v0.3.25 name: Context id: context with: @@ -257,13 +257,13 @@ jobs: if: ${{ inputs.docker-ipv6 }} # Caches - - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.25 name: Setup GCP (cache) if: ${{ inputs.gcs-cache-bucket }} with: key: ${{ secrets.gcs-cache-key }} force-install: ${{ contains(fromJSON('["envoy-arm64-medium", "github-arm64-2c-8gb"]'), inputs.runs-on) }} - - uses: envoyproxy/toolshed/gh-actions/cache/restore@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/cache/restore@actions-v0.3.25 if: ${{ inputs.gcs-cache-bucket }} name: >- Restore Bazel cache @@ -276,19 +276,19 @@ jobs: # HACK/WORKAROUND for cache scope issue (https://github.com/envoyproxy/envoy/issues/37603) - if: ${{ inputs.cache-build-image }} id: cache-lookup - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: lookup-only: true path: /tmp/cache key: ${{ inputs.cache-build-image }}${{ inputs.cache-build-image-key-suffix }} - if: ${{ inputs.cache-build-image && steps.cache-lookup.outputs.cache-hit == 'true' }} name: Restore Docker cache ${{ inputs.cache-build-image && format('({0})', inputs.cache-build-image) || '' }} - uses: envoyproxy/toolshed/gh-actions/docker/cache/restore@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/docker/cache/restore@actions-v0.3.25 with: image-tag: ${{ inputs.cache-build-image }} key-suffix: ${{ inputs.cache-build-image-key-suffix }} - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 id: appauth name: Appauth if: ${{ inputs.trusted }} @@ -299,7 +299,7 @@ jobs: # - the workaround is to allow the token to be passed through. token: ${{ github.token }} token-ok: true - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout name: Checkout Envoy repository with: @@ -316,7 +316,7 @@ jobs: token: ${{ inputs.trusted && steps.appauth.outputs.token || github.token }} # This is currently only use by mobile-docs and can be removed once they are updated to the newer website - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout-extra name: Checkout extra repository (for publishing) if: ${{ inputs.checkout-extra }} @@ -325,7 +325,7 @@ jobs: ssh-key: ${{ inputs.trusted && inputs.ssh-key-extra || '' }} - name: Import GPG key - uses: envoyproxy/toolshed/gh-actions/gpg/import@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/gpg/import@actions-v0.3.25 if: ${{ inputs.import-gpg }} with: key: ${{ secrets.gpg-key }} @@ -338,7 +338,7 @@ jobs: name: Configure PR Bazel settings if: >- ${{ fromJSON(inputs.request).request.pr != '' }} - - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/gcp/setup@actions-v0.3.25 name: Setup GCP (artefacts/rbe) id: gcp with: @@ -356,7 +356,7 @@ jobs: if: ${{ vars.ENVOY_CI_BAZELRC }} name: Configure repo Bazel settings - - uses: envoyproxy/toolshed/gh-actions/github/run@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/run@actions-v0.3.25 name: Run CI ${{ inputs.command }} ${{ inputs.target }} with: args: ${{ inputs.args != '--' && inputs.args || inputs.target }} diff --git a/.github/workflows/codeql-daily.yml b/.github/workflows/codeql-daily.yml index 407484c317679..5bbb179d608cb 100644 --- a/.github/workflows/codeql-daily.yml +++ b/.github/workflows/codeql-daily.yml @@ -30,11 +30,11 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Free disk space - uses: envoyproxy/toolshed/gh-actions/diskspace@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/diskspace@actions-v0.3.25 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # codeql-bundle-v3.29.2 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # codeql-bundle-v3.30.3 # Override language selection by uncommenting this and choosing your languages with: languages: cpp @@ -75,6 +75,6 @@ jobs: git clean -xdf - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # codeql-bundle-v3.29.2 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # codeql-bundle-v3.30.3 with: trap-caching: false diff --git a/.github/workflows/codeql-push.yml b/.github/workflows/codeql-push.yml index 9d59b4594bcbf..41de603b5c608 100644 --- a/.github/workflows/codeql-push.yml +++ b/.github/workflows/codeql-push.yml @@ -63,11 +63,11 @@ jobs: - name: Free disk space if: ${{ env.BUILD_TARGETS != '' }} - uses: envoyproxy/toolshed/gh-actions/diskspace@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/diskspace@actions-v0.3.25 - name: Initialize CodeQL if: ${{ env.BUILD_TARGETS != '' }} - uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # codeql-bundle-v3.29.2 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # codeql-bundle-v3.30.3 with: languages: cpp trap-caching: false @@ -112,6 +112,6 @@ jobs: - name: Perform CodeQL Analysis if: ${{ env.BUILD_TARGETS != '' }} - uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # codeql-bundle-v3.29.2 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # codeql-bundle-v3.30.3 with: trap-caching: false diff --git a/.github/workflows/command.yml b/.github/workflows/command.yml index 9f5262495e396..476063996363e 100644 --- a/.github/workflows/command.yml +++ b/.github/workflows/command.yml @@ -28,7 +28,7 @@ jobs: && github.actor != 'dependabot[bot]' }} steps: - - uses: envoyproxy/toolshed/gh-actions/github/command@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/command@actions-v0.3.25 name: Parse command from comment id: command with: @@ -37,14 +37,14 @@ jobs: ^/(retest) # /retest - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 if: ${{ steps.command.outputs.command == 'retest' }} id: appauth-retest name: Appauth (retest) with: key: ${{ secrets.ENVOY_CI_APP_KEY }} app_id: ${{ secrets.ENVOY_CI_APP_ID }} - - uses: envoyproxy/toolshed/gh-actions/retest@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/retest@actions-v0.3.25 if: ${{ steps.command.outputs.command == 'retest' }} name: Retest with: diff --git a/.github/workflows/envoy-dependency.yml b/.github/workflows/envoy-dependency.yml index 30e9d21982048..cfdfe729416a5 100644 --- a/.github/workflows/envoy-dependency.yml +++ b/.github/workflows/envoy-dependency.yml @@ -53,16 +53,16 @@ jobs: steps: - id: appauth name: Appauth - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 with: app_id: ${{ secrets.ENVOY_CI_DEP_APP_ID }} key: ${{ secrets.ENVOY_CI_DEP_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 with: token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/gh-actions/bson@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/bson@actions-v0.3.25 id: update name: Update dependency (${{ inputs.dependency }}) with: @@ -97,13 +97,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: envoyproxy/toolshed/gh-actions/upload/diff@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/upload/diff@actions-v0.3.25 name: Upload diff with: name: ${{ inputs.dependency }}-${{ steps.update.outputs.output }} - name: Create a PR if: ${{ inputs.pr }} - uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.25 with: base: main body: | @@ -134,11 +134,11 @@ jobs: steps: - id: appauth name: Appauth - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 with: app_id: ${{ secrets.ENVOY_CI_DEP_APP_ID }} key: ${{ secrets.ENVOY_CI_DEP_APP_KEY }} - - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 id: checkout name: Checkout Envoy repository with: @@ -180,7 +180,7 @@ jobs: - name: Check Docker SHAs id: build-images - uses: envoyproxy/toolshed/gh-actions/docker/shas@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/docker/shas@actions-v0.3.25 with: images: | sha: envoyproxy/envoy-build-ubuntu:${{ steps.build-tools.outputs.tag }} @@ -209,7 +209,7 @@ jobs: name: Update SHAs working-directory: envoy - name: Create a PR - uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.25 with: base: main body: Created by Envoy dependency bot diff --git a/.github/workflows/envoy-macos.yml b/.github/workflows/envoy-macos.yml index c09561e171778..e8b002b2c7e9f 100644 --- a/.github/workflows/envoy-macos.yml +++ b/.github/workflows/envoy-macos.yml @@ -54,10 +54,10 @@ jobs: request: ${{ needs.load.outputs.request }} # TODO: Remove these hardcoded branches when no longer supported runs-on: >- - ${{ (contains(fromJSON(needs.load.outputs.request).request.target-branch, 'v1.31') - || contains(fromJSON(needs.load.outputs.request).request.target-branch, 'v1.32') - || contains(fromJSON(needs.load.outputs.request).request.target-branch, 'v1.33') - || contains(fromJSON(needs.load.outputs.request).request.target-branch, 'v1.34')) + ${{ (contains(fromJSON(needs.load.outputs.request).request.target-branch, '1.31') + || contains(fromJSON(needs.load.outputs.request).request.target-branch, '1.32') + || contains(fromJSON(needs.load.outputs.request).request.target-branch, '1.33') + || contains(fromJSON(needs.load.outputs.request).request.target-branch, '1.34')) && 'macos-14-xlarge' || 'macos-15-xlarge' }} source: ${{ matrix.source }} diff --git a/.github/workflows/envoy-publish.yml b/.github/workflows/envoy-publish.yml index d76dc7e1d95c8..780b1bb5512c2 100644 --- a/.github/workflows/envoy-publish.yml +++ b/.github/workflows/envoy-publish.yml @@ -53,11 +53,6 @@ jobs: contents: read packages: read secrets: - dockerhub-password: >- - ${{ needs.load.outputs.trusted - && fromJSON(needs.load.outputs.trusted) - && secrets.DOCKERHUB_PASSWORD - || '' }} gcs-cache-key: ${{ secrets.GCS_CACHE_KEY }} gpg-key: >- ${{ needs.load.outputs.trusted @@ -69,18 +64,27 @@ jobs: && fromJSON(needs.load.outputs.trusted) && secrets.ENVOY_GPG_MAINTAINER_KEY_PASSWORD || secrets.ENVOY_GPG_SNAKEOIL_KEY_PASSWORD }} - if: ${{ fromJSON(needs.load.outputs.request).run.publish || fromJSON(needs.load.outputs.request).run.verify }} + if: ${{ fromJSON(needs.load.outputs.request).run.release || fromJSON(needs.load.outputs.request).run.verify }} needs: - load uses: ./.github/workflows/_publish_build.yml name: Build + strategy: + fail-fast: false + matrix: + arch: + - x64 + - arm64 with: + arch: ${{ matrix.arch }} gcs-cache-bucket: ${{ vars.ENVOY_CACHE_BUCKET }} request: ${{ needs.load.outputs.request }} trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} - publish: + release: secrets: + dockerhub-password: ${{ secrets.DOCKERHUB_PASSWORD }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} ENVOY_CI_SYNC_APP_ID: >- ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) @@ -102,15 +106,25 @@ jobs: && secrets.ENVOY_CI_PUBLISH_APP_KEY || '' }} gcs-cache-key: ${{ secrets.GCS_CACHE_KEY }} + gpg-key: >- + ${{ needs.load.outputs.trusted + && fromJSON(needs.load.outputs.trusted) + && secrets.ENVOY_GPG_MAINTAINER_KEY + || secrets.ENVOY_GPG_SNAKEOIL_KEY }} + gpg-key-password: >- + ${{ needs.load.outputs.trusted + && fromJSON(needs.load.outputs.trusted) + && secrets.ENVOY_GPG_MAINTAINER_KEY_PASSWORD + || secrets.ENVOY_GPG_SNAKEOIL_KEY_PASSWORD }} permissions: contents: read packages: read - if: ${{ fromJSON(needs.load.outputs.request).run.publish }} + if: ${{ fromJSON(needs.load.outputs.request).run.release }} needs: - load - build - uses: ./.github/workflows/_publish_publish.yml - name: Publish + uses: ./.github/workflows/_publish_release.yml + name: Release with: gcs-cache-bucket: ${{ vars.ENVOY_CACHE_BUCKET }} request: ${{ needs.load.outputs.request }} @@ -126,6 +140,7 @@ jobs: needs: - load - build + - release uses: ./.github/workflows/_publish_verify.yml name: Verify with: @@ -146,12 +161,12 @@ jobs: && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.repository.full_name == github.repository && contains(fromJSON('["pull_request_target", "push", "schedule"]'), github.event.workflow_run.event) - && (fromJSON(needs.load.outputs.request).run.publish + && (fromJSON(needs.load.outputs.request).run.release || fromJSON(needs.load.outputs.request).run.verify) needs: - load - build - - publish + - release - verify uses: ./.github/workflows/_finish.yml with: diff --git a/.github/workflows/envoy-release.yml b/.github/workflows/envoy-release.yml index 42aac79e066f4..57626420732bc 100644 --- a/.github/workflows/envoy-release.yml +++ b/.github/workflows/envoy-release.yml @@ -59,14 +59,14 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} @@ -87,10 +87,10 @@ jobs: name: Check changelog summary - if: ${{ inputs.author }} name: Validate signoff email - uses: envoyproxy/toolshed/gh-actions/email/validate@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/email/validate@actions-v0.3.25 with: email: ${{ inputs.author }} - - uses: envoyproxy/toolshed/gh-actions/github/run@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/run@actions-v0.3.25 name: Create release with: source: | @@ -115,7 +115,7 @@ jobs: name: Release version id: release - name: Create a PR - uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.25 with: base: ${{ github.ref_name }} commit: false @@ -140,20 +140,20 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} strip-prefix: release/ token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/gh-actions/github/run@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/github/run@actions-v0.3.25 name: Sync version histories with: command: >- @@ -163,7 +163,7 @@ jobs: -- --signoff="${{ env.COMMITTER_NAME }} <${{ env.COMMITTER_EMAIL }}>" - name: Create a PR - uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/pr@actions-v0.3.25 with: append-commit-message: true base: ${{ github.ref_name }} @@ -190,13 +190,13 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 with: config: | fetch-depth: 0 @@ -226,13 +226,13 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - name: Checkout repository - uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.22 + uses: envoyproxy/toolshed/gh-actions/github/checkout@actions-v0.3.25 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} diff --git a/.github/workflows/envoy-security-check.yml b/.github/workflows/envoy-security-check.yml index 459c23d3bc15b..b4e364c0f264b 100644 --- a/.github/workflows/envoy-security-check.yml +++ b/.github/workflows/envoy-security-check.yml @@ -68,7 +68,7 @@ jobs: # PR - name: Comment on PR if: matrix.action == 'comment' && github.event.workflow_run.pull_requests[0] - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | try { diff --git a/.github/workflows/envoy-sync.yml b/.github/workflows/envoy-sync.yml index 3d25bd2cbac28..4e825d8985aa1 100644 --- a/.github/workflows/envoy-sync.yml +++ b/.github/workflows/envoy-sync.yml @@ -34,12 +34,12 @@ jobs: - data-plane-api - mobile-website steps: - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 id: appauth with: app_id: ${{ secrets.ENVOY_CI_SYNC_APP_ID }} key: ${{ secrets.ENVOY_CI_SYNC_APP_KEY }} - - uses: envoyproxy/toolshed/gh-actions/dispatch@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/dispatch@actions-v0.3.25 with: repository: "envoyproxy/${{ matrix.downstream }}" ref: main @@ -61,12 +61,12 @@ jobs: downstream: - envoy-openssl steps: - - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/appauth@actions-v0.3.25 id: appauth with: app_id: ${{ secrets.ENVOY_CI_SYNC_APP_ID }} key: ${{ secrets.ENVOY_CI_SYNC_APP_KEY }} - - uses: envoyproxy/toolshed/gh-actions/dispatch@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/dispatch@actions-v0.3.25 with: repository: "envoyproxy/${{ matrix.downstream }}" ref: release/v1.28 diff --git a/.github/workflows/mobile-android_build.yml b/.github/workflows/mobile-android_build.yml index 23c3065e380d3..58dbf8f6803c7 100644 --- a/.github/workflows/mobile-android_build.yml +++ b/.github/workflows/mobile-android_build.yml @@ -82,9 +82,9 @@ jobs: target: kotlin-hello-world runs-on: ubuntu-22.04 steps-pre: | - - uses: envoyproxy/toolshed/gh-actions/envoy/android/pre@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/android/pre@actions-v0.3.25 steps-post: | - - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.25 with: apk: bazel-bin/examples/kotlin/hello_world/hello_envoy_kt.apk app: io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity @@ -111,7 +111,7 @@ jobs: target: ${{ matrix.target }} runs-on: ubuntu-22.04 steps-pre: | - - uses: envoyproxy/toolshed/gh-actions/envoy/android/pre@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/android/pre@actions-v0.3.25 steps-post: ${{ matrix.steps-post }} timeout-minutes: 50 trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} @@ -122,7 +122,7 @@ jobs: include: - name: java-hello-world steps-post: | - - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.25 with: apk: bazel-bin/examples/java/hello_world/hello_envoy.apk app: io.envoyproxy.envoymobile.helloenvoy/.MainActivity @@ -141,7 +141,7 @@ jobs: --config=mobile-remote-release-clang-android //test/kotlin/apps/baseline:hello_envoy_kt steps-post: | - - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.25 with: apk: bazel-bin/test/kotlin/apps/baseline/hello_envoy_kt.apk app: io.envoyproxy.envoymobile.helloenvoybaselinetest/.MainActivity @@ -156,7 +156,7 @@ jobs: --config=mobile-remote-release-clang-android //test/kotlin/apps/experimental:hello_envoy_kt steps-post: | - - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.22 + - uses: envoyproxy/toolshed/gh-actions/envoy/android/post@actions-v0.3.25 with: apk: bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk app: io.envoyproxy.envoymobile.helloenvoyexperimentaltest/.MainActivity diff --git a/.github/workflows/mobile-coverage.yml b/.github/workflows/mobile-coverage.yml index b174a31073685..4c27a3b3e5ad6 100644 --- a/.github/workflows/mobile-coverage.yml +++ b/.github/workflows/mobile-coverage.yml @@ -60,13 +60,6 @@ jobs: run: | cd mobile tar -czf coverage.tar.gz generated/coverage - # TODO(phlax): This is a highly undesirable workaround - remove once - # https://github.com/bazelbuild/bazel/issues/23247 is resolved/available - steps-pre: | - - name: Inject bazel version - shell: bash - run: | - echo "7.1.2" > .bazelversion target: mobile-coverage timeout-minutes: 120 upload-name: coverage.tar.gz diff --git a/.github/workflows/mobile-ios_build.yml b/.github/workflows/mobile-ios_build.yml index 57ca2fa629e27..4da2aabac7b62 100644 --- a/.github/workflows/mobile-ios_build.yml +++ b/.github/workflows/mobile-ios_build.yml @@ -73,100 +73,100 @@ jobs: target: ios timeout-minutes: 120 - hello-world: - permissions: - contents: read - packages: read - uses: ./.github/workflows/_run.yml - if: ${{ needs.load.outputs.request && fromJSON(needs.load.outputs.request).run.mobile-ios }} - needs: - - load - - build - name: ios-hello-world - with: - args: >- - build - ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} - ${{ matrix.app }} - command: ./bazelw - container-command: - docker-ipv6: false - request: ${{ needs.load.outputs.request }} - runs-on: macos-15 - source: | - source ./ci/mac_ci_setup.sh - ./bazelw shutdown - steps-post: | - - uses: envoyproxy/toolshed/gh-actions/envoy/ios/post@actions-v0.3.22 - with: - app: ${{ matrix.app }} - args: ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} - expected: received headers with status ${{ matrix.expected-status }} - env: - ANDROID_NDK_HOME: - ANDROID_HOME: - target: ${{ matrix.target }} - timeout-minutes: ${{ matrix.timeout-minutes }} - trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} - working-directory: mobile - strategy: - fail-fast: false - matrix: - include: - - name: Build swift hello world - app: //examples/swift/hello_world:app - expected-status: 200 - target: swift-hello-world - timeout-minutes: 90 + # hello-world: + # permissions: + # contents: read + # packages: read + # uses: ./.github/workflows/_run.yml + # if: ${{ needs.load.outputs.request && fromJSON(needs.load.outputs.request).run.mobile-ios }} + # needs: + # - load + # - build + # name: ios-hello-world + # with: + # args: >- + # build + # ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} + # ${{ matrix.app }} + # command: ./bazelw + # container-command: + # docker-ipv6: false + # request: ${{ needs.load.outputs.request }} + # runs-on: macos-15 + # source: | + # source ./ci/mac_ci_setup.sh + # ./bazelw shutdown + # steps-post: | + # - uses: envoyproxy/toolshed/gh-actions/envoy/ios/post@actions-v0.3.25 + # with: + # app: ${{ matrix.app }} + # args: ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} + # expected: received headers with status ${{ matrix.expected-status }} + # env: + # ANDROID_NDK_HOME: + # ANDROID_HOME: + # target: ${{ matrix.target }} + # timeout-minutes: ${{ matrix.timeout-minutes }} + # trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} + # working-directory: mobile + # strategy: + # fail-fast: false + # matrix: + # include: + # - name: Build swift hello world + # app: //examples/swift/hello_world:app + # expected-status: 200 + # target: swift-hello-world + # timeout-minutes: 90 - apps: - permissions: - contents: read - packages: read - uses: ./.github/workflows/_run.yml - if: ${{ needs.load.outputs.request && fromJSON(needs.load.outputs.request).run.mobile-ios-all }} - needs: - - load - - build - name: ios-apps - with: - args: >- - build - ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} - ${{ matrix.app }} - command: ./bazelw - container-command: - docker-ipv6: false - request: ${{ needs.load.outputs.request }} - runs-on: macos-15 - source: | - source ./ci/mac_ci_setup.sh - steps-post: | - - uses: envoyproxy/toolshed/gh-actions/envoy/ios/post@actions-v0.3.22 - with: - app: ${{ matrix.app }} - args: ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} - expected: >- - ${{ matrix.expected - || format('received headers with status {0}', matrix.expected-status) }} - timeout: ${{ matrix.timeout || '5m' }} - env: - ANDROID_NDK_HOME: - ANDROID_HOME: - target: ${{ matrix.target }} - timeout-minutes: 90 - trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} - working-directory: mobile - strategy: - fail-fast: false - matrix: - include: - - name: Build swift experimental app - args: >- - --config=mobile-remote-ci-macos-ios - app: //test/swift/apps/experimental:app - expected-status: 200 - target: swift-experimental-app + # apps: + # permissions: + # contents: read + # packages: read + # uses: ./.github/workflows/_run.yml + # if: ${{ needs.load.outputs.request && fromJSON(needs.load.outputs.request).run.mobile-ios-all }} + # needs: + # - load + # - build + # name: ios-apps + # with: + # args: >- + # build + # ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} + # ${{ matrix.app }} + # command: ./bazelw + # container-command: + # docker-ipv6: false + # request: ${{ needs.load.outputs.request }} + # runs-on: macos-15 + # source: | + # source ./ci/mac_ci_setup.sh + # steps-post: | + # - uses: envoyproxy/toolshed/gh-actions/envoy/ios/post@actions-v0.3.25 + # with: + # app: ${{ matrix.app }} + # args: ${{ matrix.args || '--config=mobile-remote-ci-macos-ios' }} + # expected: >- + # ${{ matrix.expected + # || format('received headers with status {0}', matrix.expected-status) }} + # timeout: ${{ matrix.timeout || '5m' }} + # env: + # ANDROID_NDK_HOME: + # ANDROID_HOME: + # target: ${{ matrix.target }} + # timeout-minutes: 90 + # trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} + # working-directory: mobile + # strategy: + # fail-fast: false + # matrix: + # include: + # - name: Build swift experimental app + # args: >- + # --config=mobile-remote-ci-macos-ios + # app: //test/swift/apps/experimental:app + # expected-status: 200 + # target: swift-experimental-app request: secrets: @@ -186,8 +186,8 @@ jobs: needs: - load - build - - hello-world - - apps + # - hello-world + # - apps uses: ./.github/workflows/_finish.yml with: needs: ${{ toJSON(needs) }} diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index 7cf7ffa50dd36..6e110df3ea544 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -91,7 +91,7 @@ jobs: fetch-depth: 0 - name: Add safe directory run: git config --global --add safe.directory /__w/envoy/envoy - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: ${{ matrix.output }}_android_aar_sources path: . diff --git a/.github/workflows/mobile-release_validation.yml b/.github/workflows/mobile-release_validation.yml index 5a440a900178b..0406aa2a948c6 100644 --- a/.github/workflows/mobile-release_validation.yml +++ b/.github/workflows/mobile-release_validation.yml @@ -38,47 +38,47 @@ jobs: with: check-name: mobile-release-validation - validate-swiftpm-example: - permissions: - contents: read - packages: read - if: ${{ needs.load.outputs.request && fromJSON(needs.load.outputs.request).run.mobile-release-validation }} - needs: load - uses: ./.github/workflows/_run.yml - name: Build xframework - with: - args: >- - build - --config=mobile-remote-ci-macos-ios - //:ios_xcframework - command: ./bazelw - container-command: - docker-ipv6: false - request: ${{ needs.load.outputs.request }} - # revert this to non-large once updated - runs-on: macos-15 - source: | - source ./ci/mac_ci_setup.sh - # Ignore errors: Bad CRC when unzipping large files: https://bbs.archlinux.org/viewtopic.php?id=153011 - steps-post: | - - run: | - unzip mobile/bazel-bin/library/swift/Envoy.xcframework.zip \ - -d mobile/examples/swift/swiftpm/Packages \ - || : - shell: bash - name: Unzip xcframework - - run: | - xcodebuild -project mobile/examples/swift/swiftpm/EnvoySwiftPMExample.xcodeproj \ - -scheme EnvoySwiftPMExample \ - -destination platform="iOS Simulator,name=iPhone 16 Pro Max,OS=18.1" \ - -allowProvisioningUpdates - shell: bash - name: Build app - # TODO(jpsim): Run app and inspect logs to validate - target: validate-swiftpm-example - timeout-minutes: 120 - trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} - working-directory: mobile + # validate-swiftpm-example: + # permissions: + # contents: read + # packages: read + # if: ${{ needs.load.outputs.request && fromJSON(needs.load.outputs.request).run.mobile-release-validation }} + # needs: load + # uses: ./.github/workflows/_run.yml + # name: Build xframework + # with: + # args: >- + # build + # --config=mobile-remote-ci-macos-ios + # //:ios_xcframework + # command: ./bazelw + # container-command: + # docker-ipv6: false + # request: ${{ needs.load.outputs.request }} + # # revert this to non-large once updated + # runs-on: macos-15 + # source: | + # source ./ci/mac_ci_setup.sh + # # Ignore errors: Bad CRC when unzipping large files: https://bbs.archlinux.org/viewtopic.php?id=153011 + # steps-post: | + # - run: | + # unzip mobile/bazel-bin/library/swift/Envoy.xcframework.zip \ + # -d mobile/examples/swift/swiftpm/Packages \ + # || : + # shell: bash + # name: Unzip xcframework + # - run: | + # xcodebuild -project mobile/examples/swift/swiftpm/EnvoySwiftPMExample.xcodeproj \ + # -scheme EnvoySwiftPMExample \ + # -destination platform="iOS Simulator,name=iPhone 16 Pro Max,OS=18.1" \ + # -allowProvisioningUpdates + # shell: bash + # name: Build app + # # TODO(jpsim): Run app and inspect logs to validate + # target: validate-swiftpm-example + # timeout-minutes: 120 + # trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} + # working-directory: mobile request: secrets: @@ -97,7 +97,7 @@ jobs: && fromJSON(needs.load.outputs.request).run.mobile-release-validation needs: - load - - validate-swiftpm-example + # - validate-swiftpm-example uses: ./.github/workflows/_finish.yml with: needs: ${{ toJSON(needs) }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 40676cd78d81a..feccc8750d4c0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -40,6 +40,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 28cd64da6269f..58c7d896d374a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Prune Stale - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Different amounts of days for issues/PRs are not currently supported but there is a PR diff --git a/.github/workflows/toolchain-test.yml b/.github/workflows/toolchain-test.yml index fe85d7edc1e56..04c2c9ae3a64a 100644 --- a/.github/workflows/toolchain-test.yml +++ b/.github/workflows/toolchain-test.yml @@ -16,6 +16,7 @@ concurrency: jobs: toolchain-test: runs-on: ubuntu-22.04 + if: github.repository == 'envoyproxy/envoy' strategy: fail-fast: false matrix: diff --git a/.vscode/tasks.json b/.vscode/tasks_example.json similarity index 100% rename from .vscode/tasks.json rename to .vscode/tasks_example.json diff --git a/CODEOWNERS b/CODEOWNERS index 0fba4b66f8cfb..0dcabd8184b97 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ extensions/filters/common/original_src @klarose @mattklein123 # support for on-demand VHDS requests /*/extensions/filters/http/on_demand @dmitri-d @yanavlasov @kyessenov /*/extensions/filters/network/connection_limit @mattklein123 @delong-coder +/*/extensions/filters/network/reverse_tunnel @agrawroh @basundhara-c @botengyao @yanavlasov /*/extensions/filters/http/aws_request_signing @mattklein123 @marcomagdy @nbaws @niax /*/extensions/filters/http/aws_lambda @mattklein123 @marcomagdy @nbaws @niax /*/extensions/filters/http/buffer @adisuissa @mattklein123 @@ -197,6 +198,8 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/matching/input_matchers/metadata @vikaschoudhary16 @kyessenov # environment generic input /*/extensions/matching/common_inputs/environment @donyu @mattklein123 +# network common inputs +/*/extensions/matching/common_inputs/network @agrawroh @kyessenov @wbpcode # format string matching /*/extensions/matching/actions/format_string @kyessenov @cpakulski # CEL data input @@ -206,6 +209,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 # user space socket pair, event, connection and listener /*/extensions/io_socket/user_space @kyessenov @lambdai /*/extensions/bootstrap/internal_listener @kyessenov @adisuissa +/*/extensions/bootstrap/reverse_tunnel/ @agrawroh @basundhara-c @botengyao @yanavlasov # Default UUID4 request ID extension /*/extensions/request_id/uuid @mattklein123 @botengyao # HTTP header formatters @@ -234,6 +238,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 # Formatters /*/extensions/formatter/metadata @cpakulski @ravenblackx @nezdolik /*/extensions/formatter/cel @kyessenov @zirain +/*/extensions/formatter/xfcc_value @wbpcode @vikaschoudhary16 # IP address input matcher /*/extensions/matching/input_matchers/ip @aguinet @mattklein123 # Key Value store @@ -295,9 +300,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/filters/http/stateful_session @wbpcode @cpakulski @adisuissa # tracers /*/extensions/tracers/zipkin @wbpcode @Shikugawa @basvanbeek -/*/extensions/tracers/dynamic_ot @wbpcode @Shikugawa @basvanbeek /*/extensions/tracers/common @wbpcode @Shikugawa @basvanbeek -/*/extensions/tracers/common/ot @wbpcode @Shikugawa @basvanbeek # ext_authz /*/extensions/filters/common/ext_authz @esmet @tyxia @ggreenway /*/extensions/filters/http/ext_authz @esmet @tyxia @ggreenway @@ -430,3 +433,4 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /contrib/qat/ @giantcroc @soulxu /contrib/generic_proxy/ @wbpcode @UNOWNED /contrib/tap_sinks/ @coolg92003 @yiyibaoguo +/contrib/reverse_connection/ @arun-vasudev @basundhara-c @agrawoh \ No newline at end of file diff --git a/OWNERS.md b/OWNERS.md index 046cd810f1c0b..3eb8925a4df93 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -63,6 +63,8 @@ The following Envoy maintainers have final say over any changes only affecting / * xDS, C++ integration tests. * Fredy Wijaya ([fredyw](https://github.com/fredyw)) (fredyw@google.com) * Android, Java, Kotlin, JNI. +* Dan Zhang ([danzh2010](https://github.com/danzh2010)) (danzh@google.com) + * Envoy Mobile, QUIC, HTTP/3. # Senior extension maintainers @@ -87,7 +89,7 @@ without further review. * Boteng Yao ([botengyao](https://github.com/botengyao)) (boteng@google.com) * Kevin Baichoo ([KBaichoo](https://github.com/KBaichoo)) (envoy@kevinbaichoo.com) * Tianyu Xia ([tyxia](https://github.com/tyxia)) (tyxia@google.com) -* Kirtimaan Rajshiva ([krajshiva](https://github.com/krajshiva)) +* Thomas Gschwendtner ([tgschwen](https://github.com/tgschwen)) (tgschwen@google.com) * Yanjun Xiang ([yanjunxiang-google](https://github.com/yanjunxiang-google)) (yanjunxiang@google.com) * Raven Black ([ravenblackx](https://github.com/ravenblackx)) (ravenblack@dropbox.com) diff --git a/RELEASES.md b/RELEASES.md index f5e1b90fe0bce..22643b5060138 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -100,7 +100,8 @@ deadline of 3 weeks. | 1.32.0 | 2024/10/15 | 2024/10/15 | 0 days | 2025/10/15 | | 1.33.0 | 2025/01/14 | 2025/01/14 | 0 days | 2026/01/14 | | 1.34.0 | 2025/04/15 | 2025/04/15 | 0 days | 2026/04/15 | -| 1.35.0 | 2025/07/15 | | | | +| 1.35.0 | 2025/07/15 | 2025/07/23 | 8 days | 2026/07/23 | +| 1.36.0 | 2025/10/14 | | | | ### Cutting a major release diff --git a/STYLE.md b/STYLE.md index b508ebff378a1..7eef23b3ca57c 100644 --- a/STYLE.md +++ b/STYLE.md @@ -160,6 +160,9 @@ A few general notes on our error handling philosophy: should be used where it may be useful to detect if an efficient condition is violated in production (and fatal check in debug-only builds). This will also log a stack trace of the previous calls leading up to `ENVOY_BUG`. + - `ENVOY_NOTIFICATION`: logs and increments the ``envoy_notifications`` counter. These should be + used where it may be useful to detect if an efficient condition is met in production, for + example before rolling out a potentially disruptive configuration change. * Sub-macros alias the macros above and can be used to annotate specific situations: - `ENVOY_BUG_ALPHA` (alias `ENVOY_BUG`): Used for alpha or rapidly changing protocols that need diff --git a/VERSION.txt b/VERSION.txt index 0ce1104204d7a..feba387a7d6e9 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.35.0-dev +1.36.0-dev diff --git a/api/BUILD b/api/BUILD index 47dfa333a2ec2..fb85035416075 100644 --- a/api/BUILD +++ b/api/BUILD @@ -1,6 +1,7 @@ # DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. -load("@rules_proto//proto:defs.bzl", "proto_descriptor_set", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") licenses(["notice"]) # Apache 2 @@ -137,11 +138,14 @@ proto_library( "//envoy/extensions/access_loggers/stream/v3:pkg", "//envoy/extensions/access_loggers/wasm/v3:pkg", "//envoy/extensions/bootstrap/internal_listener/v3:pkg", + "//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg", + "//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg", "//envoy/extensions/clusters/aggregate/v3:pkg", "//envoy/extensions/clusters/common/dns/v3:pkg", "//envoy/extensions/clusters/dns/v3:pkg", "//envoy/extensions/clusters/dynamic_forward_proxy/v3:pkg", "//envoy/extensions/clusters/redis/v3:pkg", + "//envoy/extensions/clusters/reverse_connection/v3:pkg", "//envoy/extensions/common/async_files/v3:pkg", "//envoy/extensions/common/aws/v3:pkg", "//envoy/extensions/common/dynamic_forward_proxy/v3:pkg", @@ -213,6 +217,7 @@ proto_library( "//envoy/extensions/filters/http/rate_limit_quota/v3:pkg", "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", + "//envoy/extensions/filters/http/reverse_conn/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", "//envoy/extensions/filters/http/set_filter_state/v3:pkg", "//envoy/extensions/filters/http/set_metadata/v3:pkg", @@ -226,6 +231,7 @@ proto_library( "//envoy/extensions/filters/listener/original_dst/v3:pkg", "//envoy/extensions/filters/listener/original_src/v3:pkg", "//envoy/extensions/filters/listener/proxy_protocol/v3:pkg", + "//envoy/extensions/filters/listener/reverse_connection/v3:pkg", "//envoy/extensions/filters/listener/tls_inspector/v3:pkg", "//envoy/extensions/filters/network/connection_limit/v3:pkg", "//envoy/extensions/filters/network/direct_response/v3:pkg", @@ -246,6 +252,7 @@ proto_library( "//envoy/extensions/filters/network/ratelimit/v3:pkg", "//envoy/extensions/filters/network/rbac/v3:pkg", "//envoy/extensions/filters/network/redis_proxy/v3:pkg", + "//envoy/extensions/filters/network/reverse_tunnel/v3:pkg", "//envoy/extensions/filters/network/set_filter_state/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3:pkg", diff --git a/api/bazel/api_build_system.bzl b/api/bazel/api_build_system.bzl index 729e771628db1..04d00e6f3a1fe 100644 --- a/api/bazel/api_build_system.bzl +++ b/api/bazel/api_build_system.bzl @@ -1,9 +1,9 @@ load("@com_envoyproxy_protoc_gen_validate//bazel:pgv_proto_library.bzl", "pgv_cc_proto_library") load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") load("@com_github_grpc_grpc//bazel:python_rules.bzl", _py_proto_library = "py_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_bazel_rules_go//go:def.bzl", "go_test") load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") load( "//bazel:external_proto_deps.bzl", "EXTERNAL_PROTO_CC_BAZEL_DEP_MAP", diff --git a/api/bazel/repositories.bzl b/api/bazel/repositories.bzl index 9a8525fb94157..217614f5e5639 100644 --- a/api/bazel/repositories.bzl +++ b/api/bazel/repositories.bzl @@ -101,8 +101,8 @@ OPENTELEMETRY_BUILD_CONTENT = """ load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") load("@com_github_grpc_grpc//bazel:python_rules.bzl", "py_proto_library", "py_grpc_library") load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library", "go_grpc_library") -load("@rules_proto//proto:defs.bzl", "proto_library") package(default_visibility = ["//visibility:public"]) diff --git a/api/bazel/repository_locations.bzl b/api/bazel/repository_locations.bzl index 7268aa608cd1b..33863e8ad5d27 100644 --- a/api/bazel/repository_locations.bzl +++ b/api/bazel/repository_locations.bzl @@ -4,11 +4,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "bazel-skylib", project_desc = "Common useful functions and rules for Bazel", project_url = "https://github.com/bazelbuild/bazel-skylib", - version = "1.7.1", - sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f", - release_date = "2024-06-03", + version = "1.8.1", + sha256 = "51b5105a760b353773f904d2bbc5e664d0987fbaf22265164de65d43e910d8ac", + release_date = "2025-07-07", urls = ["https://github.com/bazelbuild/bazel-skylib/releases/download/{version}/bazel-skylib-{version}.tar.gz"], - use_category = ["api"], + use_category = ["api", "test_only"], license = "Apache-2.0", license_url = "https://github.com/bazelbuild/bazel-skylib/blob/{version}/LICENSE", ), @@ -38,11 +38,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Java Rules for Bazel", project_desc = "Bazel rules for Java", project_url = "https://github.com/bazelbuild/rules_jvm_external", - version = "6.7", + version = "6.8", strip_prefix = "rules_jvm_external-{version}", - sha256 = "a1e351607f04fed296ba33c4977d3fe2a615ed50df7896676b67aac993c53c18", + sha256 = "704a0197e4e966f96993260418f2542568198490456c21814f647ae7091f56f2", urls = ["https://github.com/bazelbuild/rules_jvm_external/releases/download/{version}/rules_jvm_external-{version}.tar.gz"], - release_date = "2025-02-11", + release_date = "2025-07-07", use_category = ["build"], license = "Apache-2.0", license_url = "https://github.com/bazelbuild/rules_jvm_external/blob/{version}/LICENSE", @@ -105,9 +105,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Bazel rules for Buf", project_desc = "Bazel rules for Buf", project_url = "https://github.com/bufbuild/rules_buf", - version = "0.4.0", - sha256 = "bb5735f5b329d6434771879b631da184a58529a6fdbe7641f6ecfb5d4a0c506f", - release_date = "2025-05-07", + version = "0.5.2", + sha256 = "19d845cedf32c0e74a01af8d0bd904872bddc7905f087318d00b332aa36d3929", + release_date = "2025-08-22", strip_prefix = "rules_buf-{version}", urls = ["https://github.com/bufbuild/rules_buf/archive/refs/tags/v{version}.tar.gz"], use_category = ["api"], @@ -131,9 +131,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "OpenTelemetry Proto", project_desc = "Language Independent Interface Types For OpenTelemetry", project_url = "https://github.com/open-telemetry/opentelemetry-proto", - version = "1.7.0", - sha256 = "11330d850f5e24d34c4246bc8cb21fcd311e7565d219195713455a576bb11bed", - release_date = "2025-05-21", + version = "1.8.0", + sha256 = "057812cab50122c0fd504aae57b0b58424a5ec05d1b07889814bdfc7699abbe7", + release_date = "2025-09-02", strip_prefix = "opentelemetry-proto-{version}", urls = ["https://github.com/open-telemetry/opentelemetry-proto/archive/v{version}.tar.gz"], use_category = ["api"], diff --git a/api/envoy/config/bootstrap/v3/bootstrap.proto b/api/envoy/config/bootstrap/v3/bootstrap.proto index bf65f3df45c47..28b1eba6680ef 100644 --- a/api/envoy/config/bootstrap/v3/bootstrap.proto +++ b/api/envoy/config/bootstrap/v3/bootstrap.proto @@ -41,7 +41,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 42] +// [#next-free-field: 43] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v2.Bootstrap"; @@ -230,6 +230,14 @@ message Bootstrap { bool stats_flush_on_admin = 29 [(validate.rules).bool = {const: true}]; } + oneof stats_eviction { + // Optional duration to perform metric eviction. At every interval, during the stats flush + // the unused metrics are removed from the worker caches and the used metrics + // are marked as unused. Must be a multiple of the ``stats_flush_interval``. + google.protobuf.Duration stats_eviction_interval = 42 + [(validate.rules).duration = {gte {nanos: 1000000}}]; + } + // Optional watchdog configuration. // This is for a single watchdog configuration for the entire system. // Deprecated in favor of ``watchdogs`` which has finer granularity. diff --git a/api/envoy/config/common/mutation_rules/v3/mutation_rules.proto b/api/envoy/config/common/mutation_rules/v3/mutation_rules.proto index d129ef1ebbf7a..c015db2143191 100644 --- a/api/envoy/config/common/mutation_rules/v3/mutation_rules.proto +++ b/api/envoy/config/common/mutation_rules/v3/mutation_rules.proto @@ -4,6 +4,7 @@ package envoy.config.common.mutation_rules.v3; import "envoy/config/core/v3/base.proto"; import "envoy/type/matcher/v3/regex.proto"; +import "envoy/type/matcher/v3/string.proto"; import "google/protobuf/wrappers.proto"; @@ -90,6 +91,12 @@ message HeaderMutationRules { // The HeaderMutation structure specifies an action that may be taken on HTTP // headers. message HeaderMutation { + message RemoveOnMatch { + // A string matcher that will be applied to the header key. If the header key + // matches, the header will be removed. + type.matcher.v3.StringMatcher key_matcher = 1 [(validate.rules).message = {required: true}]; + } + oneof action { option (validate.required) = true; @@ -99,5 +106,8 @@ message HeaderMutation { // Append new header by the specified HeaderValueOption. core.v3.HeaderValueOption append = 2; + + // Remove the header if the key matches the specified string matcher. + RemoveOnMatch remove_on_match = 3; } } diff --git a/api/envoy/config/core/v3/address.proto b/api/envoy/config/core/v3/address.proto index 56796fc721a5d..238494a09c755 100644 --- a/api/envoy/config/core/v3/address.proto +++ b/api/envoy/config/core/v3/address.proto @@ -105,9 +105,6 @@ message SocketAddress { // .. note:: // Setting this parameter requires Envoy to run with the ``CAP_NET_ADMIN`` capability. // - // .. note:: - // Currently only used for Listener sockets. - // // .. attention:: // Network namespaces are only configurable on Linux. Otherwise, this field has no effect. string network_namespace_filepath = 7; diff --git a/api/envoy/config/core/v3/health_check.proto b/api/envoy/config/core/v3/health_check.proto index fd4440d8fa5fd..a4ed6e9181898 100644 --- a/api/envoy/config/core/v3/health_check.proto +++ b/api/envoy/config/core/v3/health_check.proto @@ -102,7 +102,8 @@ message HealthCheck { // ``/healthcheck``. string path = 2 [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_VALUE}]; - // [#not-implemented-hide:] HTTP specific payload. + // HTTP specific payload to be sent as the request body during health checking. + // If specified, the method should support a request body (POST, PUT, PATCH, etc.). Payload send = 3; // Specifies a list of HTTP expected responses to match in the first ``response_buffer_size`` bytes of the response body. @@ -161,7 +162,8 @@ message HealthCheck { type.matcher.v3.StringMatcher service_name_matcher = 11; // HTTP Method that will be used for health checking, default is "GET". - // GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH methods are supported, but making request body is not supported. + // GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH methods are supported. + // Request body payloads are supported for POST, PUT, PATCH, and OPTIONS methods only. // CONNECT method is disallowed because it is not appropriate for health check request. // If a non-200 response is expected by the method, it needs to be set in :ref:`expected_statuses `. RequestMethod method = 13 [(validate.rules).enum = {defined_only: true not_in: 6}]; diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 147caa2166614..74fe641fe3a24 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -77,7 +77,7 @@ message QuicProtocolOptions { [(validate.rules).uint32 = {lte: 16777216 gte: 1}]; // Similar to ``initial_stream_window_size``, but for connection-level - // flow-control. Valid values rage from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults + // flow-control. Valid values range from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults // to 25165824 (24 * 1024 * 1024). // // .. note:: @@ -111,10 +111,9 @@ message QuicProtocolOptions { // default 600s will be applied. // For internal corporate network, a long timeout is often fine. // But for client facing network, 30s is usually a good choice. - google.protobuf.Duration idle_network_timeout = 8 [(validate.rules).duration = { - lte {seconds: 600} - gte {seconds: 1} - }]; + // Do not add an upper bound here. A long idle timeout is useful for maintaining warm connections at non-front-line proxy for low QPS services." + google.protobuf.Duration idle_network_timeout = 8 + [(validate.rules).duration = {gte {seconds: 1}}]; // Maximum packet length for QUIC connections. It refers to the largest size of a QUIC packet that can be transmitted over the connection. // If not specified, one of the `default values in QUICHE `_ is used. @@ -503,7 +502,7 @@ message Http2ProtocolOptions { // `Maximum concurrent streams `_ // allowed for peer on one HTTP/2 connection. Valid values range from 1 to 2147483647 (2^31 - 1) - // and defaults to 2147483647. + // and defaults to 1024 for safety and should be sufficient for most use cases. // // For upstream connections, this also limits how many streams Envoy will initiate concurrently // on a single connection. If the limit is reached, Envoy may queue requests or establish @@ -517,8 +516,8 @@ message Http2ProtocolOptions { // `Initial stream-level flow-control window // `_ size. Valid values range from 65535 - // (2^16 - 1, HTTP/2 default) to 2147483647 (2^31 - 1, HTTP/2 maximum) and defaults to 268435456 - // (256 * 1024 * 1024). + // (2^16 - 1, HTTP/2 default) to 2147483647 (2^31 - 1, HTTP/2 maximum) and defaults to + // 16MiB (16 * 1024 * 1024). // // .. note:: // @@ -532,7 +531,7 @@ message Http2ProtocolOptions { [(validate.rules).uint32 = {lte: 2147483647 gte: 65535}]; // Similar to ``initial_stream_window_size``, but for connection-level flow-control - // window. Currently, this has the same minimum/maximum/default as ``initial_stream_window_size``. + // window. The default is 24MiB (24 * 1024 * 1024). google.protobuf.UInt32Value initial_connection_window_size = 4 [(validate.rules).uint32 = {lte: 2147483647 gte: 65535}]; diff --git a/api/envoy/config/metrics/v3/stats.proto b/api/envoy/config/metrics/v3/stats.proto index e7d7f80d648ad..02bb23aec9d38 100644 --- a/api/envoy/config/metrics/v3/stats.proto +++ b/api/envoy/config/metrics/v3/stats.proto @@ -298,10 +298,12 @@ message HistogramBucketSettings { // Each value is the upper bound of a bucket. Each bucket must be greater than 0 and unique. // The order of the buckets does not matter. repeated double buckets = 2 [(validate.rules).repeated = { - min_items: 1 unique: true items {double {gt: 0.0}} }]; + + // Initial number of bins for the ``circllhist`` thread local histogram per time series. Default value is 100. + google.protobuf.UInt32Value bins = 3 [(validate.rules).uint32 = {lte: 46082 gt: 0}]; } // Stats configuration proto schema for built-in ``envoy.stat_sinks.statsd`` sink. This sink does not support diff --git a/api/envoy/config/overload/v3/overload.proto b/api/envoy/config/overload/v3/overload.proto index 1f267c1863da3..b5bc2c4d830af 100644 --- a/api/envoy/config/overload/v3/overload.proto +++ b/api/envoy/config/overload/v3/overload.proto @@ -109,6 +109,13 @@ message ScaleTimersOverloadActionConfig { // :ref:`HttpConnectionManager.common_http_protocol_options.max_connection_duration // `. HTTP_DOWNSTREAM_CONNECTION_MAX = 4; + + // Adjusts the timeout for the downstream codec to flush an ended stream. + // This affects the value of :ref:`RouteAction.flush_timeout + // ` and + // :ref:`HttpConnectionManager.stream_flush_timeout + // ` + HTTP_DOWNSTREAM_STREAM_FLUSH = 5; } message ScaleTimer { @@ -134,9 +141,16 @@ message OverloadAction { option (udpa.annotations.versioning).previous_message_type = "envoy.config.overload.v2alpha.OverloadAction"; - // The name of the overload action. This is just a well-known string that listeners can - // use for registering callbacks. Custom overload actions should be named using reverse - // DNS to ensure uniqueness. + // The name of the overload action. This is just a well-known string that + // listeners can use for registering callbacks. + // Valid known overload actions include: + // - envoy.overload_actions.stop_accepting_requests + // - envoy.overload_actions.disable_http_keepalive + // - envoy.overload_actions.stop_accepting_connections + // - envoy.overload_actions.reject_incoming_connections + // - envoy.overload_actions.shrink_heap + // - envoy.overload_actions.reduce_timeouts + // - envoy.overload_actions.reset_high_memory_stream string name = 1 [(validate.rules).string = {min_len: 1}]; // A set of triggers for this action. The state of the action is the maximum @@ -148,7 +162,7 @@ message OverloadAction { // in this list. repeated Trigger triggers = 2 [(validate.rules).repeated = {min_items: 1}]; - // Configuration for the action being instantiated. + // Configuration for the action being instantiated if applicable. google.protobuf.Any typed_config = 3; } diff --git a/api/envoy/config/route/v3/route_components.proto b/api/envoy/config/route/v3/route_components.proto index 292e5b9355847..1b286f911ff78 100644 --- a/api/envoy/config/route/v3/route_components.proto +++ b/api/envoy/config/route/v3/route_components.proto @@ -41,7 +41,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // host header. This allows a single listener to service multiple top level domain path trees. Once // a virtual host is selected based on the domain, the routes are processed in order to see which // upstream cluster to route to or whether to perform a redirect. -// [#next-free-field: 25] +// [#next-free-field: 26] message VirtualHost { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.VirtualHost"; @@ -205,10 +205,37 @@ message VirtualHost { // request header in retries initiated by per try timeouts. bool include_is_timeout_retry_header = 23; - // The maximum bytes which will be buffered for retries and shadowing. - // If set and a route-specific limit is not set, the bytes actually buffered will be the minimum - // value of this and the listener per_connection_buffer_limit_bytes. - google.protobuf.UInt32Value per_request_buffer_limit_bytes = 18; + // The maximum bytes which will be buffered for retries and shadowing. If set, the bytes actually buffered will be + // the minimum value of this and the listener ``per_connection_buffer_limit_bytes``. + // + // .. attention:: + // + // This field has been deprecated. Please use :ref:`request_body_buffer_limit + // ` instead. + // Only one of ``per_request_buffer_limit_bytes`` and ``request_body_buffer_limit`` could be set. + google.protobuf.UInt32Value per_request_buffer_limit_bytes = 18 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // The maximum bytes which will be buffered for request bodies to support large request body + // buffering beyond the ``per_connection_buffer_limit_bytes``. + // + // This limit is specifically for the request body buffering and allows buffering larger payloads while maintaining + // flow control. + // + // Buffer limit precedence (from highest to lowest priority): + // + // 1. If ``request_body_buffer_limit`` is set, then ``request_body_buffer_limit`` will be used. + // 2. If :ref:`per_request_buffer_limit_bytes ` + // is set but ``request_body_buffer_limit`` is not, then ``min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes)`` + // will be used. + // 3. If neither is set, then ``per_connection_buffer_limit_bytes`` will be used. + // + // For flow control chunk sizes, ``min(per_connection_buffer_limit_bytes, 16KB)`` will be used. + // + // Only one of :ref:`per_request_buffer_limit_bytes ` + // and ``request_body_buffer_limit`` could be set. + google.protobuf.UInt64Value request_body_buffer_limit = 25 + [(validate.rules).message = {required: false}]; // Specify a set of default request mirroring policies for every route under this virtual host. // It takes precedence over the route config mirror policy entirely. @@ -244,7 +271,7 @@ message RouteList { // // Envoy supports routing on HTTP method via :ref:`header matching // `. -// [#next-free-field: 20] +// [#next-free-field: 21] message Route { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.Route"; @@ -341,7 +368,14 @@ message Route { // The maximum bytes which will be buffered for retries and shadowing. // If set, the bytes actually buffered will be the minimum value of this and the // listener per_connection_buffer_limit_bytes. - google.protobuf.UInt32Value per_request_buffer_limit_bytes = 16; + // + // .. attention:: + // + // This field has been deprecated. Please use :ref:`request_body_buffer_limit + // ` instead. + // Only one of ``per_request_buffer_limit_bytes`` and ``request_body_buffer_limit`` may be set. + google.protobuf.UInt32Value per_request_buffer_limit_bytes = 16 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // The human readable prefix to use when emitting statistics for this endpoint. // The statistics are rooted at vhost..route.. @@ -357,6 +391,25 @@ message Route { // every application endpoint. This is both not easily maintainable and // statistics use a non-trivial amount of memory(approximately 1KiB per route). string stat_prefix = 19; + + // The maximum bytes which will be buffered for request bodies to support large request body + // buffering beyond the ``per_connection_buffer_limit_bytes``. + // + // This limit is specifically for the request body buffering and allows buffering larger payloads while maintaining + // flow control. + // + // Buffer limit precedence (from highest to lowest priority): + // + // 1. If ``request_body_buffer_limit`` is set: use ``request_body_buffer_limit`` + // 2. If :ref:`per_request_buffer_limit_bytes ` + // is set but ``request_body_buffer_limit`` is not: use ``min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes)`` + // 3. If neither is set: use ``per_connection_buffer_limit_bytes`` + // + // For flow control chunk sizes, use ``min(per_connection_buffer_limit_bytes, 16KB)``. + // + // Only one of :ref:`per_request_buffer_limit_bytes ` + // and ``request_body_buffer_limit`` may be set. + google.protobuf.UInt64Value request_body_buffer_limit = 20; } // Compared to the :ref:`cluster ` field that specifies a @@ -740,7 +793,7 @@ message CorsPolicy { google.protobuf.BoolValue forward_not_matching_preflights = 13; } -// [#next-free-field: 42] +// [#next-free-field: 43] message RouteAction { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RouteAction"; @@ -1265,8 +1318,28 @@ message RouteAction { // If the :ref:`overload action ` "envoy.overload_actions.reduce_timeouts" // is configured, this timeout is scaled according to the value for // :ref:`HTTP_DOWNSTREAM_STREAM_IDLE `. + // + // This timeout may also be used in place of ``flush_timeout`` in very specific cases. See the + // documentation for ``flush_timeout`` for more details. google.protobuf.Duration idle_timeout = 24; + // Specifies the codec stream flush timeout for the route. + // + // If not specified, the first preference is the global :ref:`stream_flush_timeout + // `, + // but only if explicitly configured. + // + // If neither the explicit HCM-wide flush timeout nor this route-specific flush timeout is configured, + // the route's stream idle timeout is reused for this timeout. This is for + // backwards compatibility since both behaviors were historically controlled by the one timeout. + // + // If the route also does not have an idle timeout configured, the global :ref:`stream_idle_timeout + // `. used, again + // for backwards compatibility. That timeout defaults to 5 minutes. + // + // A value of 0 via any of the above paths will completely disable the timeout for a given route. + google.protobuf.Duration flush_timeout = 42; + // Specifies how to send request over TLS early data. // If absent, allows `safe HTTP requests `_ to be sent on early data. // [#extension-category: envoy.route.early_data_policy] diff --git a/api/envoy/config/trace/v3/zipkin.proto b/api/envoy/config/trace/v3/zipkin.proto index 2d8f3195c31e3..7405c596ed563 100644 --- a/api/envoy/config/trace/v3/zipkin.proto +++ b/api/envoy/config/trace/v3/zipkin.proto @@ -2,13 +2,14 @@ syntax = "proto3"; package envoy.config.trace.v3; +import "envoy/config/core/v3/http_service.proto"; + import "google/protobuf/wrappers.proto"; import "envoy/annotations/deprecation.proto"; import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; -import "validate/validate.proto"; option java_package = "io.envoyproxy.envoy.config.trace.v3"; option java_outer_classname = "ZipkinProto"; @@ -21,10 +22,22 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Configuration for the Zipkin tracer. // [#extension: envoy.tracers.zipkin] -// [#next-free-field: 8] +// [#next-free-field: 10] message ZipkinConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.trace.v2.ZipkinConfig"; + // Available trace context options for handling different trace header formats. + enum TraceContextOption { + // Use B3 headers only (default behavior). + USE_B3 = 0; + + // Enable B3 and W3C dual header support: + // - For downstream: Extract from B3 headers first, fallback to W3C traceparent if B3 is unavailable. + // - For upstream: Inject both B3 and W3C traceparent headers. + // When this option is NOT set, only B3 headers are used for both extraction and injection. + USE_B3_WITH_W3C_PROPAGATION = 1; + } + // Available Zipkin collector endpoint versions. enum CollectorEndpointVersion { // Zipkin API v1, JSON over HTTP. @@ -48,11 +61,17 @@ message ZipkinConfig { } // The cluster manager cluster that hosts the Zipkin collectors. - string collector_cluster = 1 [(validate.rules).string = {min_len: 1}]; + // Note: This field will be deprecated in future releases in favor of + // :ref:`collector_service `. + // Either this field or collector_service must be specified. + string collector_cluster = 1; // The API endpoint of the Zipkin service where the spans will be sent. When // using a standard Zipkin installation. - string collector_endpoint = 2 [(validate.rules).string = {min_len: 1}]; + // Note: This field will be deprecated in future releases in favor of + // :ref:`collector_service `. + // Required when using collector_cluster. + string collector_endpoint = 2; // Determines whether a 128bit trace id will be used when creating a new // trace instance. The default value is false, which will result in a 64 bit trace id being used. @@ -67,6 +86,8 @@ message ZipkinConfig { // Optional hostname to use when sending spans to the collector_cluster. Useful for collectors // that require a specific hostname. Defaults to :ref:`collector_cluster ` above. + // Note: This field will be deprecated in future releases in favor of + // :ref:`collector_service `. string collector_hostname = 6; // If this is set to true, then Envoy will be treated as an independent hop in trace chain. A complete span pair will be created for a single @@ -88,4 +109,60 @@ message ZipkinConfig { // Please use that ``spawn_upstream_span`` field to control the span creation. bool split_spans_for_request = 7 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // Determines which trace context format to use for trace header extraction and propagation. + // This controls both downstream request header extraction and upstream request header injection. + // Here is the spec for W3C trace headers: https://www.w3.org/TR/trace-context/ + // The default value is USE_B3 to maintain backward compatibility. + TraceContextOption trace_context_option = 8; + + // HTTP service configuration for the Zipkin collector. + // When specified, this configuration takes precedence over the legacy fields: + // collector_cluster, collector_endpoint, and collector_hostname. + // This provides a complete HTTP service configuration including cluster, URI, timeout, and headers. + // If not specified, the legacy fields above will be used for backward compatibility. + // + // Required fields when using collector_service: + // + // * ``http_uri.cluster`` - Must be specified and non-empty + // * ``http_uri.uri`` - Must be specified and non-empty + // * ``http_uri.timeout`` - Optional + // + // Full URI Support with Automatic Parsing: + // + // The ``uri`` field supports both path-only and full URI formats: + // + // .. code-block:: yaml + // + // tracing: + // provider: + // name: envoy.tracers.zipkin + // typed_config: + // "@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig + // collector_service: + // http_uri: + // # Full URI format - hostname and path are extracted automatically + // uri: "https://zipkin-collector.example.com/api/v2/spans" + // cluster: zipkin + // timeout: 5s + // request_headers_to_add: + // - header: + // key: "X-Custom-Token" + // value: "your-custom-token" + // - header: + // key: "X-Service-ID" + // value: "your-service-id" + // + // URI Parsing Behavior: + // + // * Full URI: ``"https://zipkin-collector.example.com/api/v2/spans"`` + // + // * Hostname: ``zipkin-collector.example.com`` (sets HTTP ``Host`` header) + // * Path: ``/api/v2/spans`` (sets HTTP request path) + // + // * Path only: ``"/api/v2/spans"`` + // + // * Hostname: Uses cluster name as fallback + // * Path: ``/api/v2/spans`` + core.v3.HttpService collector_service = 9; } diff --git a/api/envoy/data/core/v3/tlv_metadata.proto b/api/envoy/data/core/v3/tlv_metadata.proto index 8f99b004e3f35..caa7989864eb8 100644 --- a/api/envoy/data/core/v3/tlv_metadata.proto +++ b/api/envoy/data/core/v3/tlv_metadata.proto @@ -17,8 +17,7 @@ message TlvsMetadata { // Typed metadata for :ref:`Proxy protocol filter `, that represents a map of TLVs. // Each entry in the map consists of a key which corresponds to a configured // :ref:`rule key ` and a value (TLV value in bytes). - // When runtime flag ``envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener`` is enabled, // :ref:`Proxy protocol filter ` - // will populate typed metadata and regular metadata. By default filter will populate typed and untyped metadata. + // populates both typed and untyped metadata. map typed_metadata = 1; } diff --git a/api/envoy/data/tap/v3/http.proto b/api/envoy/data/tap/v3/http.proto index 2e5c566e59ed5..42ba44c1d0bab 100644 --- a/api/envoy/data/tap/v3/http.proto +++ b/api/envoy/data/tap/v3/http.proto @@ -49,6 +49,9 @@ message HttpBufferedTrace { // downstream connection Connection downstream_connection = 3; + + // upstream connection + Connection upstream_connection = 4; } // A streamed HTTP trace segment. Multiple segments make up a full trace. diff --git a/api/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/BUILD b/api/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/BUILD new file mode 100644 index 0000000000000..29ebf0741406e --- /dev/null +++ b/api/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_xds//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.proto b/api/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.proto new file mode 100644 index 0000000000000..25613fae8ac5e --- /dev/null +++ b/api/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3"; +option java_outer_classname = "DownstreamReverseConnectionSocketInterfaceProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3;downstream_socket_interfacev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Bootstrap settings for downstream reverse connection socket interface] +// [#extension: envoy.bootstrap.reverse_tunnel.downstream_socket_interface] + +// Configuration for the downstream reverse connection socket interface. +// This interface initiates reverse connections to upstream Envoys and provides +// them as socket connections for downstream requests. +message DownstreamReverseConnectionSocketInterface { + // Stat prefix to be used for downstream reverse connection socket interface stats. + string stat_prefix = 1; +} diff --git a/api/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/BUILD b/api/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/BUILD new file mode 100644 index 0000000000000..29ebf0741406e --- /dev/null +++ b/api/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_xds//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.proto b/api/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.proto new file mode 100644 index 0000000000000..ac01a7c251609 --- /dev/null +++ b/api/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3; + +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3"; +option java_outer_classname = "UpstreamReverseConnectionSocketInterfaceProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3;upstream_socket_interfacev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Upstream reverse connection socket interface] +// [#extension: envoy.bootstrap.reverse_tunnel.upstream_socket_interface] + +// Configuration for the upstream reverse connection socket interface. +message UpstreamReverseConnectionSocketInterface { + // Stat prefix for upstream reverse connection socket interface stats. + string stat_prefix = 1; + + // Number of consecutive ping failures before an idle reverse connection socket is marked dead. + // Defaults to 3 if unset. Must be at least 1. + google.protobuf.UInt32Value ping_failure_threshold = 2 [(validate.rules).uint32 = {gte: 1}]; +} diff --git a/api/envoy/extensions/clusters/reverse_connection/v3/BUILD b/api/envoy/extensions/clusters/reverse_connection/v3/BUILD new file mode 100644 index 0000000000000..13251893cdb44 --- /dev/null +++ b/api/envoy/extensions/clusters/reverse_connection/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "@com_github_cncf_xds//udpa/annotations:pkg", + "@com_github_cncf_xds//xds/type/matcher/v3:pkg", + ], +) diff --git a/api/envoy/extensions/clusters/reverse_connection/v3/reverse_connection.proto b/api/envoy/extensions/clusters/reverse_connection/v3/reverse_connection.proto new file mode 100644 index 0000000000000..8c67b8db4a44d --- /dev/null +++ b/api/envoy/extensions/clusters/reverse_connection/v3/reverse_connection.proto @@ -0,0 +1,86 @@ +syntax = "proto3"; + +package envoy.extensions.clusters.reverse_connection.v3; + +import "google/protobuf/duration.proto"; + +import "xds/type/matcher/v3/matcher.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.clusters.reverse_connection.v3"; +option java_outer_classname = "ReverseConnectionProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/reverse_connection/v3;reverse_connectionv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Reverse connection cluster] +// [#extension: envoy.clusters.reverse_connection] + +// Configuration for a cluster of type REVERSE_CONNECTION. +message RevConClusterConfig { + // Time interval after which Envoy removes unused dynamic hosts created for reverse connections. + // Hosts that are not referenced by any connection pool are deleted during cleanup. + // + // If unset, Envoy uses a default of 60s. + google.protobuf.Duration cleanup_interval = 1 [(validate.rules).duration = {gt {}}]; + + // Host identifier matcher. + // + // This matcher is evaluated on the downstream request and yields a ``HostIdAction``. + // The action's payload is used as the host identifier to select the reverse connection + // endpoint. + // + // Typical rules use built-in inputs such as: + // + // * ``HttpRequestHeaderMatchInput`` to map a request header value. + // * ``HttpAttributesCelMatchInput`` to compute a value with CEL from headers/SNI. + // + // The match tree can be a list or a map matcher. The first matching rule should + // return a ``HostIdAction`` with the desired identifier. + // + // Example: + // + // .. validated-code-block:: yaml + // :type-name: xds.type.matcher.v3.Matcher + // + // matcher_list: + // matchers: + // - predicate: + // single_predicate: + // input: + // typed_config: + // '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + // header_name: x-remote-node-id + // value_match: + // exact: node-a + // on_match: + // action: + // typed_config: + // '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + // host_id: "node-a" + // + // - predicate: + // single_predicate: + // input: + // typed_config: + // '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + // header_name: x-remote-node-id + // value_match: + // exact: node-b + // on_match: + // action: + // typed_config: + // '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + // host_id: "node-b" + // + // If the matcher does not return a ``HostIdAction``, the request will not be routed. + xds.type.matcher.v3.Matcher host_id_matcher = 2 [(validate.rules).message = {required: true}]; +} + +// Action that returns the resolved host identifier. +message HostIdAction { + // Resolved host identifier for the reverse connection endpoint. + string host_id = 1 [(validate.rules).string = {min_len: 1}]; +} diff --git a/api/envoy/extensions/common/aws/v3/credential_provider.proto b/api/envoy/extensions/common/aws/v3/credential_provider.proto index 395a2e9a9ac87..e51ade1318d54 100644 --- a/api/envoy/extensions/common/aws/v3/credential_provider.proto +++ b/api/envoy/extensions/common/aws/v3/credential_provider.proto @@ -163,7 +163,8 @@ message AssumeRoleCredentialProvider { // The ARN of the role to assume. string role_arn = 1 [(validate.rules).string = {min_len: 1}]; - // Optional string value to use as the role session name + // An optional role session name, used when identifying the role in subsequent AWS API calls. If not provided, the role session name will default + // to the current timestamp. string role_session_name = 2; // Optional string value to use as the externalId diff --git a/api/envoy/extensions/filters/http/compressor/v3/compressor.proto b/api/envoy/extensions/filters/http/compressor/v3/compressor.proto index c49ccfead8f5c..0c8d28602dade 100644 --- a/api/envoy/extensions/filters/http/compressor/v3/compressor.proto +++ b/api/envoy/extensions/filters/http/compressor/v3/compressor.proto @@ -28,21 +28,31 @@ message Compressor { "envoy.config.filter.http.compressor.v2.Compressor"; message CommonDirectionConfig { - // Runtime flag that controls whether compression is enabled or not for the direction this - // common config is put in. If set to false, the filter will operate as a pass-through filter - // in the chosen direction, unless overridden by CompressorPerRoute. - // If the field is omitted, the filter will be enabled. + // Runtime flag that controls whether compression is enabled for the direction this + // common config is applied to. When this field is ``false``, the filter will operate as a + // pass-through filter in the chosen direction, unless overridden by ``CompressorPerRoute``. + // If this field is not specified, the filter will be enabled. config.core.v3.RuntimeFeatureFlag enabled = 1; - // Minimum value of Content-Length header of request or response messages (depending on the direction - // this common config is put in), in bytes, which will trigger compression. The default value is 30. + // Minimum value of the ``Content-Length`` header in request or response messages (depending on the + // direction this common config is applied to), in bytes, that will trigger compression. Defaults to 30. google.protobuf.UInt32Value min_content_length = 2; // Set of strings that allows specifying which mime-types yield compression; e.g., - // application/json, text/html, etc. When this field is not defined, compression will be applied - // to the following mime-types: "application/javascript", "application/json", - // "application/xhtml+xml", "image/svg+xml", "text/css", "text/html", "text/plain", "text/xml" - // and their synonyms. + // ``application/json``, ``text/html``, etc. + // + // When this field is not specified, compression will be applied to these following mime-types + // and their synonyms: + // + // * ``application/javascript`` + // * ``application/json`` + // * ``application/xhtml+xml`` + // * ``image/svg+xml`` + // * ``text/css`` + // * ``text/html`` + // * ``text/plain`` + // * ``text/xml`` + // repeated string content_type = 3; } @@ -55,20 +65,21 @@ message Compressor { message ResponseDirectionConfig { CommonDirectionConfig common_config = 1; - // If true, disables compression when the response contains an etag header. When it is false, the - // filter will preserve weak etags and remove the ones that require strong validation. + // When this field is ``true``, disables compression when the response contains an ``ETag`` header. + // When this field is ``false``, the filter will preserve weak ``ETag`` values and remove those that + // require strong validation. bool disable_on_etag_header = 2; - // If true, removes accept-encoding from the request headers before dispatching it to the upstream - // so that responses do not get compressed before reaching the filter. + // When this field is ``true``, removes ``Accept-Encoding`` from the request headers before dispatching + // the request to the upstream so that responses do not get compressed before reaching the filter. // // .. attention:: // - // To avoid interfering with other compression filters in the same chain use this option in + // To avoid interfering with other compression filters in the same chain, use this option in // the filter closest to the upstream. bool remove_accept_encoding_header = 3; - // Set of response codes for which compression is disabled, e.g. 206 Partial Content should not + // Set of response codes for which compression is disabled; e.g., 206 Partial Content should not // be compressed. repeated uint32 uncompressible_response_codes = 4 [(validate.rules).repeated = { unique: true @@ -81,60 +92,69 @@ message Compressor { [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // Set of strings that allows specifying which mime-types yield compression; e.g., - // application/json, text/html, etc. When this field is not defined, compression will be applied - // to the following mime-types: "application/javascript", "application/json", - // "application/xhtml+xml", "image/svg+xml", "text/css", "text/html", "text/plain", "text/xml" - // and their synonyms. + // ``application/json``, ``text/html``, etc. + // + // When this field is not specified, compression will be applied to these following mime-types + // and their synonyms: + // + // * ``application/javascript`` + // * ``application/json`` + // * ``application/xhtml+xml`` + // * ``image/svg+xml`` + // * ``text/css`` + // * ``text/html`` + // * ``text/plain`` + // * ``text/xml`` + // repeated string content_type = 2 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // If true, disables compression when the response contains an etag header. When it is false, the - // filter will preserve weak etags and remove the ones that require strong validation. + // When this field is ``true``, disables compression when the response contains an ``ETag`` header. + // When this field is ``false``, the filter will preserve weak ``ETag`` values and remove those that + // require strong validation. bool disable_on_etag_header = 3 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // If true, removes accept-encoding from the request headers before dispatching it to the upstream - // so that responses do not get compressed before reaching the filter. + // When this field is ``true``, removes ``Accept-Encoding`` from the request headers before dispatching + // the request to the upstream so that responses do not get compressed before reaching the filter. // // .. attention:: // - // To avoid interfering with other compression filters in the same chain use this option in + // To avoid interfering with other compression filters in the same chain, use this option in // the filter closest to the upstream. bool remove_accept_encoding_header = 4 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // Runtime flag that controls whether the filter is enabled or not. If set to false, the - // filter will operate as a pass-through filter, unless overridden by - // CompressorPerRoute. If not specified, defaults to enabled. + // Runtime flag that controls whether the filter is enabled. When this field is ``false``, the + // filter will operate as a pass-through filter, unless overridden by ``CompressorPerRoute``. + // If this field is not specified, the filter is enabled by default. config.core.v3.RuntimeFeatureFlag runtime_enabled = 5 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // A compressor library to use for compression. Currently only - // :ref:`envoy.compression.gzip.compressor` - // is included in Envoy. + // A compressor library to use for compression. // [#extension-category: envoy.compression.compressor] config.core.v3.TypedExtensionConfig compressor_library = 6 [(validate.rules).message = {required: true}]; - // Configuration for request compression. Compression is disabled by default if left empty. + // Configuration for request compression. If this field is not specified, request compression is disabled. RequestDirectionConfig request_direction_config = 7; - // Configuration for response compression. Compression is enabled by default if left empty. + // Configuration for response compression. If this field is not specified, response compression is enabled. // // .. attention:: // - // If the field is not empty then the duplicate deprecated fields of the ``Compressor`` message, + // When this field is set, duplicate deprecated fields of the ``Compressor`` message, // such as ``content_length``, ``content_type``, ``disable_on_etag_header``, - // ``remove_accept_encoding_header`` and ``runtime_enabled``, are ignored. + // ``remove_accept_encoding_header``, and ``runtime_enabled``, are ignored. // - // Also all the statistics related to response compression will be rooted in + // Additionally, all statistics related to response compression will be rooted in // ``.compressor...response.*`` // instead of // ``.compressor...*``. ResponseDirectionConfig response_direction_config = 8; - // If true, chooses this compressor first to do compression when the q-values in ``Accept-Encoding`` are same. - // The last compressor which enables choose_first will be chosen if multiple compressor filters in the chain have choose_first as true. + // When this field is ``true``, this compressor is preferred when q-values in ``Accept-Encoding`` are equal. + // If multiple compressor filters set ``choose_first`` to ``true``, the last one in the filter chain is chosen. bool choose_first = 9; } @@ -159,7 +179,7 @@ message CompressorPerRoute { option (validate.required) = true; // If set, the filter will operate as a pass-through filter. - // Overrides Compressor.runtime_enabled and CommonDirectionConfig.enabled. + // Overrides ``Compressor.runtime_enabled`` and ``CommonDirectionConfig.enabled``. bool disabled = 1 [(validate.rules).bool = {const: true}]; // Per-route overrides. Fields set here will override corresponding fields in ``Compressor``. diff --git a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto index 0a2492b2ff5f5..7cf2aac64aacc 100644 --- a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto +++ b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -53,40 +53,39 @@ message ExtAuthz { config.core.v3.ApiVersion transport_api_version = 12 [(validate.rules).enum = {defined_only: true}]; - // Changes filter's behavior on errors: + // Changes the filter's behavior on errors: // - // 1. When set to true, the filter will ``accept`` client request even if the communication with - // the authorization service has failed, or if the authorization service has returned a HTTP 5xx - // error. + // #. When set to ``true``, the filter will ``accept`` the client request even if communication with + // the authorization service has failed, or if the authorization service has returned an HTTP 5xx + // error. // - // 2. When set to false, ext-authz will ``reject`` client requests and return a ``Forbidden`` - // response if the communication with the authorization service has failed, or if the - // authorization service has returned a HTTP 5xx error. + // #. When set to ``false``, the filter will ``reject`` client requests and return ``Forbidden`` + // if communication with the authorization service has failed, or if the authorization service + // has returned an HTTP 5xx error. // - // Note that errors can be ``always`` tracked in the :ref:`stats - // `. + // Errors can always be tracked in the :ref:`stats `. bool failure_mode_allow = 2; - // When ``failure_mode_allow`` and ``failure_mode_allow_header_add`` are both set to true, + // When ``failure_mode_allow`` and ``failure_mode_allow_header_add`` are both set to ``true``, // ``x-envoy-auth-failure-mode-allowed: true`` will be added to request headers if the communication // with the authorization service has failed, or if the authorization service has returned a // HTTP 5xx error. bool failure_mode_allow_header_add = 19; - // Enables filter to buffer the client request body and send it within the authorization request. - // A ``x-envoy-auth-partial-body: false|true`` metadata header will be added to the authorization - // request message indicating if the body data is partial. + // Enables the filter to buffer the client request body and send it within the authorization request. + // The ``x-envoy-auth-partial-body: false|true`` metadata header will be added to the authorization + // request indicating whether the body data is partial. BufferSettings with_request_body = 5; - // Clears route cache in order to allow the external authorization service to correctly affect - // routing decisions. Filter clears all cached routes when: + // Clears the route cache in order to allow the external authorization service to correctly affect + // routing decisions. The filter clears all cached routes when: // - // 1. The field is set to ``true``. + // #. The field is set to ``true``. // - // 2. The status returned from the authorization service is a HTTP 200 or gRPC 0. + // #. The status returned from the authorization service is an HTTP 200 or gRPC 0. // - // 3. At least one ``authorization response header`` is added to the client request, or is used for - // altering another client request header. + // #. At least one ``authorization response header`` is added to the client request, or is used to + // alter another client request header. // bool clear_route_cache = 6; @@ -94,26 +93,27 @@ message ExtAuthz { // or cannot be reached. The default status is HTTP 403 Forbidden. type.v3.HttpStatus status_on_error = 7; - // When this is set to true, the filter will check the :ref:`ext_authz response - // ` for invalid header & + // When this is set to ``true``, the filter will check the :ref:`ext_authz response + // ` for invalid header and // query parameter mutations. If the side stream response is invalid, it will send a local reply // to the downstream request with status HTTP 500 Internal Server Error. // - // Note that headers_to_remove & query_parameters_to_remove are validated, but invalid elements in - // those fields should not affect any headers & thus will not cause the filter to send a local - // reply. + // .. note:: + // Both ``headers_to_remove`` and ``query_parameters_to_remove`` are validated, but invalid elements in + // those fields should not affect any headers and thus will not cause the filter to send a local reply. // - // When set to false, any invalid mutations will be visible to the rest of envoy and may cause + // When set to ``false``, any invalid mutations will be visible to the rest of Envoy and may cause // unexpected behavior. // - // If you are using ext_authz with an untrusted ext_authz server, you should set this to true. + // If you are using ext_authz with an untrusted ext_authz server, you should set this to ``true``. bool validate_mutations = 24; // Specifies a list of metadata namespaces whose values, if present, will be passed to the // ext_authz service. The :ref:`filter_metadata ` // is passed as an opaque ``protobuf::Struct``. // - // Please note that this field exclusively applies to the gRPC ext_authz service and has no effect on the HTTP service. + // .. note:: + // This field applies exclusively to the gRPC ext_authz service and has no effect on the HTTP service. // // For example, if the ``jwt_authn`` filter is used and :ref:`payload_in_metadata // ` is set, @@ -130,10 +130,11 @@ message ExtAuthz { // ext_authz service. :ref:`typed_filter_metadata ` // is passed as a ``protobuf::Any``. // - // Please note that this field exclusively applies to the gRPC ext_authz service and has no effect on the HTTP service. + // .. note:: + // This field applies exclusively to the gRPC ext_authz service and has no effect on the HTTP service. // - // It works in a way similar to ``metadata_context_namespaces`` but allows Envoy and ext_authz server to share - // the protobuf message definition in order to do a safe parsing. + // This works similarly to ``metadata_context_namespaces`` but allows Envoy and the ext_authz server to share + // the protobuf message definition in order to perform safe parsing. // repeated string typed_metadata_context_namespaces = 16; @@ -146,7 +147,7 @@ message ExtAuthz { // Specifies a list of route metadata namespaces whose values, if present, will be passed to the // ext_authz service at :ref:`route_metadata_context ` in // :ref:`CheckRequest `. - // :ref:`typed_filter_metadata ` is passed as an ``protobuf::Any``. + // :ref:`typed_filter_metadata ` is passed as a ``protobuf::Any``. repeated string route_typed_metadata_context_namespaces = 22; // Specifies if the filter is enabled. @@ -161,11 +162,11 @@ message ExtAuthz { // If this field is not specified, the filter will be enabled for all requests. type.matcher.v3.MetadataMatcher filter_enabled_metadata = 14; - // Specifies whether to deny the requests, when the filter is disabled. + // Specifies whether to deny the requests when the filter is disabled. // If :ref:`runtime_key ` is specified, - // Envoy will lookup the runtime key to determine whether to deny request for - // filter protected path at filter disabling. If filter is disabled in - // typed_per_filter_config for the path, requests will not be denied. + // Envoy will lookup the runtime key to determine whether to deny requests for filter-protected paths + // when the filter is disabled. If the filter is disabled in ``typed_per_filter_config`` for the path, + // requests will not be denied. // // If this field is not specified, all requests will be allowed when disabled. // @@ -176,11 +177,11 @@ message ExtAuthz { // Specifies if the peer certificate is sent to the external service. // - // When this field is true, Envoy will include the peer X.509 certificate, if available, in the + // When this field is ``true``, Envoy will include the peer X.509 certificate, if available, in the // :ref:`certificate`. bool include_peer_certificate = 10; - // Optional additional prefix to use when emitting statistics. This allows to distinguish + // Optional additional prefix to use when emitting statistics. This allows distinguishing // emitted statistics between configured ``ext_authz`` filters in an HTTP filter chain. For example: // // .. code-block:: yaml @@ -210,21 +211,20 @@ message ExtAuthz { // // .. note:: // - // 1. For requests to an HTTP authorization server: in addition to the user's supplied matchers, ``Host``, ``Method``, ``Path``, - // ``Content-Length``, and ``Authorization`` are **additionally included** in the list. + // For requests to an HTTP authorization server: in addition to the user's supplied matchers, ``Host``, ``Method``, ``Path``, + // ``Content-Length``, and ``Authorization`` are **additionally included** in the list. // // .. note:: // - // 2. For requests to an HTTP authorization server: value of ``Content-Length`` will be set to 0 and the request to the + // For requests to an HTTP authorization server: the value of ``Content-Length`` will be set to ``0`` and the request to the // authorization server will not have a message body. However, the check request can include the buffered // client request body (controlled by :ref:`with_request_body - // ` setting), - // consequently the value of *Content-Length* of the authorization request reflects the size of - // its payload size. + // ` setting); + // consequently, the value of ``Content-Length`` in the authorization request reflects the size of its payload. // // .. note:: // - // 3. This can be overridden by the field ``disallowed_headers`` below. That is, if a header + // This can be overridden by the field ``disallowed_headers`` below. That is, if a header // matches for both ``allowed_headers`` and ``disallowed_headers``, the header will NOT be sent. type.matcher.v3.ListStringMatcher allowed_headers = 17; @@ -234,34 +234,35 @@ message ExtAuthz { // Specifies if the TLS session level details like SNI are sent to the external service. // - // When this field is true, Envoy will include the SNI name used for TLSClientHello, if available, in the + // When this field is ``true``, Envoy will include the SNI name used for TLSClientHello, if available, in the // :ref:`tls_session`. bool include_tls_session = 18; // Whether to increment cluster statistics (e.g. cluster..upstream_rq_*) on authorization failure. - // Defaults to true. + // Defaults to ``true``. google.protobuf.BoolValue charge_cluster_response_stats = 20; - // Whether to encode the raw headers (i.e. unsanitized values & unconcatenated multi-line headers) - // in authentication request. Works with both HTTP and gRPC clients. + // Whether to encode the raw headers (i.e., unsanitized values and unconcatenated multi-line headers) + // in the authorization request. Works with both HTTP and gRPC clients. // - // When this is set to true, header values are not sanitized. Headers with the same key will also + // When this is set to ``true``, header values are not sanitized. Headers with the same key will also // not be combined into a single, comma-separated header. // Requests to gRPC services will populate the field // :ref:`header_map`. // Requests to HTTP services will be constructed with the unsanitized header values and preserved // multi-line headers with the same key. // - // If this field is set to false, header values will be sanitized, with any non-UTF-8-compliant - // bytes replaced with '!'. Headers with the same key will have their values concatenated into a + // If this field is set to ``false``, header values will be sanitized, with any non-UTF-8-compliant + // bytes replaced with ``'!'``. Headers with the same key will have their values concatenated into a // single comma-separated header value. // Requests to gRPC services will populate the field // :ref:`headers`. // Requests to HTTP services will have their header values sanitized and will not preserve // multi-line headers with the same key. // - // It's recommended you set this to true unless you already rely on the old behavior. False is the - // default only for backwards compatibility. + // It is recommended to set this to ``true`` unless you rely on the previous behavior. + // + // It is set to ``false`` by default for backwards compatibility. bool encode_raw_headers = 23; // Rules for what modifications an ext_authz server may make to the request headers before @@ -281,15 +282,15 @@ message ExtAuthz { // This field allows the filter to reject mutations to specific headers. config.common.mutation_rules.v3.HeaderMutationRules decoder_header_mutation_rules = 26; - // Enable / disable ingestion of dynamic metadata from ext_authz service. + // Enable or disable ingestion of dynamic metadata from the ext_authz service. // - // If false, the filter will ignore dynamic metadata injected by the ext_authz service. If the + // If ``false``, the filter will ignore dynamic metadata injected by the ext_authz service. If the // ext_authz service tries injecting dynamic metadata, the filter will log, increment the // ``ignored_dynamic_metadata`` stat, then continue handling the response. // - // If true, the filter will ingest dynamic metadata entries as normal. + // If ``true``, the filter will ingest dynamic metadata entries as normal. // - // If unset, defaults to true. + // If unset, defaults to ``true``. google.protobuf.BoolValue enable_dynamic_metadata_ingestion = 27; // Additional metadata to be added to the filter state for logging purposes. The metadata will be @@ -297,14 +298,14 @@ message ExtAuthz { // name. google.protobuf.Struct filter_metadata = 28; - // When set to true, the filter will emit per-stream stats for access logging. The filter state + // When set to ``true``, the filter will emit per-stream stats for access logging. The filter state // key will be the same as the filter name. // // If using Envoy gRPC, emits latency, bytes sent / received, upstream info, and upstream cluster // info. If not using Envoy gRPC, emits only latency. Note that stats are ONLY added to filter // state if a check request is actually made to an ext_authz service. // - // If this is false the filter will not emit stats, but filter_metadata will still be respected if + // If this is ``false`` the filter will not emit stats, but filter_metadata will still be respected if // it has a value. // // Field ``latency_us`` is exposed for CEL and logging when using gRPC or HTTP service. @@ -318,21 +319,21 @@ message BufferSettings { "envoy.config.filter.http.ext_authz.v2.BufferSettings"; // Sets the maximum size of a message body that the filter will hold in memory. Envoy will return - // ``HTTP 413`` and will *not* initiate the authorization process when buffer reaches the number + // ``HTTP 413`` and will *not* initiate the authorization process when the buffer reaches the size // set in this field. Note that this setting will have precedence over :ref:`failure_mode_allow // `. uint32 max_request_bytes = 1 [(validate.rules).uint32 = {gt: 0}]; - // When this field is true, Envoy will buffer the message until ``max_request_bytes`` is reached. + // When this field is ``true``, Envoy will buffer the message until ``max_request_bytes`` is reached. // The authorization request will be dispatched and no 413 HTTP error will be returned by the // filter. bool allow_partial_message = 2; - // If true, the body sent to the external authorization service is set with raw bytes, it sets - // the :ref:`raw_body` - // field of HTTP request attribute context. Otherwise, :ref:`body - // ` will be filled - // with UTF-8 string request body. + // If ``true``, the body sent to the external authorization service is set as raw bytes and populates + // :ref:`raw_body` + // in the HTTP request attribute context. Otherwise, :ref:`body + // ` will be populated + // with a UTF-8 string request body. // // This field only affects configurations using a :ref:`grpc_service // `. In configurations that use @@ -347,7 +348,7 @@ message BufferSettings { // request. Note that in any of these events, metadata can be added, removed or overridden by the // filter: // -// *On authorization request*, a list of allowed request headers may be supplied. See +// On authorization request, a list of allowed request headers may be supplied. See // :ref:`allowed_headers // ` // for details. Additional headers metadata may be added to the authorization request. See @@ -355,7 +356,7 @@ message BufferSettings { // ` for // details. // -// On authorization response status HTTP 200 OK, the filter will allow traffic to the upstream and +// On authorization response status ``HTTP 200 OK``, the filter will allow traffic to the upstream and // additional headers metadata may be added to the original client request. See // :ref:`allowed_upstream_headers // ` @@ -392,7 +393,7 @@ message AuthorizationRequest { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.ext_authz.v2.AuthorizationRequest"; - // Authorization request includes the client request headers that have a correspondent match + // Authorization request includes the client request headers that have a corresponding match // in the :ref:`list `. // This field has been deprecated in favor of :ref:`allowed_headers // `. @@ -404,17 +405,17 @@ message AuthorizationRequest { // // .. note:: // - // By default, ``Content-Length`` header is set to ``0`` and the request to the authorization + // By default, the ``Content-Length`` header is set to ``0`` and the request to the authorization // service has no message body. However, the authorization request *may* include the buffered // client request body (controlled by :ref:`with_request_body // ` - // setting) hence the value of its ``Content-Length`` reflects the size of its payload size. + // setting); hence the value of its ``Content-Length`` reflects the size of its payload. // type.matcher.v3.ListStringMatcher allowed_headers = 1 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // Sets a list of headers that will be included to the request to authorization service. Note that - // client request of the same key will be overridden. + // Sets a list of headers that will be included in the request to the authorization service. Note that + // client request headers with the same key will be overridden. repeated config.core.v3.HeaderValue headers_to_add = 2; } @@ -466,7 +467,7 @@ message ExtAuthzPerRoute { // Disable the ext auth filter for this particular vhost or route. // If disabled is specified in multiple per-filter-configs, the most specific one will be used. - // If the filter is disabled by default and this is set to false, the filter will be enabled + // If the filter is disabled by default and this is set to ``false``, the filter will be enabled // for this vhost or route. bool disabled = 1; @@ -476,6 +477,7 @@ message ExtAuthzPerRoute { } // Extra settings for the check request. +// [#next-free-field: 6] message CheckSettings { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.ext_authz.v2.CheckSettings"; @@ -492,15 +494,14 @@ message CheckSettings { // Merge semantics for this field are such that keys from more specific configs override. // // .. note:: - // // These settings are only applied to a filter configured with a // :ref:`grpc_service`. map context_extensions = 1 [(udpa.annotations.sensitive) = true]; - // When set to true, disable the configured :ref:`with_request_body + // When set to ``true``, disable the configured :ref:`with_request_body // ` for a specific route. // - // Please note that only one of *disable_request_body_buffering* or + // Only one of ``disable_request_body_buffering`` and // :ref:`with_request_body ` // may be specified. bool disable_request_body_buffering = 2; @@ -509,8 +510,20 @@ message CheckSettings { // :ref:`with_request_body ` // option for a specific route. // - // Please note that only one of ``with_request_body`` or + // Only one of ``with_request_body`` and // :ref:`disable_request_body_buffering ` // may be specified. BufferSettings with_request_body = 3; + + // Override the external authorization service for this route. + // This allows different routes to use different external authorization service backends + // and service types (gRPC or HTTP). If specified, this overrides the filter-level service + // configuration regardless of the original service type. + oneof service_override { + // Override with a gRPC service configuration. + config.core.v3.GrpcService grpc_service = 4; + + // Override with an HTTP service configuration. + HttpService http_service = 5; + } } diff --git a/api/envoy/extensions/filters/http/ext_proc/v3/BUILD b/api/envoy/extensions/filters/http/ext_proc/v3/BUILD index 5bfeeda1b7b89..ca1f9ec869890 100644 --- a/api/envoy/extensions/filters/http/ext_proc/v3/BUILD +++ b/api/envoy/extensions/filters/http/ext_proc/v3/BUILD @@ -6,9 +6,11 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ + "//envoy/annotations:pkg", "//envoy/config/common/mutation_rules/v3:pkg", "//envoy/config/core/v3:pkg", "//envoy/type/matcher/v3:pkg", + "//envoy/type/v3:pkg", "@com_github_cncf_xds//udpa/annotations:pkg", "@com_github_cncf_xds//xds/annotations/v3:pkg", ], diff --git a/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto b/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto index 54676a15b3af9..695703b4648f1 100644 --- a/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto +++ b/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto @@ -9,6 +9,7 @@ import "envoy/config/core/v3/grpc_service.proto"; import "envoy/config/core/v3/http_service.proto"; import "envoy/extensions/filters/http/ext_proc/v3/processing_mode.proto"; import "envoy/type/matcher/v3/string.proto"; +import "envoy/type/v3/http_status.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; @@ -16,6 +17,7 @@ import "google/protobuf/wrappers.proto"; import "xds/annotations/v3/status.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "validate/validate.proto"; @@ -48,8 +50,6 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // // * Whether it receives the response message at all. // * Whether it receives the message body at all, in separate chunks, or as a single buffer. -// * Whether subsequent HTTP requests are transmitted synchronously or whether they are -// sent asynchronously. // * To modify request or response trailers if they already exist. // // The filter supports up to six different processing steps. Each is represented by @@ -57,9 +57,11 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // processor must send a matching response. // // * Request headers: Contains the headers from the original HTTP request. -// * Request body: Delivered if they are present and sent in a single message if -// the ``BUFFERED`` or ``BUFFERED_PARTIAL`` mode is chosen, in multiple messages if the -// ``STREAMED`` mode is chosen, and not at all otherwise. +// * Request body: If the body is present, the behavior depends on the +// body send mode. In ``BUFFERED`` or ``BUFFERED_PARTIAL`` mode, the body is sent to the external +// processor in a single message. In ``STREAMED`` or ``FULL_DUPLEX_STREAMED`` mode, the body will +// be split across multiple messages sent to the external processor. In ``NONE`` mode, the body +// will not be sent to the external processor. // * Request trailers: Delivered if they are present and if the trailer mode is set // to ``SEND``. // * Response headers: Contains the headers from the HTTP response. Keep in mind @@ -75,7 +77,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // from the external processor. The latter is only enabled if ``allow_mode_override`` is // set to true. This way, a processor may, for example, use information // in the request header to determine whether the message body must be examined, or whether -// the proxy should simply stream it straight through. +// the data plane should simply stream it straight through. // // All of this together allows a server to process the filter traffic in fairly // sophisticated ways. For example: @@ -84,12 +86,8 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // on the content of the headers. // * A server may choose to immediately reject some messages based on their HTTP // headers (or other dynamic metadata) and more carefully examine others. -// * A server may asynchronously monitor traffic coming through the filter by inspecting -// headers, bodies, or both, and then decide to switch to a synchronous processing -// mode, either permanently or temporarily. // -// The protocol itself is based on a bidirectional gRPC stream. Envoy will send the -// server +// The protocol itself is based on a bidirectional gRPC stream. The data plane will send the server // :ref:`ProcessingRequest ` // messages, and the server must reply with // :ref:`ProcessingResponse `. @@ -98,7 +96,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` object in a namespace matching the filter // name. // -// [#next-free-field: 24] +// [#next-free-field: 25] message ExternalProcessor { // Describes the route cache action to be taken when an external processor response // is received in response to request headers. @@ -124,7 +122,6 @@ message ExternalProcessor { reserved "async_mode"; // Configuration for the gRPC service that the filter will communicate with. - // The filter supports both the "Envoy" and "Google" gRPC clients. // Only one of ``grpc_service`` or ``http_service`` can be set. // It is required that one of them must be set. config.core.v3.GrpcService grpc_service = 1 @@ -140,14 +137,14 @@ message ExternalProcessor { // cannot be configured to send any body or trailers. i.e., ``http_service`` only supports // sending request or response headers to the side stream server. // - // With this configuration, Envoy behavior: + // With this configuration, the data plane behavior is: // // 1. The headers are first put in a proto message // :ref:`ProcessingRequest `. // // 2. This proto message is then transcoded into a JSON text. // - // 3. Envoy then sends an HTTP POST message with content-type as "application/json", + // 3. The data plane then sends an HTTP POST message with content-type as "application/json", // and this JSON text as body to the side stream server. // // After the side-stream receives this HTTP request message, it is expected to do as follows: @@ -160,7 +157,7 @@ message ExternalProcessor { // // 3. It converts the ``ProcessingResponse`` proto message into a JSON text. // - // 4. It then sends an HTTP response back to Envoy with status code as ``"200"``, + // 4. It then sends an HTTP response back to the data plane with status code as ``"200"``, // ``content-type`` as ``"application/json"`` and sets the JSON text as the body. // ExtProcHttpService http_service = 20 [ @@ -168,11 +165,7 @@ message ExternalProcessor { (xds.annotations.v3.field_status).work_in_progress = true ]; - // If the ``BodySendMode`` in the - // :ref:`processing_mode ` - // is set to ``FULL_DUPLEX_STREAMED``, ``failure_mode_allow`` can not be set to true. - // - // Otherwise, by default, if in the following cases: + // By default, if in the following cases: // // 1. The gRPC stream cannot be established. // @@ -194,28 +187,31 @@ message ExternalProcessor { // sent. See ``ProcessingMode`` for details. ProcessingMode processing_mode = 3; - // Envoy provides a number of :ref:`attributes ` + // The data plane provides a number of :ref:`attributes ` // for expressive policies. Each attribute name provided in this field will be - // matched against that list and populated in the ``request_headers`` message. + // matched against that list and populated in the + // :ref:`ProcessingRequest.attributes ` field. // See the :ref:`attribute documentation ` // for the list of supported attributes and their types. repeated string request_attributes = 5; - // Envoy provides a number of :ref:`attributes ` + // The data plane provides a number of :ref:`attributes ` // for expressive policies. Each attribute name provided in this field will be - // matched against that list and populated in the ``response_headers`` message. + // matched against that list and populated in the + // :ref:`ProcessingRequest.attributes ` field. // See the :ref:`attribute documentation ` // for the list of supported attributes and their types. repeated string response_attributes = 6; - // Specifies the timeout for each individual message sent on the stream and - // when the filter is running in synchronous mode. Whenever the proxy sends - // a message on the stream that requires a response, it will reset this timer, - // and will stop processing and return an error (subject to the processing mode) - // if the timer expires before a matching response is received. There is no - // timeout when the filter is running in asynchronous mode. Zero is a valid - // config which means the timer will be triggered immediately. If not - // configured, default is 200 milliseconds. + // Specifies the timeout for each individual message sent on the stream. + // Whenever the data plane sends a message on the stream that requires a + // response, it will reset this timer, and will stop processing and return + // an error (subject to the processing mode) if the timer expires before a + // matching response is received. There is no timeout when the filter is + // running in observability mode or when the body send mode is + // ``FULL_DUPLEX_STREAMED``. Zero is a valid config which means the timer + // will be triggered immediately. If not configured, default is 200 + // milliseconds. google.protobuf.Duration message_timeout = 7 [(validate.rules).duration = { lte {seconds: 3600} gte {} @@ -232,7 +228,7 @@ message ExternalProcessor { // :ref:`header_prefix ` // (which is usually "x-envoy"). // Note that changing headers such as "host" or ":authority" may not in itself - // change Envoy's routing decision, as routes can be cached. To also force the + // change the data plane's routing decision, as routes can be cached. To also force the // route to be recomputed, set the // :ref:`clear_route_cache ` // field to true in the same response. @@ -260,6 +256,7 @@ message ExternalProcessor { // can be overridden by the response message from the external processing server // :ref:`mode_override `. // If not set, ``mode_override`` API in the response message will be ignored. + // Mode override is not supported if the body send mode is ``FULL_DUPLEX_STREAMED``. bool allow_mode_override = 14; // If set to true, ignore the @@ -274,10 +271,10 @@ message ExternalProcessor { // If true, send each part of the HTTP request or response specified by ``ProcessingMode`` // without pausing on filter chain iteration. It is "Send and Go" mode that can be used - // by external processor to observe Envoy data and status. In this mode: + // by external processor to observe the request's data and status. In this mode: // - // 1. Only ``STREAMED`` body processing mode is supported and any other body processing modes will be - // ignored. ``NONE`` mode (i.e., skip body processing) will still work as expected. + // 1. Only ``STREAMED`` and ``NONE`` body processing modes are supported; for any other body + // processing mode, the body will not be sent. // // 2. External processor should not send back processing response, as any responses will be ignored. // This also means that @@ -314,12 +311,13 @@ message ExternalProcessor { // Specifies the deferred closure timeout for gRPC stream that connects to external processor. Currently, the deferred stream closure // is only used in :ref:`observability_mode `. // In observability mode, gRPC streams may be held open to the external processor longer than the lifetime of the regular client to - // backend stream lifetime. In this case, Envoy will eventually timeout the external processor stream according to this time limit. + // backend stream lifetime. In this case, the data plane will eventually timeout the external processor stream according to this time limit. // The default value is 5000 milliseconds (5 seconds) if not specified. google.protobuf.Duration deferred_close_timeout = 19; // Send body to the side stream server once it arrives without waiting for the header response from that server. - // It only works for ``STREAMED`` body processing mode. For any other body processing modes, it is ignored. + // It only works for ``STREAMED`` body processing mode. For any other body + // processing modes, it is ignored. // The server has two options upon receiving a header request: // // 1. Instant Response: send the header response as soon as the header request is received. @@ -328,9 +326,9 @@ message ExternalProcessor { // // In all scenarios, the header-body ordering must always be maintained. // - // If enabled Envoy will ignore the + // If enabled the data plane will ignore the // :ref:`mode_override ` - // value that the server sends in the header response. This is because Envoy may have already + // value that the server sends in the header response. This is because the data plane may have already // sent the body to the server, prior to processing the header response. bool send_body_without_waiting_for_header_response = 21; @@ -353,6 +351,12 @@ message ExternalProcessor { // [#extension-category: envoy.http.ext_proc.response_processors] config.core.v3.TypedExtensionConfig on_processing_response = 23 [(xds.annotations.v3.field_status).work_in_progress = true]; + + // Sets the HTTP status code that is returned to the client when the external processing server returns + // an error, fails to respond, or cannot be reached. + // + // The default status is ``HTTP 500 Internal Server Error``. + type.v3.HttpStatus status_on_error = 24; } // ExtProcHttpService is used for HTTP communication between the filter and the external processing service. @@ -434,7 +438,8 @@ message ExtProcOverrides { // [#not-implemented-hide:] // Set a different asynchronous processing option than the default. - bool async_mode = 2; + // Deprecated and not implemented. + bool async_mode = 2 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // [#not-implemented-hide:] // Set different optional attributes than the default setting of the diff --git a/api/envoy/extensions/filters/http/ext_proc/v3/processing_mode.proto b/api/envoy/extensions/filters/http/ext_proc/v3/processing_mode.proto index 467320d4a417b..5ad32af519f80 100644 --- a/api/envoy/extensions/filters/http/ext_proc/v3/processing_mode.proto +++ b/api/envoy/extensions/filters/http/ext_proc/v3/processing_mode.proto @@ -65,8 +65,7 @@ message ProcessingMode { // Do not send the body at all. This is the default. NONE = 0; - // Stream the body to the server in pieces as they arrive at the - // proxy. + // Stream the body to the server in pieces as they are seen. STREAMED = 1; // Buffer the message body in memory and send the entire body at once. @@ -79,11 +78,11 @@ message ProcessingMode { // up to the buffer limit will be sent. BUFFERED_PARTIAL = 3; - // Envoy streams the body to the server in pieces as they arrive. + // The ext_proc client (the data plane) streams the body to the server in pieces as they arrive. // // 1) The server may choose to buffer any number chunks of data before processing them. // After it finishes buffering, the server processes the buffered data. Then it splits the processed - // data into any number of chunks, and streams them back to Envoy one by one. + // data into any number of chunks, and streams them back to the ext_proc client one by one. // The server may continuously do so until the complete body is processed. // The individual response chunk size is recommended to be no greater than 64K bytes, or // :ref:`max_receive_message_length ` @@ -98,15 +97,15 @@ message ProcessingMode { // // In this body mode: // * The corresponding trailer mode has to be set to ``SEND``. - // * Envoy will send body and trailers (if present) to the server as they arrive. + // * The client will send body and trailers (if present) to the server as they arrive. // Sending the trailers (if present) is to inform the server the complete body arrives. - // In case there are no trailers, then Envoy will set + // In case there are no trailers, then the client will set // :ref:`end_of_stream ` // to true as part of the last body chunk request to notify the server that no other data is to be sent. // * The server needs to send // :ref:`StreamedBodyResponse ` - // to Envoy in the body response. - // * Envoy will stream the body chunks in the responses from the server to the upstream/downstream as they arrive. + // to the client in the body response. + // * The client will stream the body chunks in the responses from the server to the upstream/downstream as they arrive. FULL_DUPLEX_STREAMED = 4; } diff --git a/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto b/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto index a34568c0e57f4..662eefc4c84d7 100644 --- a/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto +++ b/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto @@ -27,6 +27,7 @@ message Config { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.header_to_metadata.v2.Config"; + // Specifies the value type to use in metadata. enum ValueType { STRING = 0; @@ -37,14 +38,18 @@ message Config { PROTOBUF_VALUE = 2; } - // ValueEncode defines the encoding algorithm. + // Specifies the encoding scheme for the value. enum ValueEncode { - // The value is not encoded. + // No encoding is applied. NONE = 0; // The value is encoded in `Base64 `_. - // Note: this is mostly used for STRING and PROTOBUF_VALUE to escape the - // non-ASCII characters in the header. + // + // .. note:: + // + // This is mostly used for ``STRING`` and ``PROTOBUF_VALUE`` to escape the + // non-ASCII characters in the header. + // BASE64 = 1; } @@ -74,7 +79,10 @@ message Config { // // This is only used for :ref:`on_header_present `. // - // Note: if the ``value`` field is non-empty this field should be empty. + // .. note:: + // + // If the ``value`` field is non-empty this field should be empty. + // type.matcher.v3.RegexMatchAndSubstitute regex_value_rewrite = 6 [(udpa.annotations.field_migrate).oneof_promotion = "value_type"]; @@ -106,15 +114,15 @@ message Config { (udpa.annotations.field_migrate).oneof_promotion = "header_cookie_specifier" ]; - // If the header or cookie is present, apply this metadata KeyValuePair. + // If the header or cookie is present, apply this metadata ``KeyValuePair``. // - // If the value in the KeyValuePair is non-empty, it'll be used instead + // If the value in the ``KeyValuePair`` is non-empty, it'll be used instead // of the header or cookie value. KeyValuePair on_header_present = 2 [(udpa.annotations.field_migrate).rename = "on_present"]; - // If the header or cookie is not present, apply this metadata KeyValuePair. + // If the header or cookie is not present, apply this metadata ``KeyValuePair``. // - // The value in the KeyValuePair must be set, since it'll be used in lieu + // The value in the ``KeyValuePair`` must be set, since it'll be used in lieu // of the missing header or cookie value. KeyValuePair on_header_missing = 3 [(udpa.annotations.field_migrate).rename = "on_missing"]; @@ -130,4 +138,15 @@ message Config { // The list of rules to apply to responses. repeated Rule response_rules = 2; + + // Optional prefix to use when emitting filter statistics. When configured, + // statistics are emitted with the prefix ``http_filter_name.``. + // + // This emits statistics such as: + // + // - ``http_filter_name.my_header_converter.rules_processed`` + // - ``http_filter_name.my_header_converter.metadata_added`` + // + // If not configured, no statistics are emitted. + string stat_prefix = 3; } diff --git a/api/envoy/extensions/filters/http/on_demand/v3/on_demand.proto b/api/envoy/extensions/filters/http/on_demand/v3/on_demand.proto index 93c9f76f8317c..3e23afe081d5c 100644 --- a/api/envoy/extensions/filters/http/on_demand/v3/on_demand.proto +++ b/api/envoy/extensions/filters/http/on_demand/v3/on_demand.proto @@ -8,7 +8,6 @@ import "google/protobuf/duration.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; -import "validate/validate.proto"; option java_package = "io.envoyproxy.envoy.extensions.filters.http.on_demand.v3"; option java_outer_classname = "OnDemandProto"; @@ -29,7 +28,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message OnDemandCds { // A configuration source for the service that will be used for // on-demand cluster discovery. - config.core.v3.ConfigSource source = 1 [(validate.rules).message = {required: true}]; + config.core.v3.ConfigSource source = 1; // xdstp:// resource locator for on-demand cluster collection. string resources_locator = 2; diff --git a/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto b/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto index e59217b9f6ce4..5c5a9f3e0b57e 100644 --- a/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto +++ b/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto @@ -23,7 +23,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Rate limit :ref:`configuration overview `. // [#extension: envoy.filters.http.ratelimit] -// [#next-free-field: 17] +// [#next-free-field: 18] message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.rate_limit.v2.RateLimit"; @@ -167,6 +167,25 @@ message RateLimit { // This means that when the rate limit service is unavailable, 50% of requests will be denied // (fail closed) and 50% will be allowed (fail open). config.core.v3.RuntimeFractionalPercent failure_mode_deny_percent = 16; + + // Rate limit configuration that is used to generate a list of descriptor entries based on + // the request context. The generated entries will be sent to the rate limit service. + // If this is set, then + // :ref:`VirtualHost.rate_limits` or + // :ref:`RouteAction.rate_limits` fields + // will be ignored. However, :ref:`RateLimitPerRoute.rate_limits` + // will take precedence over this field. + // + // .. note:: + // Not all configuration fields of + // :ref:`rate limit config ` is supported at here. + // Following fields are not supported: + // + // 1. :ref:`rate limit stage `. + // 2. :ref:`dynamic metadata `. + // 3. :ref:`disable_key `. + // 4. :ref:`override limit `. + repeated config.route.v3.RateLimit rate_limits = 17; } message RateLimitPerRoute { @@ -210,8 +229,9 @@ message RateLimitPerRoute { // the request context. The generated entries will be used to find one or multiple matched rate // limit rule from the ``descriptors``. // If this is set, then - // :ref:`VirtualHost.rate_limits` or - // :ref:`RouteAction.rate_limits` fields + // :ref:`VirtualHost.rate_limits`, + // :ref:`RouteAction.rate_limits` and + // :ref:`RateLimit.rate_limits` fields // will be ignored. // // .. note:: diff --git a/api/envoy/extensions/filters/http/reverse_conn/v3/BUILD b/api/envoy/extensions/filters/http/reverse_conn/v3/BUILD new file mode 100644 index 0000000000000..29ebf0741406e --- /dev/null +++ b/api/envoy/extensions/filters/http/reverse_conn/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_xds//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.proto b/api/envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.proto new file mode 100644 index 0000000000000..e081ba51f8b8c --- /dev/null +++ b/api/envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.reverse_conn.v3; + +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.reverse_conn.v3"; +option java_outer_classname = "ReverseConnProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/reverse_conn/v3;reverse_connv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: ReverseConn] +// ReverseConn :ref:`configuration overview `. +// [#extension: envoy.filters.http.reverse_conn] + +message ReverseConn { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.reverse_conn.v3alpha.ReverseConn"; + + google.protobuf.UInt32Value ping_interval = 1; +} diff --git a/api/envoy/extensions/filters/http/tap/v3/tap.proto b/api/envoy/extensions/filters/http/tap/v3/tap.proto index 0ed35de97095b..0c12c3f60d247 100644 --- a/api/envoy/extensions/filters/http/tap/v3/tap.proto +++ b/api/envoy/extensions/filters/http/tap/v3/tap.proto @@ -34,4 +34,7 @@ message Tap { // Indicates whether report downstream connection info bool record_downstream_connection = 3; + + // If enabled, upstream connection information will be reported. + bool record_upstream_connection = 4; } diff --git a/api/envoy/extensions/filters/listener/reverse_connection/v3/BUILD b/api/envoy/extensions/filters/listener/reverse_connection/v3/BUILD new file mode 100644 index 0000000000000..29ebf0741406e --- /dev/null +++ b/api/envoy/extensions/filters/listener/reverse_connection/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_xds//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/filters/listener/reverse_connection/v3/reverse_connection.proto b/api/envoy/extensions/filters/listener/reverse_connection/v3/reverse_connection.proto new file mode 100644 index 0000000000000..e781ae375c5f3 --- /dev/null +++ b/api/envoy/extensions/filters/listener/reverse_connection/v3/reverse_connection.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package envoy.extensions.filters.listener.reverse_connection.v3; + +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.listener.reverse_connection.v3"; +option java_outer_classname = "ReverseConnectionProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/reverse_connection/v3;reverse_connectionv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Reverse Connection Filter] +// Reverse Connection listener filter. +// [#extension: envoy.filters.listener.reverse_connection] + +message ReverseConnection { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.listener.reverse_connection.v3alpha.ReverseConnection"; + + google.protobuf.UInt32Value ping_wait_timeout = 1; +} diff --git a/api/envoy/extensions/filters/network/ext_proc/v3/ext_proc.proto b/api/envoy/extensions/filters/network/ext_proc/v3/ext_proc.proto index 744c6f7bdeb83..f37feaa94f8d1 100644 --- a/api/envoy/extensions/filters/network/ext_proc/v3/ext_proc.proto +++ b/api/envoy/extensions/filters/network/ext_proc/v3/ext_proc.proto @@ -45,11 +45,9 @@ message NetworkExternalProcessor { // prematurely with an error, the filter will fail, leading to the close of connection. // With this parameter set to true, however, then if the gRPC stream is prematurely closed // or could not be opened, processing continues without error. - // [#not-implemented-hide:] bool failure_mode_allow = 2; // Options for controlling processing behavior. - // [#not-implemented-hide:] ProcessingMode processing_mode = 3; // Specifies the timeout for each individual message sent on the stream and @@ -57,7 +55,6 @@ message NetworkExternalProcessor { // the proxy sends a message on the stream that requires a response, it will // reset this timer, and will stop processing and return an error (subject // to the processing mode) if the timer expires. Default is 200 ms. - // [#not-implemented-hide:] google.protobuf.Duration message_timeout = 4 [(validate.rules).duration = { lte {seconds: 3600} gte {} diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index e0282af86e6ef..730e065e6c417 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -37,7 +37,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 59] +// [#next-free-field: 60] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -527,16 +527,6 @@ message HttpConnectionManager { // is terminated with a 408 Request Timeout error code if no upstream response // header has been received, otherwise a stream reset occurs. // - // This timeout also specifies the amount of time that Envoy will wait for the peer to open enough - // window to write any remaining stream data once the entirety of stream data (local end stream is - // true) has been buffered pending available window. In other words, this timeout defends against - // a peer that does not release enough window to completely write the stream, even though all - // data has been proxied within available flow control windows. If the timeout is hit in this - // case, the :ref:`tx_flush_timeout ` counter will be - // incremented. Note that :ref:`max_stream_duration - // ` does not apply to - // this corner case. - // // If the :ref:`overload action ` "envoy.overload_actions.reduce_timeouts" // is configured, this timeout is scaled according to the value for // :ref:`HTTP_DOWNSTREAM_STREAM_IDLE `. @@ -549,9 +539,29 @@ message HttpConnectionManager { // // A value of 0 will completely disable the connection manager stream idle // timeout, although per-route idle timeout overrides will continue to apply. + // + // This timeout is also used as the default value for :ref:`stream_flush_timeout + // `. google.protobuf.Duration stream_idle_timeout = 24 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // The stream flush timeout for connections managed by the connection manager. + // + // If not specified, the value of stream_idle_timeout is used. This is for backwards compatibility + // since this was the original behavior. In essence this timeout is an override for the + // stream_idle_timeout that applies specifically to the end of stream flush case. + // + // This timeout specifies the amount of time that Envoy will wait for the peer to open enough + // window to write any remaining stream data once the entirety of stream data (local end stream is + // true) has been buffered pending available window. In other words, this timeout defends against + // a peer that does not release enough window to completely write the stream, even though all + // data has been proxied within available flow control windows. If the timeout is hit in this + // case, the :ref:`tx_flush_timeout ` counter will be + // incremented. Note that :ref:`max_stream_duration + // ` does not apply to + // this corner case. + google.protobuf.Duration stream_flush_timeout = 59; + // The amount of time that Envoy will wait for the entire request to be received. // The timer is activated when the request is initiated, and is disarmed when the last byte of the // request is sent upstream (i.e. all decoding filters have processed the request), OR when the @@ -1036,7 +1046,7 @@ message Rds { "envoy.config.filter.network.http_connection_manager.v2.Rds"; // Configuration source specifier for RDS. - config.core.v3.ConfigSource config_source = 1 [(validate.rules).message = {required: true}]; + config.core.v3.ConfigSource config_source = 1; // The name of the route configuration. This name will be passed to the RDS // API. This allows an Envoy configuration with multiple HTTP listeners (and diff --git a/api/envoy/extensions/filters/network/reverse_tunnel/v3/BUILD b/api/envoy/extensions/filters/network/reverse_tunnel/v3/BUILD new file mode 100644 index 0000000000000..09a37ad16b837 --- /dev/null +++ b/api/envoy/extensions/filters/network/reverse_tunnel/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.proto b/api/envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.proto new file mode 100644 index 0000000000000..fbb5bd14e30e1 --- /dev/null +++ b/api/envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.reverse_tunnel.v3; + +import "envoy/config/core/v3/base.proto"; + +import "google/protobuf/duration.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.reverse_tunnel.v3"; +option java_outer_classname = "ReverseTunnelProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/reverse_tunnel/v3;reverse_tunnelv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Reverse Tunnel Network Filter] +// Reverse Tunnel Network Filter :ref:`configuration overview `. +// [#extension: envoy.filters.network.reverse_tunnel] + +// Configuration for the reverse tunnel network filter. +// This filter handles reverse tunnel connection acceptance and rejection by processing +// HTTP requests where required identification values are provided via HTTP headers. +message ReverseTunnel { + // Ping interval for health checks on established reverse tunnel connections. + // If not specified, defaults to 2 seconds. + google.protobuf.Duration ping_interval = 1 [(validate.rules).duration = { + lte {seconds: 300} + gte {nanos: 1000000} + }]; + + // Whether to automatically close connections after processing reverse tunnel requests. + // When set to true, connections are closed after acceptance or rejection. + // When set to false, connections remain open for potential reuse. Defaults to false. + bool auto_close_connections = 2; + + // HTTP path to match for reverse tunnel requests. + // If not specified, defaults to "/reverse_connections/request". + string request_path = 3 [(validate.rules).string = {min_len: 1 max_len: 255 ignore_empty: true}]; + + // HTTP method to match for reverse tunnel requests. + // If not specified (``METHOD_UNSPECIFIED``), this defaults to ``GET``. + config.core.v3.RequestMethod request_method = 4 [(validate.rules).enum = {defined_only: true}]; +} diff --git a/api/envoy/extensions/filters/network/tcp_proxy/v3/BUILD b/api/envoy/extensions/filters/network/tcp_proxy/v3/BUILD index c9c87b7395d55..234d249142b52 100644 --- a/api/envoy/extensions/filters/network/tcp_proxy/v3/BUILD +++ b/api/envoy/extensions/filters/network/tcp_proxy/v3/BUILD @@ -9,6 +9,7 @@ api_proto_package( "//envoy/annotations:pkg", "//envoy/config/accesslog/v3:pkg", "//envoy/config/core/v3:pkg", + "//envoy/extensions/filters/network/http_connection_manager/v3:pkg", "//envoy/type/v3:pkg", "@com_github_cncf_xds//udpa/annotations:pkg", ], diff --git a/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto b/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto index f4d57c969ec72..872c736f5a019 100644 --- a/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto +++ b/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto @@ -7,6 +7,7 @@ import "envoy/config/core/v3/backoff.proto"; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/config_source.proto"; import "envoy/config/core/v3/proxy_protocol.proto"; +import "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto"; import "envoy/type/v3/hash_policy.proto"; import "google/protobuf/duration.proto"; @@ -67,7 +68,7 @@ message TcpProxy { // Configuration for tunneling TCP over other transports or application layers. // Tunneling is supported over both HTTP/1.1 and HTTP/2. Upstream protocol is // determined by the cluster configuration. - // [#next-free-field: 7] + // [#next-free-field: 8] message TunnelingConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.tcp_proxy.v2.TcpProxy.TunnelingConfig"; @@ -116,6 +117,19 @@ message TcpProxy { // Save the response trailers to the downstream info filter state for consumption // by the network filters. The filter state key is ``envoy.tcp_proxy.propagate_response_trailers``. bool propagate_response_trailers = 6; + + // The configuration of the request ID extension used for generation, validation, and + // associated tracing operations when tunneling. + // + // If this field is set, a request ID is generated using the specified extension. If + // this field is not set, no request ID is generated. + // + // When a request ID is generated, it is also stored in the downstream connection's + // dynamic metadata under the namespace ``envoy.filters.network.tcp_proxy`` with the key + // ``tunnel_request_id`` to allow emission from TCP proxy access logs via the + // ``%DYNAMIC_METADATA(envoy.filters.network.tcp_proxy:tunnel_request_id)%`` formatter. + // [#extension-category: envoy.request_id] + http_connection_manager.v3.RequestIDExtension request_id_extension = 7; } message OnDemand { diff --git a/api/envoy/extensions/filters/udp/dns_filter/v3/dns_filter.proto b/api/envoy/extensions/filters/udp/dns_filter/v3/dns_filter.proto index 70c41643ae777..322dabd24d6ec 100644 --- a/api/envoy/extensions/filters/udp/dns_filter/v3/dns_filter.proto +++ b/api/envoy/extensions/filters/udp/dns_filter/v3/dns_filter.proto @@ -102,6 +102,13 @@ message DnsFilterConfig { // Client context configuration controls Envoy's behavior when it must use external // resolvers to answer a query. This object is optional and if omitted instructs - // the filter to resolve queries from the data in the server_config + // the filter to resolve queries from the data in the server_config. + // Also, if ``client_config`` is omitted, here is the Envoy's behavior to create DNS resolver: + // + // 1. If :ref:`typed_dns_resolver_config ` + // is not empty, uses it. + // + // 2. Otherwise, uses the default c-ares DNS resolver. + // ClientContextConfig client_config = 3; } diff --git a/api/envoy/extensions/formatter/cel/v3/cel.proto b/api/envoy/extensions/formatter/cel/v3/cel.proto index 265f9dd352da1..ced34e735f00d 100644 --- a/api/envoy/extensions/formatter/cel/v3/cel.proto +++ b/api/envoy/extensions/formatter/cel/v3/cel.proto @@ -30,6 +30,23 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // * ``%CEL(request.headers['x-envoy-original-path']):10%`` // * ``%CEL(request.headers['x-log-mtls'] || request.url_path.contains('v1beta3'))%`` +// Alternatively: %TYPED_CEL(EXPRESSION):Z% +// When using a non-text access log format like JSON, this format command is +// able to emit values of non-string types, like number, boolean, and null, +// based on the output of the CEL expression. It otherwise functions the same as +// %CEL%. CEL types not native to JSON are coerced as follows: +// +// * Bytes are base64 encoded to produce a string. +// * Durations are stringified as a count of seconds, e.g. `duration("1h30m")` +// becomes "5400s". +// * Timestamps are formatted to UTC, e.g. +// `timestamp("2023-08-26T12:39:00-07:00")` becomes +// "2023-08-26T19:39:00+00:00" +// * Maps become objects, provided all keys can be coerced to strings and that +// all values can coerce to types representable in JSON. +// * Lists become lists, provided all values can coerce to types representable +// in JSON. + // Configuration for the CEL formatter. // // .. warning:: diff --git a/api/envoy/extensions/geoip_providers/common/v3/common.proto b/api/envoy/extensions/geoip_providers/common/v3/common.proto index e289751f8efe6..d4ccf4ebca2b6 100644 --- a/api/envoy/extensions/geoip_providers/common/v3/common.proto +++ b/api/envoy/extensions/geoip_providers/common/v3/common.proto @@ -17,8 +17,8 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Common configuration shared across geolocation providers. message CommonGeoipProviderConfig { - // The set of geolocation headers to add to request. If any of the configured headers is present - // in the incoming request, it will be overridden by the :ref:`Geoip filter `. + // The set of geolocation headers to add to the request. If any of the configured headers is present + // in the incoming request, it will be overridden by the :ref:`GeoIP filter `. // [#next-free-field: 13] message GeolocationHeadersToAdd { // If set, the header will be used to populate the country ISO code associated with the IP address. @@ -30,7 +30,7 @@ message CommonGeoipProviderConfig { [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; // If set, the header will be used to populate the region ISO code associated with the IP address. - // The least specific subdivision will be selected as region value. + // The least specific subdivision will be selected as the region value. string region = 3 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; @@ -38,35 +38,35 @@ message CommonGeoipProviderConfig { string asn = 4 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; - // This field is being deprecated, use ``anon`` instead. + // This field is deprecated; use ``anon`` instead. string is_anon = 5 [ deprecated = true, (validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}, (envoy.annotations.deprecated_at_minor_version) = "3.0" ]; - // If set, the IP address will be checked if it belongs to any type of anonymization network (e.g. VPN, public proxy etc) - // and header will be populated with the check result. Header value will be set to either "true" or "false" depending on the check result. + // If set, the IP address will be checked if it belongs to any type of anonymization network (e.g., VPN, public proxy). + // The header will be populated with the check result. Header value will be set to either ``true`` or ``false`` depending on the check result. string anon = 12 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; - // If set, the IP address will be checked if it belongs to a VPN and header will be populated with the check result. - // Header value will be set to either "true" or "false" depending on the check result. + // If set, the IP address will be checked if it belongs to a VPN and the header will be populated with the check result. + // Header value will be set to either ``true`` or ``false`` depending on the check result. string anon_vpn = 6 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; - // If set, the IP address will be checked if it belongs to a hosting provider and header will be populated with the check result. - // Header value will be set to either "true" or "false" depending on the check result. + // If set, the IP address will be checked if it belongs to a hosting provider and the header will be populated with the check result. + // Header value will be set to either ``true`` or ``false`` depending on the check result. string anon_hosting = 7 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; - // If set, the IP address will be checked if it belongs to a TOR exit node and header will be populated with the check result. - // Header value will be set to either "true" or "false" depending on the check result. + // If set, the IP address will be checked if it belongs to a TOR exit node and the header will be populated with the check result. + // Header value will be set to either ``true`` or ``false`` depending on the check result. string anon_tor = 8 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; - // If set, the IP address will be checked if it belongs to a public proxy and header will be populated with the check result. - // Header value will be set to either "true" or "false" depending on the check result. + // If set, the IP address will be checked if it belongs to a public proxy and the header will be populated with the check result. + // Header value will be set to either ``true`` or ``false`` depending on the check result. string anon_proxy = 9 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; @@ -74,12 +74,12 @@ message CommonGeoipProviderConfig { string isp = 10 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; - // If set, the IP address will be checked if it belongs to the ISP named iCloud Private Relay and header will be populated with the check result. - // Header value will be set to either "true" or "false" depending on the check result. + // If set, the IP address will be checked if it belongs to the ISP named iCloud Private Relay and the header will be populated with the check result. + // Header value will be set to either ``true`` or ``false`` depending on the check result. string apple_private_relay = 11 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME ignore_empty: true}]; } - // Configuration for geolocation headers to add to request. + // Configuration for geolocation headers to add to the request. GeolocationHeadersToAdd geo_headers_to_add = 1 [(validate.rules).message = {required: true}]; } diff --git a/api/envoy/extensions/geoip_providers/maxmind/v3/maxmind.proto b/api/envoy/extensions/geoip_providers/maxmind/v3/maxmind.proto index fb665ac272e70..c1d7a2480ebd1 100644 --- a/api/envoy/extensions/geoip_providers/maxmind/v3/maxmind.proto +++ b/api/envoy/extensions/geoip_providers/maxmind/v3/maxmind.proto @@ -18,29 +18,32 @@ option (xds.annotations.v3.file_status).work_in_progress = true; // [#protodoc-title: MaxMind Geolocation Provider] // MaxMind geolocation provider :ref:`configuration overview `. -// At least one geolocation database path :ref:`city_db_path `, -// :ref:`isp_db_path ` or -// :ref:`asn_db_path ` or -// :ref:`anon_db_path ` must be configured. +// +// At least one geolocation database path must be configured: +// +// * :ref:`city_db_path ` +// * :ref:`isp_db_path ` +// * :ref:`asn_db_path ` +// * :ref:`anon_db_path ` // [#extension: envoy.geoip_providers.maxmind] // [#next-free-field: 6] message MaxMindConfig { - // Full file path to the Maxmind city database, e.g. /etc/GeoLite2-City.mmdb. - // Database file is expected to have .mmdb extension. + // Full file path to the MaxMind city database, e.g., ``/etc/GeoLite2-City.mmdb``. + // Database file is expected to have ``.mmdb`` extension. string city_db_path = 1 [(validate.rules).string = {pattern: "^$|^.*\\.mmdb$"}]; - // Full file path to the Maxmind ASN database, e.g. /etc/GeoLite2-ASN.mmdb. - // Database file is expected to have .mmdb extension. - // When is defined the ASN information will always be fetched from the ``asn_db``. + // Full file path to the MaxMind ASN database, e.g., ``/etc/GeoLite2-ASN.mmdb``. + // Database file is expected to have ``.mmdb`` extension. + // When this is defined, the ASN information will always be fetched from the ``asn_db``. string asn_db_path = 2 [(validate.rules).string = {pattern: "^$|^.*\\.mmdb$"}]; - // Full file path to the Maxmind anonymous IP database, e.g. /etc/GeoIP2-Anonymous-IP.mmdb. - // Database file is expected to have .mmdb extension. + // Full file path to the MaxMind Anonymous IP database, e.g., ``/etc/GeoIP2-Anonymous-IP.mmdb``. + // Database file is expected to have ``.mmdb`` extension. string anon_db_path = 3 [(validate.rules).string = {pattern: "^$|^.*\\.mmdb$"}]; - // Full file path to the Maxmind ISP database, e.g. /etc/GeoLite2-ISP.mmdb. - // Database file is expected to have .mmdb extension. + // Full file path to the MaxMind ISP database, e.g., ``/etc/GeoLite2-ISP.mmdb``. + // Database file is expected to have ``.mmdb`` extension. // If ``asn_db_path`` is not defined, ASN information will be fetched from // ``isp_db`` instead. string isp_db_path = 5 [(validate.rules).string = {pattern: "^$|^.*\\.mmdb$"}]; diff --git a/api/envoy/extensions/http/header_validators/envoy_default/v3/header_validator.proto b/api/envoy/extensions/http/header_validators/envoy_default/v3/header_validator.proto index b0dc6ce84991e..0a1e88fb56db7 100644 --- a/api/envoy/extensions/http/header_validators/envoy_default/v3/header_validator.proto +++ b/api/envoy/extensions/http/header_validators/envoy_default/v3/header_validator.proto @@ -15,25 +15,33 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // This extension validates that HTTP request and response headers are well formed according to respective RFCs. // -// #. HTTP/1 header map validity according to `RFC 7230 section 3.2 `_ -// #. Syntax of HTTP/1 request target URI and response status -// #. HTTP/2 header map validity according to `RFC 7540 section 8.1.2 `_ -// #. Syntax of HTTP/2 pseudo headers -// #. HTTP/3 header map validity according to `RFC 9114 section 4.3 `_ -// #. Syntax of HTTP/3 pseudo headers -// #. Syntax of Content-Length and Transfer-Encoding -// #. Validation of HTTP/1 requests with both ``Content-Length`` and ``Transfer-Encoding`` headers +// The validator performs comprehensive HTTP header validation including: +// +// #. HTTP/1 header map validity according to `RFC 7230 section 3.2 `_. +// #. Syntax of HTTP/1 request target URI and response status. +// #. HTTP/2 header map validity according to `RFC 7540 section 8.1.2 `_. +// #. Syntax of HTTP/2 pseudo headers. +// #. HTTP/3 header map validity according to `RFC 9114 section 4.3 `_. +// #. Syntax of HTTP/3 pseudo headers. +// #. Syntax of Content-Length and Transfer-Encoding. +// #. Validation of HTTP/1 requests with both ``Content-Length`` and ``Transfer-Encoding`` headers. // #. Normalization of the URI path according to `Normalization and Comparison `_ -// without `case normalization `_ +// without `case normalization `_. +// +// This validator ensures that HTTP traffic processed by Envoy conforms to established +// standards and helps prevent issues caused by malformed headers or invalid HTTP syntax. // // [#comment:TODO(yanavlasov): Put #extension: envoy.http.header_validators.envoy_default after it is not hidden any more] // [#next-free-field: 6] message HeaderValidatorConfig { // Action to take when Envoy receives client request with header names containing underscore // characters. - // Underscore character is allowed in header names by the RFC-7230 and this behavior is implemented - // as a security measure due to systems that treat '_' and '-' as interchangeable. Envoy by default allows client request headers with underscore - // characters. + // + // Underscore character is allowed in header names by RFC-7230, and this behavior is implemented + // as a security measure due to systems that treat ``_`` and ``-`` as interchangeable. Envoy by + // default allows client request headers with underscore characters. + // + // This setting provides control over how to handle such headers for security and compatibility reasons. enum HeadersWithUnderscoresAction { // Allow headers with underscores. This is the default behavior. ALLOW = 0; @@ -51,102 +59,170 @@ message HeaderValidatorConfig { DROP_HEADER = 2; } + // Configuration options for URI path normalization and transformation. + // + // These options control how Envoy processes and normalizes incoming request URI paths + // to ensure consistent behavior and security. Path normalization helps prevent + // path traversal attacks and ensures that equivalent paths are handled consistently. message UriPathNormalizationOptions { // Determines the action for requests that contain ``%2F``, ``%2f``, ``%5C`` or ``%5c`` sequences in the URI path. // This operation occurs before URL normalization and the merge slashes transformations if they were enabled. + // + // Escaped slash sequences in URLs can be used for path confusion attacks, so proper handling + // is important for security. enum PathWithEscapedSlashesAction { // Default behavior specific to implementation (i.e. Envoy) of this configuration option. // Envoy, by default, takes the ``KEEP_UNCHANGED`` action. - // NOTE: the implementation may change the default behavior at-will. + // + // .. note:: + // + // The implementation may change the default behavior at-will. + // IMPLEMENTATION_SPECIFIC_DEFAULT = 0; - // Keep escaped slashes. + // Keep escaped slashes unchanged in the URI path. + // This preserves the original request path without any modifications to escaped sequences. KEEP_UNCHANGED = 1; // Reject client request with the 400 status. gRPC requests will be rejected with the ``INTERNAL`` (13) error code. - // The ``http#.downstream_rq_failed_path_normalization`` counter is incremented for each rejected request. + // The :ref:`httpN.downstream_rq_failed_path_normalization ` counter is incremented for each rejected request. + // + // This is the safest option when security is a primary concern, as it prevents any potential + // path confusion attacks by rejecting requests with escaped slashes entirely. REJECT_REQUEST = 2; // Unescape ``%2F`` and ``%5C`` sequences and redirect the request to the new path if these sequences were present. // The redirect occurs after path normalization and merge slashes transformations if they were configured. - // NOTE: gRPC requests will be rejected with the ``INTERNAL`` (13) error code. - // This option minimizes possibility of path confusion exploits by forcing request with unescaped slashes to - // traverse all parties: downstream client, intermediate proxies, Envoy and upstream server. - // The ``http#.downstream_rq_redirected_with_normalized_path`` counter is incremented for each + // + // .. note:: + // + // gRPC requests will be rejected with the ``INTERNAL`` (13) error code. + // This option minimizes possibility of path confusion exploits by forcing request with unescaped slashes to + // traverse all parties: downstream client, intermediate proxies, Envoy and upstream server. + // + // The :ref:`httpN.downstream_rq_redirected_with_normalized_path ` counter is incremented for each // redirected request. UNESCAPE_AND_REDIRECT = 3; // Unescape ``%2F`` and ``%5C`` sequences. - // Note: this option should not be enabled if intermediaries perform path based access control as - // it may lead to path confusion vulnerabilities. + // + // .. attention:: + // + // This option should not be enabled if intermediaries perform path based access control as + // it may lead to path confusion vulnerabilities. + // UNESCAPE_AND_FORWARD = 4; } // Should paths be normalized according to RFC 3986? + // // This operation overwrites the original request URI path and the new path is used for processing of // the request by HTTP filters and proxied to the upstream service. // Envoy will respond with 400 to requests with malformed paths that fail path normalization. // The default behavior is to normalize the path. + // // This value may be overridden by the runtime variable // :ref:`http_connection_manager.normalize_path`. // See `Normalization and Comparison `_ // for details of normalization. - // Note that Envoy does not perform - // `case normalization `_ - // URI path normalization can be applied to a portion of requests by setting the - // ``envoy_default_header_validator.path_normalization`` runtime value. + // + // .. note:: + // + // Envoy does not perform + // `case normalization `_. + // URI path normalization can be applied to a portion of requests by setting the + // ``envoy_default_header_validator.path_normalization`` runtime value. + // bool skip_path_normalization = 1; // Determines if adjacent slashes in the path are merged into one. + // // This operation overwrites the original request URI path and the new path is used for processing of // the request by HTTP filters and proxied to the upstream service. - // Setting this option to true will cause incoming requests with path ``//dir///file`` to not match against - // route with ``prefix`` match set to ``/dir``. Defaults to ``false``. Note that slash merging is not part of - // `HTTP spec `_ and is provided for convenience. - // Merging of slashes in URI path can be applied to a portion of requests by setting the - // ``envoy_default_header_validator.merge_slashes`` runtime value. + // Setting this option to ``true`` will cause incoming requests with path ``//dir///file`` to not match against + // route with ``prefix`` match set to ``/dir``. Defaults to ``false``. + // + // .. note:: + // + // Slash merging is not part of the + // `HTTP spec `_ and is provided for convenience. + // Merging of slashes in URI path can be applied to a portion of requests by setting the + // ``envoy_default_header_validator.merge_slashes`` runtime value. + // bool skip_merging_slashes = 2; // The action to take when request URL path contains escaped slash sequences (``%2F``, ``%2f``, ``%5C`` and ``%5c``). + // // This operation may overwrite the original request URI path and the new path is used for processing of // the request by HTTP filters and proxied to the upstream service. + // + // The handling of escaped slashes is important for security as these sequences can be used + // in path confusion attacks to bypass access controls. PathWithEscapedSlashesAction path_with_escaped_slashes_action = 3 [(validate.rules).enum = {defined_only: true}]; } + // HTTP/1 protocol specific options for header validation. + // + // These options control how Envoy handles HTTP/1 specific behaviors and edge cases + // that may not apply to HTTP/2 or HTTP/3 protocols. message Http1ProtocolOptions { // Allows Envoy to process HTTP/1 requests/responses with both ``Content-Length`` and ``Transfer-Encoding`` // headers set. By default such messages are rejected, but if option is enabled - Envoy will // remove the ``Content-Length`` header and process the message. + // // See `RFC7230, sec. 3.3.3 `_ for details. // // .. attention:: + // // Enabling this option might lead to request smuggling vulnerabilities, especially if traffic // is proxied via multiple layers of proxies. + // bool allow_chunked_length = 1; } + // HTTP/1 protocol specific options. + // These settings control HTTP/1 specific validation behaviors. Http1ProtocolOptions http1_protocol_options = 1; // The URI path normalization options. + // // By default Envoy normalizes URI path using the default values of the :ref:`UriPathNormalizationOptions // `. // URI path transformations specified by the ``uri_path_normalization_options`` configuration can be applied to a portion // of requests by setting the ``envoy_default_header_validator.uri_path_transformations`` runtime value. - // Caution: disabling path normalization may lead to path confusion vulnerabilities in access control or incorrect service - // selection. + // + // .. attention:: + // + // Disabling path normalization may lead to path confusion vulnerabilities in access control or incorrect service + // selection. + // UriPathNormalizationOptions uri_path_normalization_options = 2; - // Restrict HTTP methods to these defined in the `RFC 7231 section 4.1 `_ + // Restrict HTTP methods to these defined in the `RFC 7231 section 4.1 `_. + // // Envoy will respond with 400 to requests with disallowed methods. // By default methods with arbitrary names are accepted. + // + // This setting helps enforce HTTP compliance and can prevent attacks that rely on + // non-standard HTTP methods. bool restrict_http_methods = 3; // Action to take when a client request with a header name containing underscore characters is received. - // If this setting is not specified, the value defaults to ALLOW. + // + // If this setting is not specified, the value defaults to ``ALLOW``. + // + // This setting provides security control over headers with underscores, which can be a source + // of security issues when different systems interpret underscores and hyphens differently. HeadersWithUnderscoresAction headers_with_underscores_action = 4; // Allow requests with fragment in URL path and strip the fragment before request processing. - // By default Envoy rejects requests with fragment in URL path. + // + // By default Envoy rejects requests with fragment in URL path. When this option is enabled, + // the fragment portion (everything after ``#``) will be removed from the path before + // further processing. + // + // Fragments are typically used by client-side applications and should not normally + // be sent to the server, so stripping them can help normalize requests. bool strip_fragment_from_path = 5; } diff --git a/api/envoy/extensions/load_balancing_policies/common/v3/common.proto b/api/envoy/extensions/load_balancing_policies/common/v3/common.proto index 3efea24772cb9..22faf11b9c5b8 100644 --- a/api/envoy/extensions/load_balancing_policies/common/v3/common.proto +++ b/api/envoy/extensions/load_balancing_policies/common/v3/common.proto @@ -24,8 +24,17 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message LocalityLbConfig { // Configuration for :ref:`zone aware routing // `. - // [#next-free-field: 6] + // [#next-free-field: 7] message ZoneAwareLbConfig { + // Basis for computing per-locality percentages in zone-aware routing. + enum LocalityBasis { + // Use the number of healthy hosts in each locality. + HEALTHY_HOSTS_NUM = 0; + + // Use the weights of healthy hosts in each locality. + HEALTHY_HOSTS_WEIGHT = 1; + } + // Configures Envoy to always route requests to the local zone regardless of the // upstream zone structure. In Envoy's default configuration, traffic is distributed proportionally // across all upstream hosts while trying to maximize local routing when possible. The approach @@ -67,6 +76,12 @@ message LocalityLbConfig { [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; ForceLocalZone force_local_zone = 5; + + // Determines how locality percentages are computed: + // - HEALTHY_HOSTS_NUM: proportional to the count of healthy hosts. + // - HEALTHY_HOSTS_WEIGHT: proportional to the weights of healthy hosts. + // Default value is HEALTHY_HOSTS_NUM if unset. + LocalityBasis locality_basis = 6; } // Configuration for :ref:`locality weighted load balancing diff --git a/api/envoy/extensions/matching/common_inputs/network/v3/network_inputs.proto b/api/envoy/extensions/matching/common_inputs/network/v3/network_inputs.proto index bea415a7101ff..b62690b4f262f 100644 --- a/api/envoy/extensions/matching/common_inputs/network/v3/network_inputs.proto +++ b/api/envoy/extensions/matching/common_inputs/network/v3/network_inputs.proto @@ -148,3 +148,17 @@ message DynamicMetadataInput { // The path to retrieve the Value from the Struct. repeated PathSegment path = 2 [(validate.rules).repeated = {min_items: 1}]; } + +// Input that matches by the network namespace of the listener address. +// This input returns the network namespace filepath that was used to create the listening socket. +// On Linux systems, this corresponds to the ``network_namespace_filepath`` field in the +// :ref:`SocketAddress ` configuration. +// +// .. note:: +// +// This input is only meaningful on Linux systems where network namespaces are supported. +// On other platforms, this input will always return an empty value. +// +// [#extension: envoy.matching.inputs.network_namespace] +message NetworkNamespaceInput { +} diff --git a/api/envoy/extensions/network/dns_resolver/cares/v3/cares_dns_resolver.proto b/api/envoy/extensions/network/dns_resolver/cares/v3/cares_dns_resolver.proto index b36a3a0d095e0..b060bce969b42 100644 --- a/api/envoy/extensions/network/dns_resolver/cares/v3/cares_dns_resolver.proto +++ b/api/envoy/extensions/network/dns_resolver/cares/v3/cares_dns_resolver.proto @@ -5,6 +5,7 @@ package envoy.extensions.network.dns_resolver.cares.v3; import "envoy/config/core/v3/address.proto"; import "envoy/config/core/v3/resolver.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; import "udpa/annotations/status.proto"; @@ -20,7 +21,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.network.dns_resolver.cares] // Configuration for c-ares DNS resolver. -// [#next-free-field: 9] +// [#next-free-field: 11] message CaresDnsResolverConfig { // A list of DNS resolver addresses. // :ref:`use_resolvers_as_fallback ` @@ -77,4 +78,25 @@ message CaresDnsResolverConfig { // This setting overrides any system configuration for name server rotation. // bool rotate_nameservers = 8; + + // Maximum EDNS0 UDP payload size in bytes. + // If set, c-ares will include EDNS0 in DNS queries and use this value as the maximum UDP response size. + // + // Recommended values: + // + // * **1232**: Safe default (avoids fragmentation). + // * **4096**: Maximum allowed. + // + // If unset, c-ares uses its internal default (usually 1232). + google.protobuf.UInt32Value edns0_max_payload_size = 9 + [(validate.rules).uint32 = {lte: 4096 gte: 512}]; + + // The maximum duration for which a UDP channel will be kept alive before being refreshed. + // + // If set, the DNS resolver will periodically reinitialize its c-ares channel after the + // specified duration. This can help with avoiding stale socket states, and providing + // better load distribution across UDP ports. + // + // If not specified, no periodic refresh will be performed. + google.protobuf.Duration max_udp_channel_duration = 10 [(validate.rules).duration = {gte {}}]; } diff --git a/api/envoy/extensions/quic/connection_id_generator/quic_lb/v3/quic_lb.proto b/api/envoy/extensions/quic/connection_id_generator/quic_lb/v3/quic_lb.proto index 446ff959d08e8..0b89583969138 100644 --- a/api/envoy/extensions/quic/connection_id_generator/quic_lb/v3/quic_lb.proto +++ b/api/envoy/extensions/quic/connection_id_generator/quic_lb/v3/quic_lb.proto @@ -31,7 +31,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // // This is still a work in progress. Performance is expected to be poor. Interoperability testing // has not yet been performed. -// [#next-free-field: 6] +// [#next-free-field: 7] message Config { option (xds.annotations.v3.message_status).work_in_progress = true; @@ -45,6 +45,13 @@ message Config { // See https://datatracker.ietf.org/doc/html/draft-ietf-quic-load-balancers#name-server-id-allocation. config.core.v3.DataSource server_id = 2 [(validate.rules).message = {required: true}]; + // If true, indicates that the :ref:`server_id + // ` is base64 encoded. + // + // This can be useful if the ID may contain binary data and must be transmitted as a string, for example in + // an environment variable. + bool server_id_base64_encoded = 6; + // Optional validation of the expected server ID length. If this is non-zero and the value in ``server_id`` // does not have a matching length, a configuration error is generated. This can be useful for validating // that the server ID is valid. diff --git a/api/envoy/extensions/stat_sinks/open_telemetry/v3/open_telemetry.proto b/api/envoy/extensions/stat_sinks/open_telemetry/v3/open_telemetry.proto index eb72322976288..34ffaf634d424 100644 --- a/api/envoy/extensions/stat_sinks/open_telemetry/v3/open_telemetry.proto +++ b/api/envoy/extensions/stat_sinks/open_telemetry/v3/open_telemetry.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.extensions.stat_sinks.open_telemetry.v3; +import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/grpc_service.proto"; import "google/protobuf/wrappers.proto"; @@ -19,7 +20,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Stats configuration proto schema for ``envoy.stat_sinks.open_telemetry`` sink. // [#extension: envoy.stat_sinks.open_telemetry] -// [#next-free-field: 7] +// [#next-free-field: 8] message SinkConfig { oneof protocol_specifier { option (validate.required) = true; @@ -28,6 +29,10 @@ message SinkConfig { config.core.v3.GrpcService grpc_service = 1 [(validate.rules).message = {required: true}]; } + // Attributes to be associated with the resource in the OTLP message. + // [#extension-category: envoy.tracers.opentelemetry.resource_detectors] + repeated config.core.v3.TypedExtensionConfig resource_detectors = 7; + // If set to true, counters will be emitted as deltas, and the OTLP message will have // ``AGGREGATION_TEMPORALITY_DELTA`` set as AggregationTemporality. bool report_counters_as_deltas = 2; diff --git a/api/envoy/service/ext_proc/v3/external_processor.proto b/api/envoy/service/ext_proc/v3/external_processor.proto index e77d60d0b9700..406c5c195a2a7 100644 --- a/api/envoy/service/ext_proc/v3/external_processor.proto +++ b/api/envoy/service/ext_proc/v3/external_processor.proto @@ -27,29 +27,31 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // as part of a filter chain. // The overall external processing protocol works like this: // -// 1. Envoy sends to the service information about the HTTP request. -// 2. The service sends back a ProcessingResponse message that directs Envoy -// to either stop processing, continue without it, or send it the -// next chunk of the message body. -// 3. If so requested, Envoy sends the server the message body in chunks, -// or the entire body at once. In either case, the server may send back -// a ProcessingResponse for each message it receives, or wait for certain amount -// of body chunks received before streams back the ProcessingResponse messages. -// 4. If so requested, Envoy sends the server the HTTP trailers, +// 1. The data plane sends to the service information about the HTTP request. +// 2. The service sends back a ProcessingResponse message that directs +// the data plane to either stop processing, continue without it, or send +// it the next chunk of the message body. +// 3. If so requested, the data plane sends the server the message body in +// chunks, or the entire body at once. In either case, the server may send +// back a ProcessingResponse for each message it receives, or wait for +// a certain amount of body chunks received before streaming back the +// ProcessingResponse messages. +// 4. If so requested, the data plane sends the server the HTTP trailers, // and the server sends back a ProcessingResponse. // 5. At this point, request processing is done, and we pick up again -// at step 1 when Envoy receives a response from the upstream server. +// at step 1 when the data plane receives a response from the upstream +// server. // 6. At any point above, if the server closes the gRPC stream cleanly, -// then Envoy proceeds without consulting the server. +// then the data plane proceeds without consulting the server. // 7. At any point above, if the server closes the gRPC stream with an error, -// then Envoy returns a 500 error to the client, unless the filter +// then the data plane returns a 500 error to the client, unless the filter // was configured to ignore errors. // // In other words, the process is a request/response conversation, but // using a gRPC stream to make it easier for the server to // maintain state. service ExternalProcessor { - // This begins the bidirectional stream that Envoy will use to + // This begins the bidirectional stream that the data plane will use to // give the server control over what the filter does. The actual // protocol is described by the ProcessingRequest and ProcessingResponse // messages below. @@ -79,7 +81,7 @@ message ProtocolConfiguration { bool send_body_without_waiting_for_header_response = 3; } -// This represents the different types of messages that Envoy can send +// This represents the different types of messages that the data plane can send // to an external processing server. // [#next-free-field: 12] message ProcessingRequest { @@ -132,7 +134,7 @@ message ProcessingRequest { // The values of properties selected by the ``request_attributes`` // or ``response_attributes`` list in the configuration. Each entry // in the list is populated from the standard - // :ref:`attributes ` supported across Envoy. + // :ref:`attributes ` supported in the data plane. map attributes = 9; // Specify whether the filter that sent this request is running in :ref:`observability_mode @@ -153,7 +155,7 @@ message ProcessingRequest { ProtocolConfiguration protocol_config = 11; } -// This represents the different types of messages the server may send back to Envoy +// This represents the different types of messages the server may send back to the data plane // when the ``observability_mode`` field in the received ProcessingRequest is set to false. // // * If the corresponding ``BodySendMode`` in the @@ -212,8 +214,8 @@ message ProcessingResponse { // may use this to intelligently control how requests are processed // based on the headers and other metadata that they see. // This field is only applicable when servers responding to the header requests. - // If it is set in the response to the body or trailer requests, it will be ignored by Envoy. - // It is also ignored by Envoy when the ext_proc filter config + // If it is set in the response to the body or trailer requests, it will be ignored by the data plane. + // It is also ignored by the data plane when the ext_proc filter config // :ref:`allow_mode_override // ` // is set to false, or @@ -224,16 +226,16 @@ message ProcessingResponse { // When ext_proc server receives a request message, in case it needs more // time to process the message, it sends back a ProcessingResponse message - // with a new timeout value. When Envoy receives this response message, - // it ignores other fields in the response, just stop the original timer, - // which has the timeout value specified in + // with a new timeout value. When the data plane receives this response + // message, it ignores other fields in the response, just stop the original + // timer, which has the timeout value specified in // :ref:`message_timeout // ` // and start a new timer with this ``override_message_timeout`` value and keep the - // Envoy ext_proc filter state machine intact. + // data plane ext_proc filter state machine intact. // Has to be >= 1ms and <= // :ref:`max_message_timeout ` - // Such message can be sent at most once in a particular Envoy ext_proc filter processing state. + // Such message can be sent at most once in a particular data plane ext_proc filter processing state. // To enable this API, one has to set ``max_message_timeout`` to a number >= 1ms. google.protobuf.Duration override_message_timeout = 10; } @@ -283,26 +285,26 @@ message HttpTrailers { // The following are messages that may be sent back by the server. -// This message is sent by the external server to Envoy after ``HttpHeaders`` was +// This message is sent by the external server to the data plane after ``HttpHeaders`` was // sent to it. message HeadersResponse { - // Details the modifications (if any) to be made by Envoy to the current + // Details the modifications (if any) to be made by the data plane to the current // request/response. CommonResponse response = 1; } -// This message is sent by the external server to Envoy after ``HttpBody`` was +// This message is sent by the external server to the data plane after ``HttpBody`` was // sent to it. message BodyResponse { - // Details the modifications (if any) to be made by Envoy to the current + // Details the modifications (if any) to be made by the data plane to the current // request/response. CommonResponse response = 1; } -// This message is sent by the external server to Envoy after ``HttpTrailers`` was +// This message is sent by the external server to the data plane after ``HttpTrailers`` was // sent to it. message TrailersResponse { - // Details the modifications (if any) to be made by Envoy to the current + // Details the modifications (if any) to be made by the data plane to the current // request/response trailers. HeaderMutation header_mutation = 1; } @@ -332,7 +334,7 @@ message CommonResponse { CONTINUE_AND_REPLACE = 1; } - // If set, provide additional direction on how the Envoy proxy should + // If set, provide additional direction on how the data plane should // handle the rest of the HTTP filter chain. ResponseStatus status = 1 [(validate.rules).enum = {defined_only: true}]; @@ -361,7 +363,7 @@ message CommonResponse { // Clear the route cache for the current client request. This is necessary // if the remote server modified headers that are used to calculate the route. // This field is ignored in the response direction. This field is also ignored - // if the Envoy ext_proc filter is in the upstream filter chain. + // if the data plane ext_proc filter is in the upstream filter chain. bool clear_route_cache = 5; } @@ -415,7 +417,7 @@ message HeaderMutation { // The body response message corresponding to FULL_DUPLEX_STREAMED body mode. message StreamedBodyResponse { - // The body response chunk that will be passed to the upstream/downstream by Envoy. + // The body response chunk that will be passed to the upstream/downstream by the data plane. bytes body = 1; // The server sets this flag to true if it has received a body request with @@ -424,7 +426,7 @@ message StreamedBodyResponse { bool end_of_stream = 2; } -// This message specifies the body mutation the server sends to Envoy. +// This message specifies the body mutation the server sends to the data plane. message BodyMutation { // The type of mutation for the body. oneof mutation { diff --git a/api/envoy/service/network_ext_proc/v3/network_external_processor.proto b/api/envoy/service/network_ext_proc/v3/network_external_processor.proto index bc5d8d73488e9..c148baf31c494 100644 --- a/api/envoy/service/network_ext_proc/v3/network_external_processor.proto +++ b/api/envoy/service/network_ext_proc/v3/network_external_processor.proto @@ -31,10 +31,11 @@ option (xds.annotations.v3.file_status).work_in_progress = true; // 3. Control connection lifecycle (continue, close gracefully, or reset) // // Use cases include: -// * Custom protocol inspection and modification -// * Advanced traffic manipulation -// * Security scanning and filtering -// * Dynamic connection management +// +// 1. Custom protocol inspection and modification +// 2. Advanced traffic manipulation +// 3. Security scanning and filtering +// 4. Dynamic connection management // // The service uses a bidirectional gRPC stream, maintaining state throughout // the connection lifetime while allowing asynchronous processing. diff --git a/api/envoy/type/matcher/v3/value.proto b/api/envoy/type/matcher/v3/value.proto index d773c6057fccc..8d65c457ccca7 100644 --- a/api/envoy/type/matcher/v3/value.proto +++ b/api/envoy/type/matcher/v3/value.proto @@ -17,7 +17,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Value matcher] -// Specifies the way to match a ProtobufWkt::Value. Primitive values and ListValue are supported. +// Specifies the way to match a Protobuf::Value. Primitive values and ListValue are supported. // StructValue is not supported and is always not matched. // [#next-free-field: 8] message ValueMatcher { diff --git a/api/envoy/type/matcher/value.proto b/api/envoy/type/matcher/value.proto index 89d341bbbaa4c..6452fcedec4f3 100644 --- a/api/envoy/type/matcher/value.proto +++ b/api/envoy/type/matcher/value.proto @@ -16,7 +16,7 @@ option (udpa.annotations.file_status).package_version_status = FROZEN; // [#protodoc-title: Value matcher] -// Specifies the way to match a ProtobufWkt::Value. Primitive values and ListValue are supported. +// Specifies the way to match a Protobuf::Value. Primitive values and ListValue are supported. // StructValue is not supported and is always not matched. // [#next-free-field: 7] message ValueMatcher { diff --git a/api/versioning/BUILD b/api/versioning/BUILD index cdcf5f206f24c..f93b5124a4f34 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -1,6 +1,6 @@ # DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") licenses(["notice"]) # Apache 2 @@ -75,11 +75,14 @@ proto_library( "//envoy/extensions/access_loggers/stream/v3:pkg", "//envoy/extensions/access_loggers/wasm/v3:pkg", "//envoy/extensions/bootstrap/internal_listener/v3:pkg", + "//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg", + "//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg", "//envoy/extensions/clusters/aggregate/v3:pkg", "//envoy/extensions/clusters/common/dns/v3:pkg", "//envoy/extensions/clusters/dns/v3:pkg", "//envoy/extensions/clusters/dynamic_forward_proxy/v3:pkg", "//envoy/extensions/clusters/redis/v3:pkg", + "//envoy/extensions/clusters/reverse_connection/v3:pkg", "//envoy/extensions/common/async_files/v3:pkg", "//envoy/extensions/common/aws/v3:pkg", "//envoy/extensions/common/dynamic_forward_proxy/v3:pkg", @@ -151,6 +154,7 @@ proto_library( "//envoy/extensions/filters/http/rate_limit_quota/v3:pkg", "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", + "//envoy/extensions/filters/http/reverse_conn/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", "//envoy/extensions/filters/http/set_filter_state/v3:pkg", "//envoy/extensions/filters/http/set_metadata/v3:pkg", @@ -164,6 +168,7 @@ proto_library( "//envoy/extensions/filters/listener/original_dst/v3:pkg", "//envoy/extensions/filters/listener/original_src/v3:pkg", "//envoy/extensions/filters/listener/proxy_protocol/v3:pkg", + "//envoy/extensions/filters/listener/reverse_connection/v3:pkg", "//envoy/extensions/filters/listener/tls_inspector/v3:pkg", "//envoy/extensions/filters/network/connection_limit/v3:pkg", "//envoy/extensions/filters/network/direct_response/v3:pkg", @@ -184,6 +189,7 @@ proto_library( "//envoy/extensions/filters/network/ratelimit/v3:pkg", "//envoy/extensions/filters/network/rbac/v3:pkg", "//envoy/extensions/filters/network/redis_proxy/v3:pkg", + "//envoy/extensions/filters/network/reverse_tunnel/v3:pkg", "//envoy/extensions/filters/network/set_filter_state/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3:pkg", diff --git a/bazel/README.md b/bazel/README.md index b99d1f10704fd..4544cfcf5cba2 100644 --- a/bazel/README.md +++ b/bazel/README.md @@ -98,7 +98,7 @@ for how to update or override dependencies. ``` ### Linux - On Linux, we recommend using the prebuilt Clang+LLVM package from [LLVM official site](http://releases.llvm.org/download.html) for Clang 14. + On Linux, we recommend using the prebuilt Clang+LLVM package from [LLVM official site](http://releases.llvm.org/download.html) for Clang 18. Extract the tar.xz and run the following: ```console @@ -365,7 +365,7 @@ for more details. ## Supported compiler versions We now require Clang >= 18 due to C++20 support (for Clang >= 14, your mileage may vary) and tcmalloc requirement. GCC >= 13 is also known to work for C++20. -Currently the CI is running with Clang 14. +Currently the CI is running with Clang 18. ## Clang STL debug symbols @@ -846,7 +846,7 @@ have seen some issues with seeing the artifacts tab. If you can't see it, log ou then log back in and it should start working. The latest coverage report for main is available -[here](https://storage.googleapis.com/envoy-postsubmit/main/coverage/index.html). The latest fuzz coverage report for main is available [here](https://storage.googleapis.com/envoy-postsubmit/main/fuzz_coverage/index.html). +[here](https://storage.googleapis.com/envoy-cncf-postsubmit/main/coverage/index.html). The latest fuzz coverage report for main is available [here](https://storage.googleapis.com/envoy-cncf-postsubmit/main/fuzz_coverage/index.html). It's also possible to specialize the coverage build to a specified test or test dir. This is useful when doing things like exploring the coverage of a fuzzer over its corpus. This can be done by diff --git a/bazel/abseil.patch b/bazel/abseil.patch index ce228f90ba832..5650f4d13a2a8 100644 --- a/bazel/abseil.patch +++ b/bazel/abseil.patch @@ -21,7 +21,7 @@ index 88949fe9740..a4d47a7ee65 100644 + "absl/debugging/internal/stacktrace_unimplemented-inl.inc" #elif defined(__ANDROID__) && __ANDROID_API__ >= 33 - + #ifdef ABSL_HAVE_THREAD_LOCAL diff --git a/absl/debugging/symbolize.cc b/absl/debugging/symbolize.cc index 344436f9d10..6f8088d1d08 100644 --- a/absl/debugging/symbolize.cc diff --git a/bazel/dependency_imports.bzl b/bazel/dependency_imports.bzl index 4615eed5c9ade..d2ab0fc039089 100644 --- a/bazel/dependency_imports.bzl +++ b/bazel/dependency_imports.bzl @@ -14,6 +14,7 @@ load("@fuzzing_pip3//:requirements.bzl", pip_fuzzing_dependencies = "install_dep load("@io_bazel_rules_go//go:deps.bzl", "go_download_sdk", "go_register_toolchains", "go_rules_dependencies") load("@proxy_wasm_rust_sdk//bazel:dependencies.bzl", "proxy_wasm_rust_sdk_dependencies") load("@rules_buf//buf:repositories.bzl", "rules_buf_toolchains") +load("@rules_cc//cc:extensions.bzl", "compatibility_proxy_repo") load("@rules_foreign_cc//foreign_cc:repositories.bzl", "rules_foreign_cc_dependencies") load("@rules_fuzzing//fuzzing:repositories.bzl", "rules_fuzzing_dependencies") load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") @@ -24,13 +25,13 @@ load("@rules_rust//rust:defs.bzl", "rust_common") load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_register_toolchains", "rust_repository_set") # go version for rules_go -GO_VERSION = "1.23.1" +GO_VERSION = "1.24.6" JQ_VERSION = "1.7" YQ_VERSION = "4.24.4" -BUF_SHA = "736e74d1697dcf253bc60b2f0fb4389c39dbc7be68472a7d564a953df8b19d12" -BUF_VERSION = "v1.50.0" +BUF_SHA = "5790beb45aaf51a6d7e68ca2255b22e1b14c9ae405a6c472cdcfc228c66abfc1" +BUF_VERSION = "v1.56.0" def envoy_dependency_imports( go_version = GO_VERSION, @@ -38,6 +39,7 @@ def envoy_dependency_imports( yq_version = YQ_VERSION, buf_sha = BUF_SHA, buf_version = BUF_VERSION): + compatibility_proxy_repo() rules_foreign_cc_dependencies() go_rules_dependencies() go_register_toolchains(go_version) diff --git a/bazel/envoy_build_system.bzl b/bazel/envoy_build_system.bzl index ea8d9dbf1d0ec..0d67a91976e15 100644 --- a/bazel/envoy_build_system.bzl +++ b/bazel/envoy_build_system.bzl @@ -138,7 +138,7 @@ def envoy_cmake( if pdb_name == "": pdb_name = name - copy_command = "cp {cmake_files_dir}/{pdb_name}.dir/{pdb_name}.pdb $INSTALLDIR/lib/{pdb_name}.pdb".format(cmake_files_dir = cmake_files_dir, pdb_name = pdb_name) + copy_command = "cp {cmake_files_dir}/{pdb_name}.dir/{pdb_name}.pdb $$INSTALLDIR/lib/{pdb_name}.pdb".format(cmake_files_dir = cmake_files_dir, pdb_name = pdb_name) if postfix_script != "": copy_command = copy_command + " && " + postfix_script diff --git a/bazel/external/dragonbox.BUILD b/bazel/external/dragonbox.BUILD new file mode 100644 index 0000000000000..d0bdf231e94b2 --- /dev/null +++ b/bazel/external/dragonbox.BUILD @@ -0,0 +1,12 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +licenses(["notice"]) # Apache 2 + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "dragonbox", + srcs = [], + hdrs = ["include/dragonbox/dragonbox.h"], + includes = ["include/"], +) diff --git a/bazel/external/fmtlib.BUILD b/bazel/external/fmtlib.BUILD index 3000f503fd7fd..0039e1eedf3e3 100644 --- a/bazel/external/fmtlib.BUILD +++ b/bazel/external/fmtlib.BUILD @@ -1,7 +1,7 @@ licenses(["notice"]) # Apache 2 cc_library( - name = "fmtlib", + name = "fmt", hdrs = glob([ "include/fmt/*.h", ]), diff --git a/bazel/external/fp16.BUILD b/bazel/external/fp16.BUILD new file mode 100644 index 0000000000000..f3fbfda1f59f6 --- /dev/null +++ b/bazel/external/fp16.BUILD @@ -0,0 +1,15 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +licenses(["notice"]) # MIT + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "FP16", + hdrs = [ + "include/fp16.h", + "include/fp16/bitcasts.h", + "include/fp16/fp16.h", + ], + includes = ["include/"], +) diff --git a/bazel/external/intel_ittapi.BUILD b/bazel/external/intel_ittapi.BUILD new file mode 100644 index 0000000000000..13351d38abb66 --- /dev/null +++ b/bazel/external/intel_ittapi.BUILD @@ -0,0 +1,21 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +licenses(["notice"]) + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "lib_ittapi", + srcs = [ + "include/ittnotify.h", + "include/jitprofiling.h", + "src/ittnotify/ittnotify_config.h", + "src/ittnotify/jitprofiling.c", + ], + hdrs = [ + "include/ittnotify.h", + "src/ittnotify/ittnotify_types.h", + ], + includes = ["include/"], + visibility = ["//visibility:public"], +) diff --git a/bazel/external/quiche.BUILD b/bazel/external/quiche.BUILD index b60b80b8d156a..b97fe1b7bd94d 100644 --- a/bazel/external/quiche.BUILD +++ b/bazel/external/quiche.BUILD @@ -1,4 +1,5 @@ load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load( "@envoy//bazel:envoy_build_system.bzl", "envoy_cc_library", @@ -13,7 +14,6 @@ load( "envoy_quiche_platform_impl_cc_test_library", "quiche_copts", ) -load("@rules_proto//proto:defs.bzl", "proto_library") licenses(["notice"]) # Apache 2 @@ -2884,12 +2884,12 @@ envoy_cc_library( "quiche/quic/core/frames/quic_blocked_frame.cc", "quiche/quic/core/frames/quic_connection_close_frame.cc", "quiche/quic/core/frames/quic_crypto_frame.cc", + "quiche/quic/core/frames/quic_datagram_frame.cc", "quiche/quic/core/frames/quic_frame.cc", "quiche/quic/core/frames/quic_goaway_frame.cc", "quiche/quic/core/frames/quic_handshake_done_frame.cc", "quiche/quic/core/frames/quic_immediate_ack_frame.cc", "quiche/quic/core/frames/quic_max_streams_frame.cc", - "quiche/quic/core/frames/quic_message_frame.cc", "quiche/quic/core/frames/quic_new_connection_id_frame.cc", "quiche/quic/core/frames/quic_new_token_frame.cc", "quiche/quic/core/frames/quic_padding_frame.cc", @@ -2911,13 +2911,13 @@ envoy_cc_library( "quiche/quic/core/frames/quic_blocked_frame.h", "quiche/quic/core/frames/quic_connection_close_frame.h", "quiche/quic/core/frames/quic_crypto_frame.h", + "quiche/quic/core/frames/quic_datagram_frame.h", "quiche/quic/core/frames/quic_frame.h", "quiche/quic/core/frames/quic_goaway_frame.h", "quiche/quic/core/frames/quic_handshake_done_frame.h", "quiche/quic/core/frames/quic_immediate_ack_frame.h", "quiche/quic/core/frames/quic_inlined_frame.h", "quiche/quic/core/frames/quic_max_streams_frame.h", - "quiche/quic/core/frames/quic_message_frame.h", "quiche/quic/core/frames/quic_mtu_discovery_frame.h", "quiche/quic/core/frames/quic_new_connection_id_frame.h", "quiche/quic/core/frames/quic_new_token_frame.h", @@ -3029,13 +3029,17 @@ envoy_cc_library( envoy_quic_cc_library( name = "quic_core_http_client_lib", srcs = [ + "quiche/quic/core/http/quic_connection_migration_manager.cc", "quiche/quic/core/http/quic_spdy_client_session.cc", "quiche/quic/core/http/quic_spdy_client_session_base.cc", + "quiche/quic/core/http/quic_spdy_client_session_with_migration.cc", "quiche/quic/core/http/quic_spdy_client_stream.cc", ], hdrs = [ + "quiche/quic/core/http/quic_connection_migration_manager.h", "quiche/quic/core/http/quic_spdy_client_session.h", "quiche/quic/core/http/quic_spdy_client_session_base.h", + "quiche/quic/core/http/quic_spdy_client_session_with_migration.h", "quiche/quic/core/http/quic_spdy_client_stream.h", ], deps = [ @@ -3047,10 +3051,12 @@ envoy_quic_cc_library( ":quic_core_http_server_initiated_spdy_stream_lib", ":quic_core_http_spdy_session_lib", ":quic_core_packets_lib", + ":quic_core_path_context_factory_interface_lib", ":quic_core_qpack_qpack_streams_lib", ":quic_core_server_id_lib", ":quic_core_types_lib", ":quic_core_utils_lib", + ":quic_force_blockable_packet_writer_lib", ":quic_platform_base", ], ) @@ -3204,6 +3210,12 @@ envoy_quic_cc_library( ], ) +envoy_quic_cc_library( + name = "quic_force_blockable_packet_writer_lib", + hdrs = ["quiche/quic/core/quic_force_blockable_packet_writer.h"], + deps = [":quic_core_packet_writer_lib"], +) + envoy_quic_cc_library( name = "quic_core_http_spdy_stream_body_manager_lib", srcs = ["quiche/quic/core/http/quic_spdy_stream_body_manager.cc"], @@ -3394,6 +3406,16 @@ envoy_quic_cc_library( ], ) +envoy_quic_cc_library( + name = "quic_core_path_context_factory_interface_lib", + hdrs = ["quiche/quic/core/quic_path_context_factory.h"], + deps = [ + ":quic_core_path_validator_lib", + ":quic_platform_export", + ":quic_platform_socket_address", + ], +) + envoy_cc_library( name = "quic_core_syscall_wrapper_lib", srcs = select({ diff --git a/bazel/external/quiche.bzl b/bazel/external/quiche.bzl index e5df747a0020f..5559dac02609e 100644 --- a/bazel/external/quiche.bzl +++ b/bazel/external/quiche.bzl @@ -11,6 +11,9 @@ quiche_common_copts = [ # hpack_huffman_decoder.cc overloads operator<<. "-Wno-unused-function", "-Wno-old-style-cast", + + # Envoy build should not fail if a dependency has a warning. + "-Wno-error", ] quiche_copts = select({ diff --git a/bazel/external/simdutf.BUILD b/bazel/external/simdutf.BUILD new file mode 100644 index 0000000000000..ee48964946afc --- /dev/null +++ b/bazel/external/simdutf.BUILD @@ -0,0 +1,11 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +licenses(["notice"]) # Apache 2 + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "simdutf", + srcs = ["simdutf.cpp"], + hdrs = ["simdutf.h"], +) diff --git a/bazel/external/spdlog.BUILD b/bazel/external/spdlog.BUILD index bcf82ad23f272..3ac39dbfcce54 100644 --- a/bazel/external/spdlog.BUILD +++ b/bazel/external/spdlog.BUILD @@ -11,5 +11,5 @@ cc_library( ], includes = ["include"], visibility = ["//visibility:public"], - deps = ["@com_github_fmtlib_fmt//:fmtlib"], + deps = ["@com_github_fmtlib_fmt//:fmt"], ) diff --git a/bazel/foreign_cc/BUILD b/bazel/foreign_cc/BUILD index 2cd885639a300..dc07b536d3829 100644 --- a/bazel/foreign_cc/BUILD +++ b/bazel/foreign_cc/BUILD @@ -336,8 +336,8 @@ envoy_cmake( "//conditions:default": ["libcares.a"], }), postfix_script = select({ - "//bazel:windows_x86_64": "cp -L $EXT_BUILD_ROOT/external/com_github_c_ares_c_ares/src/lib/ares_nameser.h $INSTALLDIR/include/ares_nameser.h && cp -L $EXT_BUILD_ROOT/external/com_github_c_ares_c_ares/include/ares_dns.h $INSTALLDIR/include/ares_dns.h", - "//conditions:default": "rm -f $INSTALLDIR/include/ares_dns.h && cp -L $EXT_BUILD_ROOT/external/com_github_c_ares_c_ares/include/ares_dns.h $INSTALLDIR/include/ares_dns.h", + "//bazel:windows_x86_64": "cp -L $$EXT_BUILD_ROOT/external/com_github_c_ares_c_ares/src/lib/ares_nameser.h $$INSTALLDIR/include/ares_nameser.h && cp -L $$EXT_BUILD_ROOT/external/com_github_c_ares_c_ares/include/ares_dns.h $$INSTALLDIR/include/ares_dns.h", + "//conditions:default": "rm -f $$INSTALLDIR/include/ares_dns.h && cp -L $$EXT_BUILD_ROOT/external/com_github_c_ares_c_ares/include/ares_dns.h $$INSTALLDIR/include/ares_dns.h", }), ) @@ -534,6 +534,14 @@ envoy_cmake( "//bazel:windows_x86_64": ["zlib.lib"], "//conditions:default": ["libz.a"], }), + postfix_script = select({ + "@platforms//cpu:wasm32": """ + if [[ -f libzlib.a ]]; then + cp libzlib.a $$INSTALLDIR/lib/libz.a + fi + """, + "//conditions:default": "", + }), ) envoy_cmake( diff --git a/bazel/foreign_cc/cel-cpp.patch b/bazel/foreign_cc/cel-cpp.patch new file mode 100644 index 0000000000000..0e3287b2eaea9 --- /dev/null +++ b/bazel/foreign_cc/cel-cpp.patch @@ -0,0 +1,85 @@ +diff --git a/common/internal/byte_string.cc b/common/internal/byte_string.cc +index e01c797f8..12345678a 100644 +--- a/common/internal/byte_string.cc ++++ b/common/internal/byte_string.cc +@@ -104,6 +104,13 @@ + + ByteString::ByteString(Allocator<> allocator, absl::string_view string) { + ABSL_DCHECK_LE(string.size(), max_size()); ++ ++ // Check for null data pointer in the string_view ++ if (string.data() == nullptr) { ++ // Handle null data by creating an empty ByteString ++ SetSmallEmpty(allocator.arena()); ++ return; ++ } + auto* arena = allocator.arena(); + if (string.size() <= kSmallByteStringCapacity) { + SetSmall(arena, string); +diff --git a/common/typeinfo.h b/common/typeinfo.h +index 06a03c13d..9f5d77980 100644 +--- a/common/typeinfo.h ++++ b/common/typeinfo.h +@@ -80,7 +80,7 @@ + std::conjunction_v, + std::negation>>, + TypeInfo> +-TypeId(const T& t) { ++TypeId(const T& t [[maybe_unused]]) { + return NativeTypeTraits>::Id(t); + } + +@@ -90,7 +90,7 @@ + std::negation>, + std::is_final>, + TypeInfo> +-TypeId(const T& t) { ++TypeId(const T& t [[maybe_unused]]) { + return cel::TypeId>(); + } + +@@ -99,7 +99,7 @@ + std::conjunction_v>, + common_internal::HasCelTypeId>, + TypeInfo> +-TypeId(const T& t) { ++TypeId(const T& t [[maybe_unused]]) { + return CelTypeId(t); + } + +diff --git a/common/value.h b/common/value.h +index abcdef123..987654fed 100644 +--- a/common/value.h ++++ b/common/value.h +@@ -2726,7 +2726,7 @@ + const google::protobuf::DescriptorPool* absl_nonnull descriptor_pool, + google::protobuf::MessageFactory* absl_nonnull message_factory, + google::protobuf::Arena* absl_nonnull arena) const { +- ABSL_DCHECK_GT(qualifiers.size(), 0); ++ ABSL_DCHECK_GT(static_cast(qualifiers.size()), 0); + ABSL_DCHECK(descriptor_pool != nullptr); + ABSL_DCHECK(message_factory != nullptr); + ABSL_DCHECK(arena != nullptr); +diff --git a/common/values/value_variant.h b/common/values/value_variant.h +index 61c19ce5f..fc7969bc8 100644 +--- a/common/values/value_variant.h ++++ b/common/values/value_variant.h +@@ -737,9 +737,6 @@ + #if defined(__GNUC__) && !defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wclass-memaccess" +-#elif defined(__clang__) +-#pragma clang diagnostic push +-#pragma clang diagnostic ignored "-Wnontrivial-memcall" + #endif + alignas(ValueVariant) std::byte tmp[sizeof(ValueVariant)]; + // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation) +@@ -751,8 +748,6 @@ + std::memcpy(std::addressof(rhs), tmp, sizeof(ValueVariant)); + #if defined(__GNUC__) && !defined(__clang__) + #pragma GCC diagnostic pop +-#elif defined(__clang__) +-#pragma clang diagnostic pop + #endif + } else { + SlowSwap(lhs, rhs, lhs_trivial, rhs_trivial); \ No newline at end of file diff --git a/bazel/foreign_cc/vpp_vcl.patch b/bazel/foreign_cc/vpp_vcl.patch index 74085b777cef1..f75138e7180e7 100644 --- a/bazel/foreign_cc/vpp_vcl.patch +++ b/bazel/foreign_cc/vpp_vcl.patch @@ -1,7 +1,7 @@ -diff --git src/CMakeLists.txt src/CMakeLists.txt -index 68d0a4f..9bf7ade 100644 ---- src/CMakeLists.txt -+++ src/CMakeLists.txt +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 68d0a4fe6..9bf7adede 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt @@ -50,13 +50,8 @@ include(cmake/ccache.cmake) ############################################################################## # VPP Version @@ -27,10 +27,10 @@ index 68d0a4f..9bf7ade 100644 ) elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin") set(SUBDIRS vppinfra) -diff --git src/cmake/ccache.cmake src/cmake/ccache.cmake -index a7b395b..d6a4c5b 100644 ---- src/cmake/ccache.cmake -+++ src/cmake/ccache.cmake +diff --git a/src/cmake/ccache.cmake b/src/cmake/ccache.cmake +index a7b395bc6..d6a4c5b30 100644 +--- a/src/cmake/ccache.cmake ++++ b/src/cmake/ccache.cmake @@ -14,7 +14,7 @@ ############################################################################## # ccache @@ -40,10 +40,10 @@ index a7b395b..d6a4c5b 100644 if(VPP_USE_CCACHE) find_program(CCACHE_FOUND ccache) message(STATUS "Looking for ccache") -diff --git src/cmake/library.cmake src/cmake/library.cmake -index 45b3944..b1dcc56 100644 ---- src/cmake/library.cmake -+++ src/cmake/library.cmake +diff --git a/src/cmake/library.cmake b/src/cmake/library.cmake +index 45b3944eb..b1dcc56e1 100644 +--- a/src/cmake/library.cmake ++++ b/src/cmake/library.cmake @@ -24,7 +24,7 @@ macro(add_vpp_library lib) set_target_properties(${lo} PROPERTIES POSITION_INDEPENDENT_CODE ON) target_compile_options(${lo} PUBLIC ${VPP_DEFAULT_MARCH_FLAGS}) @@ -53,10 +53,10 @@ index 45b3944..b1dcc56 100644 target_sources(${lib} PRIVATE $) if(VPP_LIB_VERSION) -diff --git src/tools/vppapigen/CMakeLists.txt src/tools/vppapigen/CMakeLists.txt -index 04ebed5..bfabc3a 100644 ---- src/tools/vppapigen/CMakeLists.txt -+++ src/tools/vppapigen/CMakeLists.txt +diff --git a/src/tools/vppapigen/CMakeLists.txt b/src/tools/vppapigen/CMakeLists.txt +index 04ebed548..bfabc3a67 100644 +--- a/src/tools/vppapigen/CMakeLists.txt ++++ b/src/tools/vppapigen/CMakeLists.txt @@ -11,22 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. @@ -80,28 +80,29 @@ index 04ebed5..bfabc3a 100644 install( FILES vppapigen.py RENAME vppapigen -diff --git src/tools/vppapigen/vppapigen.py src/tools/vppapigen/vppapigen.py -index 2b0ce99..f28a173 100755 ---- src/tools/vppapigen/vppapigen.py -+++ src/tools/vppapigen/vppapigen.py -@@ -7,6 +7,13 @@ import logging +diff --git a/src/tools/vppapigen/vppapigen.py b/src/tools/vppapigen/vppapigen.py +index 2b0ce9999..f8a7586ea 100755 +--- a/src/tools/vppapigen/vppapigen.py ++++ b/src/tools/vppapigen/vppapigen.py +@@ -7,6 +7,14 @@ import logging import binascii import os from subprocess import Popen, PIPE + ++ +# Put ply on the path ... +plypath = os.path.join( + os.environ["EXT_BUILD_ROOT"], -+ os.path.dirname(os.environ["PLYPATHS"].split()[0])) ++ os.path.dirname(os.path.dirname(os.environ["PLYPATHS"].split()[0]))) +sys.path += [plypath] + import ply.lex as lex import ply.yacc as yacc -diff --git src/vcl/CMakeLists.txt src/vcl/CMakeLists.txt -index 610b422..c5e6f8c 100644 ---- src/vcl/CMakeLists.txt -+++ src/vcl/CMakeLists.txt +diff --git a/src/vcl/CMakeLists.txt b/src/vcl/CMakeLists.txt +index 610b422d1..c5e6f8ca8 100644 +--- a/src/vcl/CMakeLists.txt ++++ b/src/vcl/CMakeLists.txt @@ -35,6 +35,8 @@ if (LDP_HAS_GNU_SOURCE) add_compile_definitions(HAVE_GNU_SOURCE) endif(LDP_HAS_GNU_SOURCE) @@ -111,10 +112,10 @@ index 610b422..c5e6f8c 100644 add_vpp_library(vcl_ldpreload SOURCES ldp_socket_wrapper.c -diff --git src/vppinfra/CMakeLists.txt src/vppinfra/CMakeLists.txt -index f34ceed..51fd2be 100644 ---- src/vppinfra/CMakeLists.txt -+++ src/vppinfra/CMakeLists.txt +diff --git a/src/vppinfra/CMakeLists.txt b/src/vppinfra/CMakeLists.txt +index f34ceed9d..51fd2becf 100644 +--- a/src/vppinfra/CMakeLists.txt ++++ b/src/vppinfra/CMakeLists.txt @@ -233,13 +233,28 @@ option(VPP_USE_EXTERNAL_LIBEXECINFO "Use external libexecinfo (useful for non-gl if(VPP_USE_EXTERNAL_LIBEXECINFO) set(EXECINFO_LIB execinfo) diff --git a/bazel/proxy_wasm_cpp_host.patch b/bazel/proxy_wasm_cpp_host.patch index ce159af5ed429..b626f431e03ef 100644 --- a/bazel/proxy_wasm_cpp_host.patch +++ b/bazel/proxy_wasm_cpp_host.patch @@ -1,8 +1,304 @@ +From d6e7129f1d3f52fd1eca3bdaf91d06bb8a14e70d Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Wed, 16 Jul 2025 20:05:48 -0400 +Subject: [PATCH 1/3] chore: Update v8 to use 13.8 interface + +Signed-off-by: Matt Leon +--- + src/v8/v8.cc | 84 ++++++++++++++++++++++++++-------------------------- + 1 file changed, 42 insertions(+), 42 deletions(-) + +diff --git a/src/v8/v8.cc b/src/v8/v8.cc +index 61779c1..1047f5c 100644 +--- a/src/v8/v8.cc ++++ b/src/v8/v8.cc +@@ -140,20 +140,20 @@ private: + + static std::string printValue(const wasm::Val &value) { + switch (value.kind()) { +- case wasm::I32: ++ case wasm::ValKind::I32: + return std::to_string(value.get()); +- case wasm::I64: ++ case wasm::ValKind::I64: + return std::to_string(value.get()); +- case wasm::F32: ++ case wasm::ValKind::F32: + return std::to_string(value.get()); +- case wasm::F64: ++ case wasm::ValKind::F64: + return std::to_string(value.get()); + default: + return "unknown"; + } + } + +-static std::string printValues(const wasm::Val values[], size_t size) { ++static std::string printValues(const wasm::vec &values, size_t size) { + if (size == 0) { + return ""; + } +@@ -170,17 +170,17 @@ static std::string printValues(const wasm::Val values[], size_t size) { + + static const char *printValKind(wasm::ValKind kind) { + switch (kind) { +- case wasm::I32: ++ case wasm::ValKind::I32: + return "i32"; +- case wasm::I64: ++ case wasm::ValKind::I64: + return "i64"; +- case wasm::F32: ++ case wasm::ValKind::F32: + return "f32"; +- case wasm::F64: ++ case wasm::ValKind::F64: + return "f64"; +- case wasm::ANYREF: +- return "anyref"; +- case wasm::FUNCREF: ++ case wasm::ValKind::EXTERNREF: ++ return "externref"; ++ case wasm::ValKind::FUNCREF: + return "funcref"; + default: + return "unknown"; +@@ -229,11 +229,11 @@ template wasm::Val makeVal(T t) { return wasm::Val::make(t); } + template <> wasm::Val makeVal(Word t) { return wasm::Val::make(static_cast(t.u64_)); } + + template constexpr auto convertArgToValKind(); +-template <> constexpr auto convertArgToValKind() { return wasm::I32; }; +-template <> constexpr auto convertArgToValKind() { return wasm::I32; }; +-template <> constexpr auto convertArgToValKind() { return wasm::I64; }; +-template <> constexpr auto convertArgToValKind() { return wasm::I64; }; +-template <> constexpr auto convertArgToValKind() { return wasm::F64; }; ++template <> constexpr auto convertArgToValKind() { return wasm::ValKind::I32; }; ++template <> constexpr auto convertArgToValKind() { return wasm::ValKind::I32; }; ++template <> constexpr auto convertArgToValKind() { return wasm::ValKind::I64; }; ++template <> constexpr auto convertArgToValKind() { return wasm::ValKind::I64; }; ++template <> constexpr auto convertArgToValKind() { return wasm::ValKind::F64; }; + + template + constexpr auto convertArgsTupleToValTypesImpl(std::index_sequence /*comptime*/) { +@@ -343,7 +343,8 @@ bool V8::link(std::string_view /*debug_name*/) { + assert(module_ != nullptr); + + const auto import_types = module_.get()->imports(); +- std::vector imports; ++ wasm::vec imports = ++ wasm::vec::make_uninitialized(import_types.size()); + + for (size_t i = 0; i < import_types.size(); i++) { + std::string_view module(import_types[i]->module().get(), import_types[i]->module().size()); +@@ -352,7 +353,7 @@ bool V8::link(std::string_view /*debug_name*/) { + + switch (import_type->kind()) { + +- case wasm::EXTERN_FUNC: { ++ case wasm::ExternKind::FUNC: { + auto it = host_functions_.find(std::string(module) + "." + std::string(name)); + if (it == host_functions_.end()) { + fail(FailState::UnableToInitializeCode, +@@ -372,10 +373,10 @@ bool V8::link(std::string_view /*debug_name*/) { + printValTypes(func->type()->results())); + return false; + } +- imports.push_back(func); ++ imports[i] = func; + } break; + +- case wasm::EXTERN_GLOBAL: { ++ case wasm::ExternKind::GLOBAL: { + // TODO(PiotrSikora): add support when/if needed. + fail(FailState::UnableToInitializeCode, + "Failed to load Wasm module due to a missing import: " + std::string(module) + "." + +@@ -383,7 +384,7 @@ bool V8::link(std::string_view /*debug_name*/) { + return false; + } break; + +- case wasm::EXTERN_MEMORY: { ++ case wasm::ExternKind::MEMORY: { + assert(memory_ == nullptr); + auto type = wasm::MemoryType::make(import_type->memory()->limits()); + if (type == nullptr) { +@@ -393,10 +394,10 @@ bool V8::link(std::string_view /*debug_name*/) { + if (memory_ == nullptr) { + return false; + } +- imports.push_back(memory_.get()); ++ imports[i] = memory_.get(); + } break; + +- case wasm::EXTERN_TABLE: { ++ case wasm::ExternKind::TABLE: { + assert(table_ == nullptr); + auto type = + wasm::TableType::make(wasm::ValType::make(import_type->table()->element()->kind()), +@@ -408,16 +409,12 @@ bool V8::link(std::string_view /*debug_name*/) { + if (table_ == nullptr) { + return false; + } +- imports.push_back(table_.get()); ++ imports[i] = table_.get(); + } break; + } + } + +- if (import_types.size() != imports.size()) { +- return false; +- } +- +- instance_ = wasm::Instance::make(store_.get(), module_.get(), imports.data()); ++ instance_ = wasm::Instance::make(store_.get(), module_.get(), imports); + if (instance_ == nullptr) { + fail(FailState::UnableToInitializeCode, "Failed to create new Wasm instance"); + return false; +@@ -435,16 +432,16 @@ bool V8::link(std::string_view /*debug_name*/) { + + switch (export_type->kind()) { + +- case wasm::EXTERN_FUNC: { ++ case wasm::ExternKind::FUNC: { + assert(export_item->func() != nullptr); + module_functions_.insert_or_assign(std::string(name), export_item->func()->copy()); + } break; + +- case wasm::EXTERN_GLOBAL: { ++ case wasm::ExternKind::GLOBAL: { + // TODO(PiotrSikora): add support when/if needed. + } break; + +- case wasm::EXTERN_MEMORY: { ++ case wasm::ExternKind::MEMORY: { + assert(export_item->memory() != nullptr); + assert(memory_ == nullptr); + memory_ = exports[i]->memory()->copy(); +@@ -453,7 +450,7 @@ bool V8::link(std::string_view /*debug_name*/) { + } + } break; + +- case wasm::EXTERN_TABLE: { ++ case wasm::ExternKind::TABLE: { + // TODO(PiotrSikora): add support when/if needed. + } break; + } +@@ -531,7 +528,8 @@ void V8::registerHostFunctionImpl(std::string_view module_name, std::string_view + convertArgsTupleToValTypes>()); + auto func = wasm::Func::make( + store_.get(), type.get(), +- [](void *data, const wasm::Val params[], wasm::Val /*results*/[]) -> wasm::own { ++ [](void *data, const wasm::vec ¶ms, ++ wasm::vec & /*results*/) -> wasm::own { + auto *func_data = reinterpret_cast(data); + const bool log = func_data->vm_->cmpLogLevel(LogLevel::trace); + if (log) { +@@ -567,7 +565,8 @@ void V8::registerHostFunctionImpl(std::string_view module_name, std::string_view + convertArgsTupleToValTypes>()); + auto func = wasm::Func::make( + store_.get(), type.get(), +- [](void *data, const wasm::Val params[], wasm::Val results[]) -> wasm::own { ++ [](void *data, const wasm::vec ¶ms, ++ wasm::vec &results) -> wasm::own { + auto *func_data = reinterpret_cast(data); + const bool log = func_data->vm_->cmpLogLevel(LogLevel::trace); + if (log) { +@@ -621,20 +620,21 @@ void V8::getModuleFunctionImpl(std::string_view function_name, + const bool log = cmpLogLevel(LogLevel::trace); + SaveRestoreContext saved_context(context); + wasm::own trap = nullptr; ++ wasm::vec results = wasm::vec::make_uninitialized(); + + // Workaround for MSVC++ not supporting zero-sized arrays. + if constexpr (sizeof...(args) > 0) { +- wasm::Val params[] = {makeVal(args)...}; ++ wasm::vec params = wasm::vec::make(makeVal(args)...); + if (log) { + integration()->trace("[host->vm] " + std::string(function_name) + "(" + + printValues(params, sizeof...(Args)) + ")"); + } +- trap = func->call(params, nullptr); ++ trap = func->call(params, results); + } else { + if (log) { + integration()->trace("[host->vm] " + std::string(function_name) + "()"); + } +- trap = func->call(nullptr, nullptr); ++ trap = func->call(wasm::vec::make_uninitialized(), results); + } + + if (trap) { +@@ -671,12 +671,12 @@ void V8::getModuleFunctionImpl(std::string_view function_name, + *function = [func, function_name, this](ContextBase *context, Args... args) -> R { + const bool log = cmpLogLevel(LogLevel::trace); + SaveRestoreContext saved_context(context); +- wasm::Val results[1]; ++ wasm::vec results = wasm::vec::make_uninitialized(1); + wasm::own trap = nullptr; + + // Workaround for MSVC++ not supporting zero-sized arrays. + if constexpr (sizeof...(args) > 0) { +- wasm::Val params[] = {makeVal(args)...}; ++ wasm::vec params = wasm::vec::make(makeVal(args)...); + if (log) { + integration()->trace("[host->vm] " + std::string(function_name) + "(" + + printValues(params, sizeof...(Args)) + ")"); +@@ -686,7 +686,7 @@ void V8::getModuleFunctionImpl(std::string_view function_name, + if (log) { + integration()->trace("[host->vm] " + std::string(function_name) + "()"); + } +- trap = func->call(nullptr, results); ++ trap = func->call(wasm::vec::make_uninitialized(), results); + } + + if (trap) { +-- +2.50.0.727.gbf7dc18ff4-goog + + +From 6c2ffcb9d797d86e817dc29e0dea53e8bd53bdcf Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Fri, 18 Jul 2025 09:29:26 -0400 +Subject: [PATCH 2/3] fix: remove racy call to + isolate->IsExecutionTerminating() + +Signed-off-by: Matt Leon +--- + src/v8/v8.cc | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/src/v8/v8.cc b/src/v8/v8.cc +index 1047f5c..bc5b828 100644 +--- a/src/v8/v8.cc ++++ b/src/v8/v8.cc +@@ -706,9 +706,6 @@ void V8::terminate() { + auto *store_impl = reinterpret_cast(store_.get()); + auto *isolate = store_impl->isolate(); + isolate->TerminateExecution(); +- while (isolate->IsExecutionTerminating()) { +- std::this_thread::yield(); +- } + } + + std::string V8::getFailMessage(std::string_view function_name, wasm::own trap) { +-- +2.50.0.727.gbf7dc18ff4-goog + + +From 23327ea30f714a6ac25f197e47764d1f8960986e Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Tue, 22 Jul 2025 10:45:41 -0400 +Subject: [PATCH 3/3] Use Envoy boringssl + +Signed-off-by: Matt Leon +--- + BUILD | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + diff --git a/BUILD b/BUILD -index 69c9bda..d293092 100644 +index 6db5fd9..aaf7bd2 100644 --- a/BUILD +++ b/BUILD -@@ -88,7 +88,7 @@ cc_library( +@@ -91,7 +91,7 @@ cc_library( ":headers", ] + select({ "//bazel:crypto_system": [], @@ -11,3 +307,6 @@ index 69c9bda..d293092 100644 }), alwayslink = 1, ) +-- +2.50.0.727.gbf7dc18ff4-goog + diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index b7b3e75711744..b142dba79cc02 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -186,7 +186,12 @@ def envoy_dependencies(skip_targets = []): _com_google_protobuf() _com_github_envoyproxy_sqlparser() _v8() - _com_googlesource_chromium_base_trace_event_common() + _fast_float() + _highway() + _dragonbox() + _fp16() + _simdutf() + _intel_ittapi() _com_github_google_quiche() _com_googlesource_googleurl() _io_hyperscan() @@ -501,7 +506,29 @@ def _com_github_facebook_zstd(): def _com_google_cel_cpp(): external_http_archive( - "com_google_cel_cpp", + name = "com_google_cel_cpp", + patch_args = ["-p1"], + patches = ["@envoy//bazel/foreign_cc:cel-cpp.patch"], + ) + + # Load required dependencies that cel-cpp expects. + external_http_archive("com_google_cel_spec") + + # cel-cpp references ``@antlr4-cpp-runtime//:antlr4-cpp-runtime`` but it internally + # defines ``antlr4_runtimes`` with a cpp target. + # We are creating a repository alias to avoid duplicating the ANTLR4 dependency. + native.new_local_repository( + name = "antlr4-cpp-runtime", + path = ".", + build_file_content = """ +package(default_visibility = ["//visibility:public"]) + +# Alias to cel-cpp's embedded ANTLR4 runtime. +alias( + name = "antlr4-cpp-runtime", + actual = "@antlr4_runtimes//:cpp", +) +""", ) def _com_github_google_perfetto(): @@ -710,10 +737,19 @@ def _v8(): name = "v8", patches = [ "@envoy//bazel:v8.patch", - "@envoy//bazel:v8_include.patch", "@envoy//bazel:v8_ppc64le.patch", ], patch_args = ["-p1"], + patch_cmds = [ + "find ./src ./include -type f -exec sed -i.bak -e 's!#include \"third_party/simdutf/simdutf.h\"!#include \"simdutf.h\"!' {} \\;", + "find ./src ./include -type f -exec sed -i.bak -e 's!#include \"third_party/fp16/src/include/fp16.h\"!#include \"fp16.h\"!' {} \\;", + "find ./src ./include -type f -exec sed -i.bak -e 's!#include \"third_party/dragonbox/src/include/dragonbox/dragonbox.h\"!#include \"dragonbox/dragonbox.h\"!' {} \\;", + "find ./src ./include -type f -exec sed -i.bak -e 's!#include \"third_party/fast_float/src/include/fast_float/!#include \"fast_float/!' {} \\;", + ], + repo_mapping = { + "@abseil-cpp": "@com_google_absl", + "@icu": "@com_github_unicode_org_icu", + }, ) # Needed by proxy_wasm_cpp_host. @@ -722,16 +758,38 @@ def _v8(): actual = "@v8//:wee8", ) -def _com_googlesource_chromium_base_trace_event_common(): +def _fast_float(): external_http_archive( - name = "com_googlesource_chromium_base_trace_event_common", - build_file = "@v8//:bazel/BUILD.trace_event_common", + name = "fast_float", ) - # Needed by v8. - native.bind( - name = "base_trace_event_common", - actual = "@com_googlesource_chromium_base_trace_event_common//:trace_event_common", +def _highway(): + external_http_archive( + name = "highway", + ) + +def _dragonbox(): + external_http_archive( + name = "dragonbox", + build_file = "@envoy//bazel/external:dragonbox.BUILD", + ) + +def _fp16(): + external_http_archive( + name = "fp16", + build_file = "@envoy//bazel/external:fp16.BUILD", + ) + +def _simdutf(): + external_http_archive( + name = "simdutf", + build_file = "@envoy//bazel/external:simdutf.BUILD", + ) + +def _intel_ittapi(): + external_http_archive( + name = "intel_ittapi", + build_file = "@envoy//bazel/external:intel_ittapi.BUILD", ) def _com_github_google_quiche(): @@ -963,6 +1021,7 @@ def _com_github_fdio_vpp_vcl(): name = "com_github_fdio_vpp_vcl", build_file_content = _build_all_content(exclude = ["**/*doc*/**", "**/examples/**", "**/plugins/**"]), patches = ["@envoy//bazel/foreign_cc:vpp_vcl.patch"], + patch_args = ["-p1"], ) def _rules_ruby(): diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 955b14b0be1b3..9d90805be8f57 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -34,11 +34,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Bazel features", project_desc = "Support Bazel feature detection from starlark", project_url = "https://github.com/bazel-contrib/bazel_features", - version = "1.32.0", - sha256 = "07bd2b18764cdee1e0d6ff42c9c0a6111ffcbd0c17f0de38e7f44f1519d1c0cd", + version = "1.36.0", + sha256 = "9390b391a68d3b24aef7966bce8556d28003fe3f022a5008efc7807e8acaaf1a", urls = ["https://github.com/bazel-contrib/bazel_features/releases/download/v{version}/bazel_features-v{version}.tar.gz"], strip_prefix = "bazel_features-{version}", - release_date = "2025-06-17", + release_date = "2025-09-17", use_category = ["build"], license = "Apache-2.0", license_url = "https://github.com/bazel-contrib/bazel_features/blob/v{version}/LICENSE", @@ -47,10 +47,10 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Gazelle", project_desc = "Bazel BUILD file generator for Go projects", project_url = "https://github.com/bazelbuild/bazel-gazelle", - version = "0.44.0", - sha256 = "49b14c691ceec841f445f8642d28336e99457d1db162092fd5082351ea302f1d", + version = "0.45.0", + sha256 = "e467b801046b6598c657309b45d2426dc03513777bd1092af2c62eebf990aca5", urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/v{version}/bazel-gazelle-v{version}.tar.gz"], - release_date = "2025-06-13", + release_date = "2025-08-04", use_category = ["build"], license = "Apache-2.0", license_url = "https://github.com/bazelbuild/bazel-gazelle/blob/v{version}/LICENSE", @@ -97,12 +97,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "envoy_examples", project_desc = "Envoy proxy examples", project_url = "https://github.com/envoyproxy/examples", - version = "0.0.18", - sha256 = "093b6598a1498445eea071065fd276ab0b9b0e127bbfabc437b8f6d7ec5116b5", + version = "0.1.2", + sha256 = "42b005266f1a6650bd215afe34c8ec3086cfd1f8c7337dbc873fa336c57ac720", strip_prefix = "examples-{version}", urls = ["https://github.com/envoyproxy/examples/archive/v{version}.tar.gz"], use_category = ["test_only"], - release_date = "2025-07-08", + release_date = "2025-08-19", cpe = "N/A", license = "Apache-2.0", license_url = "https://github.com/envoyproxy/examples/blob/v{version}/LICENSE", @@ -130,11 +130,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_desc = "Bazel rules for fuzz tests", project_url = "https://github.com/bazelbuild/rules_fuzzing", # Patch contains workaround for https://github.com/bazelbuild/rules_python/issues/1221 - version = "0.5.3", - sha256 = "08274422c4383416df5f982943e40d58141f749c09008bb780440eece6b113e4", + version = "0.6.0", + sha256 = "850897989ebc06567ea06c959eb4a6129fa509ed2dbbd0d147d62d2b986714a9", strip_prefix = "rules_fuzzing-{version}", urls = ["https://github.com/bazelbuild/rules_fuzzing/archive/v{version}.tar.gz"], - release_date = "2025-02-18", + release_date = "2025-07-18", use_category = ["test_only"], implied_untracked_deps = [ # This is a repository rule generated to define an OSS-Fuzz fuzzing @@ -164,12 +164,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "AWS libcrypto (AWS-LC)", project_desc = "OpenSSL compatible general-purpose crypto library", project_url = "https://github.com/aws/aws-lc", - version = "1.54.0", - sha256 = "d491b6d6b233e88314a15170d435e28259f7cf4f950a427acc80a0e977aa683a", + version = "1.61.3", + sha256 = "77a48f7cc33fd9712d89b28335933c329946665a8cee8ed91c47b9594db64090", strip_prefix = "aws-lc-{version}", urls = ["https://github.com/aws/aws-lc/archive/v{version}.tar.gz"], use_category = ["controlplane", "dataplane_core"], - release_date = "2025-06-27", + release_date = "2025-09-22", cpe = "cpe:2.3:a:google:boringssl:*", ), aspect_bazel_lib = dict( @@ -190,12 +190,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Abseil", project_desc = "Open source collection of C++ libraries drawn from the most fundamental pieces of Google’s internal codebase", project_url = "https://abseil.io/", - version = "20250512.0", - sha256 = "7262daa7c1711406248c10f41026d685e88223bc92817d16fb93c19adb57f669", + version = "20250814.1", + sha256 = "1692f77d1739bacf3f94337188b78583cf09bab7e420d2dc6c5605a4f86785a1", strip_prefix = "abseil-cpp-{version}", urls = ["https://github.com/abseil/abseil-cpp/archive/{version}.tar.gz"], use_category = ["dataplane_core", "controlplane"], - release_date = "2025-05-12", + release_date = "2025-09-22", cpe = "N/A", license = "Apache-2.0", license_url = "https://github.com/abseil/abseil-cpp/blob/{version}/LICENSE", @@ -204,11 +204,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Shellcheck rules for bazel", project_desc = "Now you do not need to depend on the system shellcheck version in your bazel-managed (mono)repos.", project_url = "https://github.com/aignas/rules_shellcheck", - version = "0.3.3", - sha256 = "936ece8097b734ac7fab10f833a68f7d646b4bc760eb5cde3ab17beb85779d50", + version = "0.4.0", + sha256 = "cef935ea1088d2b45c5bc3630f8178c91ba367b071af2bfdcd16c042c5efe8ae", strip_prefix = "rules_shellcheck-{version}", urls = ["https://github.com/aignas/rules_shellcheck/archive/{version}.tar.gz"], - release_date = "2024-02-15", + release_date = "2025-08-13", use_category = ["build"], cpe = "N/A", license = "MIT", @@ -218,15 +218,15 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "aws-c-auth", project_desc = "C99 library implementation of AWS client-side authentication: standard credentials providers and signing", project_url = "https://github.com/awslabs/aws-c-auth", - version = "0.9.0", - sha256 = "aa6e98864fefb95c249c100da4ae7aed36ba13a8a91415791ec6fad20bec0427", + version = "0.9.1", + sha256 = "adae1e725d9725682366080b8bf8e49481650c436b846ceeb5efe955d5e03273", strip_prefix = "aws-c-auth-{version}", urls = ["https://github.com/awslabs/aws-c-auth/archive/refs/tags/v{version}.tar.gz"], use_category = ["test_only"], extensions = [ "envoy.filters.http.aws_request_signing", ], - release_date = "2025-03-25", + release_date = "2025-09-04", cpe = "N/A", license = "Apache-2.0", license_url = "https://github.com/awslabs/aws-c-auth/blob/v{version}/LICENSE", @@ -235,12 +235,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "liburing", project_desc = "C helpers to set up and tear down io_uring instances", project_url = "https://github.com/axboe/liburing", - version = "2.11", - sha256 = "462c35ef21d67e50490f8684c76641ee2c7796e83d43de796852ef4e40662e33", + version = "2.12", + sha256 = "f1d10cb058c97c953b4c0c446b11e9177e8c8b32a5a88b309f23fdd389e26370", strip_prefix = "liburing-liburing-{version}", urls = ["https://github.com/axboe/liburing/archive/liburing-{version}.tar.gz"], use_category = ["dataplane_core", "controlplane"], - release_date = "2025-06-16", + release_date = "2025-08-23", cpe = "N/A", ), # This dependency is built only when performance tracing is enabled with the @@ -249,12 +249,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Perfetto", project_desc = "Perfetto Tracing SDK", project_url = "https://perfetto.dev/", - version = "50.1", - sha256 = "c2230d04790eb50231a58616a3f1ff6dcf772d8e220333a7711605f99c5c6db9", + version = "52.0", + sha256 = "1872349439820e241003a9862cb69f4d535926c437d5316b49ce4a820613539d", strip_prefix = "perfetto-{version}/sdk", urls = ["https://github.com/google/perfetto/archive/v{version}.tar.gz"], use_category = ["dataplane_core", "controlplane"], - release_date = "2025-04-22", + release_date = "2025-09-16", cpe = "N/A", license = "Apache-2.0", license_url = "https://github.com/google/perfetto/blob/v{version}/LICENSE", @@ -337,12 +337,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "fmt", project_desc = "{fmt} is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams", project_url = "https://fmt.dev", - version = "11.2.0", - sha256 = "203eb4e8aa0d746c62d8f903df58e0419e3751591bb53ff971096eaa0ebd4ec3", + version = "12.0.0", + sha256 = "1c32293203449792bf8e94c7f6699c643887e826f2d66a80869b4f279fb07d25", strip_prefix = "fmt-{version}", urls = ["https://github.com/fmtlib/fmt/releases/download/{version}/fmt-{version}.zip"], use_category = ["dataplane_core", "controlplane"], - release_date = "2025-05-03", + release_date = "2025-09-17", cpe = "cpe:2.3:a:fmt:fmt:*", license = "fmt", license_url = "https://github.com/fmtlib/fmt/blob/{version}/LICENSE", @@ -407,11 +407,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "gperftools", project_desc = "tcmalloc and profiling libraries", project_url = "https://github.com/gperftools/gperftools", - version = "2.10", - sha256 = "83e3bfdd28b8bcf53222c3798d4d395d52dadbbae59e8730c4a6d31a9c3732d8", + version = "2.17.2", + sha256 = "bb172a54312f623b53d8b94cab040248c559decdb87574ed873e80b516e6e8eb", strip_prefix = "gperftools-{version}", urls = ["https://github.com/gperftools/gperftools/releases/download/gperftools-{version}/gperftools-{version}.tar.gz"], - release_date = "2022-05-31", + release_date = "2025-08-15", use_category = ["dataplane_core", "controlplane"], cpe = "cpe:2.3:a:gperftools_project:gperftools:*", license = "BSD-3-Clause", @@ -600,8 +600,8 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "OpenTelemetry", project_desc = "Observability framework and toolkit designed to create and manage telemetry data such as traces, metrics, and logs.", project_url = "https://opentelemetry.io", - version = "1.21.0", - sha256 = "98e5546f577a11b52a57faed1f4cc60d8c1daa44760eba393f43eab5a8ec46a2", + version = "1.22.0", + sha256 = "3428f433f4b435ed1fad64cbdbe75b7288c06f6297786a7036d65d5b9a1d215b", strip_prefix = "opentelemetry-cpp-{version}", urls = ["https://github.com/open-telemetry/opentelemetry-cpp/archive/refs/tags/v{version}.tar.gz"], use_category = ["observability_ext"], @@ -612,8 +612,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( "envoy.tracers.opentelemetry.samplers.cel", "envoy.tracers.opentelemetry.samplers.parent_based", "envoy.tracers.opentelemetry.samplers.trace_id_ratio_based", + "envoy.tracers.zipkin", ], - release_date = "2025-05-29", + release_date = "2025-07-11", cpe = "N/A", license = "Apache-2.0", license_url = "https://github.com/open-telemetry/opentelemetry-cpp/blob/v{version}/LICENSE", @@ -817,12 +818,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "zlib-ng", project_desc = "zlib fork (higher performance)", project_url = "https://github.com/zlib-ng/zlib-ng", - version = "2.2.4", - sha256 = "a73343c3093e5cdc50d9377997c3815b878fd110bf6511c2c7759f2afb90f5a3", + version = "2.2.5", + sha256 = "5b3b022489f3ced82384f06db1e13ba148cbce38c7941e424d6cb414416acd18", strip_prefix = "zlib-ng-{version}", urls = ["https://github.com/zlib-ng/zlib-ng/archive/{version}.tar.gz"], use_category = ["controlplane", "dataplane_core"], - release_date = "2025-02-10", + release_date = "2025-08-07", cpe = "N/A", license = "zlib", license_url = "https://github.com/zlib-ng/zlib-ng/blob/{version}/LICENSE.md", @@ -1052,9 +1053,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "C++ rules for Bazel", project_desc = "Bazel rules for the C++ language", project_url = "https://github.com/bazelbuild/rules_cc", - version = "0.1.1", - sha256 = "712d77868b3152dd618c4d64faaddefcc5965f90f5de6e6dd1d5ddcd0be82d42", - release_date = "2025-02-07", + version = "0.2.8", + sha256 = "207ea073dd20a705f9e8bc5ac02f5203e9621fc672774bb1a0935aefab7aebfa", + release_date = "2025-09-12", strip_prefix = "rules_cc-{version}", urls = ["https://github.com/bazelbuild/rules_cc/releases/download/{version}/rules_cc-{version}.tar.gz"], use_category = [ @@ -1069,11 +1070,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Rules for using foreign build systems in Bazel", project_desc = "Rules for using foreign build systems in Bazel", project_url = "https://github.com/bazelbuild/rules_foreign_cc", - version = "0.14.0", - sha256 = "e0f0ebb1a2223c99a904a565e62aa285bf1d1a8aeda22d10ea2127591624866c", + version = "0.15.1", + sha256 = "32759728913c376ba45b0116869b71b68b1c2ebf8f2bcf7b41222bc07b773d73", strip_prefix = "rules_foreign_cc-{version}", urls = ["https://github.com/bazelbuild/rules_foreign_cc/archive/{version}.tar.gz"], - release_date = "2025-02-11", + release_date = "2025-06-24", use_category = ["build", "dataplane_core", "controlplane"], license = "Apache-2.0", license_url = "https://github.com/bazelbuild/rules_foreign_cc/blob/{version}/LICENSE", @@ -1094,9 +1095,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Python rules for Bazel", project_desc = "Bazel rules for the Python language", project_url = "https://github.com/bazelbuild/rules_python", - version = "1.4.1", - sha256 = "9f9f3b300a9264e4c77999312ce663be5dee9a56e361a1f6fe7ec60e1beef9a3", - release_date = "2025-05-08", + version = "1.6.3", + sha256 = "2f5c284fbb4e86045c2632d3573fc006facbca5d1fa02976e89dc0cd5488b590", + release_date = "2025-09-21", strip_prefix = "rules_python-{version}", urls = ["https://github.com/bazelbuild/rules_python/archive/{version}.tar.gz"], use_category = ["build", "controlplane", "dataplane_core"], @@ -1134,12 +1135,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Shell script rules for Bazel", project_desc = "Bazel rules for shell scripts", project_url = "https://github.com/bazelbuild/rules_shell", - version = "0.5.0", - sha256 = "b15cc2e698a3c553d773ff4af35eb4b3ce2983c319163707dddd9e70faaa062d", + version = "0.6.1", + sha256 = "e6b87c89bd0b27039e3af2c5da01147452f240f75d505f5b6880874f31036307", strip_prefix = "rules_shell-{version}", urls = ["https://github.com/bazelbuild/rules_shell/releases/download/v{version}/rules_shell-v{version}.tar.gz"], use_category = ["build"], - release_date = "2025-06-12", + release_date = "2025-09-10", license = "Apache-2.0", license_url = "https://github.com/protocolbuffers/rules_shell/blob/{version}/LICENSE", ), @@ -1162,11 +1163,11 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "wasmtime", project_desc = "A standalone runtime for WebAssembly", project_url = "https://github.com/bytecodealliance/wasmtime", - version = "24.0.2", - sha256 = "76a5eedf3d57de8a97492006cfa9c2c5eedf81ad82ba173f0615e85695cecdf7", + version = "24.0.4", + sha256 = "d714d987a50cfc7d0b384ef4720e7c757cf4f5b7df617cbf38e432a3dc6c400d", strip_prefix = "wasmtime-{version}", urls = ["https://github.com/bytecodealliance/wasmtime/archive/v{version}.tar.gz"], - release_date = "2024-11-05", + release_date = "2025-07-18", use_category = ["dataplane_ext"], extensions = ["envoy.wasm.runtime.wasmtime"], cpe = "cpe:2.3:a:bytecodealliance:wasmtime:*", @@ -1177,43 +1178,111 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "V8", project_desc = "Google’s open source high-performance JavaScript and WebAssembly engine, written in C++", project_url = "https://v8.dev", - # NOTE: Update together with com_googlesource_chromium_base_trace_event_common. + # NOTE: Update together with proxy_wasm_cpp_host, highway, fast_float, dragonbox, simdutf, and fp16. # Patch contains workaround for https://github.com/bazelbuild/rules_python/issues/1221 - version = "10.7.193.13", + version = "13.8.258.26", # Follow this guide to pick next stable release: https://v8.dev/docs/version-numbers#which-v8-version-should-i-use%3F strip_prefix = "v8-{version}", - sha256 = "6fb91b839e9c36ca4c151268f772e7a6a888a75bcb947f37be9758e49f485db7", + sha256 = "4ffc27074d3f79e8e6401e390443dcf02755349002be4a1b01e72a3cd9457d15", urls = ["https://github.com/v8/v8/archive/refs/tags/{version}.tar.gz"], use_category = ["dataplane_ext"], extensions = ["envoy.wasm.runtime.v8"], - release_date = "2022-09-28", + release_date = "2025-07-09", cpe = "cpe:2.3:a:google:v8:*", ), - com_googlesource_chromium_base_trace_event_common = dict( - project_name = "Chromium's trace event headers", - project_desc = "Chromium's trace event headers", - project_url = "https://chromium.googlesource.com/chromium/src/base/trace_event/common/", - # NOTE: Update together with v8. - # Use version and sha256 from https://storage.googleapis.com/envoyproxy-wee8/v8--deps.sha256. - version = "521ac34ebd795939c7e16b37d9d3ddb40e8ed556", - # Static snapshot created using https://storage.googleapis.com/envoyproxy-wee8/wee8-fetch-deps.sh. - sha256 = "d99726bd452d1dd6cd50ab33060774e8437d9f0fc6079589f657fe369c66ec09", - urls = ["https://storage.googleapis.com/envoyproxy-wee8/chromium-base_trace_event_common-{version}.tar.gz"], + fast_float = dict( + project_name = "fast_float number parsing library", + project_desc = "Fast and exact implementation of the C++ from_chars functions for number types: 4x to 10x faster than strtod, part of GCC 12, Chromium, Redis and WebKit/Safari", + project_url = "https://github.com/fastfloat/fast_float", + # NOTE: Update together with v8 and proxy_wasm_cpp_host. + version = "7.0.0", + # Follow this guide to pick next stable release: https://v8.dev/docs/version-numbers#which-v8-version-should-i-use%3F + strip_prefix = "fast_float-{version}", + sha256 = "d2a08e722f461fe699ba61392cd29e6b23be013d0f56e50c7786d0954bffcb17", + urls = ["https://github.com/fastfloat/fast_float/archive/refs/tags/v{version}.tar.gz"], + use_category = ["dataplane_ext"], + extensions = ["envoy.wasm.runtime.v8"], + release_date = "2024-11-21", + cpe = "N/A", + ), + highway = dict( + project_name = "Efficient and performance-portable vector software", + project_desc = "Performance-portable, length-agnostic SIMD with runtime dispatch", + project_url = "https://github.com/google/highway", + # NOTE: Update together with v8 and proxy_wasm_cpp_host. + version = "1.2.0", + strip_prefix = "highway-{version}", + sha256 = "7e0be78b8318e8bdbf6fa545d2ecb4c90f947df03f7aadc42c1967f019e63343", + urls = ["https://github.com/google/highway/archive/refs/tags/{version}.tar.gz"], + use_category = ["dataplane_ext"], + extensions = ["envoy.wasm.runtime.v8"], + release_date = "2024-05-31", + cpe = "N/A", + ), + dragonbox = dict( + project_name = "Dragonbox", + project_desc = "Reference implementation of dragonbox, a float-to-string conversion algorithm based on a beautiful algorithm Schubfach, developed by Raffaello Giulietti in 2017-2018. Dragonbox is further inspired by Grisu and Grisu-Exact.", + project_url = "https://github.com/jk-jeon/dragonbox", + # NOTE: Update together with v8 and proxy_wasm_cpp_host. + version = "6c7c925b571d54486b9ffae8d9d18a822801cbda", + strip_prefix = "dragonbox-{version}", + sha256 = "2f10448d665355b41f599e869ac78803f82f13b070ce7ef5ae7b5cceb8a178f3", + urls = ["https://github.com/jk-jeon/dragonbox/archive/{version}.zip"], use_category = ["dataplane_ext"], extensions = ["envoy.wasm.runtime.v8"], - release_date = "2022-10-12", + release_date = "2024-10-28", + cpe = "N/A", + ), + fp16 = dict( + project_name = "Conversion to/from half-precision floating point formats", + project_desc = "Header-only library for conversion to/from half-precision floating point formats.", + project_url = "https://github.com/Maratyszcza/FP16", + # NOTE: Update together with v8 and proxy_wasm_cpp_host. + version = "0a92994d729ff76a58f692d3028ca1b64b145d91", + strip_prefix = "FP16-{version}", + sha256 = "e66e65515fa09927b348d3d584c68be4215cfe664100d01c9dbc7655a5716d70", + urls = ["https://github.com/Maratyszcza/FP16/archive/{version}.zip"], + use_category = ["dataplane_ext"], + extensions = ["envoy.wasm.runtime.v8"], + release_date = "2021-03-20", + cpe = "N/A", + ), + simdutf = dict( + project_name = "Unicode validation and transcoding at billions of characters per second", + project_desc = "Unicode routines (UTF8, UTF16, UTF32) and Base64: billions of characters per second using SSE2, AVX2, NEON, AVX-512, RISC-V Vector Extension, LoongArch64, POWER. Part of Node.js, WebKit/Safari, Ladybird, Chromium, Cloudflare Workers and Bun.", + project_url = "https://github.com/simdutf/simdutf", + # NOTE: Update together with v8 and proxy_wasm_cpp_host. + version = "7.3.4", + sha256 = "a8d2b481a2089280b84df7dc234223b658056b5bbd40bd4d476902d25d353a1f", + urls = ["https://github.com/simdutf/simdutf/releases/download/v{version}/singleheader.zip"], + use_category = ["dataplane_ext"], + extensions = ["envoy.wasm.runtime.v8"], + release_date = "2025-08-01", + cpe = "N/A", + ), + intel_ittapi = dict( + project_name = "Intel ITT API", + project_desc = "Intel Instrumentation and Tracing Technology API", + project_url = "https://github.com/intel/ittapi", + version = "a3911fff01a775023a06af8754f9ec1e5977dd97", + sha256 = "1d0dddfc5abb786f2340565c82c6edd1cff10c917616a18ce62ee0b94dbc2ed4", + urls = ["https://github.com/intel/ittapi/archive/{version}.tar.gz"], + strip_prefix = "ittapi-{version}", + use_category = ["dataplane_ext"], + extensions = ["envoy.wasm.runtime.v8"], + release_date = "2021-05-25", cpe = "N/A", ), com_github_google_quiche = dict( project_name = "QUICHE", project_desc = "QUICHE (QUIC, HTTP/2, Etc) is Google‘s implementation of QUIC and related protocols", project_url = "https://github.com/google/quiche", - version = "377a484ce2b0d00bd1dd5a6cc0fa7a7032603bba", - sha256 = "7c7cb027e611e5e5cb14e18da0f6d643674646425bebaab7a8915afc9cd62009", + version = "c84d87b7025c8f89f3630fa485e90dbafee304d6", + sha256 = "0a11a692849d760979b1d6e70498e33fa3587b0c05baf002dc17c274dc706ecc", urls = ["https://github.com/google/quiche/archive/{version}.tar.gz"], strip_prefix = "quiche-{version}", use_category = ["controlplane", "dataplane_core"], - release_date = "2025-07-15", + release_date = "2025-09-14", cpe = "N/A", license = "BSD-3-Clause", license_url = "https://github.com/google/quiche/blob/{version}/LICENSE", @@ -1233,12 +1302,44 @@ REPOSITORY_LOCATIONS_SPEC = dict( license = "googleurl", license_url = "https://quiche.googlesource.com/googleurl/+/{version}/LICENSE", ), + com_google_cel_spec = dict( + project_name = "Common Expression Language (CEL) spec", + project_desc = "Common Expression Language (CEL) spec and conformance tests", + project_url = "https://opensource.google/projects/cel", + version = "0.24.0", + sha256 = "5cba6b0029e727d1f4d8fd134de4e747cecc0bc293d026017d7edc48058d09f7", + strip_prefix = "cel-spec-{version}", + urls = ["https://github.com/google/cel-spec/archive/v{version}.tar.gz"], + use_category = ["dataplane_ext"], + extensions = [ + "envoy.access_loggers.extension_filters.cel", + "envoy.access_loggers.wasm", + "envoy.bootstrap.wasm", + "envoy.rate_limit_descriptors.expr", + "envoy.filters.http.ext_proc", + "envoy.filters.http.rate_limit_quota", + "envoy.filters.http.rbac", + "envoy.filters.http.wasm", + "envoy.filters.network.rbac", + "envoy.filters.network.wasm", + "envoy.stat_sinks.wasm", + "envoy.formatter.cel", + "envoy.matching.inputs.cel_data_input", + "envoy.matching.matchers.cel_matcher", + "envoy.tracers.opentelemetry", + "envoy.tracers.opentelemetry.samplers.cel", + ], + release_date = "2025-05-09", + cpe = "N/A", + license = "Apache-2.0", + license_url = "https://github.com/google/cel-spec/blob/v{version}/LICENSE", + ), com_google_cel_cpp = dict( project_name = "Common Expression Language (CEL) C++ library", project_desc = "Common Expression Language (CEL) C++ library", project_url = "https://opensource.google/projects/cel", - version = "0.10.0", - sha256 = "dd06b708a9f4c3728e76037ec9fb14fc9f6d9c9980e5d5f3a1d047f3855a8b98", + version = "0.13.0", + sha256 = "5766e26732c779b83daf37f9e7584ddee9748adcde662bf1f2291bbeb13e1a0a", strip_prefix = "cel-cpp-{version}", urls = ["https://github.com/google/cel-cpp/archive/v{version}.tar.gz"], use_category = ["dataplane_ext"], @@ -1260,7 +1361,7 @@ REPOSITORY_LOCATIONS_SPEC = dict( "envoy.tracers.opentelemetry", "envoy.tracers.opentelemetry.samplers.cel", ], - release_date = "2024-10-25", + release_date = "2025-07-30", cpe = "N/A", license = "Apache-2.0", license_url = "https://github.com/google/cel-cpp/blob/v{version}/LICENSE", diff --git a/bazel/v8.patch b/bazel/v8.patch index f94a21235b772..f94ac360b881a 100644 --- a/bazel/v8.patch +++ b/bazel/v8.patch @@ -1,62 +1,160 @@ -# 1. Use already imported python dependencies -# 2. Disable pointer compression (limits the maximum number of WasmVMs). -# 3. Add support for --define=no_debug_info=1. -# 4. Allow compiling v8 on macOS 10.15 to 13.0. TODO(dio): Will remove this patch when https://bugs.chromium.org/p/v8/issues/detail?id=13428 is fixed. -# 5. Don't expose Wasm C API (only Wasm C++ API). +From bc2a85e39fd55879b9baed51429c08b27d5514c8 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Wed, 16 Jul 2025 16:55:02 -0400 +Subject: [PATCH 1/8] Disable pointer compression + +Pointer compression limits the maximum number of WasmVMs. + +Signed-off-by: Matt Leon +--- + BUILD.bazel | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.bazel b/BUILD.bazel -index 4e89f90..0df4f67 100644 +index 3f5a87d054e..0a693b7ee10 100644 +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -292,7 +292,7 @@ v8_int( + # If no explicit value for v8_enable_pointer_compression, we set it to 'none'. + v8_string( + name = "v8_enable_pointer_compression", +- default = "none", ++ default = "False", + ) + + # Default setting for v8_enable_pointer_compression. +-- +2.50.0.727.gbf7dc18ff4-goog + + +From e9fb84e11334342ee8fcb50d7322412ab35e2ad0 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Tue, 22 Jul 2025 10:53:33 -0400 +Subject: [PATCH 2/8] Use already imported python dependencies + +Signed-off-by: Matt Leon +--- + BUILD.bazel | 2 +- + third_party/inspector_protocol/code_generator.py | 2 ++ + 2 files changed, 3 insertions(+), 1 deletion(-) + +diff --git a/BUILD.bazel b/BUILD.bazel +index 0a693b7ee10..522f00555e0 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -4,7 +4,7 @@ load("@bazel_skylib//lib:selects.bzl", "selects") - load("@rules_python//python:defs.bzl", "py_binary") + load("@rules_python//python:defs.bzl", "py_binary", "py_test") -load("@v8_python_deps//:requirements.bzl", "requirement") +load("@base_pip3//:requirements.bzl", "requirement") load( "@v8//:bazel/defs.bzl", "v8_binary", -@@ -157,7 +157,7 @@ v8_int( - # If no explicit value for v8_enable_pointer_compression, we set it to 'none'. - v8_string( - name = "v8_enable_pointer_compression", -- default = "none", -+ default = "False", - ) +diff --git a/third_party/inspector_protocol/code_generator.py b/third_party/inspector_protocol/code_generator.py +index b1bedb58951..e85fe664618 100755 +--- a/third_party/inspector_protocol/code_generator.py ++++ b/third_party/inspector_protocol/code_generator.py +@@ -16,6 +16,8 @@ try: + except ImportError: + import simplejson as json - # Default setting for v8_enable_pointer_compression. ++sys.path += [os.path.dirname(__file__)] ++ + import pdl + + try: +-- +2.50.0.727.gbf7dc18ff4-goog + + +From 251ef6027d97bade5e5d8eb5b173baa849c163b8 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Wed, 16 Jul 2025 20:29:10 -0400 +Subject: [PATCH 3/8] Add build flags to make V8 compile with GCC + +Signed-off-by: Matt Leon +--- + bazel/defs.bzl | 3 +++ + 1 file changed, 3 insertions(+) + diff --git a/bazel/defs.bzl b/bazel/defs.bzl -index e957c0f..0327669 100644 +index 0539ea176ac..92c7aeb904f 100644 --- a/bazel/defs.bzl +++ b/bazel/defs.bzl -@@ -116,6 +116,7 @@ def _default_args(): - }) + select({ - "@v8//bazel/config:is_clang": [ - "-Wno-invalid-offsetof", -+ "-Wno-unneeded-internal-declaration", - "-std=c++17", - ], - "@v8//bazel/config:is_gcc": [ -@@ -131,6 +132,9 @@ def _default_args(): - "-Wno-redundant-move", - "-Wno-return-type", - "-Wno-stringop-overflow", -+ "-Wno-nonnull", -+ "-Wno-pessimizing-move", +@@ -106,7 +106,7 @@ def _default_args(): + "@v8//bazel/config:is_posix": [ + "-fPIC", + "-fno-strict-aliasing", +- "-Werror", ++ "-Wno-error", # Envoy build should not fail for warnings in dependencies + "-Wextra", + "-Wno-unneeded-internal-declaration", + "-Wno-unknown-warning-option", # b/330781959 +@@ -117,6 +117,9 @@ def _default_args(): + "-Wno-implicit-int-float-conversion", + "-Wno-deprecated-copy", + "-Wno-non-virtual-dtor", ++ "-Wno-invalid-offsetof", + "-Wno-dangling-pointer", - # Use GNU dialect, because GCC doesn't allow using - # ##__VA_ARGS__ when in standards-conforming mode. - "-std=gnu++17", -@@ -151,6 +154,23 @@ def _default_args(): - "-fno-integrated-as", ++ "-Wno-dangling-reference", + "-isystem .", ], "//conditions:default": [], +-- +2.50.0.727.gbf7dc18ff4-goog + + +From e85e6b017a4843f660bf5331b389a57945a2c21d Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Tue, 22 Jul 2025 10:55:14 -0400 +Subject: [PATCH 4/8] Add support for --define=no_debug_info=1 + +Signed-off-by: Matt Leon +--- + bazel/defs.bzl | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/bazel/defs.bzl b/bazel/defs.bzl +index 92c7aeb904f..937157ccb06 100644 +--- a/bazel/defs.bzl ++++ b/bazel/defs.bzl +@@ -184,6 +184,11 @@ def _default_args(): + }) + select({ + ":should_add_rdynamic": ["-rdynamic"], + "//conditions:default": [], + }) + select({ + "@envoy//bazel:no_debug_info": [ + "-g0", + ], + "//conditions:default": [], + }), + ) + +-- +2.50.0.727.gbf7dc18ff4-goog + + +From b1d40bf065bb46a47cfe2eaf5d3477934f5f8c29 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Tue, 22 Jul 2025 10:55:51 -0400 +Subject: [PATCH 5/8] Allow compiling v8 on macOS 10.15 to 13.0. TODO(dio): + Will remove this patch when + https://bugs.chromium.org/p/v8/issues/detail?id=13428 is fixed. + +Signed-off-by: Matt Leon +--- + bazel/defs.bzl | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/bazel/defs.bzl b/bazel/defs.bzl +index 937157ccb06..39663c97df4 100644 +--- a/bazel/defs.bzl ++++ b/bazel/defs.bzl +@@ -189,6 +189,18 @@ def _default_args(): + "-g0", + ], + "//conditions:default": [], + }) + select({ + "@v8//bazel/config:is_macos": [ + # The clang available on macOS catalina has a warning that isn't clean on v8 code. @@ -70,27 +168,27 @@ index e957c0f..0327669 100644 + ], + "//conditions:default": [], }), - includes = ["include"], - linkopts = select({ -diff --git a/src/compiler/control-equivalence.cc b/src/compiler/control-equivalence.cc -index 4649cf0..6fc6e57 100644 ---- a/src/compiler/control-equivalence.cc -+++ b/src/compiler/control-equivalence.cc -@@ -157,8 +157,8 @@ void ControlEquivalence::RunUndirectedDFS(Node* exit) { - // Pop node from stack when done with all inputs and uses. - DCHECK(entry.input == node->input_edges().end()); - DCHECK(entry.use == node->use_edges().end()); -- DFSPop(stack, node); - VisitPost(node, entry.parent_node, entry.direction); -+ DFSPop(stack, node); - } - } + ) +-- +2.50.0.727.gbf7dc18ff4-goog + + +From 9ed4744c3aecf18f0cc546ea7f4ba7d8cca1f0e7 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Wed, 16 Jul 2025 16:56:52 -0400 +Subject: [PATCH 6/8] Don't expose Wasm C API (only Wasm C++ API). + +Signed-off-by: Matt Leon +--- + src/wasm/c-api.cc | 4 ++++ + 1 file changed, 4 insertions(+) + diff --git a/src/wasm/c-api.cc b/src/wasm/c-api.cc -index 4473e20..65a6ec7 100644 +index 05e4029f183..d705be96a16 100644 --- a/src/wasm/c-api.cc +++ b/src/wasm/c-api.cc -@@ -2247,6 +2247,8 @@ auto Instance::exports() const -> ownvec { +@@ -2472,6 +2472,8 @@ WASM_EXPORT auto Instance::exports() const -> ownvec { } // namespace wasm @@ -99,22 +197,188 @@ index 4473e20..65a6ec7 100644 // BEGIN FILE wasm-c.cc extern "C" { -@@ -3274,3 +3276,5 @@ wasm_instance_t* wasm_frame_instance(const wasm_frame_t* frame) { +@@ -3518,3 +3520,5 @@ wasm_instance_t* wasm_frame_instance(const wasm_frame_t* frame) { #undef WASM_DEFINE_SHARABLE_REF } // extern "C" + +#endif -diff --git a/third_party/inspector_protocol/code_generator.py b/third_party/inspector_protocol/code_generator.py -index c3768b8..d4a1dda 100755 ---- a/third_party/inspector_protocol/code_generator.py -+++ b/third_party/inspector_protocol/code_generator.py -@@ -16,6 +16,8 @@ try: - except ImportError: - import simplejson as json +-- +2.50.0.727.gbf7dc18ff4-goog + + +From 2d4bb2b8bff5a45fd0452912b936e2f9f463b001 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Wed, 16 Jul 2025 20:04:05 -0400 +Subject: [PATCH 7/8] Stub out vendored dependencies for bazel-sourced versions + +Signed-off-by: Matt Leon +--- + BUILD.bazel | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/BUILD.bazel b/BUILD.bazel +index 522f00555e0..f775a934c17 100644 +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -4437,10 +4437,10 @@ v8_library( + ":noicu/generated_torque_definitions", + ], + deps = [ +- ":lib_dragonbox", +- "//third_party/fast_float/src:fast_float", +- ":lib_fp16", +- ":simdutf", ++ "@dragonbox//:dragonbox", ++ "@fast_float//:fast_float", ++ "@fp16//:FP16", ++ "@simdutf//:simdutf", + ":v8_libbase", + "@abseil-cpp//absl/container:btree", + "@abseil-cpp//absl/container:flat_hash_map", +-- +2.50.0.727.gbf7dc18ff4-goog + + +From 5c0418f72733f62242d0c83eaacdb5bfd91c1e67 Mon Sep 17 00:00:00 2001 +From: Matt Leon +Date: Fri, 18 Jul 2025 17:28:42 -0400 +Subject: [PATCH 8/8] Hack out atomic simd support in V8. + +Atomic simdutf requires __cpp_lib_atomic_ref >= 201806, which is only +supported in clang libc++ 19+. The version of LLVM used in Envoy as of +2025-07-18 is libc++ 18, so this is not supported. + +The simdutf documentation indicates this atomic form is not tested and +is not recommended for use: +https://github.com/simdutf/simdutf/blob/5d1b6248f29a8ed0eb90f79be268be41730e39f8/include/simdutf/implementation.h#L3066-L3068 + +In addition, this is in the implementation of a JS array buffer. Since +proxy-wasm-cpp-host does not make use of JS array buffers or shared +memory between web workers, we're stubbing it out. + +Mostly reverts +https://github.com/v8/v8/commit/6d6c1e680c7b8ea5f62a76e9c3d88d3fb0a88df0. + +Signed-off-by: Matt Leon +--- + bazel/defs.bzl | 2 +- + src/builtins/builtins-typed-array.cc | 8 ++++++++ + src/objects/simd.cc | 10 ++++++++++ + 3 files changed, 19 insertions(+), 1 deletion(-) + +diff --git a/bazel/defs.bzl b/bazel/defs.bzl +index 39663c97df4..14b90ec6905 100644 +--- a/bazel/defs.bzl ++++ b/bazel/defs.bzl +@@ -180,7 +180,7 @@ def _default_args(): + "Advapi32.lib", + ], + "@v8//bazel/config:is_macos": ["-pthread"], +- "//conditions:default": ["-Wl,--no-as-needed -ldl -latomic -pthread"], ++ "//conditions:default": ["-Wl,--no-as-needed -ldl -pthread"], + }) + select({ + ":should_add_rdynamic": ["-rdynamic"], + "//conditions:default": [], +diff --git a/src/builtins/builtins-typed-array.cc b/src/builtins/builtins-typed-array.cc +index 918cb873481..bc933e8dc1d 100644 +--- a/src/builtins/builtins-typed-array.cc ++++ b/src/builtins/builtins-typed-array.cc +@@ -520,17 +520,21 @@ simdutf::result ArrayBufferSetFromBase64( + DirectHandle typed_array, size_t& output_length) { + output_length = array_length; + simdutf::result simd_result; ++#ifdef WANT_ATOMIC_REF + if (typed_array->buffer()->is_shared()) { + simd_result = simdutf::atomic_base64_to_binary_safe( + reinterpret_cast(input_vector), input_length, + reinterpret_cast(typed_array->DataPtr()), output_length, + alphabet, last_chunk_handling, /*decode_up_to_bad_char*/ true); + } else { ++#endif + simd_result = simdutf::base64_to_binary_safe( + reinterpret_cast(input_vector), input_length, + reinterpret_cast(typed_array->DataPtr()), output_length, + alphabet, last_chunk_handling, /*decode_up_to_bad_char*/ true); ++#ifdef WANT_ATOMIC_REF + } ++#endif -+sys.path += [os.path.dirname(__file__)] -+ - import pdl + return simd_result; + } +@@ -833,15 +837,19 @@ BUILTIN(Uint8ArrayPrototypeToBase64) { + // 11. Return CodePointsToString(outAscii). - try: + size_t simd_result_size; ++#ifdef WANT_ATOMIC_REF + if (uint8array->buffer()->is_shared()) { + simd_result_size = simdutf::atomic_binary_to_base64( + std::bit_cast(uint8array->DataPtr()), length, + reinterpret_cast(output->GetChars(no_gc)), alphabet); + } else { ++#endif + simd_result_size = simdutf::binary_to_base64( + std::bit_cast(uint8array->DataPtr()), length, + reinterpret_cast(output->GetChars(no_gc)), alphabet); ++#ifdef WANT_ATOMIC_REF + } ++#endif + DCHECK_EQ(simd_result_size, output_length); + USE(simd_result_size); + } +diff --git a/src/objects/simd.cc b/src/objects/simd.cc +index 0ef570ceb7d..9217fa76072 100644 +--- a/src/objects/simd.cc ++++ b/src/objects/simd.cc +@@ -477,6 +477,7 @@ void Uint8ArrayToHexSlow(const char* bytes, size_t length, + } + } + ++#ifdef WANT_ATOMIC_REF + void AtomicUint8ArrayToHexSlow(const char* bytes, size_t length, + DirectHandle string_output) { + int index = 0; +@@ -492,6 +493,7 @@ void AtomicUint8ArrayToHexSlow(const char* bytes, size_t length, + index += 2; + } + } ++#endif + + inline uint16_t ByteToHex(uint8_t byte) { + const uint16_t correction = (('a' - '0' - 10) << 8) + ('a' - '0' - 10); +@@ -645,11 +647,15 @@ Tagged Uint8ArrayToHex(const char* bytes, size_t length, bool is_shared, + } + #endif + ++#ifdef WANT_ATOMIC_REF + if (is_shared) { + AtomicUint8ArrayToHexSlow(bytes, length, string_output); + } else { ++#endif + Uint8ArrayToHexSlow(bytes, length, string_output); ++#ifdef WANT_ATOMIC_REF + } ++#endif + return *string_output; + } + +@@ -1082,12 +1088,16 @@ bool ArrayBufferFromHex(const base::Vector& input_vector, bool is_shared, + for (uint32_t i = 0; i < output_length * 2; i += 2) { + result = HandleRemainingHexValues(input_vector, i); + if (result.has_value()) { ++#ifdef WANT_ATOMIC_REF + if (is_shared) { + std::atomic_ref(buffer[index++]) + .store(result.value(), std::memory_order_relaxed); + } else { ++#endif + buffer[index++] = result.value(); ++#ifdef WANT_ATOMIC_REF + } ++#endif + } else { + return false; + } +-- +2.50.0.727.gbf7dc18ff4-goog + diff --git a/bazel/v8_include.patch b/bazel/v8_include.patch deleted file mode 100644 index 3e6b492bf05d8..0000000000000 --- a/bazel/v8_include.patch +++ /dev/null @@ -1,41 +0,0 @@ -# fix include types for late clang (15.0.7) / gcc (13.2.1) -# for Arch linux / Fedora, like in -# In file included from external/v8/src/torque/torque.cc:5: -# In file included from external/v8/src/torque/source-positions.h:10: -# In file included from external/v8/src/torque/contextual.h:10: -# In file included from external/v8/src/base/macros.h:12: -# external/v8/src/base/logging.h:154:26: error: use of undeclared identifier 'uint16_t' - -diff --git a/src/base/logging.h b/src/base/logging.h ---- a/src/base/logging.h -+++ b/src/base/logging.h -@@ -5,6 +5,7 @@ - #ifndef V8_BASE_LOGGING_H_ - #define V8_BASE_LOGGING_H_ - -+#include - #include - #include - #include -diff --git a/src/base/macros.h b/src/base/macros.h ---- a/src/base/macros.h -+++ b/src/base/macros.h -@@ -5,6 +5,7 @@ - #ifndef V8_BASE_MACROS_H_ - #define V8_BASE_MACROS_H_ - -+#include - #include - #include - -diff --git a/src/inspector/v8-string-conversions.h b/src/inspector/v8-string-conversions.h ---- a/src/inspector/v8-string-conversions.h -+++ b/src/inspector/v8-string-conversions.h -@@ -5,6 +5,7 @@ - #ifndef V8_INSPECTOR_V8_STRING_CONVERSIONS_H_ - #define V8_INSPECTOR_V8_STRING_CONVERSIONS_H_ - -+#include - #include - - // Conversion routines between UT8 and UTF16, used by string-16.{h,cc}. You may diff --git a/changelogs/1.31.10.yaml b/changelogs/1.31.10.yaml new file mode 100644 index 0000000000000..b6f30a929f7f9 --- /dev/null +++ b/changelogs/1.31.10.yaml @@ -0,0 +1,10 @@ +date: July 18, 2025 + +bug_fixes: +- area: release + change: | + Container (Ubuntu/distroless) updates, and fixed permissions for distroless config directory. +- area: dynatrace + change: | + Fixed a division by zero bug in the Dynatrace sampling controller that occurred when ``total_wanted`` was less than + ``top_k_size``. The calculation was refactored to avoid the intermediate division that could result in zero. diff --git a/changelogs/1.32.10.yaml b/changelogs/1.32.10.yaml new file mode 100644 index 0000000000000..139bd7fe2bebc --- /dev/null +++ b/changelogs/1.32.10.yaml @@ -0,0 +1,21 @@ +date: August 19, 2025 + +bug_fixes: +- area: http + change: | + Fixed a bug where the premature resets of streams may result in the recursive draining and potential + stack overflow. Setting proper ``max_concurrent_streams`` value for HTTP/2 or HTTP/3 could eliminate + the risk of the stack overflow before this fix. +- area: listeners + change: | + Fixed issue where :ref:`TLS inspector listener filter ` timed out + when used with other listener filters. The bug was triggered when a previous listener filter processed more data + than the TLS inspector had requested, causing the TLS inspector to incorrectly calculate its buffer growth strategy. + The fix ensures that buffer growth is now based on actual bytes available rather than the previously requested amount. + +new_features: +- area: http + change: | + Added :ref:`ignore_http_11_upgrade + ` + to ignore HTTP/1.1 Upgrade values matching any of the supplied matchers. diff --git a/changelogs/1.32.11.yaml b/changelogs/1.32.11.yaml new file mode 100644 index 0000000000000..935f1c45a820e --- /dev/null +++ b/changelogs/1.32.11.yaml @@ -0,0 +1,7 @@ +date: September 2, 2025 + +bug_fixes: +- area: oauth2 + change: | + Fixed an issue where cookies prefixed with ``__Secure-`` or ``__Host-`` were not receiving a + Secure attribute (`CVE-2025-55162 `_). diff --git a/changelogs/1.32.12.yaml b/changelogs/1.32.12.yaml new file mode 100644 index 0000000000000..169bf1af2d390 --- /dev/null +++ b/changelogs/1.32.12.yaml @@ -0,0 +1,6 @@ +date: September 4, 2025 + +bug_fixes: +- area: release + change: | + Fix distroless image to ensure nonroot. diff --git a/changelogs/1.32.8.yaml b/changelogs/1.32.8.yaml new file mode 100644 index 0000000000000..b6f30a929f7f9 --- /dev/null +++ b/changelogs/1.32.8.yaml @@ -0,0 +1,10 @@ +date: July 18, 2025 + +bug_fixes: +- area: release + change: | + Container (Ubuntu/distroless) updates, and fixed permissions for distroless config directory. +- area: dynatrace + change: | + Fixed a division by zero bug in the Dynatrace sampling controller that occurred when ``total_wanted`` was less than + ``top_k_size``. The calculation was refactored to avoid the intermediate division that could result in zero. diff --git a/changelogs/1.32.9.yaml b/changelogs/1.32.9.yaml new file mode 100644 index 0000000000000..c44dc2829743d --- /dev/null +++ b/changelogs/1.32.9.yaml @@ -0,0 +1,6 @@ +date: July 24, 2025 + +behavior_changes: +- area: wasm + change: | + Bumped up V8 version to ``12.9.202.27`` to address multiple CVEs. diff --git a/changelogs/1.33.5.yaml b/changelogs/1.33.5.yaml new file mode 100644 index 0000000000000..fed61d514fd2e --- /dev/null +++ b/changelogs/1.33.5.yaml @@ -0,0 +1,17 @@ +date: July 18, 2025 + +bug_fixes: +- area: tls + change: | + Fixed an issue with incorrectly cached connection properties on TLS connections. + If TLS connection data was queried before it was available, an empty value was being incorrectly cached, preventing later calls from + getting the correct value. This could be triggered with a ``tcp_proxy`` access log configured to emit a log upon connection + establishment if the log contains fields of the the TLS peer certificate. Then a later use of the data, such as the network RBAC + filter validating a peer certificate SAN, may incorrectly fail due to the empty cached value. +- area: release + change: | + Container (Ubuntu/distroless) updates, and fixed permissions for distroless config directory. +- area: dynatrace + change: | + Fixed a division by zero bug in the Dynatrace sampling controller that occurred when ``total_wanted`` was less than + ``top_k_size``. The calculation was refactored to avoid the intermediate division that could result in zero. diff --git a/changelogs/1.33.6.yaml b/changelogs/1.33.6.yaml new file mode 100644 index 0000000000000..01eef0de2069f --- /dev/null +++ b/changelogs/1.33.6.yaml @@ -0,0 +1,11 @@ +date: July 24, 2025 + +behavior_changes: +- area: wasm + change: | + Bumped up V8 version to ``12.9.202.27`` to address multiple CVEs. + +bug_fixes: +- area: wasm + change: | + Bumped wasmtime version to 24.0.2 to address CVE. diff --git a/changelogs/1.33.7.yaml b/changelogs/1.33.7.yaml new file mode 100644 index 0000000000000..139bd7fe2bebc --- /dev/null +++ b/changelogs/1.33.7.yaml @@ -0,0 +1,21 @@ +date: August 19, 2025 + +bug_fixes: +- area: http + change: | + Fixed a bug where the premature resets of streams may result in the recursive draining and potential + stack overflow. Setting proper ``max_concurrent_streams`` value for HTTP/2 or HTTP/3 could eliminate + the risk of the stack overflow before this fix. +- area: listeners + change: | + Fixed issue where :ref:`TLS inspector listener filter ` timed out + when used with other listener filters. The bug was triggered when a previous listener filter processed more data + than the TLS inspector had requested, causing the TLS inspector to incorrectly calculate its buffer growth strategy. + The fix ensures that buffer growth is now based on actual bytes available rather than the previously requested amount. + +new_features: +- area: http + change: | + Added :ref:`ignore_http_11_upgrade + ` + to ignore HTTP/1.1 Upgrade values matching any of the supplied matchers. diff --git a/changelogs/1.33.8.yaml b/changelogs/1.33.8.yaml new file mode 100644 index 0000000000000..ec53a9b62b5cd --- /dev/null +++ b/changelogs/1.33.8.yaml @@ -0,0 +1,7 @@ +date: September 2, 2025 + +bug_fixes: +- area: oauth2 + change: | + Fixed an issue where cookies prefixed with ``__Secure-`` or ``__Host-`` were not receiving a + Secure attribute. diff --git a/changelogs/1.33.9.yaml b/changelogs/1.33.9.yaml new file mode 100644 index 0000000000000..ffaacf445b462 --- /dev/null +++ b/changelogs/1.33.9.yaml @@ -0,0 +1,6 @@ +date: September 5, 2025 + +bug_fixes: +- area: release + change: | + Fix distroless image to ensure nonroot. diff --git a/changelogs/1.34.3.yaml b/changelogs/1.34.3.yaml new file mode 100644 index 0000000000000..e01bff77426c0 --- /dev/null +++ b/changelogs/1.34.3.yaml @@ -0,0 +1,20 @@ +date: July 18, 2025 + +bug_fixes: +- area: tls + change: | + Fixed an issue with incorrectly cached connection properties on TLS connections. + If TLS connection data was queried before it was available, an empty value was being incorrectly cached, preventing later calls from + getting the correct value. This could be triggered with a ``tcp_proxy`` access log configured to emit a log upon connection + establishment if the log contains fields of the the TLS peer certificate. Then a later use of the data, such as the network RBAC + filter validating a peer certificate SAN, may incorrectly fail due to the empty cached value. +- area: http2 + change: | + Fixed an issue where http/2 connections using the default codec of ``oghttp2`` could get stuck due to a window buffer leak. +- area: release + change: | + Container (Ubuntu/distroless) updates, and fixed permissions for distroless config directory. +- area: dynatrace + change: | + Fixed a division by zero bug in the Dynatrace sampling controller that occurred when ``total_wanted`` was less than + ``top_k_size``. The calculation was refactored to avoid the intermediate division that could result in zero. diff --git a/changelogs/1.34.4.yaml b/changelogs/1.34.4.yaml new file mode 100644 index 0000000000000..01eef0de2069f --- /dev/null +++ b/changelogs/1.34.4.yaml @@ -0,0 +1,11 @@ +date: July 24, 2025 + +behavior_changes: +- area: wasm + change: | + Bumped up V8 version to ``12.9.202.27`` to address multiple CVEs. + +bug_fixes: +- area: wasm + change: | + Bumped wasmtime version to 24.0.2 to address CVE. diff --git a/changelogs/1.34.5.yaml b/changelogs/1.34.5.yaml new file mode 100644 index 0000000000000..786669800f88b --- /dev/null +++ b/changelogs/1.34.5.yaml @@ -0,0 +1,14 @@ +date: August 19, 2025 + +bug_fixes: +- area: http + change: | + Fixed a bug where the premature resets of streams may result in the recursive draining and potential + stack overflow. Setting proper ``max_concurrent_streams`` value for HTTP/2 or HTTP/3 could eliminate + the risk of the stack overflow before this fix. +- area: listeners + change: | + Fixed issue where :ref:`TLS inspector listener filter ` timed out + when used with other listener filters. The bug was triggered when a previous listener filter processed more data + than the TLS inspector had requested, causing the TLS inspector to incorrectly calculate its buffer growth strategy. + The fix ensures that buffer growth is now based on actual bytes available rather than the previously requested amount. diff --git a/changelogs/1.34.6.yaml b/changelogs/1.34.6.yaml new file mode 100644 index 0000000000000..58aba7edbc15c --- /dev/null +++ b/changelogs/1.34.6.yaml @@ -0,0 +1,11 @@ +date: September 2, 2025 + +bug_fixes: +- area: oauth2 + change: | + Fixed an issue where cookies prefixed with ``__Secure-`` or ``__Host-`` were not receiving a + Secure attribute. +- area: dns + change: | + Fixed an UAF in DNS cache that can occur when the Host header is modified between the Dynamic Forwarding and Router + filters. diff --git a/changelogs/1.34.7.yaml b/changelogs/1.34.7.yaml new file mode 100644 index 0000000000000..ffaacf445b462 --- /dev/null +++ b/changelogs/1.34.7.yaml @@ -0,0 +1,6 @@ +date: September 5, 2025 + +bug_fixes: +- area: release + change: | + Fix distroless image to ensure nonroot. diff --git a/changelogs/1.35.0.yaml b/changelogs/1.35.0.yaml new file mode 100644 index 0000000000000..71562fafc19cb --- /dev/null +++ b/changelogs/1.35.0.yaml @@ -0,0 +1,403 @@ +date: July 23, 2025 + +behavior_changes: +- area: aws_iam + change: | + As announced in November 2024 (see https://github.com/envoyproxy/envoy/issues/37621), the + ``grpc_credentials/aws_iam`` extension is being deleted. Any configuration referencing this extension + will fail to load. +- area: prefix_match_map + change: | + :ref:`prefix_match_map ` + now continues to search for a match with a shorter prefix if a longer match + does not find an action. This brings it in line with the behavior of ``matcher_list``. + This change can temporarily be reverted by setting the runtime guard + ``envoy.reloadable_features.prefix_map_matcher_resume_after_subtree_miss`` to ``false``. + If the old behavior is desired more permanently, this can be achieved in config by setting + an ``on_no_match`` action that responds with ``404`` for each subtree. +- area: server + change: | + Envoy will automatically raise the soft limit on the file descriptors to the hard limit. This behavior + can be reverted using the runtime guard ``envoy_restart_features_raise_file_limits``. +- area: build + change: | + Removed the ``clang-libstdc++`` toolchain setup as this is no longer used or tested by the project. + Consolidated Clang and GCC toolchains which can be used with ``--config=clang`` or ``--config=gcc``. + These use ``libc++`` and ``libstdc++`` respectively. +- area: squash_filter + change: | + The Squash HTTP filter in ``contrib`` has been deleted. The project it provided integration with has been idle for + five years and appears abandoned. +- area: wasm + change: | + Bumped up V8 version to ``13.8.258.26`` to address multiple CVEs. + +minor_behavior_changes: +- area: geoip + change: | + The lookup for ASN information is fetched from ``asn_db`` if ``asn_db_path`` is defined and from ``isp_db`` if + ``asn_db_path`` is not defined. +- area: lua + change: | + The ``metadata()`` of the Lua filter now will search the metadata by the :ref:`filter config name + ` first. + And if not found, it will search by the canonical name of the filter ``envoy.filters.http.lua``. +- area: grpc-json + change: | + Made the :ref:`gRPC JSON transcoder filter's ` JSON print options configurable. +- area: oauth2 + change: | + Reset CSRF token when token validation fails during redirection. + If the CSRF token cookie is present during the redirection to the authorization server, it will be validated. + Previously, if this validation failed, the OAuth flow would fail. Now the CSRF token will simply be reset. This fixes + the case where an HMAC secret change causes a redirect flow, but the CSRF token cookie hasn't yet expired + causing a CSRF token validation failure. +- area: cel + change: | + Precompile regexes in CEL expressions. This can be disabled by setting the runtime guard + ``envoy.reloadable_features.enable_cel_regex_precompilation`` to ``false``. +- area: dns + change: | + Allow ``getaddrinfo`` to be configured to run by a thread pool, controlled by :ref:`num_resolver_threads + `. +- area: dns + change: | + Honor the default DNS resolver configuration in the bootstrap config + :ref:`typed_dns_resolver_config ` + if the DNS cache configuration in the dynamic forward proxy filter is empty + :ref:`dns_cache_config `. +- area: grpc-json-transcoding + change: | + Added SSE style message framing for streamed responses in :ref:`gRPC JSON transcoder filter `. +- area: http + change: | + :ref:`response_headers_to_add ` and + :ref:`response_headers_to_remove ` + will also be applied to the local responses from the ``envoy.filters.http.router`` filter. +- area: tracing + change: | + Added :ref:`max_cache_size ` + to the OpenTelemetry tracer config. This limits the number of spans that can be cached before flushing. +- area: aws + change: | + :ref:`AwsCredentialProvider ` now supports all defined credential + providers, allowing complete customization of the credential provider chain when using AWS request signing extension. +- area: ext_proc + change: | + If the ext_proc server sends a spurious response message to Envoy, Envoy now performs fail-open or fail-close action based on + :ref:`failure_mode_allow ` + configuration. This change can be reverted by setting the runtime guard + ``envoy.reloadable_features.ext_proc_fail_close_spurious_resp`` to ``false``. +- area: filters + change: | + :ref:`Credential injector filter ` is no longer + a work in progress field. +- area: oauth2 + change: | + The access token, ID token and refresh token in the cookies are now encrypted using the HMAC secret. This behavior can + be reverted by setting the runtime guard ``envoy.reloadable_features.oauth2_encrypt_tokens`` to ``false``. +- area: http3 + change: | + Validate HTTP/3 pseudo headers. Can be disabled by setting ``envoy.restart_features.validate_http3_pseudo_headers`` to ``false``. +- area: formatter + change: | + Now the ``METADATA`` and ``CEL`` substitution formatters can access or log the metadata of + the virtual host in case the route is not matched but the virtual host is found. +- area: oauth2 + change: | + Extension status changed from ``alpha`` to ``stable``. +- area: oauth2 + change: | + Starting from this release, these cookies: oauth_hmac,oauth_expires,refresh_token,oauth_nonce,code_verifier will + not be forwarded to the upstream. This behavior can be reverted by setting the runtime guard + ``envoy.reloadable_features.oauth2_cleanup_cookies`` to ``false``. + +bug_fixes: +- area: geoip + change: | + Fixed a bug where the ``apple_private_relay`` header was not populated correctly when isp header wasn't set. +- area: conn_pool + change: | + Fixed an issue that could lead to too many connections when using + :ref:`AutoHttpConfig ` if the + established connection is HTTP/2 and Envoy predicted it would have lower concurrent capacity. +- area: conn_pool + change: | + Fixed an issue that could lead to insufficient connections for current pending requests. If a connection starts draining while it + has negative unused capacity (which happens if an HTTP/2 ``SETTINGS`` frame reduces allowed concurrency to below the current number + of requests), that connection's unused capacity will be included in total pool capacity even though it is unusable because it is + draining. This can result in not enough connections being established for current pending requests. This is most problematic for + long-lived requests (such as streaming gRPC requests or long-poll requests) because a connection could be in the draining state + for a long time. +- area: hcm + change: | + Fixed a bug where the lifetime of the ``HttpConnectionManager``'s ``ActiveStream`` can be out of sync + with the lifetime of the codec stream. +- area: config_validation + change: | + Fixed a bug where the config validation server will crash when the configuration contains + ``%CEL%`` or ``%METADATA%`` substitution formatter. +- area: tls + change: | + Fixed an issue with incorrectly cached connection properties on TLS connections. + If TLS connection data was queried before it was available, an empty value was being incorrectly cached, preventing later calls from + getting the correct value. This could be triggered with a ``tcp_proxy`` access log configured to emit a log upon connection + establishment if the log contains fields of the TLS peer certificate. Then a later use of the data, such as the network RBAC + filter validating a peer certificate SAN, may incorrectly fail due to the empty cached value. +- area: quic + change: | + Fixed a bug in Envoy's HTTP/3-to-HTTP/1 proxying when a ``transfer-encoding`` header is incorrectly appended. + Protected by runtime guard ``envoy.reloadable_features.quic_signal_headers_only_to_http1_backend``. +- area: runtime + change: | + Fixed a bug which resulted in an ``ENVOY_BUG`` being incorrectly triggered when runtime settings + ``envoy.reloadable_features.max_request_headers_count``, ``envoy.reloadable_features.max_response_headers_count``, + ``envoy.reloadable_features.max_request_headers_size_kb``, or ``envoy.reloadable_features.max_response_headers_size_kb`` were set. +- area: tls + change: | + Fixed a bug where empty trusted CA file or inline string is accepted and causes Envoy to successfully validate any certificate + chain. This fix addresses this issue by rejecting such configuration with empty value. This behavior can be reverted by setting + the runtime guard ``envoy.reloadable_features.reject_empty_trusted_ca_file`` to ``false``. +- area: tls_inspector + change: | + Fixed a bug where the TLS inspector filter would not correctly report ``client_hello_too_large`` stat for too big client + hello messages, i.e., bigger than 16 KB. +- area: wasm + change: | + Fixed a bug where the Wasm filter hangs when the VM is crashed in the request callbacks. +- area: dynatrace + change: | + Fixed a division by zero bug in the Dynatrace sampling controller that occurred when ``total_wanted`` was less than + ``top_k_size``. The calculation was refactored to avoid the intermediate division that could result in zero. +- area: wasm + change: | + Bumped wasmtime version to 24.0.2 to address CVE. + +removed_config_or_runtime: +- area: websocket + change: | + Removed runtime guard ``envoy.reloadable_features.switch_protocol_websocket_handshake`` and legacy code paths. +- area: http2 + change: | + Removed runtime guard ``envoy.reloadable_features.http2_no_protocol_error_upon_clean_close`` and legacy code paths. +- area: access_log + change: | + Removed runtime guard ``envoy.reloadable_features.sanitize_sni_in_access_log`` and legacy code paths. +- area: quic + change: | + Removed runtime guard ``envoy.reloadable_features.quic_connect_client_udp_sockets`` and legacy code paths. +- area: quic + change: | + Removed runtime guard ``envoy.reloadable_features.quic_support_certificate_compression`` and legacy code paths. +- area: http + change: | + Removed runtime guard ``envoy.reloadable_features.internal_authority_header_validator`` and legacy code paths. +- area: http + change: | + Removed runtime guard ``envoy_reloadable_features_filter_access_loggers_first`` and legacy code paths. +- area: tcp_proxy + change: | + Removed runtime guard ``envoy.reloadable_features.tcp_tunneling_send_downstream_fin_on_upstream_trailers`` and legacy code paths. +- area: runtime + change: | + Removed runtime guard ``envoy_reloadable_features_boolean_to_string_fix`` and legacy code paths. +- area: logging + change: | + Removed runtime guard ``envoy.reloadable_features.logging_with_fast_json_formatter`` and legacy code paths. +- area: sni + change: | + Removed runtime guard ``envoy.reloadable_features.use_route_host_mutation_for_auto_sni_san`` and legacy code paths. +- area: ext_proc + change: | + Removed runtime guard ``envoy.reloadable_features.ext_proc_timeout_error`` and legacy code paths. +- area: quic + change: | + Removed runtime guard ``envoy.reloadable_features.extend_h3_accept_untrusted`` and legacy code paths. +- area: lua + change: | + Removed runtime guard ``envoy.reloadable_features.lua_flow_control_while_http_call`` and legacy code paths. +- area: http1 + change: | + Removed runtime guard ``envoy.reloadable_features.envoy_reloadable_features_http1_use_balsa_parser`` and legacy code paths. + +new_features: +- area: build + change: | + Upgraded Envoy to build with C++20; Envoy developers can use C++20 features now. +- area: redis + change: | + Added support for ``SCAN``, ``INFO`` and ``ROLE``. +- area: http + change: | + Added :ref:`x-envoy-original-host ` that + is used to record the original host header value before it is mutated by the router filter. +- area: stateful_session + change: | + Support for envelope stateful session extension to keep the existing session header value + from upstream server. See :ref:`mode + ` + for more details. +- area: transport_tap + change: | + Added counter in transport tap for streaming and buffer trace. + Streamed trace can send tapped message based on configured size. +- area: udp_sink + change: | + Enhanced UDP sink to support a single message whose size is bigger than 64 KB. +- area: load_balancing + change: | + Added Override Host Load Balancing policy. See + :ref:`load balancing policies overview ` for more details. +- area: load_balancing + change: | + Added :ref:`hash policy support + ` + to the ring hash and maglev load balancing policies. If the hash policy in the load balancer is + configured, the + :ref:`route level hash policy ` + will be ignored. +- area: lua + change: | + Added support for accessing filter context. + See :ref:`filterContext() ` for more details. +- area: resource_monitors + change: | + Added new cgroup memory resource monitor that reads memory usage/limit from cgroup v1/v2 subsystems and calculates + memory pressure, with configurable ``max_memory_bytes`` limit + :ref:`existing extension `. +- area: ext_authz + change: | + Added ``grpc_status`` to ``ExtAuthzLoggingInfo`` in ``ext_authz`` HTTP filter. +- area: http + change: | + Added :ref:`response trailers mutations + ` and + :ref:`request trailers mutations + ` + to :ref:`Header Mutation Filter ` + for adding/removing trailers from the request and the response. +- area: postgres + change: | + Added support for requiring downstream SSL. +- area: url_template + change: | + Included the asterisk ``*`` in the match pattern when using the ``*`` or ``**`` operators in the URL template. + This behavioral change can be temporarily reverted by setting runtime guard + ``envoy.reloadable_features.uri_template_match_on_asterisk`` to ``false``. +- area: socket + change: | + Added ``network_namespace_filepath`` to ``SocketAddress``. Currently only used by listeners. +- area: rbac_filter + change: | + Allow-listed ``FilterStateInput`` to be used with the xDS matcher in the HTTP RBAC filter. +- area: rbac_filter + change: | + Allow-listed ``FilterStateInput`` to be used with the xDS matcher in the Network RBAC filter. +- area: tls_inspector_filter + change: | + Added :ref:`enable_ja4_fingerprinting + ` to create + a JA4 fingerprint hash from the Client Hello message. +- area: local_ratelimit + change: | + ``local_ratelimit`` will return ``x-ratelimit-reset`` header when the rate limit is exceeded. +- area: oauth2 + change: | + Added :ref:`end_session_endpoint ` + to the ``oauth2`` filter to support OIDC RP initiated logout. This field is only used when + ``openid`` is in the :ref:`auth_scopes ` field. + If configured, the OAuth2 filter will redirect users to this endpoint when they access the + :ref:`signout_path `. This allows users to + be logged out of the Authorization server. +- area: tcp_access_logs + change: | + Added support for ``%BYTES_RECEIVED%``, ``%BYTES_SENT%``, ``%UPSTREAM_HEADER_BYTES_SENT%``, ``%UPSTREAM_HEADER_BYTES_RECEIVED%``, + ``%UPSTREAM_WIRE_BYTES_SENT%``, ``%UPSTREAM_WIRE_BYTES_RECEIVED%`` access log substitution strings for TCP tunneling flows. +- area: oauth2 + change: | + Added configurable :ref:`csrf_token_expires_in + ` + and :ref:`code_verifier_token_expires_in + ` + fields to the ``oauth2`` filter. Both default to ``600s`` (10 minutes) if not specified, keeping backward compatibility. +- area: load_shed_point + change: | + Added load shed point ``envoy.load_shed_points.connection_pool_new_connection`` in the connection pool, and it will not + create new connections when Envoy is under pressure, and the pending downstream requests will be cancelled. +- area: api_key_auth + change: | + Added :ref:`forwarding configuration ` + to the API Key Auth filter, which allows forwarding the authenticated client identity + using a custom header, and also offers the option to remove the API key from the request + before forwarding. +- area: lua + change: | + Added a new ``dynamicTypedMetadata()`` on ``connectionStreamInfo()`` which can be used to access the typed metadata from + network filters, such as the Proxy Protocol, etc. +- area: aws + change: | + Implementation of `IAM Roles Anywhere support `_ in the + AWS common components, providing this capability to the AWS Lambda and AWS Request Signing extensions. +- area: redis + change: | + ``redis_proxy`` filter now supports AWS IAM Authentication. +- area: router + change: | + Added matcher based router cluster specifier plugin to support selecting cluster dynamically based on a matcher tree. + See + :ref:`matcher cluster specifier plugin ` + for more details. +- area: router + change: | + Added new ``refreshRouteCluster()`` method to stream filter callbacks to support refreshing the route cluster and + does not need to update the route cache. See :ref:`http route mutation ` for + more details. +- area: lua + change: | + Added a new ``dynamicTypedMetadata()`` on ``streamInfo()`` which can be used to access the typed metadata from + HTTP filters, such as the Set Metadata filter, etc. +- area: ratelimit + change: | + Added a new ``failure_mode_deny_percent`` field of type ``Envoy::Runtime::FractionalPercent`` attached to the rate + limit filter to configure the failure mode for rate limit service errors in runtime. + It acts as an override for the existing ``failure_mode_deny`` field in the filter config. +- area: tls + change: | + Added new metric for emitting seconds since UNIX epoch of expirations of TLS and CA certificates. + They are rooted at ``cluster..ssl.certificate..`` + and at ``listener.
.ssl.certificate..`` namespace. +- area: ext_proc + change: | + The :ref:`failure_mode_allow ` + setting may now be overridden on a per-route basis. +- area: matcher + change: | + Added support for :ref:`ServerNameMatcher ` trie-based matching. +- area: stateful_session + change: | + Added support for cookie attributes to stateful session cookie. +- area: http3 + change: | + Added :ref:`disable_connection_flow_control_for_streams + `, an experimental + option for disabling connection level flow control for streams. This is useful in situations where the streams share the same + connection but originate from different end-clients, so that each stream can make progress independently at non-front-line proxies. +- area: dfp + change: | + Added :ref:`allow_dynamic_host_from_filter_state + ` + flag to HTTP Dynamic Forward Proxy filter. When enabled, the filter will check for ``envoy.upstream.dynamic_host`` and + ``envoy.upstream.dynamic_port`` filter state values before using the HTTP Host header, providing consistency with SNI + and UDP DFP filters. When disabled (default), maintains backward compatibility by using the HTTP Host header directly. +- area: alts + change: | + Added environment variable-protected gRPC keepalive params to the ALTS handshaker client. +- area: wasm + change: | + Added support for returning ``StopIteration`` from plugin ``onRequestHeader`` and ``onResponseHeader`` callbacks. See + :ref:`allow_on_headers_stop_iteration ` + for more details. By default, current behavior is maintained. +- area: aws + change: | + Added ``assume_role_credential_provider`` to add support for role chaining in AWS filters. This allows envoy to + assume an additional role before SigV4 signing occurs. diff --git a/changelogs/1.35.1.yaml b/changelogs/1.35.1.yaml new file mode 100644 index 0000000000000..daca86aecd04a --- /dev/null +++ b/changelogs/1.35.1.yaml @@ -0,0 +1,19 @@ +date: August 19, 2025 + +behavior_changes: +- area: ext_proc + change: | + Reverted https://github.com/envoyproxy/envoy/pull/39740 to re-enable fail_open+FULL_DUPLEX_STREAMED configuraton combination. + +bug_fixes: +- area: http + change: | + Fixed a bug where the premature resets of streams may result in the recursive draining and potential + stack overflow. Setting proper ``max_concurrent_streams`` value for HTTP/2 or HTTP/3 could eliminate + the risk of the stack overflow before this fix. +- area: listeners + change: | + Fixed issue where :ref:`TLS inspector listener filter ` timed out + when used with other listener filters. The bug was triggered when a previous listener filter processed more data + than the TLS inspector had requested, causing the TLS inspector to incorrectly calculate its buffer growth strategy. + The fix ensures that buffer growth is now based on actual bytes available rather than the previously requested amount. diff --git a/changelogs/1.35.2.yaml b/changelogs/1.35.2.yaml new file mode 100644 index 0000000000000..176322ce426c1 --- /dev/null +++ b/changelogs/1.35.2.yaml @@ -0,0 +1,17 @@ +date: September 3, 2025 + +bug_fixes: +- area: oauth2 + change: | + Fixed an issue where cookies prefixed with ``__Secure-`` or ``__Host-`` were not receiving a + Secure attribute. +- area: dns + change: | + Fixed an UAF in DNS cache that can occur when the Host header is modified between the Dynamic Forwarding and Router + filters. +- area: stats + change: | + Fixed a bug where the metric name ``expiration_unix_time_seconds`` of + ``cluster..ssl.certificate..`` + and ``listener.
.ssl.certificate..`` + was not being properly extracted in the final prometheus stat name. diff --git a/changelogs/1.35.3.yaml b/changelogs/1.35.3.yaml new file mode 100644 index 0000000000000..4d41014e50317 --- /dev/null +++ b/changelogs/1.35.3.yaml @@ -0,0 +1,10 @@ +date: September 8, 2025 + +bug_fixes: +- area: http + change: | + Fixed a bug where the ``response_headers_to_add`` may be processed multiple times for the local responses from + the router filter. +- area: release + change: | + Fix distroless image to ensure nonroot. diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 087adf526ea56..d89ec4b7c0169 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -1,400 +1,503 @@ date: Pending behavior_changes: -# *Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required* -- area: aws_iam - change: | - As announced in November 2024 (see https://github.com/envoyproxy/envoy/issues/37621), the - ``grpc_credentials/aws_iam`` extension is being deleted. Any configuration referencing this extension - will fail to load. -- area: prefix_match_map - change: | - :ref:`prefix_match_map ` - now continues to search for a match with a shorter prefix if a longer match - does not find an action. This brings it in line with the behavior of ``matcher_list``. - This change can temporarily be reverted by setting the runtime guard - ``envoy.reloadable_features.prefix_map_matcher_resume_after_subtree_miss`` to ``false``. - If the old behavior is desired more permanently, this can be achieved in config by setting - an ``on_no_match`` action that responds with ``404`` for each subtree. -- area: server - change: | - Envoy will automatically raise the soft limit on the file descriptors to the hard limit. This behavior - can be reverted using the runtime guard ``envoy_restart_features_raise_file_limits``. -- area: build - change: | - Removed the ``clang-libstdc++`` toolchain setup as this is no longer used or tested by the project. - Consolidated Clang and GCC toolchains which can be used with ``--config=clang`` or ``--config=gcc``. - These use ``libc++`` and ``libstdc++`` respectively. -- area: squash_filter - change: | - The Squash HTTP filter in ``contrib`` has been deleted. The project it provided integration with has been idle for - five years and appears abandoned. +- area: http + change: | + A route refresh will now result in a tracing refresh. The trace sampling decision and decoration + of the new route will be applied to the active span. + This change can be reverted by setting the runtime guard + ``envoy.reloadable_features.trace_refresh_after_route_refresh`` to ``false``. + Note, if :ref:`pack_trace_reason + ` is set + to ``true`` (it is ``true`` by default), a request marked as traced cannot be unmarked as traced + after the tracing refresh. +- area: http2 + change: | + The default value for the :ref:`maximum number of concurrent streams in HTTP/2 + ` + has been changed from 2147483647 to 1024. + The default value for the :ref:`initial stream window size in HTTP/2 + ` + has been changed from 256MiB to 16MiB. + The default value for the :ref:`initial connection window size in HTTP/2 + ` + has been changed from 256MiB to 24MiB. + This change could be reverted temporarily by + setting the runtime guard ``envoy.reloadable_features.safe_http2_options`` + to ``false``. +- area: ext_proc + change: | + Reverted `#39740 `_ to re-enable ``fail_open`` + + ``FULL_DUPLEX_STREAMED`` configuration combination. +- area: load balancing + change: | + Moved locality WRR structures out of ``HostSetImpl`` and into a separate class. Locality WRR schedulers are now by default owned + and constructed by the underlying Zone Aware LB, instead of owned and constructed by the Host Set. There should be no visible + behavior change for existing users of Zone Aware LBs. minor_behavior_changes: # *Changes that may cause incompatibilities for some users, but should not for most* -- area: geoip +- area: tap change: | - The lookup for ASN information is fetched from ``asn_db`` if ``asn_db_path`` is defined and from ``isp_db`` if - ``asn_db_path`` is not defined. -- area: lua - change: | - The ``metadata()`` of the Lua filter now will search the metadata by the :ref:`filter config name - ` first. - And if not found, it will search by the canonical name of the filter ``envoy.filters.http.lua``. -- area: grpc-json + Previously, streamed trace buffered data was only flushed when it reached the configured size. + If the threshold was never met, the data remained buffered until the connection was closed. + With this change, buffered data will be flushed proactively. Specifically, if the buffer does not + reach the configured size but has been held for more than 15 seconds, it will be sent immediately. +- area: websocket change: | - Made the :ref:`gRPC JSON transcoder filter's ` JSON print options configurable. -- area: oauth2 + Allow 4xx and 5xx to go through the filter chain for the WebSocket handshake response check. This behavior can be + disabled by the runtime guard ``envoy.reloadable_features.websocket_allow_4xx_5xx_through_filter_chain``. +- area: websocket change: | - Reset CSRF token when token validation fails during redirection. - If the CSRF token cookie is present during the redirection to the authorization server, it will be validated. - Previously, if this validation failed, the OAuth flow would fail. Now the CSRF token will simply be reset. This fixes - the case where an HMAC secret change causes a redirect flow, but the CSRF token cookie hasn't yet expired - causing a CSRF token validation failure. -- area: cel + Support route and per-try timeouts on websocket upgrade. This can be disabled by the runtime guard + ``envoy.reloadable_features.websocket_enable_timeout_on_upgrade_response``. +- area: testing change: | - Precompile regexes in CEL expressions. This can be disabled by setting the runtime guard - ``envoy.reloadable_features.enable_cel_regex_precompilation`` to ``false``. -- area: dns + In test code for external extensions, matchers ``Http::HeaderValueOf``, ``HasHeader``, and ``HeaderHasValueRef`` + must be replaced with ``ContainsHeader``. + Any uses of matcher ``HeaderHasValue(...)`` should be replaced with ``::testing::Pointee(ContainsHeader(...))``. +- area: generic_proxy change: | - Allow ``getaddrinfo`` to be configured to run by a thread pool, controlled by :ref:`num_resolver_threads - `. -- area: dns + Generic proxy codec adds the same buffer limit as the connection buffer limit. If the buffer limit is + exceeded, the connection is disconnected. This behavior can be reverted by setting the runtime guard + ``envoy.reloadable_features.generic_proxy_codec_buffer_limit`` to ``false``. +- area: http3 change: | - Honor the default DNS resolver configuration in the bootstrap config - :ref:`typed_dns_resolver_config ` - if the DNS cache configuration in the dynamic forward proxy filter is empty - :ref:`dns_cache_config `. -- area: grpc-json-transcoding + Turned off HTTP/3 happy eyeballs in upstream via the runtime guard ``envoy.reloadable_features.http3_happy_eyeballs``. + It was found to favor TCP over QUIC when UDP does not work on IPv6 but works on IPv4. +- area: mobile change: | - Added SSE style message framing for streamed responses in :ref:`gRPC JSON transcoder filter `. + Explicitly drain connections upon network change events regardless of whether the DNS cache is refreshed or not. + This behavior can be reverted by setting the runtime guard + ``envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh`` to ``false``. - area: http change: | - :ref:`response_headers_to_add ` and - :ref:`response_headers_to_remove ` - will also be applied to the local responses from the ``envoy.filters.http.router`` filter. -- area: tracing + Added accounting for decompressed HTTP header bytes sent and received. Existing stats only count wire-encoded header bytes. + This can be accessed through the ``%UPSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED%``, + ``%DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED%``, ``%UPSTREAM_DECOMPRESSED_HEADER_BYTES_SENT%``, and + ``%DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_SENT%`` access log command operators. +- area: formatter change: | - Added :ref:`max_cache_size ` - to the OpenTelemetry tracer config. This limits the number of spans that can be cached before flushing. -- area: aws + Deprecated legacy header formatter support for ``%DYNAMIC_METADATA(["namespace", "key", ...])%`` + , ``%UPSTREAM_METADATA(["namespace", "key", ...])%`` and ``%PER_REQUEST_STATE(key)%``. Please use + ``%DYNAMIC_METADATA(namespace:key:...])%``, ``%UPSTREAM_METADATA(namespace:key:...])%`` + and ``%FILTER_STATE(key:PLAIN)%`` as alternatives. + This change is guarded by the runtime flag + ``envoy.reloadable_features.remove_legacy_route_formatter`` and default to ``false`` for now + and will be flipped to ``true`` after two release periods. +- area: oauth2 change: | - :ref:`AwsCredentialProvider ` now supports all defined credential - providers, allowing complete customization of the credential provider chain when using AWS request signing extension. + Added response code details to ``401`` local responses generated by the OAuth2 filter. - area: ext_proc change: | - If the ext_proc server sends a spurious response message to Envoy, Envoy now performs fail-open or fail-close action based on - :ref:`failure_mode_allow ` - configuration. This change can be reverted by setting the runtime guard - ``envoy.reloadable_features.ext_proc_fail_close_spurious_resp`` to ``false``. -- area: filters + If :ref:`failure_mode_allow ` is true, + save the gRPC failure status code returned from the ext_proc server in the filter state. + Previously, all fail-open cases would return ``call_status`` ``Grpc::Status::Aborted``. +- area: dns_filter change: | - :ref:`Credential injector filter ` is no longer - a work in progress field. -- area: oauth2 + Honor the default DNS resolver configuration in the bootstrap config + :ref:`typed_dns_resolver_config ` if the + :ref:`client_config ` is empty. +- area: grpc_json_transcoder + change: | + Cap the frame size for streamed grpc at 1MB. Without this change there was a small chance + that if a request streamed in sufficiently faster than it was processed, a frame larger than + 4MB could be encoded, which most upstream grpc services would, by default, treat as an error. + +bug_fixes: +# *Changes expected to improve the state of the world and are unlikely to have negative effects* +- area: tcp_proxy + change: | + Fixed a bug where when a downstream TCP connection is created and the upstream connection is not fully established, no idle timeout + is set on the downstream connection, which may lead to a connection leak if the client does not close the connection. + The fix is to set an idle timeout on the downstream connection immediately after creation. + This fix can be reverted by setting the runtime guard + ``envoy.reloadable_features.tcp_proxy_set_idle_timer_immediately_on_new_connection`` to ``false``. +- area: udp_proxy change: | - The access token, ID token and refresh token in the cookies are now encrypted using the HMAC secret. This behavior can - be reverted by setting the runtime guard ``envoy.reloadable_features.oauth2_encrypt_tokens`` to ``false``. + Fixed a crash in the UDP proxy that occurred during ``ENVOY_SIGTERM`` when active tunneling sessions were present. +- area: geoip + change: | + Fixed a bug in the MaxMind provider where the ``found_entry`` field in the lookup result was not checked before + trying to populate headers with data. If this field is not checked the provider could try to populate headers + with wrong data, as per the documentation for the MaxMind library + `libmaxminddb.md `_. - area: http3 change: | - Validate HTTP/3 pseudo headers. Can be disabled by setting ``envoy.restart_features.validate_http3_pseudo_headers`` to ``false``. -- area: formatter + Fixed a bug where the access log was skipped for HTTP/3 requests when the stream was half closed. This behavior can be + reverted by setting the runtime guard + ``envoy.reloadable_features.quic_fix_defer_logging_miss_for_half_closed_stream`` to ``false``. +- area: http change: | - Now the ``METADATA`` and ``CEL`` substitution formatters can access or log the metadata of - the virtual host in case the route is not matched but the virtual host is found. -- area: oauth2 + Fixed a bug where premature resets of streams could result in recursive draining and a potential + stack overflow. Setting a proper ``max_concurrent_streams`` value for HTTP/2 or HTTP/3 could eliminate + the risk of a stack overflow before this fix. +- area: listener change: | - Extension status changed from ``alpha`` to ``stable``. -- area: oauth2 + Fixed a bug where comparing listeners did not consider the network namespace they were listening in. +- area: http change: | - Starting from this release, these cookies: oauth_hmac,oauth_expires,refresh_token,oauth_nonce,code_verifier will - not be forwarded to the upstream. This behavior can be reverted by setting the runtime guard - ``envoy.reloadable_features.oauth2_cleanup_cookies`` to ``false``. - -bug_fixes: -# *Changes expected to improve the state of the world and are unlikely to have negative effects* -- area: conn_pool - change: | - Fixed an issue that could lead to too many connections when using - :ref:`AutoHttpConfig ` if the - established connection is HTTP/2 and Envoy predicted it would have lower concurrent capacity. -- area: conn_pool - change: | - Fixed an issue that could lead to insufficient connections for current pending requests. If a connection starts draining while it - has negative unused capacity (which happens if an HTTP/2 ``SETTINGS`` frame reduces allowed concurrency to below the current number - of requests), that connection's unused capacity will be included in total pool capacity even though it is unusable because it is - draining. This can result in not enough connections being established for current pending requests. This is most problematic for - long-lived requests (such as streaming gRPC requests or long-poll requests) because a connection could be in the draining state - for a long time. -- area: hcm - change: | - Fixed a bug where the lifetime of the ``HttpConnectionManager``'s ``ActiveStream`` can be out of sync - with the lifetime of the codec stream. -- area: config_validation - change: | - Fixed a bug where the config validation server will crash when the configuration contains - ``%CEL%`` or ``%METADATA%`` substitution formatter. -- area: tls - change: | - Fixed an issue with incorrectly cached connection properties on TLS connections. - If TLS connection data was queried before it was available, an empty value was being incorrectly cached, preventing later calls from - getting the correct value. This could be triggered with a ``tcp_proxy`` access log configured to emit a log upon connection - establishment if the log contains fields of the TLS peer certificate. Then a later use of the data, such as the network RBAC - filter validating a peer certificate SAN, may incorrectly fail due to the empty cached value. -- area: quic + Fixed a bug where the ``response_headers_to_add`` may be processed multiple times for the local responses from + the router filter. +- area: formatter change: | - Fixed a bug in Envoy's HTTP/3-to-HTTP/1 proxying when a ``transfer-encoding`` header is incorrectly appended. - Protected by runtime guard ``envoy.reloadable_features.quic_signal_headers_only_to_http1_backend``. -- area: runtime + Fixed a bug where the ``%TRACE_ID%`` command cannot work properly at the header mutations. +- area: listeners change: | - Fixed a bug which resulted in an ``ENVOY_BUG`` being incorrectly triggered when runtime settings - ``envoy.reloadable_features.max_request_headers_count``, ``envoy.reloadable_features.max_response_headers_count``, - ``envoy.reloadable_features.max_request_headers_size_kb``, or ``envoy.reloadable_features.max_response_headers_size_kb`` were set. -- area: tls + Fixed an issue where :ref:`TLS inspector listener filter ` timed out + when used with other listener filters. The bug was triggered when a previous listener filter processed more data + than the TLS inspector had requested, causing the TLS inspector to incorrectly calculate its buffer growth strategy. + The fix ensures that buffer growth is now based on actual bytes available rather than the previously requested amount. +- area: listener change: | - Fixed a bug where empty trusted CA file or inline string is accepted and causes Envoy to successfully validate any certificate - chain. This fix addresses this issue by rejecting such configuration with empty value. This behavior can be reverted by setting - the runtime guard ``envoy.reloadable_features.reject_empty_trusted_ca_file`` to ``false``. -- area: tls_inspector + Fixed a bug where a failure to create listener sockets in different Linux network namespaces was + not handled properly. The success of the netns switch was not checked before attempting to + access the result of the socket creation. This is only relevant for Linux and if a listening + socket address was specified with a non-default network namespace. +- area: aws + change: | + Added missing session name, session duration, and ``external_id`` parameters in AssumeRole credentials provider. +- area: oauth2 + change: | + Fixed a bug introduced in PR `#40228 `_, where OAuth2 cookies were + removed for requests matching the ``pass_through_matcher`` configuration. This broke setups with multiple OAuth2 + filter instances using different ``pass_through_matcher`` configurations, because the first matching instance removed + the OAuth2 cookies--even when a passthrough was intended--impacting subsequent filters that still needed those cookies. +- area: stats + change: | + Fixed a bug where the metric name ``expiration_unix_time_seconds`` of + ``cluster..ssl.certificate..`` + and ``listener.
.ssl.certificate..`` + was not being properly extracted in the final Prometheus stat name. +- area: odcds + change: | + Fixed a bug where using OD-CDS without cds_config would not work in some + cases. This change introduces a new internal OD-CDS component. This change + could be reverted temporarily by setting the runtime guard + ``envoy.reloadable_features.odcds_over_ads_fix`` to ``false``. +- area: oauth2 change: | - Fixed a bug where the TLS inspector filter would not correctly report ``client_hello_too_large`` stat for too big client - hello messages, i.e., bigger than 16 KB. -- area: wasm + Fixed an issue where cookies prefixed with ``__Secure-`` or ``__Host-`` were not receiving a + ``Secure`` attribute. +- area: dns change: | - Fixed a bug where the Wasm filter hangs when the VM is crashed in the request callbacks. -- area: dynatrace + Fixed a use-after-free (UAF) in DNS cache that can occur when the ``Host`` header is modified between the Dynamic + Forwarding Proxy and Router filters. +- area: release change: | - Fixed a division by zero bug in the Dynatrace sampling controller that occurred when ``total_wanted`` was less than - ``top_k_size``. The calculation was refactored to avoid the intermediate division that could result in zero. + Fixed the distroless image to ensure nonroot. removed_config_or_runtime: # *Normally occurs at the end of the* :ref:`deprecation period ` -- area: websocket - change: | - Removed runtime guard ``envoy.reloadable_features.switch_protocol_websocket_handshake`` and legacy code paths. -- area: http2 +- area: router change: | - Removed runtime guard ``envoy.reloadable_features.http2_no_protocol_error_upon_clean_close`` and legacy code paths. -- area: access_log + Removed runtime guard ``envoy.reloadable_features.shadow_policy_inherit_trace_sampling`` and legacy code paths. +- area: dns change: | - Removed runtime guard ``envoy.reloadable_features.sanitize_sni_in_access_log`` and legacy code paths. -- area: quic + Removed runtime guard ``envoy.reloadable_features.prefer_ipv6_dns_on_macos`` and legacy code paths. +- area: dynamic_forward_proxy change: | - Removed runtime guard ``envoy.reloadable_features.quic_connect_client_udp_sockets`` and legacy code paths. -- area: quic + Removed runtime guard ``envoy.reloadable_features.avoid_dfp_cluster_removal_on_cds_update`` and legacy code paths. +- area: oauth2 change: | - Removed runtime guard ``envoy.reloadable_features.quic_support_certificate_compression`` and legacy code paths. -- area: http + Removed runtime guard ``envoy.reloadable_features.oauth2_use_refresh_token`` and legacy code paths. +- area: http_connection_manager change: | - Removed runtime guard ``envoy.reloadable_features.internal_authority_header_validator`` and legacy code paths. -- area: http + Removed runtime guard ``envoy.reloadable_features.explicit_internal_address_config`` and legacy code paths. +- area: dfp change: | - Removed runtime guard ``envoy_reloadable_features_filter_access_loggers_first`` and legacy code paths. -- area: tcp_proxy + Removed runtime guard ``envoy.reloadable_features.dfp_fail_on_empty_host_header`` and legacy code paths. +- area: quic change: | - Removed runtime guard ``envoy.reloadable_features.tcp_tunneling_send_downstream_fin_on_upstream_trailers`` and legacy code paths. -- area: runtime + Removed runtime guard ``envoy.reloadable_features.prefer_quic_client_udp_gro`` and legacy code paths. +- area: udp_proxy change: | - Removed runtime guard ``envoy_reloadable_features_boolean_to_string_fix`` and legacy code paths. -- area: logging + Removed runtime guard ``envoy.reloadable_features.enable_udp_proxy_outlier_detection`` and legacy code paths. +- area: xds change: | - Removed runtime guard ``envoy.reloadable_features.logging_with_fast_json_formatter`` and legacy code paths. -- area: sni + Removed runtime guard ``envoy.reloadable_features.xds_prevent_resource_copy`` and legacy code paths. +- area: rds change: | - Removed runtime guard ``envoy.reloadable_features.use_route_host_mutation_for_auto_sni_san`` and legacy code paths. -- area: ext_proc + Removed runtime guard ``envoy.reloadable_features.normalize_rds_provider_config`` and legacy code paths. +- area: http change: | - Removed runtime guard ``envoy.reloadable_features.ext_proc_timeout_error`` and legacy code paths. + Removed runtime guard ``envoy.reloadable_features.local_reply_traverses_filter_chain_after_1xx`` and legacy code paths. - area: quic change: | - Removed runtime guard ``envoy.reloadable_features.extend_h3_accept_untrusted`` and legacy code paths. -- area: lua + Removed runtime guard ``envoy.reloadable_features.report_stream_reset_error_code`` and legacy code paths. +- area: router change: | - Removed runtime guard ``envoy.reloadable_features.lua_flow_control_while_http_call`` and legacy code paths. -- area: http1 + Removed runtime guard ``envoy.reloadable_features.streaming_shadow`` and legacy code paths. +- area: http3 change: | - Removed runtime guard ``envoy.reloadable_features.envoy_reloadable_features_http1_use_balsa_parser`` and legacy code paths. - -new_features: -- area: build + Removed runtime guard ``envoy.reloadable_features.http3_remove_empty_trailers`` and legacy code paths. +- area: stats change: | - Upgraded Envoy to build with C++20; Envoy developers can use C++20 features now. -- area: redis + Removed runtime guard ``envoy.reloadable_features.enable_include_histograms`` and legacy code paths. +- area: network change: | - Added support for ``SCAN`` and ``INFO``. + Removed runtime guard ``envoy.reloadable_features.udp_socket_apply_aggregated_read_limit`` and legacy code paths. - area: http change: | - Added :ref:`x-envoy-original-host ` that - is used to record the original host header value before it is mutated by the router filter. -- area: stateful_session + Removed runtime guard ``envoy.reloadable_features.proxy_status_mapping_more_core_response_flags`` and legacy code paths. +- area: http change: | - Support for envelope stateful session extension to keep the existing session header value - from upstream server. See :ref:`mode - ` - for more details. -- area: transport_tap + Removed runtime guard ``envoy.reloadable_features.allow_alt_svc_for_ips`` and legacy code paths. +- area: http change: | - Added counter in transport tap for streaming and buffer trace. - Streamed trace can send tapped message based on configured size. -- area: udp_sink + Removed runtime guard ``envoy.reloadable_features.filter_chain_aborted_can_not_continue`` and legacy code paths. +- area: http change: | - Enhanced UDP sink to support a single message whose size is bigger than 64 KB. -- area: load_balancing + Removed runtime guard ``envoy.reloadable_features.use_filter_manager_state_for_downstream_end_stream`` and legacy code paths. +- area: balsa change: | - Added Override Host Load Balancing policy. See - :ref:`load balancing policies overview ` for more details. -- area: load_balancing + Removed runtime guard ``envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done`` and legacy code paths. +- area: geoip_providers change: | - Added :ref:`hash policy support - ` - to the ring hash and maglev load balancing policies. If the hash policy in the load balancer is - configured, the - :ref:`route level hash policy ` - will be ignored. -- area: lua + Removed runtime guard ``envoy.reloadable_features.mmdb_files_reload_enabled`` and legacy code paths. +- area: proxy_protocol change: | - Added support for accessing filter context. - See :ref:`filterContext() ` for more details. -- area: resource_monitors + Removed runtime guard ``envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener`` and legacy code paths. +- area: dns_resolver change: | - Added new cgroup memory resource monitor that reads memory usage/limit from cgroup v1/v2 subsystems and calculates - memory pressure, with configurable ``max_memory_bytes`` limit - :ref:`existing extension `. -- area: ext_authz + Removed runtime guard ``envoy.reloadable_features.getaddrinfo_num_retries`` and legacy code paths. +- area: proxy_filter change: | - Added ``grpc_status`` to ``ExtAuthzLoggingInfo`` in ``ext_authz`` HTTP filter. -- area: http + Removed runtime guard ``envoy.reloadable_features.proxy_ssl_port`` and legacy code paths. +- area: gcp_authn change: | - Added :ref:`response trailers mutations - ` and - :ref:`request trailers mutations - ` - to :ref:`Header Mutation Filter ` - for adding/removing trailers from the request and the response. -- area: postgres + Removed runtime guard ``envoy.reloadable_features.gcp_authn_use_fixed_url`` and legacy code paths. +- area: jwt_authn change: | - Added support for requiring downstream SSL. -- area: url_template + Removed runtime guard ``envoy.reloadable_features.jwt_authn_remove_jwt_from_query_params`` and legacy code paths. +- area: jwt_authn change: | - Included the asterisk ``*`` in the match pattern when using the ``*`` or ``**`` operators in the URL template. - This behavioral change can be temporarily reverted by setting runtime guard - ``envoy.reloadable_features.uri_template_match_on_asterisk`` to ``false``. -- area: socket + Removed runtime guard ``envoy.reloadable_features.jwt_authn_validate_uri`` and legacy code paths. +- area: dispatcher change: | - Added ``network_namespace_filepath`` to ``SocketAddress``. Currently only used by listeners. -- area: rbac_filter + Removed runtime guard ``envoy.restart_features.fix_dispatcher_approximate_now`` and legacy code paths. +- area: upstream change: | - Allow-listed ``FilterStateInput`` to be used with the xDS matcher in the HTTP RBAC filter. -- area: rbac_filter + Removed runtime guard ``envoy.reloadable_features.use_config_in_happy_eyeballs`` and legacy code paths. +- area: http change: | - Allow-listed ``FilterStateInput`` to be used with the xDS matcher in the Network RBAC filter. -- area: tls_inspector_filter + Removed runtime guard ``envoy.reloadable_features.proxy_104`` and legacy code paths. + +new_features: +- area: stats change: | - Added :ref:`enable_ja4_fingerprinting - ` to create - a JA4 fingerprint hash from the Client Hello message. -- area: local_ratelimit + Added support to remove unused metrics from memory for extensions that + support evictable metrics. This is done :ref:`periodically + ` + during the metric flush. +- area: tap change: | - ``local_ratelimit`` will return ``x-ratelimit-reset`` header when the rate limit is exceeded. -- area: oauth2 + Added :ref:`record_upstream_connection ` + to determine whether upstream connection information is recorded in the HTTP buffer trace output. +- area: quic change: | - Added :ref:`end_session_endpoint ` - to the ``oauth2`` filter to support OIDC RP initiated logout. This field is only used when - ``openid`` is in the :ref:`auth_scopes ` field. - If configured, the OAuth2 filter will redirect users to this endpoint when they access the - :ref:`signout_path `. This allows users to - be logged out of the Authorization server. -- area: tcp_access_logs + Added new option to support :ref:`base64 encoded server ID + ` + in QUIC-LB. +- area: health_check + change: | + Added support for request payloads in HTTP health checks. The ``send`` field in ``HttpHealthCheck`` can now be + used to specify a request body to be sent during health checking. This feature supports both hex-encoded text + and binary payloads, similar to TCP health checks. The payload can only be used with HTTP methods that support + request bodies (``POST``, ``PUT``, ``PATCH``, ``OPTIONS``). Methods that must not have request bodies + (``GET``, ``HEAD``, ``DELETE``, ``TRACE``) are validated and will throw an error if combined with payloads. + The implementation is optimized to process the payload once during configuration and reuse it for all health + check requests. See :ref:`HttpHealthCheck ` for configuration details. +- area: tcp_proxy change: | - Added support for ``%BYTES_RECEIVED%``, ``%BYTES_SENT%``, ``%UPSTREAM_HEADER_BYTES_SENT%``, ``%UPSTREAM_HEADER_BYTES_RECEIVED%``, - ``%UPSTREAM_WIRE_BYTES_SENT%``, ``%UPSTREAM_WIRE_BYTES_RECEIVED%`` access log substitution strings for TCP tunneling flows. -- area: oauth2 + Added support for generating and propagating a request ID on synthesized upstream HTTP requests when tunneling requests. + It can be configured using :ref:`request_id_extension + `. +- area: router_check_tool change: | - Added configurable :ref:`csrf_token_expires_in - ` - and :ref:`code_verifier_token_expires_in - ` - fields to the ``oauth2`` filter. Both default to ``600s`` (10 minutes) if not specified, keeping backward compatibility. -- area: load_shed_point - change: | - Added load shed point ``envoy.load_shed_points.connection_pool_new_connection`` in the connection pool, and it will not - create new connections when Envoy is under pressure, and the pending downstream requests will be cancelled. -- area: api_key_auth - change: | - Added :ref:`forwarding configuration ` - to the API Key Auth filter, which allows forwarding the authenticated client identity - using a custom header, and also offers the option to remove the API key from the request - before forwarding. + Added support for testing routes with :ref:`dynamic metadata matchers ` + in the router check tool. The tool now accepts a ``dynamic_metadata`` field in test input to set metadata + that can be matched by route configuration. This allows comprehensive testing of routes that depend on + dynamic metadata for routing decisions. - area: lua change: | - Added a new ``dynamicTypedMetadata()`` on ``connectionStreamInfo()`` which can be used to access the typed metadata from - network filters, such as the Proxy Protocol, etc. -- area: aws + Added a new ``filterState()`` to ``streamInfo()`` which provides access to filter state objects stored during request processing. + This allows Lua scripts to retrieve string, boolean, and numeric values stored by various filters for use in routing decisions, + header modifications, and other processing logic. See :ref:`Filter State API ` + for more details. +- area: socket + change: | + Added ``network_namespace_filepath`` to :ref:`SocketAddress `. This field allows + specifying a Linux network namespace filepath for socket creation, enabling network isolation in containerized environments. +- area: ratelimit change: | - Implementation of `IAM Roles Anywhere support `_ in the - AWS common components, providing this capability to the AWS Lambda and AWS Request Signing extensions. + Added the :ref:`rate_limits + ` + field to generate rate limit descriptors. If this field is set, the + :ref:`VirtualHost.rate_limits` or + :ref:`RouteAction.rate_limits` fields will be ignored. However, + :ref:`RateLimitPerRoute.rate_limits` + will take precedence over this field. +- area: ratelimit + change: | + Enhanced the rate limit filter to support substitution formatters for descriptors that generated + at the stream complete phase. Before this change, substitution formatters at the stream complete + phase cannot work because rate limit filter does not provide the necessary context. - area: redis change: | - ``redis_proxy`` filter now supports AWS IAM Authentication. + Added support for thirty-three new Redis commands including ``COPY``, ``RPOPLPUSH``, ``SMOVE``, ``SUNION``, ``SDIFF``, + ``SINTER``, ``SINTERSTORE``, ``ZUNIONSTORE``, ``ZINTERSTORE``, ``PFMERGE``, ``GEORADIUS``, ``GEORADIUSBYMEMBER``, + ``RENAME``, ``SORT``, ``SORT_RO``, ``ZMSCORE``, ``SDIFFSTORE``, ``MSETNX``, ``SUBSTR``, ``ZRANGESTORE``, ``ZUNION``, + ``ZDIFF``, ``SUNIONSTORE``, ``SMISMEMBER``, ``HRANDFIELD``, ``GEOSEARCHSTORE``, ``ZDIFFSTORE``, ``ZINTER``, ``ZRANDMEMBER``, + ``BITOP``, ``LPOS``, ``RENAMENX``. +- area: observability + change: | + Added ``ENVOY_NOTIFICATION`` macro to track specific conditions in production environments. +- area: dns_filter, redis_proxy and prefix_matcher_map + change: | + Switch to using Radix Tree instead of Trie for performance improvements. +- area: header_to_metadata + change: | + Added optional statistics collection for the Header-To-Metadata filter. When the :ref:`stat_prefix + ` field is configured, + the filter emits detailed counters for rule processing, metadata operations, etc. See + :ref:`Header-To-Metadata filter statistics ` for details. +- area: load_reporting + change: | + Added support for endpoint-level load stats and metrics reporting. Locality load reports now include per + endpoint statistics and metrics, but only for endpoints with updated stats, optimizing report size and efficiency. +- area: overload management + change: | + Added load shed point ``envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch`` + that sends ``GOAWAY`` and closes connections for HTTP/2 server processing of requests. When + a ``GOAWAY`` frame is submitted by this load shed point, the counter ``http2.goaway_sent`` will be + incremented. - area: router change: | - Added matcher based router cluster specifier plugin to support selecting cluster dynamically based on a matcher tree. - See - :ref:`matcher cluster specifier plugin ` - for more details. -- area: router + Added :ref:`request_body_buffer_limit + ` and + :ref:`request_body_buffer_limit + ` configuration fields + to enable buffering of large request bodies beyond connection buffer limits. +- area: otlp_stat_sink change: | - Added new ``refreshRouteCluster()`` method to stream filter callbacks to support refreshing the route cluster and - does not need to update the route cache. See :ref:`http route mutation ` for - more details. + Added support for resource attributes. The stat sink will use the resource attributes configured for the OpenTelemetry tracer via + :ref:`resource_detectors `. - area: lua change: | - Added a new ``dynamicTypedMetadata()`` on ``streamInfo()`` which can be used to access the typed metadata from - HTTP filters, such as the Set Metadata filter, etc. -- area: ratelimit - change: | - Added a new ``failure_mode_deny_percent`` field of type ``Envoy::Runtime::FractionalPercent`` attached to the rate - limit filter to configure the failure mode for rate limit service errors in runtime. - It acts as an override for the existing ``failure_mode_deny`` field in the filter config. -- area: tls + Added ``virtualHost()`` to the Stream handle API, allowing Lua scripts to retrieve virtual host information. So far, the only method + implemented is ``metadata()``, allowing Lua scripts to access virtual host metadata scoped to the specific filter name. See + :ref:`Virtual host object API ` for more details. +- area: ext_authz change: | - Added new metric for emitting seconds since UNIX epoch of expirations of TLS and CA certificates. - They are rooted at ``cluster..ssl.certificate..`` - and at ``listener.
.ssl.certificate..`` namespace. + Added support for per-route gRPC service override in the ``ext_authz`` HTTP filter. This allows different routes + to use different external authorization backends by configuring a + :ref:`grpc_service ` + in the per-route ``check_settings``. Routes without this configuration continue to use the default + authorization service. - area: ext_proc change: | - The :ref:`failure_mode_allow ` - setting may now be overridden on a per-route basis. -- area: matcher + Added :ref:`status_on_error ` + to the ``ext_proc`` HTTP filter. This allows configuring the HTTP status code returned to the downstream + client when communication with the external processor fails (e.g., gRPC error). Previously, these cases returned a + fixed ``500``. +- area: tracing change: | - Added support for :ref:`ServerNameMatcher ` trie-based matching. -- area: stateful_session + Added :ref:`trace_context_option ` enum + in the Zipkin tracer config. When set to ``USE_B3_WITH_W3C_PROPAGATION``, the tracer will: + extract trace information from W3C trace headers when B3 headers are not present (downstream), + and inject both B3 and W3C trace headers for upstream requests to maximize compatibility. + The default value ``USE_B3`` maintains backward compatibility with B3-only behavior. +- area: tracing change: | - Added support for cookie attributes to stateful session cookie. -- area: http3 + Enhanced Zipkin tracer with advanced collector configuration via + :ref:`collector_service ` + using ``HttpService``. New features include: + + #. **Custom HTTP Headers**: Add headers to collector requests for custom metadata, service identification, + and collector-specific routing. + + #. **Full URI Parsing**: The ``uri`` field now supports both path-only (``/api/v2/spans``) and + full URI formats (``https://zipkin-collector.example.com/api/v2/spans``). When using full URIs, + Envoy automatically extracts hostname and path components - hostname sets the HTTP ``Host`` header, + and path sets the request path. Path-only URIs fall back to using the cluster name as the hostname. + + When configured, ``collector_service`` takes precedence over legacy configuration fields (``collector_cluster``, + ``collector_endpoint``, ``collector_hostname``), which will be deprecated in a future release. Legacy configuration + does not support custom headers or URI parsing. +- area: composite change: | - Added :ref:`disable_connection_flow_control_for_streams - `, an experimental - option for disabling connection level flow control for streams. This is useful in situations where the streams share the same - connection but originate from different end-clients, so that each stream can make progress independently at non-front-line proxies. -- area: dfp + Allow the composite filter to be configured to insert a filter into the filter chain outside of the decode headers lifecycle phase. +- area: rbac + change: | + Switched the IP matcher to use LC-Trie for performance improvements. +- area: tls_inspector change: | - Added :ref:`allow_dynamic_host_from_filter_state - ` - flag to HTTP Dynamic Forward Proxy filter. When enabled, the filter will check for ``envoy.upstream.dynamic_host`` and - ``envoy.upstream.dynamic_port`` filter state values before using the HTTP Host header, providing consistency with SNI - and UDP DFP filters. When disabled (default), maintains backward compatibility by using the HTTP Host header directly. -- area: alts + Added dynamic metadata when failing to parse the ``ClientHello``. +- area: matching + change: | + Added :ref:`NetworkNamespaceInput + ` to the + matcher framework. This input returns the listener address's ``network_namespace_filepath`` + for use with :ref:`filter_chain_matcher + `, enabling filter chain + selection based on the Linux network namespace of the bound socket. On non-Linux platforms, + the input returns an empty value and connections use the default filter chain. +- area: rbac + change: | + Enabled use of :ref:`NetworkNamespaceInput + ` in the + network RBAC filter's matcher. This allows RBAC policies to evaluate the Linux network namespace + of the listening socket via the generic matcher API. +- area: lua change: | - Added environment variable-protected gRPC keepalive params to the ALTS handshaker client. -- area: wasm + Added ``route()`` to the Stream handle API, allowing Lua scripts to retrieve route information. So far, the only method + implemented is ``metadata()``, allowing Lua scripts to access route metadata scoped to the specific filter name. See + :ref:`Route object API ` for more details. +- area: cel change: | - Added support for returning ``StopIteration`` from plugin ``onRequestHeader`` and ``onResponseHeader`` callbacks. See - :ref:`allow_on_headers_stop_iteration ` - for more details. By default, current behavior is maintained. -- area: aws + Added a new ``%TYPED_CEL%`` formatter command that, unlike ``%CEL%``, can output non-string values (number, boolean, null, etc.) + when used in formatting contexts that accept non-string values, such as + :ref:`json_format `. The new command is introduced + so as to not break compatibility with the existing command's behavior. +- area: dynamic_modules + change: | + Added a new Logging ABI that allows modules to emit logs in the standard Envoy logging stream under ``dynamic_modules`` ID. + In the Rust SDK, they are available as ``envoy_log_info``, etc. +- area: http + change: | + Added ``upstream_rq_per_cx`` histogram to track requests per connection for monitoring connection reuse efficiency. +- area: http + change: | + Added + :ref:`stream_flush_timeout + ` + to allow for configuring a stream flush timeout independently from the stream idle timeout. +- area: http + change: | + Added support for header removal based on header key matching. The new + :ref:`remove_on_match ` + allows removing headers that match a specified key pattern. This enables more flexible and + dynamic header manipulation based on header names. +- area: geoip + change: | + Added a new metric ``db_build_epoch`` to track the build timestamp of the MaxMind geolocation database files. + This can be used to monitor the freshness of the databases currently in use by the filter. + See `MaxMind-DB build_epoch `_ for more details. +- area: overload management + change: | + Added a new scaled timer type ``HttpDownstreamStreamFlush`` to the overload manager. This allows + Envoy to scale the periodic timer for flushing downstream responses based on resource pressure. + The new timer can be configured via the + :ref:`ScaleTimersOverloadActionConfig `. +- area: dynamic_modules + change: | + Added support for counters, gauges, histograms, and their vector variants to the dynamic modules API. +- area: dns_resolver change: | - Added ``assume_role_credential_provider`` to add support for role chaining in AWS filters. This allows envoy to - assume an additional role before SigV4 signing occurs. + Added :ref:`max_udp_channel_duration + ` + configuration field to the c-ares DNS resolver. This allows periodic refresh of the UDP channel + to help avoid stale socket states and provide better load distribution across UDP ports. deprecated: diff --git a/ci/Dockerfile-buildkit b/ci/Dockerfile-buildkit index b974a589eb2c8..a3634314bf0c7 100644 --- a/ci/Dockerfile-buildkit +++ b/ci/Dockerfile-buildkit @@ -1,3 +1,3 @@ # We dont build from this dockerfile - we just parse the version, but storing # here means we get the dependabot updates -FROM moby/buildkit:v0.23.2 +FROM moby/buildkit:v0.24.0 diff --git a/ci/Dockerfile-ntnx b/ci/Dockerfile-ntnx new file mode 100644 index 0000000000000..cf57136118ecd --- /dev/null +++ b/ci/Dockerfile-ntnx @@ -0,0 +1,90 @@ +ARG BUILD_OS=ubuntu +ARG BUILD_TAG=20.04 +ARG ENVOY_VRP_BASE_IMAGE=envoy + + +FROM scratch AS binary + +ARG TARGETPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} +ARG ENVOY_BINARY=envoy +ARG ENVOY_BINARY_SUFFIX= +ADD ${TARGETPLATFORM}/build_${ENVOY_BINARY}_debug${ENVOY_BINARY_SUFFIX}/envoy* /usr/local/bin/ +ADD configs/envoyproxy_io_proxy.yaml /etc/envoy/envoy.yaml +COPY ${TARGETPLATFORM}/build_${ENVOY_BINARY}_debug${ENVOY_BINARY_SUFFIX}/schema_validator_tool /usr/local/bin/schema_validator_tool +COPY ci/docker-entrypoint.sh / + + +# STAGE: envoy +FROM ${BUILD_OS}:${BUILD_TAG} AS envoy + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get upgrade -qq -y \ + && apt-get install -qq --no-install-recommends -y ca-certificates iproute2 iputils-ping curl wget \ + && apt-get autoremove -y -qq && apt-get clean \ + && rm -rf /tmp/* /var/tmp/* \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /etc/envoy + +COPY --from=binary /usr/local/bin/envoy* /usr/local/bin/ +COPY --from=binary /etc/envoy/envoy.yaml /etc/envoy/envoy.yaml +COPY --from=binary /docker-entrypoint.sh / + +RUN adduser --group --system envoy + +EXPOSE 10000 + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["envoy", "-c", "/etc/envoy/envoy.yaml"] + + +# STAGE: envoy-distroless +# gcr.io/distroless/base-nossl-debian11:nonroot +FROM gcr.io/distroless/base-nossl-debian11:nonroot@sha256:f10e1fbf558c630a4b74a987e6c754d45bf59f9ddcefce090f6b111925996767 AS envoy-distroless + +COPY --from=binary /usr/local/bin/envoy* /usr/local/bin/ +COPY --from=binary /etc/envoy/envoy.yaml /etc/envoy/envoy.yaml + +EXPOSE 10000 + +ENTRYPOINT ["/usr/local/bin/envoy"] +CMD ["-c", "/etc/envoy/envoy.yaml"] + + +# STAGE: envoy-google-vrp +FROM ${ENVOY_VRP_BASE_IMAGE} AS envoy-google-vrp + +RUN apt-get update \ + && apt-get upgrade -y -qq \ + && apt-get install -y -qq libc++1 supervisor gdb strace tshark \ + && apt-get autoremove -y \ + && apt-get clean \ + && rm -rf /tmp/* /var/tmp/* \ + && rm -rf /var/lib/apt/lists/* + +ADD configs/google-vrp/envoy-edge.yaml /etc/envoy/envoy-edge.yaml +ADD configs/google-vrp/envoy-origin.yaml /etc/envoy/envoy-origin.yaml +ADD configs/google-vrp/launch_envoy.sh /usr/local/bin/launch_envoy.sh +ADD configs/google-vrp/supervisor.conf /etc/supervisor.conf +ADD test/config/integration/certs/serverkey.pem /etc/envoy/certs/serverkey.pem +ADD test/config/integration/certs/servercert.pem /etc/envoy/certs/servercert.pem +# ADD %local envoy bin% /usr/local/bin/envoy +RUN chmod 777 /var/log/supervisor +RUN chmod a+r /etc/supervisor.conf /etc/envoy/* /etc/envoy/certs/* +RUN chmod a+rx /usr/local/bin/launch_envoy.sh + +EXPOSE 10000 +EXPOSE 10001 + +CMD ["supervisord", "-c", "/etc/supervisor.conf"] + +# STAGE: envoy-tools +FROM ${BUILD_OS}:${BUILD_TAG} AS envoy-tools + +COPY --from=binary /usr/local/bin/schema_validator_tool /usr/local/bin/ + + +# Make envoy image as last stage so it is built by default +FROM envoy \ No newline at end of file diff --git a/ci/README.md b/ci/README.md index 598cffbaf5fe4..b50fe1f7ebf61 100644 --- a/ci/README.md +++ b/ci/README.md @@ -90,6 +90,16 @@ To force the Envoy build image to be refreshed by Docker you can set `ENVOY_DOCK ENVOY_DOCKER_PULL=true ./ci/run_envoy_docker.sh ``` +## Resource Requirements and Troubleshooting + +Envoy requires a lot of resources (disk/memory/cpu) to build, especially the first time its built, as bazel does not yet have anything cached. + +**Memory Requirements:** +- Envoy builds can be memory-intensive and require substantial RAM +- If you have less than 2GB of RAM per CPU core, you may want to limit the number of parallel build jobs +- To limit build parallelism, add or modify the jobs setting in your `user.bazelrc` file with a line that follows the format "build --jobs=X/2" where X is the number of GB of RAM that your system has e.g.: `"build --jobs=4"` for a system with 8GB of memory in the `user.bazlerc` file that you created + +This configuration helps prevent out-of-memory errors that can cause builds to crash. # Generating compile commands diff --git a/ci/build_setup.sh b/ci/build_setup.sh index 1ef213757975b..9569ddb4e4f9e 100755 --- a/ci/build_setup.sh +++ b/ci/build_setup.sh @@ -62,9 +62,14 @@ if [[ -f "/etc/redhat-release" ]]; then fi function cleanup() { - # Remove build artifacts. This doesn't mess with incremental builds as these - # are just symlinks. - rm -rf "${ENVOY_SRCDIR}"/bazel-* clang.bazelrc + if [[ "${ENVOY_BUILD_SKIP_CLEANUP}" == "true" ]]; then + echo "Skipping cleanup as requested." + return + fi + + # Remove build artifacts. This doesn't mess with incremental builds as these + # are just symlinks. + rm -rf "${ENVOY_SRCDIR}"/bazel-* clang.bazelrc } cleanup diff --git a/ci/do_ci.sh b/ci/do_ci.sh index 10eca5ac26b49..8b42bbd869b97 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -13,7 +13,6 @@ CURRENT_SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" # shellcheck source=ci/build_setup.sh . "${CURRENT_SCRIPT_DIR}"/build_setup.sh -echo "building using ${NUM_CPUS} CPUs" echo "building for ${ENVOY_BUILD_ARCH}" cd "${SRCDIR}" @@ -478,11 +477,12 @@ case $CI_TARGET in # Using todays date as an action_env expires the NIST cache daily, which is the update frequency TODAY_DATE=$(date -u -I"date") export TODAY_DATE + # TODO(phlax): Re-enable cve tests bazel run "${BAZEL_BUILD_OPTIONS[@]}" //tools/dependency:check \ --//tools/dependency:preload_cve_data \ --action_env=TODAY_DATE \ -- -v warn \ - -c cves release_dates releases + -c release_dates releases # Run dependabot tests echo "Check dependabot ..." bazel run "${BAZEL_BUILD_OPTIONS[@]}" \ @@ -566,6 +566,8 @@ case $CI_TARGET in fi ENVOY_ARCH_DIR="$(dirname "${ENVOY_BUILD_DIR}")" ENVOY_TARBALL_DIR="${ENVOY_TARBALL_DIR:-${ENVOY_ARCH_DIR}}" + ENVOY_OCI_DIR="${ENVOY_BUILD_DIR}/${ENVOY_OCI_DIR}" + export ENVOY_OCI_DIR _PLATFORMS=() PLATFORM_NAMES=( x64:linux/amd64 @@ -591,12 +593,12 @@ case $CI_TARGET in fi PLATFORMS="$(IFS=, ; echo "${_PLATFORMS[*]}")" export DOCKER_PLATFORM="$PLATFORMS" - if [[ -z "${DOCKERHUB_PASSWORD}" && "${#_PLATFORMS[@]}" -eq 1 && -z $ENVOY_DOCKER_SAVE_IMAGE ]]; then - # if you are not pushing the images and there is only one platform - # then load to Docker (ie local build) + if [[ -z "$ENVOY_DOCKER_SAVE_IMAGE" ]]; then + # if you are not saving the images as OCI then load to Docker (ie local build) export DOCKER_LOAD_IMAGES=1 fi - "${ENVOY_SRCDIR}/ci/docker_ci.sh" + echo "BUILDING FOR: ${PLATFORMS}" + "${ENVOY_SRCDIR}/distribution/docker/build.sh" ;; dockerhub-publish) @@ -666,14 +668,14 @@ case $CI_TARGET in CONFIG="${CONFIG_PREFIX}gcc" BAZEL_BUILD_OPTIONS+=("--config=${CONFIG}") echo "gcc toolchain configured: ${CONFIG}" + echo "bazel fastbuild build with gcc..." + bazel_envoy_binary_build fastbuild echo "Testing ${TEST_TARGETS[*]}" bazel_with_collection \ test "${BAZEL_BUILD_OPTIONS[@]}" \ -c fastbuild \ --remote_download_minimal \ -- "${TEST_TARGETS[@]}" - echo "bazel release build with gcc..." - bazel_envoy_binary_build fastbuild ;; info) diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml new file mode 100644 index 0000000000000..76fbd03f14aa5 --- /dev/null +++ b/ci/docker-compose.yml @@ -0,0 +1,113 @@ +x-envoy-build-base: &envoy-build-base + image: >- + ${ENVOY_BUILD_IMAGE:-envoyproxy/envoy-build-ubuntu:f4a881a1205e8e6db1a57162faf3df7aed88eae8@sha256:b10346fe2eee41733dbab0e02322c47a538bf3938d093a5daebad9699860b814} + user: root:root + working_dir: ${ENVOY_DOCKER_SOURCE_DIR:-/source} + stdin_open: true + tty: true + platform: ${ENVOY_DOCKER_PLATFORM:-} + environment: + # Core build environment + - BUILD_DIR=/build + - ENVOY_DOCKER_SOURCE_DIR=${ENVOY_DOCKER_SOURCE_DIR:-/source} + - ENVOY_DOCKER_BUILD_DIR="${ENVOY_DOCKER_BUILD_DIR:-/tmp/envoy-docker-build}" + - ENVOY_OCI_DIR + + # Proxy settings + - HTTP_PROXY + - HTTPS_PROXY + - NO_PROXY + - GOPROXY + + # Bazel configuration + - BAZEL_STARTUP_OPTIONS + - BAZEL_BUILD_EXTRA_OPTIONS + - BAZEL_EXTRA_TEST_OPTIONS + - BAZEL_REMOTE_CACHE + - BAZEL_STARTUP_EXTRA_OPTIONS + - BAZEL_REMOTE_INSTANCE + - BAZELISK_BASE_URL + + # CI/CD variables + - CI_BRANCH + - CI_SHA1 + - CI_TARGET_BRANCH + - BUILD_REASON + - GITHUB_REF_NAME + - GITHUB_REF_TYPE + - GITHUB_TOKEN + - GITHUB_APP_ID + - GITHUB_INSTALL_ID + + # Build configuration + - NUM_CPUS + - ENVOY_BRANCH + - ENVOY_RBE + - ENVOY_BUILD_IMAGE + - ENVOY_SRCDIR + - ENVOY_BUILD_TARGET + - ENVOY_BUILD_DEBUG_INFORMATION + - ENVOY_BUILD_FILTER_EXAMPLE + - ENVOY_COMMIT + - ENVOY_HEAD_REF + - ENVOY_REPO + - ENVOY_BUILD_ARCH + - ENVOY_GEN_COMPDB_OPTIONS + + # Publishing and artifacts + - DOCKERHUB_USERNAME + - DOCKERHUB_PASSWORD + - ENVOY_DOCKER_SAVE_IMAGE + - ENVOY_PUBLISH_DRY_RUN + - ENVOY_TARBALL_DIR + - GCS_ARTIFACT_BUCKET + - GCS_REDIRECT_PATH + - GCP_SERVICE_ACCOUNT_KEY + - GCP_SERVICE_ACCOUNT_KEY_PATH + + - MOBILE_DOCS_CHECKOUT_DIR + - SYSTEM_STAGEDISPLAYNAME + - SYSTEM_JOBDISPLAYNAME + - SSH_AUTH_SOCK + + entrypoint: + - "/bin/bash" + - "-c" + - | + groupadd --gid ${DOCKER_GID:-${USER_GID:-$(id -g)}} -f envoygroup + useradd -o \ + --uid ${USER_UID:-$(id -u)} \ + --gid ${DOCKER_GID:-${USER_GID:-$(id -g)}} \ + --no-create-home \ + -s /bin/bash \ + --home-dir /build envoybuild + usermod -a -G pcap envoybuild + chown envoybuild:envoygroup /build + chown envoybuild /proc/self/fd/2 2>/dev/null || true + [[ -e /entrypoint-extra.sh ]] && /entrypoint-extra.sh + sudo -EHs -u envoybuild bash -c 'cd ${ENVOY_DOCKER_SOURCE_DIR:-/source} && exec ${DOCKER_COMMAND:-bash}' + +services: + envoy-build: + <<: *envoy-build-base + volumes: + - ${ENVOY_DOCKER_BUILD_DIR:-/tmp/envoy-docker-build}:/build + - ${SOURCE_DIR:-..}:/source + - ${SHARED_TMP_DIR:-/tmp/bazel-shared}:${SHARED_TMP_DIR:-/tmp/bazel-shared} + + envoy-build-gpg: + <<: *envoy-build-base + volumes: + - ${ENVOY_DOCKER_BUILD_DIR:-/tmp/envoy-docker-build}:/build + - ${SOURCE_DIR:-..}:/source + - ${ENVOY_GPG_DIR-${HOME}/.gnupg}:/build/.gnupg + - ${SHARED_TMP_DIR:-/tmp/bazel-shared}:${SHARED_TMP_DIR:-/tmp/bazel-shared} + + envoy-build-dind: + privileged: true + <<: *envoy-build-base + volumes: + - ${ENVOY_DOCKER_BUILD_DIR:-/tmp/envoy-docker-build}:/build + - ${SOURCE_DIR:-..}:/source + - /var/run/docker.sock:/var/run/docker.sock + - ${SHARED_TMP_DIR:-/tmp/bazel-shared}:${SHARED_TMP_DIR:-/tmp/bazel-shared} diff --git a/ci/docker-entrypoint-ntnx.sh b/ci/docker-entrypoint-ntnx.sh new file mode 100755 index 0000000000000..5819cc3fa8bcb --- /dev/null +++ b/ci/docker-entrypoint-ntnx.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -e + +# if the first argument look like a parameter (i.e. start with '-'), run Envoy +if [ "${1#-}" != "$1" ]; then + set -- envoy "$@" +fi + +if [ "$1" = 'envoy' ]; then + # set the log level if the $loglevel variable is set + if [ -n "$loglevel" ]; then + set -- "$@" --log-level "$loglevel" + fi +fi + +exec "$@" \ No newline at end of file diff --git a/ci/docker_ci.sh b/ci/docker_ci.sh deleted file mode 100755 index 637ba8b369257..0000000000000 --- a/ci/docker_ci.sh +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env bash - -# Do not ever set -x here, it is a security hazard as it will place the credentials below in the -# CI logs. -set -e - -## DEBUGGING (NB: Set these in your env to avoided unwanted changes) -## Set this to _not_ build/push just print what would be -# DOCKER_CI_DRYRUN=true -# -## Set these to tag/push images to your own repo -# DOCKER_IMAGE_PREFIX=mydocker/repo -# DOCKERHUB_USERNAME=me -# DOCKERHUB_PASSWORD=mypassword -# -## Set these to simulate types of CI run -# CI_SHA1=MOCKSHA -# CI_BRANCH=refs/heads/main -# CI_BRANCH=refs/heads/release/v1.43 -# CI_BRANCH=refs/tags/v1.77.3 -## - -# Workaround for https://github.com/envoyproxy/envoy/issues/26634 -DOCKER_BUILD_TIMEOUT="${DOCKER_BUILD_TIMEOUT:-500}" - -DOCKER_PLATFORM="${DOCKER_PLATFORM:-linux/arm64,linux/amd64}" - -if [[ -n "$DOCKER_CI_DRYRUN" ]]; then - CI_SHA1="${CI_SHA1:-MOCKSHA}" -fi - -MAIN_BRANCH="refs/heads/main" -RELEASE_BRANCH_REGEX="^refs/heads/release/v.*" -DEV_VERSION_REGEX="-dev$" -DOCKER_REGISTRY="${DOCKER_REGISTRY:-docker.io}" -PUSH_IMAGES_TO_REGISTRY= -if [[ -z "$ENVOY_VERSION" ]]; then - ENVOY_VERSION="$(cat VERSION.txt)" -fi - -if [[ "$ENVOY_VERSION" =~ $DEV_VERSION_REGEX ]]; then - # Dev version - IMAGE_POSTFIX="-dev" - IMAGE_NAME="${CI_SHA1}" -else - # Non-dev version - IMAGE_POSTFIX="" - IMAGE_NAME="v${ENVOY_VERSION}" -fi - -# Only push images for main builds, and non-dev release branch builds -if [[ -n "$DOCKER_LOAD_IMAGES" ]]; then - LOAD_IMAGES=1 -elif [[ -n "$DOCKERHUB_USERNAME" ]] && [[ -n "$DOCKERHUB_PASSWORD" ]]; then - if [[ "${CI_BRANCH}" == "${MAIN_BRANCH}" ]]; then - echo "Pushing images for main." - PUSH_IMAGES_TO_REGISTRY=1 - elif [[ "${CI_BRANCH}" =~ ${RELEASE_BRANCH_REGEX} ]] && ! [[ "$ENVOY_VERSION" =~ $DEV_VERSION_REGEX ]]; then - echo "Pushing images for release branch ${CI_BRANCH}." - PUSH_IMAGES_TO_REGISTRY=1 - else - echo 'Ignoring non-release branch for docker push.' - fi -else - echo 'No credentials for docker push.' -fi - -ENVOY_DOCKER_IMAGE_DIRECTORY="${ENVOY_DOCKER_IMAGE_DIRECTORY:-${BUILD_DIR:-.}/build_images}" -# This prefix is altered for the private security images on setec builds. -DOCKER_IMAGE_PREFIX="${DOCKER_IMAGE_PREFIX:-envoyproxy/envoy}" -if [[ -z "$DOCKER_CI_DRYRUN" ]]; then - mkdir -p "${ENVOY_DOCKER_IMAGE_DIRECTORY}" -fi - -# Setting environments for buildx tools -config_env() { - BUILDKIT_VERSION=$(grep '^FROM moby/buildkit:' ci/Dockerfile-buildkit | cut -d ':' -f2) - echo ">> BUILDX: install ${BUILDKIT_VERSION}" - echo "> docker run --rm --privileged tonistiigi/binfmt --install all" - echo "> docker buildx rm multi-builder 2> /dev/null || :" - echo "> docker buildx create --use --name multi-builder --platform ${DOCKER_PLATFORM}" - - if [[ -n "$DOCKER_CI_DRYRUN" ]]; then - return - fi - - # Install QEMU emulators - docker run --rm --privileged tonistiigi/binfmt:qemu-v7.0.0 --install all - - # Remove older build instance - docker buildx rm multi-builder 2> /dev/null || : - docker buildx create --use --name multi-builder --platform "${DOCKER_PLATFORM}" --driver-opt "image=moby/buildkit:${BUILDKIT_VERSION}" -} - -# "-google-vrp" must come afer "" to ensure we rebuild the local base image dependency. -BUILD_TYPES=("" "-debug" "-contrib" "-contrib-debug" "-distroless" "-google-vrp" "-tools") - -# Configure docker-buildx tools -BUILD_COMMAND=("buildx" "build") -config_env - -old_image_tag_name () { - # envoyproxy/envoy-dev:latest - # envoyproxy/envoy-debug:v1.73.3 - # envoyproxy/envoy-debug:v1.73-latest - local build_type="$1" image_name="$2" - if [[ -z "$image_name" ]]; then - image_name="$IMAGE_NAME" - fi - echo -n "${DOCKER_IMAGE_PREFIX}${build_type}${IMAGE_POSTFIX}:${image_name}" -} - -new_image_tag_name () { - # envoyproxy/envoy:dev - # envoyproxy/envoy:debug-v1.73.3 - # envoyproxy/envoy:debug-v1.73-latest - - local build_type="$1" image_name="$2" image_tag - parts=() - if [[ -n "$build_type" ]]; then - parts+=("${build_type:1}") - fi - if [[ -n ${IMAGE_POSTFIX:1} ]]; then - parts+=("${IMAGE_POSTFIX:1}") - fi - if [[ -z "$image_name" ]]; then - parts+=("$IMAGE_NAME") - elif [[ "$image_name" != "latest" ]]; then - parts+=("$image_name") - fi - image_tag=$(IFS=- ; echo "${parts[*]}") - echo -n "${DOCKER_IMAGE_PREFIX}:${image_tag}" -} - -build_platforms() { - local build_type=$1 - - if [[ "${build_type}" == *-google-vrp ]]; then - echo -n "linux/amd64" - else - echo -n "$DOCKER_PLATFORM" - fi -} - -build_args() { - local build_type=$1 target - - target="${build_type/-debug/}" - target="${target/-contrib/}" - printf ' -f ci/Dockerfile-envoy --target %s' "envoy${target}" - - if [[ "${build_type}" == *-contrib* ]]; then - printf ' --build-arg ENVOY_BINARY=envoy-contrib' - fi - - if [[ "${build_type}" == *-debug ]]; then - printf ' --build-arg ENVOY_BINARY_PREFIX=dbg/' - fi -} - -use_builder() { - echo ">> BUILDX: use multi-builder" - echo "> docker buildx use multi-builder" - - if [[ -n "$DOCKER_CI_DRYRUN" ]]; then - return - fi - docker buildx use multi-builder -} - -build_and_maybe_push_image () { - # If the image is not required for local testing and this is main or a release this will push immediately - # If it is required for testing on a main or release branch (ie non-debug) it will push to a tar archive - # and then push to the registry from there. - local image_type="$1" platform docker_build_args _args args=() docker_build_args docker_image_tarball build_tag action platform - - action="BUILD" - use_builder "${image_type}" - _args=$(build_args "${image_type}") - read -ra args <<<"$_args" - platform="$(build_platforms "${image_type}")" - build_tag="$(old_image_tag_name "${image_type}")" - docker_image_tarball="${ENVOY_DOCKER_IMAGE_DIRECTORY}/envoy${image_type}.tar" - - # `--sbom` and `--provenance` args added for skopeo 1.5.0 compat, - # can probably be removed for later versions. - args+=( - "--sbom=false" - "--provenance=false") - if [[ -n "$LOAD_IMAGES" ]]; then - action="BUILD+LOAD" - args+=("--load") - elif [[ "${image_type}" =~ debug ]]; then - # For linux if its the debug image then push immediately for release branches, - # otherwise just test the build - if [[ -n "$PUSH_IMAGES_TO_REGISTRY" ]]; then - action="BUILD+PUSH" - args+=("--push") - fi - else - # For linux non-debug builds, save it first in the tarball, we will push it - # with skopeo from there if needed. - args+=("-o" "type=oci,dest=${docker_image_tarball}") - fi - - docker_build_args=( - "${BUILD_COMMAND[@]}" - "--platform" "${platform}" - "${args[@]}" - -t "${build_tag}" - .) - echo ">> ${action}: ${build_tag}" - echo "> docker ${docker_build_args[*]}" - - if [[ -z "$DOCKER_CI_DRYRUN" ]]; then - echo "..." - timeout "$DOCKER_BUILD_TIMEOUT" docker "${docker_build_args[@]}" || { - if [[ "$?" == 124 ]]; then - echo "Docker build timed out ..." >&2 - else - echo "Docker build errored ..." >&2 - fi - sleep 5 - echo "trying again ..." >&2 - docker "${docker_build_args[@]}" - } - fi - if [[ -z "$PUSH_IMAGES_TO_REGISTRY" ]]; then - return - fi - - if ! [[ "${image_type}" =~ debug ]]; then - push_image_from_tarball "$build_tag" "$docker_image_tarball" - fi -} - -tag_image () { - local build_tag="$1" tag="$2" docker_tag_args - - if [[ "$build_tag" == "$tag" ]]; then - return - fi - - echo ">> TAG: ${build_tag} -> ${tag}" - - docker_tag_args=( - buildx imagetools create - "${DOCKER_REGISTRY}/${build_tag}" - "--tag" "${DOCKER_REGISTRY}/${tag}") - - echo "> docker ${docker_tag_args[*]}" - - if [[ -z "$DOCKER_CI_DRYRUN" ]]; then - echo "..." - docker "${docker_tag_args[@]}" || { - echo "Retry Docker tag in 5s ..." >&2 - sleep 5 - docker "${docker_tag_args[@]}" - } - fi -} - -push_image_from_tarball () { - # Use skopeo to push from the created oci archive - - local build_tag="$1" docker_image_tarball="$2" src dest - - src="oci-archive:${docker_image_tarball}" - dest="docker://${DOCKER_REGISTRY}/${build_tag}" - # dest="oci-archive:${docker_image_tarball2}" - - echo ">> PUSH: ${src} -> ${dest}" - echo "> skopeo copy --all ${src} ${dest}" - - if [[ -n "$DOCKER_CI_DRYRUN" ]]; then - return - fi - - # NB: this command works with skopeo 1.5.0, later versions may require - # different flags, eg `--multi-arch all` - skopeo copy --all "${src}" "${dest}" - - # Test specific versions using a container, eg - # docker run -v "${HOME}/.docker:/root/.docker" -v "${PWD}/build_images:/build_images" --rm -it \ - # quay.io/skopeo/stable:v1.5.0 copy --all "${src}" "${dest}" -} - -tag_variants () { - # Tag image variants - local image_type="$1" build_tag new_image_name release_line variant_type tag_name new_tag_name - - if [[ -z "$PUSH_IMAGES_TO_REGISTRY" ]]; then - return - fi - - build_tag="$(old_image_tag_name "${image_type}")" - new_image_name="$(new_image_tag_name "${image_type}")" - - if [[ "$build_tag" != "$new_image_name" ]]; then - tag_image "${build_tag}" "${new_image_name}" - fi - - # Only push latest on main/dev builds. - if [[ "$ENVOY_VERSION" =~ $DEV_VERSION_REGEX ]]; then - if [[ "${CI_BRANCH}" == "${MAIN_BRANCH}" ]]; then - variant_type="latest" - fi - else - # Push vX.Y-latest to tag the latest image in a release line - release_line="$(echo "$ENVOY_VERSION" | sed -E 's/([0-9]+\.[0-9]+)\.[0-9]+/\1-latest/')" - variant_type="v${release_line}" - fi - if [[ -n "$variant_type" ]]; then - tag_name="$(old_image_tag_name "${image_type}" "${variant_type}")" - new_tag_name="$(new_image_tag_name "${image_type}" "${variant_type}")" - tag_image "${build_tag}" "${tag_name}" - if [[ "$tag_name" != "$new_tag_name" ]]; then - tag_image "${build_tag}" "${new_tag_name}" - fi - fi -} - -build_and_maybe_push_image_and_variants () { - local image_type="$1" - - build_and_maybe_push_image "$image_type" - tag_variants "$image_type" - - # Leave blank line before next build - echo -} - -login_docker () { - echo ">> LOGIN" - if [[ -z "$DOCKER_CI_DRYRUN" ]]; then - docker login -u "$DOCKERHUB_USERNAME" -p "$DOCKERHUB_PASSWORD" - fi -} - -do_docker_ci () { - local build_type - - if [[ -n "$PUSH_IMAGES_TO_REGISTRY" ]]; then - login_docker - fi - - for build_type in "${BUILD_TYPES[@]}"; do - build_and_maybe_push_image_and_variants "${build_type}" - done -} - -do_docker_ci diff --git a/ci/docker_rebuild_google-vrp.sh b/ci/docker_rebuild_google-vrp.sh index a65804f876f6f..d5633aeb70441 100755 --- a/ci/docker_rebuild_google-vrp.sh +++ b/ci/docker_rebuild_google-vrp.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Script to rebuild Dockerfile-envoy-google-vrp locally (i.e. not in CI) for development purposes. +# Script to rebuild Dockerfile-envoy google-vrp target locally (i.e. not in CI) for development purposes. # This makes use of the latest envoy:dev base image on Docker Hub as the base and takes an # optional local path for an Envoy binary. When a custom local Envoy binary is used, the script # switches to using ${BASE_DOCKER_IMAGE} for the build, which should be configured to provide @@ -22,7 +22,7 @@ set -e # Don't use the local envoy:dev, but pull from Docker Hub instead, this avoids having to rebuild # this local dep which is fairly stable. BASE_DOCKER_IMAGE="envoyproxy/envoy:dev" -declare -r DOCKER_BUILD_FILE="ci/Dockerfile-envoy" +declare -r DOCKER_BUILD_FILE="distribution/docker/Dockerfile-envoy" DOCKER_CONTEXT=. DOCKER_BUILD_ARGS=( diff --git a/ci/repokitteh/modules/coverage.star b/ci/repokitteh/modules/coverage.star index 76485748d7c01..d1a5347d388c7 100644 --- a/ci/repokitteh/modules/coverage.star +++ b/ci/repokitteh/modules/coverage.star @@ -3,9 +3,13 @@ COVERAGE_LINK_MESSAGE = """ Coverage for this Pull Request will be rendered here: -https://storage.googleapis.com/envoy-pr/%s/coverage/index.html +https://storage.googleapis.com/envoy-cncf-pr/%s/coverage/index.html -The coverage results are (re-)rendered each time the CI `envoy-presubmit (check linux_x64 coverage)` job completes. +For comparison, current coverage on `main` branch is here: + +https://storage.googleapis.com/envoy-cncf-postsubmit/main/coverage/index.html + +The coverage results are (re-)rendered each time the CI `Envoy/Checks (coverage)` job completes. """ diff --git a/ci/repokitteh/modules/docs.star b/ci/repokitteh/modules/docs.star index 8bd268946d798..b94d838d836ea 100644 --- a/ci/repokitteh/modules/docs.star +++ b/ci/repokitteh/modules/docs.star @@ -3,9 +3,9 @@ DOCS_LINK_MESSAGE = """ Docs for this Pull Request will be rendered here: -https://storage.googleapis.com/envoy-pr/%s/docs/index.html +https://storage.googleapis.com/envoy-cncf-pr/%s/docs/index.html -The docs are (re-)rendered each time the CI `envoy-presubmit (precheck docs)` job completes. +The docs are (re-)rendered each time the CI `Envoy/Prechecks (docs)` job completes. """ diff --git a/ci/run_envoy_docker.sh b/ci/run_envoy_docker.sh index a27186d37b5ab..033f34149da71 100755 --- a/ci/run_envoy_docker.sh +++ b/ci/run_envoy_docker.sh @@ -2,172 +2,56 @@ set -e -CURRENT_SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +# TODO(phlax): Add a check that a usable version of docker compose is available -# shellcheck source=ci/envoy_build_sha.sh -. "${CURRENT_SCRIPT_DIR}"/envoy_build_sha.sh -function is_windows() { - [[ "$(uname -s)" == *NT* ]] -} +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source build SHA information +# shellcheck source=ci/envoy_build_sha.sh +source "${SCRIPT_DIR}/envoy_build_sha.sh" -read -ra ENVOY_DOCKER_OPTIONS <<< "${ENVOY_DOCKER_OPTIONS:-}" +# User/group IDs +USER_UID="$(id -u)" +USER_GID="$(id -g)" +export USER_UID +export USER_GID +# These should probably go in users .env as docker compose will pick that up export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}" export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-}}" export NO_PROXY="${NO_PROXY:-${no_proxy:-}}" export GOPROXY="${GOPROXY:-${go_proxy:-}}" -if is_windows; then - [[ -z "${IMAGE_NAME}" ]] && IMAGE_NAME="envoyproxy/envoy-build-windows2019" - # TODO(sunjayBhatia): Currently ENVOY_DOCKER_OPTIONS is ignored on Windows because - # CI sets it to a Linux-specific value. Undo this once https://github.com/envoyproxy/envoy/issues/13272 - # is resolved. - ENVOY_DOCKER_OPTIONS=() - # Replace MSYS style drive letter (/c/) with Windows drive letter designation (C:/) - DEFAULT_ENVOY_DOCKER_BUILD_DIR=$(echo "${TEMP}" | sed -E "s#^/([a-zA-Z])/#\1:/#")/envoy-docker-build - BUILD_DIR_MOUNT_DEST=C:/build - SOURCE_DIR=$(echo "${PWD}" | sed -E "s#^/([a-zA-Z])/#\1:/#") - SOURCE_DIR_MOUNT_DEST=C:/source - START_COMMAND=("bash" "-c" "cd /c/source && export HOME=/c/build && $*") -else - [[ -z "${IMAGE_NAME}" ]] && IMAGE_NAME="envoyproxy/envoy-build-ubuntu" - # We run as root and later drop permissions. This is required to setup the USER - # in useradd below, which is need for correct Python execution in the Docker - # environment. - ENVOY_DOCKER_OPTIONS+=(-u root:root) - DOCKER_USER_ARGS=() - DOCKER_GROUP_ARGS=() - DEFAULT_ENVOY_DOCKER_BUILD_DIR=/tmp/envoy-docker-build - USER_UID="$(id -u)" - USER_GID="$(id -g)" - if [[ -n "$ENVOY_DOCKER_IN_DOCKER" ]]; then - ENVOY_DOCKER_OPTIONS+=(-v /var/run/docker.sock:/var/run/docker.sock) - DOCKER_GID="$(stat -c %g /var/run/docker.sock 2>/dev/null || stat -f %g /var/run/docker.sock)" - DOCKER_USER_ARGS=(--gid "${DOCKER_GID}") - DOCKER_GROUP_ARGS=(--gid "${DOCKER_GID}") - else - DOCKER_GROUP_ARGS+=(--gid "${USER_GID}") - DOCKER_USER_ARGS+=(--gid "${USER_GID}") - fi - BUILD_DIR_MOUNT_DEST=/build - SOURCE_DIR="${PWD}" - SOURCE_DIR_MOUNT_DEST=/source - ENVOY_DOCKER_SOURCE_DIR="${ENVOY_DOCKER_SOURCE_DIR:-${SOURCE_DIR_MOUNT_DEST}}" - START_COMMAND=( - "/bin/bash" - "-lc" - "groupadd ${DOCKER_GROUP_ARGS[*]} -f envoygroup \ - && useradd -o --uid ${USER_UID} ${DOCKER_USER_ARGS[*]} --no-create-home --home-dir /build envoybuild \ - && usermod -a -G pcap envoybuild \ - && chown envoybuild:envoygroup /build \ - && chown envoybuild /proc/self/fd/2 \ - && sudo -EHs -u envoybuild bash -c 'cd ${ENVOY_DOCKER_SOURCE_DIR} && $*'") +# Docker-in-Docker handling +if [[ -n "$ENVOY_DOCKER_IN_DOCKER" ]]; then + DOCKER_GID="$(stat -c %g /var/run/docker.sock 2>/dev/null || echo "$USER_GID")" + export DOCKER_GID +fi + +if [[ -n "$ENVOY_DOCKER_IN_DOCKER" || -n "$ENVOY_SHARED_TMP_DIR" ]]; then + export SHARED_TMP_DIR="${ENVOY_SHARED_TMP_DIR:-/tmp/bazel-shared}" + mkdir -p "${SHARED_TMP_DIR}" + chmod 777 "${SHARED_TMP_DIR}" fi if [[ -n "$ENVOY_DOCKER_PLATFORM" ]]; then echo "Setting Docker platform: ${ENVOY_DOCKER_PLATFORM}" docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - ENVOY_DOCKER_OPTIONS+=(--platform "$ENVOY_DOCKER_PLATFORM") fi -# The IMAGE_ID defaults to the CI hash but can be set to an arbitrary image ID (found with 'docker -# images'). -if [[ -z "${IMAGE_ID}" ]]; then - IMAGE_ID="${ENVOY_BUILD_SHA}" - if ! is_windows && [[ -n "$ENVOY_BUILD_CONTAINER_SHA" ]]; then - IMAGE_ID="${ENVOY_BUILD_SHA}@sha256:${ENVOY_BUILD_CONTAINER_SHA}" - fi -fi -[[ -z "${ENVOY_DOCKER_BUILD_DIR}" ]] && ENVOY_DOCKER_BUILD_DIR="${DEFAULT_ENVOY_DOCKER_BUILD_DIR}" -# Replace backslash with forward slash for Windows style paths -ENVOY_DOCKER_BUILD_DIR="${ENVOY_DOCKER_BUILD_DIR//\\//}" -mkdir -p "${ENVOY_DOCKER_BUILD_DIR}" - -[[ -t 1 ]] && ENVOY_DOCKER_OPTIONS+=("-it") -[[ -f .git ]] && [[ ! -d .git ]] && ENVOY_DOCKER_OPTIONS+=(-v "$(git rev-parse --git-common-dir):$(git rev-parse --git-common-dir)") -[[ -n "${SSH_AUTH_SOCK}" ]] && ENVOY_DOCKER_OPTIONS+=(-v "${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK}" -e SSH_AUTH_SOCK) - -export ENVOY_BUILD_IMAGE="${IMAGE_NAME}:${IMAGE_ID}" - -VOLUMES=( - -v "${ENVOY_DOCKER_BUILD_DIR}":"${BUILD_DIR_MOUNT_DEST}" - -v "${SOURCE_DIR}":"${SOURCE_DIR_MOUNT_DEST}") +export DOCKER_COMMAND="${*:-bash}" +COMPOSE_SERVICE="envoy-build" if [[ -n "$MOUNT_GPG_HOME" ]]; then - VOLUMES+=( - -v "${HOME}/.gnupg:${BUILD_DIR_MOUNT_DEST}/.gnupg") -fi - -if ! is_windows; then - export BUILD_DIR="${BUILD_DIR_MOUNT_DEST}" -fi - -if [[ -n "$ENVOY_DOCKER_IN_DOCKER" || -n "$ENVOY_SHARED_TMP_DIR" ]]; then - # Create a "shared" directory that has the same path in/outside the container - # This allows the host docker engine to see artefacts using a temporary path created inside the container, - # at the same path. - # For example, a directory created with `mktemp -d --tmpdir /tmp/bazel-shared` can be mounted as a volume - # from within the build container. - SHARED_TMP_DIR="${ENVOY_SHARED_TMP_DIR:-/tmp/bazel-shared}" - mkdir -p "${SHARED_TMP_DIR}" - chmod +rwx "${SHARED_TMP_DIR}" - VOLUMES+=(-v "${SHARED_TMP_DIR}":"${SHARED_TMP_DIR}") -fi - -if [[ -n "${ENVOY_DOCKER_PULL}" ]]; then - time docker pull "${ENVOY_BUILD_IMAGE}" + COMPOSE_SERVICE="envoy-build-gpg" +elif [[ -n "$ENVOY_DOCKER_IN_DOCKER" ]]; then + COMPOSE_SERVICE="envoy-build-dind" fi -# Since we specify an explicit hash, docker-run will pull from the remote repo if missing. -docker run --rm \ - "${ENVOY_DOCKER_OPTIONS[@]}" \ - "${VOLUMES[@]}" \ - -e BUILD_DIR \ - -e HTTP_PROXY \ - -e HTTPS_PROXY \ - -e NO_PROXY \ - -e GOPROXY \ - -e BAZEL_STARTUP_OPTIONS \ - -e BAZEL_BUILD_EXTRA_OPTIONS \ - -e BAZEL_EXTRA_TEST_OPTIONS \ - -e BAZEL_REMOTE_CACHE \ - -e BAZEL_STARTUP_EXTRA_OPTIONS \ - -e CI_BRANCH \ - -e CI_SHA1 \ - -e CI_TARGET_BRANCH \ - -e DOCKERHUB_USERNAME \ - -e DOCKERHUB_PASSWORD \ - -e ENVOY_DOCKER_SAVE_IMAGE \ - -e BUILD_REASON \ - -e BAZEL_REMOTE_INSTANCE \ - -e GCP_SERVICE_ACCOUNT_KEY \ - -e GCP_SERVICE_ACCOUNT_KEY_PATH \ - -e NUM_CPUS \ - -e ENVOY_BRANCH \ - -e ENVOY_RBE \ - -e ENVOY_BUILD_IMAGE \ - -e ENVOY_SRCDIR \ - -e ENVOY_BUILD_TARGET \ - -e ENVOY_BUILD_DEBUG_INFORMATION \ - -e ENVOY_BUILD_FILTER_EXAMPLE \ - -e ENVOY_COMMIT \ - -e ENVOY_HEAD_REF \ - -e ENVOY_PUBLISH_DRY_RUN \ - -e ENVOY_REPO \ - -e ENVOY_TARBALL_DIR \ - -e ENVOY_GEN_COMPDB_OPTIONS \ - -e GCS_ARTIFACT_BUCKET \ - -e GCS_REDIRECT_PATH \ - -e GITHUB_REF_NAME \ - -e GITHUB_REF_TYPE \ - -e GITHUB_TOKEN \ - -e GITHUB_APP_ID \ - -e GITHUB_INSTALL_ID \ - -e MOBILE_DOCS_CHECKOUT_DIR \ - -e BAZELISK_BASE_URL \ - -e ENVOY_BUILD_ARCH \ - -e SYSTEM_STAGEDISPLAYNAME \ - -e SYSTEM_JOBDISPLAYNAME \ - "${ENVOY_BUILD_IMAGE}" \ - "${START_COMMAND[@]}" +exec docker-compose \ + -f "${SCRIPT_DIR}/docker-compose.yml" \ + ${ENVOY_DOCKER_PLATFORM:+-p "$ENVOY_DOCKER_PLATFORM"} \ + run \ + --rm \ + "${COMPOSE_SERVICE}" diff --git a/ci/test_docker_ci.sh b/ci/test_docker_ci.sh index cb33828714a23..35b73cce63a69 100755 --- a/ci/test_docker_ci.sh +++ b/ci/test_docker_ci.sh @@ -64,16 +64,16 @@ _test () { if [[ "$DOCKER_CI_TEST_COMMIT" ]]; then echo "COMMIT(${name}): > ${testdata}" - echo " ENVOY_VERSION=${version} ENVOY_DOCKER_IMAGE_DIRECTORY=/non/existent/test/path CI_BRANCH=${branch} DOCKER_CI_DRYRUN=1 ./ci/docker_ci.sh | grep -E \"^>\"" - ./ci/docker_ci.sh | grep -E "^>" > "$testdata" + echo " ENVOY_VERSION=${version} ENVOY_DOCKER_IMAGE_DIRECTORY=/non/existent/test/path CI_BRANCH=${branch} DOCKER_CI_DRYRUN=1 ./distribution/docker/docker_ci.sh | grep -E \"^>\"" + ./distribution/docker/docker_ci.sh | grep -E "^>" > "$testdata" return fi echo "TEST(${name}): <> ${testdata}" - echo " ENVOY_VERSION=${version} ENVOY_DOCKER_IMAGE_DIRECTORY=/non/existent/test/path CI_BRANCH=${branch} DOCKER_CI_DRYRUN=1 ./ci/docker_ci.sh | grep -E \"^>\"" + echo " ENVOY_VERSION=${version} ENVOY_DOCKER_IMAGE_DIRECTORY=/non/existent/test/path CI_BRANCH=${branch} DOCKER_CI_DRYRUN=1 ./distribution/docker/docker_ci.sh | grep -E \"^>\"" generated="$(mktemp)" - ./ci/docker_ci.sh | grep -E "^>" > "$generated" + ./distribution/docker/docker_ci.sh | grep -E "^>" > "$generated" cmp --silent "$testdata" "$generated" || { echo "files are different" >&2 diff --git a/configs/proxy_connect.yaml b/configs/proxy_connect.yaml index 4393f764cb469..4db7ab459606b 100644 --- a/configs/proxy_connect.yaml +++ b/configs/proxy_connect.yaml @@ -34,8 +34,6 @@ static_resources: cluster: cluster_0 upgrade_configs: - upgrade_type: CONNECT - connect_config: - {} http_filters: - name: envoy.filters.http.router typed_config: diff --git a/configs/proxy_connect_udp_http3_downstream.yaml b/configs/proxy_connect_udp_http3_downstream.yaml index 8c3269defabe2..de0cca5a94b9f 100644 --- a/configs/proxy_connect_udp_http3_downstream.yaml +++ b/configs/proxy_connect_udp_http3_downstream.yaml @@ -44,8 +44,6 @@ static_resources: cluster: cluster_0 upgrade_configs: - upgrade_type: CONNECT-UDP - connect_config: - {} http_filters: - name: envoy.filters.http.router typed_config: diff --git a/configs/reverse_connection/initiator-envoy.yaml b/configs/reverse_connection/initiator-envoy.yaml new file mode 100644 index 0000000000000..1852061ba4d91 --- /dev/null +++ b/configs/reverse_connection/initiator-envoy.yaml @@ -0,0 +1,91 @@ +--- +node: + id: downstream-node + cluster: downstream + +# Enable reverse connection bootstrap extension which registers the custom resolver +bootstrap_extensions: +- name: envoy.bootstrap.reverse_tunnel.downstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" + +static_resources: + listeners: + # Initiates reverse connections to upstream using custom resolver + - name: reverse_conn_listener + listener_filters_timeout: 0s + listener_filters: + # Use custom address with reverse connection metadata encoded in URL format + address: + socket_address: + # This encodes: src_node_id=downstream-node, src_cluster_id=downstream, src_tenant_id=downstream + # and remote clusters: upstream with 1 connection + address: "rc://downstream-node:downstream-cluster:downstream-tenant@upstream-cluster:1" + port_value: 0 + # Use custom resolver that can parse reverse connection metadata + resolver_name: "envoy.resolvers.reverse_connection" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_conn_listener + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/downstream_service' + route: + cluster: downstream-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Cluster designating upstream-envoy + clusters: + - name: upstream-cluster + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: upstream-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: upstream-envoy # Container name of upstream-envoy in docker-compose + port_value: 9000 # Port where upstream-envoy's rev_conn_api_listener listens + + # Backend HTTP service behind downstream which + # we will access via reverse connections + - name: downstream-service + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: downstream-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: downstream-service + port_value: 80 + +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8888 + +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 \ No newline at end of file diff --git a/configs/reverse_connection/responder-envoy.yaml b/configs/reverse_connection/responder-envoy.yaml new file mode 100644 index 0000000000000..8b73234256f39 --- /dev/null +++ b/configs/reverse_connection/responder-envoy.yaml @@ -0,0 +1,83 @@ +--- +node: + id: upstream-node + cluster: upstream-cluster +static_resources: + listeners: + # Accepts reverse tunnel requests + - name: rev_conn_api_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 9000 + filter_chains: + - filters: + - name: envoy.filters.network.reverse_tunnel + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel + ping_interval: 2s + + # Listener that will route the downstream request to the reverse connection cluster + - name: egress_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 8085 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: "/downstream_service" + route: + cluster: reverse_connection_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Cluster used to write requests to cached sockets + clusters: + - name: reverse_connection_cluster + connect_timeout: 200s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + # The following headers are expected in downstream requests + # to be sent over reverse connections + http_header_names: + - x-remote-node-id # Should be set to the node ID of the downstream envoy node, ie., downstream-node + - x-dst-cluster-uuid # Should be set to the cluster ID of the downstream envoy node, ie., downstream + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + # Right the moment, reverse connections are supported over HTTP/2 only + http2_protocol_options: {} +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + address: 0.0.0.0 + port_value: 8888 +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 + envoy.reloadable_features.reverse_conn_force_local_reply: true +# Enable reverse connection bootstrap extension +bootstrap_extensions: +- name: envoy.bootstrap.reverse_tunnel.upstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3.UpstreamReverseConnectionSocketInterface + stat_prefix: "upstream_reverse_connection" \ No newline at end of file diff --git a/contrib/common/sqlutils/source/sqlutils.cc b/contrib/common/sqlutils/source/sqlutils.cc index dffa393bd0390..8f20108dc422c 100644 --- a/contrib/common/sqlutils/source/sqlutils.cc +++ b/contrib/common/sqlutils/source/sqlutils.cc @@ -6,7 +6,7 @@ namespace Common { namespace SQLUtils { bool SQLUtils::setMetadata(const std::string& query, const DecoderAttributes& attr, - ProtobufWkt::Struct& metadata) { + Protobuf::Struct& metadata) { hsql::SQLParserResult result; hsql::SQLParser::parse(query, &result); diff --git a/contrib/common/sqlutils/source/sqlutils.h b/contrib/common/sqlutils/source/sqlutils.h index 4c62f2a5e91d9..5b731f30a7744 100644 --- a/contrib/common/sqlutils/source/sqlutils.h +++ b/contrib/common/sqlutils/source/sqlutils.h @@ -24,7 +24,7 @@ class SQLUtils { * stored in metadata.mutable_fields. **/ static bool setMetadata(const std::string& query, const DecoderAttributes& attr, - ProtobufWkt::Struct& metadata); + Protobuf::Struct& metadata); }; } // namespace SQLUtils diff --git a/contrib/common/sqlutils/test/sqlutils_test.cc b/contrib/common/sqlutils/test/sqlutils_test.cc index e58f03eab5f7d..6a97edde1a574 100644 --- a/contrib/common/sqlutils/test/sqlutils_test.cc +++ b/contrib/common/sqlutils/test/sqlutils_test.cc @@ -40,7 +40,7 @@ TEST_P(MetadataFromSQLTest, ParsingAndMetadataTest) { while (!test_queries.empty()) { std::string test_query = test_queries.back(); - ProtobufWkt::Struct metadata; + Protobuf::Struct metadata; // Check if the parsing result is what expected. ASSERT_EQ(std::get<1>(GetParam()), diff --git a/contrib/config/source/kv_store_xds_delegate.cc b/contrib/config/source/kv_store_xds_delegate.cc index 8d53446cceed4..5a228d2ffeb76 100644 --- a/contrib/config/source/kv_store_xds_delegate.cc +++ b/contrib/config/source/kv_store_xds_delegate.cc @@ -148,7 +148,7 @@ std::string KeyValueStoreXdsDelegateFactory::name() const { }; Envoy::Config::XdsResourcesDelegatePtr KeyValueStoreXdsDelegateFactory::createXdsResourcesDelegate( - const ProtobufWkt::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor, + const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor, Api::Api& api, Event::Dispatcher& dispatcher) { const auto& validator_config = Envoy::MessageUtil::anyConvertAndValidate(config, diff --git a/contrib/config/source/kv_store_xds_delegate.h b/contrib/config/source/kv_store_xds_delegate.h index c0e29b7792993..e3c62ec8ae098 100644 --- a/contrib/config/source/kv_store_xds_delegate.h +++ b/contrib/config/source/kv_store_xds_delegate.h @@ -79,7 +79,7 @@ class KeyValueStoreXdsDelegateFactory : public Envoy::Config::XdsResourcesDelega std::string name() const override; Envoy::Config::XdsResourcesDelegatePtr - createXdsResourcesDelegate(const ProtobufWkt::Any& config, + createXdsResourcesDelegate(const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor, Api::Api& api, Event::Dispatcher& dispatcher) override; }; diff --git a/contrib/config/test/kv_store_xds_delegate_integration_test.cc b/contrib/config/test/kv_store_xds_delegate_integration_test.cc index 912d7a2ad4d6a..a383cf93a30d7 100644 --- a/contrib/config/test/kv_store_xds_delegate_integration_test.cc +++ b/contrib/config/test/kv_store_xds_delegate_integration_test.cc @@ -308,19 +308,19 @@ TEST_P(KeyValueStoreXdsDelegateIntegrationTest, BasicSuccess) { // SDS. initXdsStream(*getSdsUpstream(), sds_connection_, sds_stream_); EXPECT_TRUE(compareSotwDiscoveryRequest( - /*expected_type_url=*/Config::TypeUrl::get().Secret, /*expected_version=*/"", + /*expected_type_url=*/Config::TestTypeUrl::get().Secret, /*expected_version=*/"", /*expected_resource_names=*/{std::string(CLIENT_CERT_NAME)}, /*expect_node=*/true, /*expected_error_code=*/Grpc::Status::WellKnownGrpcStatus::Ok, /*expected_error_message=*/"", sds_stream_.get())); auto sds_resource = getClientSecret(); sendSotwDiscoveryResponse( - Config::TypeUrl::get().Secret, {sds_resource}, "1", sds_stream_.get()); + Config::TestTypeUrl::get().Secret, {sds_resource}, "1", sds_stream_.get()); } { // RTDS. initXdsStream(*getRtdsUpstream(), rtds_connection_, rtds_stream_); EXPECT_TRUE(compareSotwDiscoveryRequest( - /*expected_type_url=*/Config::TypeUrl::get().Runtime, + /*expected_type_url=*/Config::TestTypeUrl::get().Runtime, /*expected_version=*/"", /*expected_resource_names=*/{"some_rtds_layer"}, /*expect_node=*/true, /*expected_error_code=*/Grpc::Status::WellKnownGrpcStatus::Ok, @@ -332,7 +332,7 @@ TEST_P(KeyValueStoreXdsDelegateIntegrationTest, BasicSuccess) { baz: meh )EOF"); sendSotwDiscoveryResponse( - Config::TypeUrl::get().Runtime, {rtds_resource}, "1", rtds_stream_.get()); + Config::TestTypeUrl::get().Runtime, {rtds_resource}, "1", rtds_stream_.get()); } }; @@ -351,7 +351,7 @@ TEST_P(KeyValueStoreXdsDelegateIntegrationTest, BasicSuccess) { // Send an update to the RTDS resource, from the RTDS cluster to the Envoy test server. EXPECT_TRUE(compareSotwDiscoveryRequest( - /*expected_type_url=*/Config::TypeUrl::get().Runtime, /*expected_version=*/"1", + /*expected_type_url=*/Config::TestTypeUrl::get().Runtime, /*expected_version=*/"1", /*expected_resource_names=*/{"some_rtds_layer"}, /*expect_node=*/false, /*expected_error_code=*/Grpc::Status::WellKnownGrpcStatus::Ok, /*expected_error_message=*/"", rtds_stream_.get())); @@ -361,7 +361,7 @@ TEST_P(KeyValueStoreXdsDelegateIntegrationTest, BasicSuccess) { baz: saz )EOF"); sendSotwDiscoveryResponse( - Config::TypeUrl::get().Runtime, {rtds_resource}, "2", rtds_stream_.get()); + Config::TestTypeUrl::get().Runtime, {rtds_resource}, "2", rtds_stream_.get()); test_server_->waitForCounterGe("runtime.load_success", 3); EXPECT_EQ("whatevs", getRuntimeKey("foo")); @@ -399,7 +399,7 @@ TEST_P(KeyValueStoreXdsDelegateIntegrationTest, BasicSuccess) { baz: jazz )EOF"); sendSotwDiscoveryResponse( - Config::TypeUrl::get().Runtime, {rtds_resource_v2}, /*version=*/"3", rtds_stream_.get()); + Config::TestTypeUrl::get().Runtime, {rtds_resource_v2}, /*version=*/"3", rtds_stream_.get()); test_server_->waitForCounterGe("runtime.load_success", 3); @@ -421,7 +421,7 @@ class InvalidProtoKeyValueStore : public KeyValueStore { // We only have a cds_config making wildcard requests, so we only need to implement the iterate // function. void iterate(ConstIterateCb cb) const override { - const Config::XdsConfigSourceId source_id{CDS_CLUSTER_NAME, Config::TypeUrl::get().Cluster}; + const Config::XdsConfigSourceId source_id{CDS_CLUSTER_NAME, Config::TestTypeUrl::get().Cluster}; const std::string cluster_name = "cluster_A"; const std::string key = absl::StrCat(source_id.toKey(), "+", cluster_name); diff --git a/contrib/config/test/kv_store_xds_delegate_test.cc b/contrib/config/test/kv_store_xds_delegate_test.cc index a73b9b3470382..3f364c55f9478 100644 --- a/contrib/config/test/kv_store_xds_delegate_test.cc +++ b/contrib/config/test/kv_store_xds_delegate_test.cc @@ -108,7 +108,7 @@ TEST_F(KeyValueStoreXdsDelegateTest, SaveAndRetrieve) { )EOF"); const auto saved_resources = TestUtility::decodeResources({runtime_resource_1, runtime_resource_2}); - const XdsConfigSourceId source_id{authority_1, Config::TypeUrl::get().Runtime}; + const XdsConfigSourceId source_id{authority_1, Config::TestTypeUrl::get().Runtime}; // Save xDS resources. xds_delegate_->onConfigUpdated(source_id, saved_resources.refvec_); @@ -146,9 +146,9 @@ TEST_F(KeyValueStoreXdsDelegateTest, MultipleAuthoritiesAndTypes) { const auto authority_2_runtime_resources = TestUtility::decodeResources({runtime_resource_2}); const auto authority_2_cluster_resources = TestUtility::decodeResources({cluster_resource_1}); - const XdsConfigSourceId source_id_1{authority_1, Config::TypeUrl::get().Runtime}; - const XdsConfigSourceId source_id_2_runtime{authority_2, Config::TypeUrl::get().Runtime}; - const XdsConfigSourceId source_id_2_cluster{authority_2, Config::TypeUrl::get().Cluster}; + const XdsConfigSourceId source_id_1{authority_1, Config::TestTypeUrl::get().Runtime}; + const XdsConfigSourceId source_id_2_runtime{authority_2, Config::TestTypeUrl::get().Runtime}; + const XdsConfigSourceId source_id_2_cluster{authority_2, Config::TestTypeUrl::get().Cluster}; // Save xDS resources. xds_delegate_->onConfigUpdated(source_id_1, authority_1_runtime_resources.refvec_); @@ -184,7 +184,7 @@ TEST_F(KeyValueStoreXdsDelegateTest, UpdatedSotwResources) { abc: xyz )EOF"); - const XdsConfigSourceId source_id{authority_1, Config::TypeUrl::get().Runtime}; + const XdsConfigSourceId source_id{authority_1, Config::TestTypeUrl::get().Runtime}; // Save xDS resources. const auto saved_resources = @@ -232,7 +232,7 @@ TEST_F(KeyValueStoreXdsDelegateTest, Wildcard) { )EOF"); const auto saved_resources = TestUtility::decodeResources({runtime_resource_1, runtime_resource_2}); - const XdsConfigSourceId source_id{authority_1, Config::TypeUrl::get().Runtime}; + const XdsConfigSourceId source_id{authority_1, Config::TestTypeUrl::get().Runtime}; // Save xDS resources. xds_delegate_->onConfigUpdated(source_id, saved_resources.refvec_); @@ -257,7 +257,7 @@ TEST_F(KeyValueStoreXdsDelegateTest, ResourceNotFound) { baz: meh )EOF"); const auto saved_resources = TestUtility::decodeResources({runtime_resource_1}); - const XdsConfigSourceId source_id{authority_1, Config::TypeUrl::get().Runtime}; + const XdsConfigSourceId source_id{authority_1, Config::TestTypeUrl::get().Runtime}; // Save xDS resources. xds_delegate_->onConfigUpdated(source_id, saved_resources.refvec_); @@ -309,7 +309,7 @@ TEST_F(KeyValueStoreXdsDelegateTest, ResourcesWithTTL) { resources, /*version=*/"1"); // Save xDS resources. - const XdsConfigSourceId source_id{authority_1, Config::TypeUrl::get().Runtime}; + const XdsConfigSourceId source_id{authority_1, Config::TestTypeUrl::get().Runtime}; xds_delegate_->onConfigUpdated(source_id, decoded_resources.refvec_); // TTL hasn't expired, so we should have all three xDS resources. diff --git a/contrib/dlb/source/BUILD b/contrib/dlb/source/BUILD index 46c28a3dad186..c4e777a90ce14 100644 --- a/contrib/dlb/source/BUILD +++ b/contrib/dlb/source/BUILD @@ -18,7 +18,7 @@ make( env = {"DLB_DISABLE_DOMAIN_SERVER": "TRUE"}, lib_source = "@intel_dlb//:libdlb", out_static_libs = ["libdlb.a"], - postfix_script = "mv libdlb.a $INSTALLDIR/lib && rm -rf $INSTALLDIR/include && mkdir -p $INSTALLDIR/include && cp -L *.h $INSTALLDIR/include", + postfix_script = "mv libdlb.a $$INSTALLDIR/lib && rm -rf $$INSTALLDIR/include && mkdir -p $$INSTALLDIR/include && cp -L *.h $$INSTALLDIR/include", tags = ["skip_on_windows"], target_compatible_with = envoy_contrib_linux_x86_64_constraints(), targets = ["libdlb.a"], diff --git a/contrib/generic_proxy/filters/network/source/codecs/kafka/BUILD b/contrib/generic_proxy/filters/network/source/codecs/kafka/BUILD index e96582eba3a1e..30acc63a67f44 100644 --- a/contrib/generic_proxy/filters/network/source/codecs/kafka/BUILD +++ b/contrib/generic_proxy/filters/network/source/codecs/kafka/BUILD @@ -19,6 +19,7 @@ envoy_cc_contrib_extension( deps = [ "//contrib/kafka/filters/network/source:kafka_request_codec_lib", "//contrib/kafka/filters/network/source:kafka_response_codec_lib", + "//source/common/runtime:runtime_features_lib", "//source/extensions/filters/network/generic_proxy/interface:codec_interface", "@envoy_api//contrib/envoy/extensions/filters/network/generic_proxy/codecs/kafka/v3:pkg_cc_proto", ], diff --git a/contrib/generic_proxy/filters/network/source/codecs/kafka/config.cc b/contrib/generic_proxy/filters/network/source/codecs/kafka/config.cc index 35c6d9572311b..cc817b73af46e 100644 --- a/contrib/generic_proxy/filters/network/source/codecs/kafka/config.cc +++ b/contrib/generic_proxy/filters/network/source/codecs/kafka/config.cc @@ -1,5 +1,7 @@ #include "contrib/generic_proxy/filters/network/source/codecs/kafka/config.h" +#include "source/common/runtime/runtime_features.h" + namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -17,6 +19,13 @@ void KafkaServerCodec::setCodecCallbacks(GenericProxy::ServerCodecCallbacks& cal void KafkaServerCodec::decode(Envoy::Buffer::Instance& buffer, bool) { request_buffer_.move(buffer); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.generic_proxy_codec_buffer_limit")) { + if (request_buffer_.length() > request_callbacks_->callbacks_.connection()->bufferLimit()) { + request_callbacks_->callbacks_.onDecodingFailure(); + return; + } + } request_decoder_->onData(request_buffer_); // All data has been consumed, so we can drain the buffer. request_buffer_.drain(request_buffer_.length()); @@ -60,6 +69,13 @@ void KafkaClientCodec::setCodecCallbacks(GenericProxy::ClientCodecCallbacks& cal void KafkaClientCodec::decode(Envoy::Buffer::Instance& buffer, bool) { response_buffer_.move(buffer); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.generic_proxy_codec_buffer_limit")) { + if (response_buffer_.length() > response_callbacks_->callbacks_.connection()->bufferLimit()) { + response_callbacks_->callbacks_.onDecodingFailure(); + return; + } + } response_decoder_->onData(response_buffer_); // All data has been consumed, so we can drain the buffer. response_buffer_.drain(response_buffer_.length()); diff --git a/contrib/generic_proxy/filters/network/test/codecs/kafka/config_test.cc b/contrib/generic_proxy/filters/network/test/codecs/kafka/config_test.cc index d6d4c46819e27..2925622f8d9de 100644 --- a/contrib/generic_proxy/filters/network/test/codecs/kafka/config_test.cc +++ b/contrib/generic_proxy/filters/network/test/codecs/kafka/config_test.cc @@ -129,6 +129,7 @@ TEST(KafkaCodecTest, KafkaServerCodecTest) { { // Test decode() method. + ON_CALL(mock_connection, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); EXPECT_CALL(callbacks, onDecodingSuccess(_, _)) .WillOnce(testing::Invoke([](RequestHeaderFramePtr request, absl::optional) { EXPECT_EQ(dynamic_cast(request.get()) @@ -150,6 +151,24 @@ TEST(KafkaCodecTest, KafkaServerCodecTest) { server_codec.decode(buffer, false); } + { + // Test decode buffer limit. + ON_CALL(mock_connection, bufferLimit()).WillByDefault(testing::Return(4)); + EXPECT_CALL(callbacks, onDecodingFailure(_)); + auto request = + std::make_shared>( + NetworkFilters::Kafka::RequestHeader(NetworkFilters::Kafka::FETCH_REQUEST_API_KEY, 0, 3, + absl::nullopt), + NetworkFilters::Kafka::FetchRequest({}, {}, {}, {})); + + Buffer::OwnedImpl buffer; + const uint32_t size = htobe32(request->computeSize()); + buffer.add(&size, sizeof(size)); // Encode data length. + + request->encode(buffer); + server_codec.decode(buffer, false); + } + { // Test encode() method with non-response frame. @@ -217,6 +236,7 @@ TEST(KafkaCodecTest, KafkaClientCodecTest) { { // Test decode() method. + ON_CALL(mock_connection, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); EXPECT_CALL(callbacks, onDecodingSuccess(_, _)) .WillOnce(testing::Invoke([](ResponseHeaderFramePtr response, absl::optional) { EXPECT_EQ(dynamic_cast(response.get()) @@ -240,6 +260,24 @@ TEST(KafkaCodecTest, KafkaClientCodecTest) { client_codec.decode(buffer, false); } + { + // Test decode buffer limit. + ON_CALL(mock_connection, bufferLimit()).WillByDefault(testing::Return(4)); + EXPECT_CALL(callbacks, onDecodingFailure(_)); + auto response = + std::make_shared>( + NetworkFilters::Kafka::ResponseMetadata(NetworkFilters::Kafka::FETCH_REQUEST_API_KEY, 0, + 3), + NetworkFilters::Kafka::FetchResponse({}, {})); + + Buffer::OwnedImpl buffer; + const uint32_t size = htobe32(response->computeSize()); + buffer.add(&size, sizeof(size)); // Encode data length. + + response->encode(buffer); + client_codec.decode(buffer, false); + } + { // Test encode() method with non-request frame. diff --git a/contrib/golang/filters/http/source/golang_filter.cc b/contrib/golang/filters/http/source/golang_filter.cc index 2b8af6ffb3508..c3bc4e3250d3f 100644 --- a/contrib/golang/filters/http/source/golang_filter.cc +++ b/contrib/golang/filters/http/source/golang_filter.cc @@ -1225,8 +1225,8 @@ CAPIStatus Filter::setDynamicMetadata(std::string filter_name, std::string key, void Filter::setDynamicMetadataInternal(std::string filter_name, std::string key, const absl::string_view& buf) { - ProtobufWkt::Struct value; - ProtobufWkt::Value v; + Protobuf::Struct value; + Protobuf::Value v; v.ParseFromArray(buf.data(), buf.length()); (*value.mutable_fields())[key] = v; diff --git a/contrib/golang/filters/http/source/golang_filter.h b/contrib/golang/filters/http/source/golang_filter.h index a49be6cbc2643..f5aa4409d9556 100644 --- a/contrib/golang/filters/http/source/golang_filter.h +++ b/contrib/golang/filters/http/source/golang_filter.h @@ -97,7 +97,7 @@ class FilterConfig : public std::enable_shared_from_this, const std::string plugin_name_; const std::string so_id_; const std::string so_path_; - const ProtobufWkt::Any plugin_config_; + const Protobuf::Any plugin_config_; uint32_t concurrency_; GolangFilterStats stats_; @@ -126,7 +126,7 @@ class RoutePluginConfig : public std::enable_shared_from_this private: const std::string plugin_name_; - const ProtobufWkt::Any plugin_config_; + const Protobuf::Any plugin_config_; Dso::HttpFilterDsoPtr dso_lib_; uint64_t config_id_{0}; diff --git a/contrib/golang/filters/http/test/golang_integration_test.cc b/contrib/golang/filters/http/test/golang_integration_test.cc index 18f2b9f52c9bb..aea3b97c3b941 100644 --- a/contrib/golang/filters/http/test/golang_integration_test.cc +++ b/contrib/golang/filters/http/test/golang_integration_test.cc @@ -202,12 +202,12 @@ name: golang set: bar )EOF"; auto yaml = absl::StrFormat(yaml_fmt, so_id); - ProtobufWkt::Any value; + Protobuf::Any value; TestUtility::loadFromYaml(yaml, value); hcm.mutable_route_config() ->mutable_virtual_hosts(0) ->mutable_typed_per_filter_config() - ->insert(Protobuf::MapPair(key, value)); + ->insert(Protobuf::MapPair(key, value)); // route level per route config const auto yaml_fmt2 = @@ -223,13 +223,13 @@ name: golang set: baz )EOF"; auto yaml2 = absl::StrFormat(yaml_fmt2, so_id); - ProtobufWkt::Any value2; + Protobuf::Any value2; TestUtility::loadFromYaml(yaml2, value2); auto* new_route2 = hcm.mutable_route_config()->mutable_virtual_hosts(0)->add_routes(); new_route2->mutable_match()->set_prefix("/route-config-test"); new_route2->mutable_typed_per_filter_config()->insert( - Protobuf::MapPair(key, value2)); + Protobuf::MapPair(key, value2)); new_route2->mutable_route()->set_cluster("cluster_0"); }); @@ -1758,14 +1758,14 @@ TEST_P(GolangIntegrationTest, RefreshRouteCache) { value: )EOF"; auto yaml = absl::StrFormat(yaml_fmt, so_id); - ProtobufWkt::Any value; + Protobuf::Any value; TestUtility::loadFromYaml(yaml, value); auto* route_first_matched = hcm.mutable_route_config()->mutable_virtual_hosts(0)->add_routes(); route_first_matched->mutable_match()->set_prefix("/disney/api"); route_first_matched->mutable_typed_per_filter_config()->insert( - Protobuf::MapPair(key, value)); + Protobuf::MapPair(key, value)); auto* resp_header = route_first_matched->add_response_headers_to_add(); auto* header = resp_header->mutable_header(); header->set_key("add-header-from"); diff --git a/contrib/golang/filters/http/test/test_data/go.mod b/contrib/golang/filters/http/test/test_data/go.mod index 242314aef2241..430821a0e3196 100644 --- a/contrib/golang/filters/http/test/test_data/go.mod +++ b/contrib/golang/filters/http/test/test_data/go.mod @@ -1,12 +1,12 @@ module example.com/test-data -go 1.22 +go 1.23 require github.com/envoyproxy/envoy v1.33.2 require ( github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 - google.golang.org/protobuf v1.36.6 + google.golang.org/protobuf v1.36.9 ) require ( diff --git a/contrib/golang/filters/network/source/golang.h b/contrib/golang/filters/network/source/golang.h index a60a3a4d86042..e35913c2d4920 100644 --- a/contrib/golang/filters/network/source/golang.h +++ b/contrib/golang/filters/network/source/golang.h @@ -33,13 +33,13 @@ class FilterConfig { const std::string& libraryID() const { return library_id_; } const std::string& libraryPath() const { return library_path_; } const std::string& pluginName() const { return plugin_name_; } - const ProtobufWkt::Any& pluginConfig() const { return plugin_config_; } + const Protobuf::Any& pluginConfig() const { return plugin_config_; } private: const std::string library_id_; const std::string library_path_; const std::string plugin_name_; - const ProtobufWkt::Any plugin_config_; + const Protobuf::Any plugin_config_; }; using FilterConfigSharedPtr = std::shared_ptr; diff --git a/contrib/golang/router/cluster_specifier/source/golang_cluster_specifier.h b/contrib/golang/router/cluster_specifier/source/golang_cluster_specifier.h index b7a5ce0401559..df0918164f81e 100644 --- a/contrib/golang/router/cluster_specifier/source/golang_cluster_specifier.h +++ b/contrib/golang/router/cluster_specifier/source/golang_cluster_specifier.h @@ -24,7 +24,7 @@ class ClusterConfig : Logger::Loggable { const std::string so_id_; const std::string so_path_; const std::string default_cluster_; - const ProtobufWkt::Any config_; + const Protobuf::Any config_; uint64_t plugin_id_{0}; Dso::ClusterSpecifierDsoPtr dynamic_lib_; }; diff --git a/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod b/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod index a985bc3accf08..fe8c2a1a46a1e 100644 --- a/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod +++ b/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod @@ -1,6 +1,6 @@ module example.com/routeconfig -go 1.22 +go 1.23 require ( github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa @@ -15,7 +15,7 @@ require ( require ( github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect github.com/golang/protobuf v1.5.3 // indirect - google.golang.org/protobuf v1.36.6 + google.golang.org/protobuf v1.36.9 ) replace github.com/envoyproxy/envoy => ../../../../../../../ diff --git a/contrib/golang/upstreams/http/tcp/source/upstream_request.h b/contrib/golang/upstreams/http/tcp/source/upstream_request.h index 44a51a284689c..6d24342007940 100644 --- a/contrib/golang/upstreams/http/tcp/source/upstream_request.h +++ b/contrib/golang/upstreams/http/tcp/source/upstream_request.h @@ -58,7 +58,7 @@ class BridgeConfig : httpConfig, const std::string plugin_name_; const std::string so_id_; const std::string so_path_; - const ProtobufWkt::Any plugin_config_; + const Protobuf::Any plugin_config_; Dso::HttpTcpBridgeDsoPtr dso_lib_; uint64_t config_id_{0}; diff --git a/contrib/golang/upstreams/http/tcp/test/test_data/go.mod b/contrib/golang/upstreams/http/tcp/test/test_data/go.mod index 52a21b5806345..d01d093aad5cc 100644 --- a/contrib/golang/upstreams/http/tcp/test/test_data/go.mod +++ b/contrib/golang/upstreams/http/tcp/test/test_data/go.mod @@ -1,9 +1,9 @@ module example.com/test-data -go 1.22 +go 1.23 require github.com/envoyproxy/envoy v1.33.2 -require google.golang.org/protobuf v1.36.6 +require google.golang.org/protobuf v1.36.9 replace github.com/envoyproxy/envoy => ../../../../../../../ diff --git a/contrib/mysql_proxy/filters/network/source/mysql_filter.cc b/contrib/mysql_proxy/filters/network/source/mysql_filter.cc index 6c1f450d35bfb..1574bd2a23e80 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_filter.cc +++ b/contrib/mysql_proxy/filters/network/source/mysql_filter.cc @@ -108,7 +108,7 @@ void MySQLFilter::onCommand(Command& command) { // Parse a given query envoy::config::core::v3::Metadata& dynamic_metadata = read_callbacks_->connection().streamInfo().dynamicMetadata(); - ProtobufWkt::Struct metadata( + Protobuf::Struct metadata( (*dynamic_metadata.mutable_filter_metadata())[NetworkFilterNames::get().MySQLProxy]); auto result = Common::SQLUtils::SQLUtils::setMetadata(command.getData(), diff --git a/contrib/postgres_proxy/filters/network/source/postgres_filter.cc b/contrib/postgres_proxy/filters/network/source/postgres_filter.cc index ebf6955d8619f..ed2ce7447f280 100644 --- a/contrib/postgres_proxy/filters/network/source/postgres_filter.cc +++ b/contrib/postgres_proxy/filters/network/source/postgres_filter.cc @@ -185,7 +185,7 @@ void PostgresFilter::incStatements(StatementType type) { void PostgresFilter::processQuery(const std::string& sql) { if (config_->enable_sql_parsing_) { - ProtobufWkt::Struct metadata; + Protobuf::Struct metadata; auto result = Common::SQLUtils::SQLUtils::setMetadata(sql, decoder_->getAttributes(), metadata); diff --git a/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc index f4be5d2973d8b..cb6f420e25165 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc @@ -728,8 +728,8 @@ class FakeBuffer : public Buffer::Instance { MOCK_METHOD(ssize_t, search, (const void*, uint64_t, size_t, size_t), (const, override)); MOCK_METHOD(bool, startsWith, (absl::string_view), (const, override)); MOCK_METHOD(std::string, toString, (), (const, override)); - MOCK_METHOD(void, setWatermarks, (uint32_t, uint32_t), (override)); - MOCK_METHOD(uint32_t, highWatermark, (), (const, override)); + MOCK_METHOD(void, setWatermarks, (uint64_t, uint32_t), (override)); + MOCK_METHOD(uint64_t, highWatermark, (), (const, override)); MOCK_METHOD(bool, highWatermarkTriggered, (), (const, override)); MOCK_METHOD(size_t, addFragments, (absl::Span)); }; diff --git a/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc index e08d81f65e0a5..f1fc73ab820ad 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc @@ -51,9 +51,9 @@ class PostgresFilterTest EXPECT_CALL(read_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); EXPECT_CALL(connection_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); ON_CALL(stream_info_, setDynamicMetadata(NetworkFilterNames::get().PostgresProxy, _)) - .WillByDefault(Invoke([this](const std::string&, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([this](const std::string&, const Protobuf::Struct& obj) { stream_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair( + Protobuf::MapPair( NetworkFilterNames::get().PostgresProxy, obj)); })); } diff --git a/contrib/rocketmq_proxy/filters/network/source/active_message.cc b/contrib/rocketmq_proxy/filters/network/source/active_message.cc index 4f3023b178abb..119c102ca77d8 100644 --- a/contrib/rocketmq_proxy/filters/network/source/active_message.cc +++ b/contrib/rocketmq_proxy/filters/network/source/active_message.cc @@ -206,7 +206,7 @@ void ActiveMessage::onQueryTopicRoute() { } ENVOY_LOG(trace, "Prepare TopicRouteData for {} OK", topic_name); TopicRouteData topic_route_data(std::move(queue_data_list), std::move(broker_data_list)); - ProtobufWkt::Struct data_struct; + Protobuf::Struct data_struct; topic_route_data.encode(data_struct); std::string json = MessageUtil::getJsonStringFromMessageOrError(data_struct); ENVOY_LOG(trace, "Serialize TopicRouteData for {} OK:\n{}", cluster_name, json); diff --git a/contrib/rocketmq_proxy/filters/network/source/codec.cc b/contrib/rocketmq_proxy/filters/network/source/codec.cc index cffed93ce6c54..0b78e117340db 100644 --- a/contrib/rocketmq_proxy/filters/network/source/codec.cc +++ b/contrib/rocketmq_proxy/filters/network/source/codec.cc @@ -59,7 +59,7 @@ RemotingCommandPtr Decoder::decode(Buffer::Instance& buffer, bool& underflow, bo int32_t code, version, opaque; uint32_t flag; if (isJsonHeader(mark)) { - ProtobufWkt::Struct header_struct; + Protobuf::Struct header_struct; // Parse header JSON text try { @@ -247,8 +247,7 @@ std::string Decoder::decodeMsgId(Buffer::Instance& buffer, int32_t cursor) { return msg_id; } -CommandCustomHeaderPtr Decoder::decodeExtHeader(RequestCode code, - ProtobufWkt::Struct& header_struct) { +CommandCustomHeaderPtr Decoder::decodeExtHeader(RequestCode code, Protobuf::Struct& header_struct) { const auto& filed_value_pair = header_struct.fields(); switch (code) { case RequestCode::SendMessage: { @@ -320,7 +319,7 @@ CommandCustomHeaderPtr Decoder::decodeExtHeader(RequestCode code, } CommandCustomHeaderPtr Decoder::decodeResponseExtHeader(ResponseCode response_code, - ProtobufWkt::Struct& header_struct, + Protobuf::Struct& header_struct, RequestCode request_code) { // No need to decode a failed response. if (response_code != ResponseCode::Success && @@ -352,41 +351,41 @@ CommandCustomHeaderPtr Decoder::decodeResponseExtHeader(ResponseCode response_co void Encoder::encode(const RemotingCommandPtr& command, Buffer::Instance& data) { - ProtobufWkt::Struct command_struct; + Protobuf::Struct command_struct; auto* fields = command_struct.mutable_fields(); - ProtobufWkt::Value code_v; + Protobuf::Value code_v; code_v.set_number_value(command->code_); (*fields)["code"] = code_v; - ProtobufWkt::Value language_v; + Protobuf::Value language_v; language_v.set_string_value(command->language()); (*fields)["language"] = language_v; - ProtobufWkt::Value version_v; + Protobuf::Value version_v; version_v.set_number_value(command->version_); (*fields)["version"] = version_v; - ProtobufWkt::Value opaque_v; + Protobuf::Value opaque_v; opaque_v.set_number_value(command->opaque_); (*fields)["opaque"] = opaque_v; - ProtobufWkt::Value flag_v; + Protobuf::Value flag_v; flag_v.set_number_value(command->flag_); (*fields)["flag"] = flag_v; if (!command->remark_.empty()) { - ProtobufWkt::Value remark_v; + Protobuf::Value remark_v; remark_v.set_string_value(command->remark_); (*fields)["remark"] = remark_v; } - ProtobufWkt::Value serialization_type_v; + Protobuf::Value serialization_type_v; serialization_type_v.set_string_value(command->serializeTypeCurrentRPC()); (*fields)["serializeTypeCurrentRPC"] = serialization_type_v; if (command->custom_header_) { - ProtobufWkt::Value ext_fields_v; + Protobuf::Value ext_fields_v; command->custom_header_->encode(ext_fields_v); (*fields)["extFields"] = ext_fields_v; } diff --git a/contrib/rocketmq_proxy/filters/network/source/codec.h b/contrib/rocketmq_proxy/filters/network/source/codec.h index 6ee9c8a9a97bb..e42398d0099e0 100644 --- a/contrib/rocketmq_proxy/filters/network/source/codec.h +++ b/contrib/rocketmq_proxy/filters/network/source/codec.h @@ -60,11 +60,10 @@ class Decoder : Logger::Loggable { static bool isJsonHeader(uint32_t len) { return (len >> 24u) == 0; } - static CommandCustomHeaderPtr decodeExtHeader(RequestCode code, - ProtobufWkt::Struct& header_struct); + static CommandCustomHeaderPtr decodeExtHeader(RequestCode code, Protobuf::Struct& header_struct); static CommandCustomHeaderPtr decodeResponseExtHeader(ResponseCode response_code, - ProtobufWkt::Struct& header_struct, + Protobuf::Struct& header_struct, RequestCode request_code); static bool isComplete(Buffer::Instance& buffer, int32_t cursor); diff --git a/contrib/rocketmq_proxy/filters/network/source/conn_manager.cc b/contrib/rocketmq_proxy/filters/network/source/conn_manager.cc index 90e05219c45cd..6bc53596324f9 100644 --- a/contrib/rocketmq_proxy/filters/network/source/conn_manager.cc +++ b/contrib/rocketmq_proxy/filters/network/source/conn_manager.cc @@ -168,7 +168,7 @@ void ConnectionManager::onHeartbeat(RemotingCommandPtr request) { purgeDirectiveTable(); - ProtobufWkt::Struct body_struct; + Protobuf::Struct body_struct; try { MessageUtil::loadFromJson(body, body_struct); } catch (std::exception& e) { @@ -294,7 +294,7 @@ void ConnectionManager::onGetConsumerListByGroup(RemotingCommandPtr request) { ENVOY_LOG(warn, "There is no consumer belongs to consumer_group: {}", requestExtHeader->consumerGroup()); } - ProtobufWkt::Struct body_struct; + Protobuf::Struct body_struct; getConsumerListByGroupResponseBody.encode(body_struct); diff --git a/contrib/rocketmq_proxy/filters/network/source/protocol.cc b/contrib/rocketmq_proxy/filters/network/source/protocol.cc index cd0481710ba13..6bc63066bde3b 100644 --- a/contrib/rocketmq_proxy/filters/network/source/protocol.cc +++ b/contrib/rocketmq_proxy/filters/network/source/protocol.cc @@ -10,133 +10,133 @@ namespace Extensions { namespace NetworkFilters { namespace RocketmqProxy { -void SendMessageRequestHeader::encode(ProtobufWkt::Value& root) { +void SendMessageRequestHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); switch (version_) { case SendMessageRequestVersion::V1: { - ProtobufWkt::Value producer_group_v; + Protobuf::Value producer_group_v; producer_group_v.set_string_value(producer_group_); members["producerGroup"] = producer_group_v; - ProtobufWkt::Value topic_v; + Protobuf::Value topic_v; topic_v.set_string_value(topic_.c_str(), topic_.length()); members["topic"] = topic_v; - ProtobufWkt::Value default_topic_v; + Protobuf::Value default_topic_v; default_topic_v.set_string_value(default_topic_); members["defaultTopic"] = default_topic_v; - ProtobufWkt::Value default_topic_queue_number_v; + Protobuf::Value default_topic_queue_number_v; default_topic_queue_number_v.set_number_value(default_topic_queue_number_); members["defaultTopicQueueNums"] = default_topic_queue_number_v; - ProtobufWkt::Value queue_id_v; + Protobuf::Value queue_id_v; queue_id_v.set_number_value(queue_id_); members["queueId"] = queue_id_v; - ProtobufWkt::Value sys_flag_v; + Protobuf::Value sys_flag_v; sys_flag_v.set_number_value(sys_flag_); members["sysFlag"] = sys_flag_v; - ProtobufWkt::Value born_timestamp_v; + Protobuf::Value born_timestamp_v; born_timestamp_v.set_number_value(born_timestamp_); members["bornTimestamp"] = born_timestamp_v; - ProtobufWkt::Value flag_v; + Protobuf::Value flag_v; flag_v.set_number_value(flag_); members["flag"] = flag_v; if (!properties_.empty()) { - ProtobufWkt::Value properties_v; + Protobuf::Value properties_v; properties_v.set_string_value(properties_.c_str(), properties_.length()); members["properties"] = properties_v; } if (reconsume_time_ > 0) { - ProtobufWkt::Value reconsume_times_v; + Protobuf::Value reconsume_times_v; reconsume_times_v.set_number_value(reconsume_time_); members["reconsumeTimes"] = reconsume_times_v; } if (unit_mode_) { - ProtobufWkt::Value unit_mode_v; + Protobuf::Value unit_mode_v; unit_mode_v.set_bool_value(unit_mode_); members["unitMode"] = unit_mode_v; } if (batch_) { - ProtobufWkt::Value batch_v; + Protobuf::Value batch_v; batch_v.set_bool_value(batch_); members["batch"] = batch_v; } if (max_reconsume_time_ > 0) { - ProtobufWkt::Value max_reconsume_time_v; + Protobuf::Value max_reconsume_time_v; max_reconsume_time_v.set_number_value(max_reconsume_time_); members["maxReconsumeTimes"] = max_reconsume_time_v; } break; } case SendMessageRequestVersion::V2: { - ProtobufWkt::Value producer_group_v; + Protobuf::Value producer_group_v; producer_group_v.set_string_value(producer_group_.c_str(), producer_group_.length()); members["a"] = producer_group_v; - ProtobufWkt::Value topic_v; + Protobuf::Value topic_v; topic_v.set_string_value(topic_.c_str(), topic_.length()); members["b"] = topic_v; - ProtobufWkt::Value default_topic_v; + Protobuf::Value default_topic_v; default_topic_v.set_string_value(default_topic_.c_str(), default_topic_.length()); members["c"] = default_topic_v; - ProtobufWkt::Value default_topic_queue_number_v; + Protobuf::Value default_topic_queue_number_v; default_topic_queue_number_v.set_number_value(default_topic_queue_number_); members["d"] = default_topic_queue_number_v; - ProtobufWkt::Value queue_id_v; + Protobuf::Value queue_id_v; queue_id_v.set_number_value(queue_id_); members["e"] = queue_id_v; - ProtobufWkt::Value sys_flag_v; + Protobuf::Value sys_flag_v; sys_flag_v.set_number_value(sys_flag_); members["f"] = sys_flag_v; - ProtobufWkt::Value born_timestamp_v; + Protobuf::Value born_timestamp_v; born_timestamp_v.set_number_value(born_timestamp_); members["g"] = born_timestamp_v; - ProtobufWkt::Value flag_v; + Protobuf::Value flag_v; flag_v.set_number_value(flag_); members["h"] = flag_v; if (!properties_.empty()) { - ProtobufWkt::Value properties_v; + Protobuf::Value properties_v; properties_v.set_string_value(properties_.c_str(), properties_.length()); members["i"] = properties_v; } if (reconsume_time_ > 0) { - ProtobufWkt::Value reconsume_times_v; + Protobuf::Value reconsume_times_v; reconsume_times_v.set_number_value(reconsume_time_); members["j"] = reconsume_times_v; } if (unit_mode_) { - ProtobufWkt::Value unit_mode_v; + Protobuf::Value unit_mode_v; unit_mode_v.set_bool_value(unit_mode_); members["k"] = unit_mode_v; } if (batch_) { - ProtobufWkt::Value batch_v; + Protobuf::Value batch_v; batch_v.set_bool_value(batch_); members["m"] = batch_v; } if (max_reconsume_time_ > 0) { - ProtobufWkt::Value max_reconsume_time_v; + Protobuf::Value max_reconsume_time_v; max_reconsume_time_v.set_number_value(max_reconsume_time_); members["l"] = max_reconsume_time_v; } @@ -147,7 +147,7 @@ void SendMessageRequestHeader::encode(ProtobufWkt::Value& root) { } } -void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { +void SendMessageRequestHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); switch (version_) { case SendMessageRequestVersion::V1: { @@ -164,31 +164,31 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { topic_ = members.at("topic").string_value(); default_topic_ = members.at("defaultTopic").string_value(); - if (members.at("defaultTopicQueueNums").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("defaultTopicQueueNums").kind_case() == Protobuf::Value::kNumberValue) { default_topic_queue_number_ = members.at("defaultTopicQueueNums").number_value(); } else { default_topic_queue_number_ = std::stoi(members.at("defaultTopicQueueNums").string_value()); } - if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("queueId").kind_case() == Protobuf::Value::kNumberValue) { queue_id_ = members.at("queueId").number_value(); } else { queue_id_ = std::stoi(members.at("queueId").string_value()); } - if (members.at("sysFlag").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("sysFlag").kind_case() == Protobuf::Value::kNumberValue) { sys_flag_ = static_cast(members.at("sysFlag").number_value()); } else { sys_flag_ = std::stoi(members.at("sysFlag").string_value()); } - if (members.at("bornTimestamp").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("bornTimestamp").kind_case() == Protobuf::Value::kNumberValue) { born_timestamp_ = static_cast(members.at("bornTimestamp").number_value()); } else { born_timestamp_ = std::stoll(members.at("bornTimestamp").string_value()); } - if (members.at("flag").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("flag").kind_case() == Protobuf::Value::kNumberValue) { flag_ = static_cast(members.at("flag").number_value()); } else { flag_ = std::stoi(members.at("flag").string_value()); @@ -199,7 +199,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("reconsumeTimes")) { - if (members.at("reconsumeTimes").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("reconsumeTimes").kind_case() == Protobuf::Value::kNumberValue) { reconsume_time_ = members.at("reconsumeTimes").number_value(); } else { reconsume_time_ = std::stoi(members.at("reconsumeTimes").string_value()); @@ -207,7 +207,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("unitMode")) { - if (members.at("unitMode").kind_case() == ProtobufWkt::Value::kBoolValue) { + if (members.at("unitMode").kind_case() == Protobuf::Value::kBoolValue) { unit_mode_ = members.at("unitMode").bool_value(); } else { unit_mode_ = (members.at("unitMode").string_value() == std::string("true")); @@ -215,7 +215,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("batch")) { - if (members.at("batch").kind_case() == ProtobufWkt::Value::kBoolValue) { + if (members.at("batch").kind_case() == Protobuf::Value::kBoolValue) { batch_ = members.at("batch").bool_value(); } else { batch_ = (members.at("batch").string_value() == std::string("true")); @@ -223,7 +223,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("maxReconsumeTimes")) { - if (members.at("maxReconsumeTimes").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("maxReconsumeTimes").kind_case() == Protobuf::Value::kNumberValue) { max_reconsume_time_ = static_cast(members.at("maxReconsumeTimes").number_value()); } else { max_reconsume_time_ = std::stoi(members.at("maxReconsumeTimes").string_value()); @@ -246,31 +246,31 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { topic_ = members.at("b").string_value(); default_topic_ = members.at("c").string_value(); - if (members.at("d").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("d").kind_case() == Protobuf::Value::kNumberValue) { default_topic_queue_number_ = members.at("d").number_value(); } else { default_topic_queue_number_ = std::stoi(members.at("d").string_value()); } - if (members.at("e").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("e").kind_case() == Protobuf::Value::kNumberValue) { queue_id_ = members.at("e").number_value(); } else { queue_id_ = std::stoi(members.at("e").string_value()); } - if (members.at("f").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("f").kind_case() == Protobuf::Value::kNumberValue) { sys_flag_ = static_cast(members.at("f").number_value()); } else { sys_flag_ = std::stoi(members.at("f").string_value()); } - if (members.at("g").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("g").kind_case() == Protobuf::Value::kNumberValue) { born_timestamp_ = static_cast(members.at("g").number_value()); } else { born_timestamp_ = std::stoll(members.at("g").string_value()); } - if (members.at("h").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("h").kind_case() == Protobuf::Value::kNumberValue) { flag_ = static_cast(members.at("h").number_value()); } else { flag_ = std::stoi(members.at("h").string_value()); @@ -281,7 +281,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("j")) { - if (members.at("j").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("j").kind_case() == Protobuf::Value::kNumberValue) { reconsume_time_ = members.at("j").number_value(); } else { reconsume_time_ = std::stoi(members.at("j").string_value()); @@ -289,7 +289,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("k")) { - if (members.at("k").kind_case() == ProtobufWkt::Value::kBoolValue) { + if (members.at("k").kind_case() == Protobuf::Value::kBoolValue) { unit_mode_ = members.at("k").bool_value(); } else { unit_mode_ = (members.at("k").string_value() == std::string("true")); @@ -297,7 +297,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("m")) { - if (members.at("m").kind_case() == ProtobufWkt::Value::kBoolValue) { + if (members.at("m").kind_case() == Protobuf::Value::kBoolValue) { batch_ = members.at("m").bool_value(); } else { batch_ = (members.at("m").string_value() == std::string("true")); @@ -305,7 +305,7 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } if (members.contains("l")) { - if (members.at("l").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("l").kind_case() == Protobuf::Value::kNumberValue) { max_reconsume_time_ = members.at("l").number_value(); } else { max_reconsume_time_ = std::stoi(members.at("l").string_value()); @@ -319,32 +319,32 @@ void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } } -void SendMessageResponseHeader::encode(ProtobufWkt::Value& root) { +void SendMessageResponseHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); ASSERT(!msg_id_.empty()); - ProtobufWkt::Value msg_id_v; + Protobuf::Value msg_id_v; msg_id_v.set_string_value(msg_id_.c_str(), msg_id_.length()); members["msgId"] = msg_id_v; ASSERT(queue_id_ >= 0); - ProtobufWkt::Value queue_id_v; + Protobuf::Value queue_id_v; queue_id_v.set_number_value(queue_id_); members["queueId"] = queue_id_v; ASSERT(queue_offset_ >= 0); - ProtobufWkt::Value queue_offset_v; + Protobuf::Value queue_offset_v; queue_offset_v.set_number_value(queue_offset_); members["queueOffset"] = queue_offset_v; if (!transaction_id_.empty()) { - ProtobufWkt::Value transaction_id_v; + Protobuf::Value transaction_id_v; transaction_id_v.set_string_value(transaction_id_.c_str(), transaction_id_.length()); members["transactionId"] = transaction_id_v; } } -void SendMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { +void SendMessageResponseHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("msgId")); ASSERT(members.contains("queueId")); @@ -352,13 +352,13 @@ void SendMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { msg_id_ = members.at("msgId").string_value(); - if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("queueId").kind_case() == Protobuf::Value::kNumberValue) { queue_id_ = members.at("queueId").number_value(); } else { queue_id_ = std::stoi(members.at("queueId").string_value()); } - if (members.at("queueOffset").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("queueOffset").kind_case() == Protobuf::Value::kNumberValue) { queue_offset_ = members.at("queueOffset").number_value(); } else { queue_offset_ = std::stoll(members.at("queueOffset").string_value()); @@ -369,71 +369,71 @@ void SendMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { } } -void GetRouteInfoRequestHeader::encode(ProtobufWkt::Value& root) { +void GetRouteInfoRequestHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); - ProtobufWkt::Value topic_v; + Protobuf::Value topic_v; topic_v.set_string_value(topic_.c_str(), topic_.length()); members["topic"] = topic_v; } -void GetRouteInfoRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { +void GetRouteInfoRequestHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("topic")); topic_ = members.at("topic").string_value(); } -void PopMessageRequestHeader::encode(ProtobufWkt::Value& root) { +void PopMessageRequestHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); ASSERT(!consumer_group_.empty()); - ProtobufWkt::Value consumer_group_v; + Protobuf::Value consumer_group_v; consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); members["consumerGroup"] = consumer_group_v; ASSERT(!topic_.empty()); - ProtobufWkt::Value topicNode; + Protobuf::Value topicNode; topicNode.set_string_value(topic_.c_str(), topic_.length()); members["topic"] = topicNode; - ProtobufWkt::Value queue_id_v; + Protobuf::Value queue_id_v; queue_id_v.set_number_value(queue_id_); members["queueId"] = queue_id_v; - ProtobufWkt::Value max_msg_nums_v; + Protobuf::Value max_msg_nums_v; max_msg_nums_v.set_number_value(max_msg_nums_); members["maxMsgNums"] = max_msg_nums_v; - ProtobufWkt::Value invisible_time_v; + Protobuf::Value invisible_time_v; invisible_time_v.set_number_value(invisible_time_); members["invisibleTime"] = invisible_time_v; - ProtobufWkt::Value poll_time_v; + Protobuf::Value poll_time_v; poll_time_v.set_number_value(poll_time_); members["pollTime"] = poll_time_v; - ProtobufWkt::Value born_time_v; + Protobuf::Value born_time_v; born_time_v.set_number_value(born_time_); members["bornTime"] = born_time_v; - ProtobufWkt::Value init_mode_v; + Protobuf::Value init_mode_v; init_mode_v.set_number_value(init_mode_); members["initMode"] = init_mode_v; if (!exp_type_.empty()) { - ProtobufWkt::Value exp_type_v; + Protobuf::Value exp_type_v; exp_type_v.set_string_value(exp_type_.c_str(), exp_type_.size()); members["expType"] = exp_type_v; } if (!exp_.empty()) { - ProtobufWkt::Value exp_v; + Protobuf::Value exp_v; exp_v.set_string_value(exp_.c_str(), exp_.size()); members["exp"] = exp_v; } } -void PopMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { +void PopMessageRequestHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("consumerGroup")); ASSERT(members.contains("topic")); @@ -447,37 +447,37 @@ void PopMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { consumer_group_ = members.at("consumerGroup").string_value(); topic_ = members.at("topic").string_value(); - if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("queueId").kind_case() == Protobuf::Value::kNumberValue) { queue_id_ = members.at("queueId").number_value(); } else { queue_id_ = std::stoi(members.at("queueId").string_value()); } - if (members.at("maxMsgNums").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("maxMsgNums").kind_case() == Protobuf::Value::kNumberValue) { max_msg_nums_ = members.at("maxMsgNums").number_value(); } else { max_msg_nums_ = std::stoi(members.at("maxMsgNums").string_value()); } - if (members.at("invisibleTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("invisibleTime").kind_case() == Protobuf::Value::kNumberValue) { invisible_time_ = members.at("invisibleTime").number_value(); } else { invisible_time_ = std::stoll(members.at("invisibleTime").string_value()); } - if (members.at("pollTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("pollTime").kind_case() == Protobuf::Value::kNumberValue) { poll_time_ = members.at("pollTime").number_value(); } else { poll_time_ = std::stoll(members.at("pollTime").string_value()); } - if (members.at("bornTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("bornTime").kind_case() == Protobuf::Value::kNumberValue) { born_time_ = members.at("bornTime").number_value(); } else { born_time_ = std::stoll(members.at("bornTime").string_value()); } - if (members.at("initMode").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("initMode").kind_case() == Protobuf::Value::kNumberValue) { init_mode_ = members.at("initMode").number_value(); } else { init_mode_ = std::stol(members.at("initMode").string_value()); @@ -492,70 +492,70 @@ void PopMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { } } -void PopMessageResponseHeader::encode(ProtobufWkt::Value& root) { +void PopMessageResponseHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); - ProtobufWkt::Value pop_time_v; + Protobuf::Value pop_time_v; pop_time_v.set_number_value(pop_time_); members["popTime"] = pop_time_v; - ProtobufWkt::Value invisible_time_v; + Protobuf::Value invisible_time_v; invisible_time_v.set_number_value(invisible_time_); members["invisibleTime"] = invisible_time_v; - ProtobufWkt::Value revive_qid_v; + Protobuf::Value revive_qid_v; revive_qid_v.set_number_value(revive_qid_); members["reviveQid"] = revive_qid_v; - ProtobufWkt::Value rest_num_v; + Protobuf::Value rest_num_v; rest_num_v.set_number_value(rest_num_); members["restNum"] = rest_num_v; if (!start_offset_info_.empty()) { - ProtobufWkt::Value start_offset_info_v; + Protobuf::Value start_offset_info_v; start_offset_info_v.set_string_value(start_offset_info_.c_str(), start_offset_info_.size()); members["startOffsetInfo"] = start_offset_info_v; } if (!msg_off_set_info_.empty()) { - ProtobufWkt::Value msg_offset_info_v; + Protobuf::Value msg_offset_info_v; msg_offset_info_v.set_string_value(msg_off_set_info_.c_str(), msg_off_set_info_.size()); members["msgOffsetInfo"] = msg_offset_info_v; } if (!order_count_info_.empty()) { - ProtobufWkt::Value order_count_info_v; + Protobuf::Value order_count_info_v; order_count_info_v.set_string_value(order_count_info_.c_str(), order_count_info_.size()); members["orderCountInfo"] = order_count_info_v; } } -void PopMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { +void PopMessageResponseHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("popTime")); ASSERT(members.contains("invisibleTime")); ASSERT(members.contains("reviveQid")); ASSERT(members.contains("restNum")); - if (members.at("popTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("popTime").kind_case() == Protobuf::Value::kNumberValue) { pop_time_ = members.at("popTime").number_value(); } else { pop_time_ = std::stoull(members.at("popTime").string_value()); } - if (members.at("invisibleTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("invisibleTime").kind_case() == Protobuf::Value::kNumberValue) { invisible_time_ = members.at("invisibleTime").number_value(); } else { invisible_time_ = std::stoull(members.at("invisibleTime").string_value()); } - if (members.at("reviveQid").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("reviveQid").kind_case() == Protobuf::Value::kNumberValue) { revive_qid_ = members.at("reviveQid").number_value(); } else { revive_qid_ = std::stoul(members.at("reviveQid").string_value()); } - if (members.at("restNum").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("restNum").kind_case() == Protobuf::Value::kNumberValue) { rest_num_ = members.at("restNum").number_value(); } else { rest_num_ = std::stoull(members.at("restNum").string_value()); @@ -574,36 +574,36 @@ void PopMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { } } -void AckMessageRequestHeader::encode(ProtobufWkt::Value& root) { +void AckMessageRequestHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); ASSERT(!consumer_group_.empty()); - ProtobufWkt::Value consumer_group_v; + Protobuf::Value consumer_group_v; consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); members["consumerGroup"] = consumer_group_v; ASSERT(!topic_.empty()); - ProtobufWkt::Value topic_v; + Protobuf::Value topic_v; topic_v.set_string_value(topic_.c_str(), topic_.size()); members["topic"] = topic_v; ASSERT(queue_id_ >= 0); - ProtobufWkt::Value queue_id_v; + Protobuf::Value queue_id_v; queue_id_v.set_number_value(queue_id_); members["queueId"] = queue_id_v; ASSERT(!extra_info_.empty()); - ProtobufWkt::Value extra_info_v; + Protobuf::Value extra_info_v; extra_info_v.set_string_value(extra_info_.c_str(), extra_info_.size()); members["extraInfo"] = extra_info_v; ASSERT(offset_ >= 0); - ProtobufWkt::Value offset_v; + Protobuf::Value offset_v; offset_v.set_number_value(offset_); members["offset"] = offset_v; } -void AckMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { +void AckMessageRequestHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("consumerGroup")); ASSERT(members.contains("topic")); @@ -615,7 +615,7 @@ void AckMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { topic_ = members.at("topic").string_value(); - if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("queueId").kind_case() == Protobuf::Value::kNumberValue) { queue_id_ = members.at("queueId").number_value(); } else { queue_id_ = std::stoi(members.at("queueId").string_value()); @@ -623,36 +623,36 @@ void AckMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { extra_info_ = members.at("extraInfo").string_value(); - if (members.at("offset").kind_case() == ProtobufWkt::Value::kNumberValue) { + if (members.at("offset").kind_case() == Protobuf::Value::kNumberValue) { offset_ = members.at("offset").number_value(); } else { offset_ = std::stoll(members.at("offset").string_value()); } } -void UnregisterClientRequestHeader::encode(ProtobufWkt::Value& root) { +void UnregisterClientRequestHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); ASSERT(!client_id_.empty()); - ProtobufWkt::Value client_id_v; + Protobuf::Value client_id_v; client_id_v.set_string_value(client_id_.c_str(), client_id_.size()); members["clientID"] = client_id_v; ASSERT(!producer_group_.empty() || !consumer_group_.empty()); if (!producer_group_.empty()) { - ProtobufWkt::Value producer_group_v; + Protobuf::Value producer_group_v; producer_group_v.set_string_value(producer_group_.c_str(), producer_group_.size()); members["producerGroup"] = producer_group_v; } if (!consumer_group_.empty()) { - ProtobufWkt::Value consumer_group_v; + Protobuf::Value consumer_group_v; consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); members["consumerGroup"] = consumer_group_v; } } -void UnregisterClientRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { +void UnregisterClientRequestHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("clientID")); ASSERT(members.contains("producerGroup") || members.contains("consumerGroup")); @@ -668,20 +668,20 @@ void UnregisterClientRequestHeader::decode(const ProtobufWkt::Value& ext_fields) } } -void GetConsumerListByGroupResponseBody::encode(ProtobufWkt::Struct& root) { +void GetConsumerListByGroupResponseBody::encode(Protobuf::Struct& root) { auto& members = *(root.mutable_fields()); - ProtobufWkt::Value consumer_id_list_v; + Protobuf::Value consumer_id_list_v; auto member_list = consumer_id_list_v.mutable_list_value(); for (const auto& consumerId : consumer_id_list_) { - auto consumer_id_v = new ProtobufWkt::Value; + auto consumer_id_v = new Protobuf::Value; consumer_id_v->set_string_value(consumerId.c_str(), consumerId.size()); member_list->mutable_values()->AddAllocated(consumer_id_v); } members["consumerIdList"] = consumer_id_list_v; } -bool HeartbeatData::decode(ProtobufWkt::Struct& doc) { +bool HeartbeatData::decode(Protobuf::Struct& doc) { const auto& members = doc.fields(); if (!members.contains("clientID")) { return false; @@ -700,23 +700,23 @@ bool HeartbeatData::decode(ProtobufWkt::Struct& doc) { return true; } -void HeartbeatData::encode(ProtobufWkt::Struct& root) { +void HeartbeatData::encode(Protobuf::Struct& root) { auto& members = *(root.mutable_fields()); - ProtobufWkt::Value client_id_v; + Protobuf::Value client_id_v; client_id_v.set_string_value(client_id_.c_str(), client_id_.size()); members["clientID"] = client_id_v; } -void GetConsumerListByGroupRequestHeader::encode(ProtobufWkt::Value& root) { +void GetConsumerListByGroupRequestHeader::encode(Protobuf::Value& root) { auto& members = *(root.mutable_struct_value()->mutable_fields()); - ProtobufWkt::Value consumer_group_v; + Protobuf::Value consumer_group_v; consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); members["consumerGroup"] = consumer_group_v; } -void GetConsumerListByGroupRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { +void GetConsumerListByGroupRequestHeader::decode(const Protobuf::Value& ext_fields) { const auto& members = ext_fields.struct_value().fields(); ASSERT(members.contains("consumerGroup")); diff --git a/contrib/rocketmq_proxy/filters/network/source/protocol.h b/contrib/rocketmq_proxy/filters/network/source/protocol.h index 2de55a04130fa..52a7cf06d5a59 100644 --- a/contrib/rocketmq_proxy/filters/network/source/protocol.h +++ b/contrib/rocketmq_proxy/filters/network/source/protocol.h @@ -48,9 +48,9 @@ class CommandCustomHeader { virtual ~CommandCustomHeader() = default; - virtual void encode(ProtobufWkt::Value& root) PURE; + virtual void encode(Protobuf::Value& root) PURE; - virtual void decode(const ProtobufWkt::Value& ext_fields) PURE; + virtual void decode(const Protobuf::Value& ext_fields) PURE; const std::string& targetBrokerName() const { return target_broker_name_; } @@ -264,9 +264,9 @@ class SendMessageRequestHeader : public RoutingCommandCustomHeader, void producerGroup(std::string producer_group) { producer_group_ = std::move(producer_group); } - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; const std::string& producerGroup() const { return producer_group_; } @@ -336,9 +336,9 @@ class SendMessageResponseHeader : public CommandCustomHeader { : msg_id_(std::move(msg_id)), queue_id_(queue_id), queue_offset_(queue_offset), transaction_id_(std::move(transaction_id)) {} - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; const std::string& msgId() const { return msg_id_; } @@ -376,9 +376,9 @@ class SendMessageResponseHeader : public CommandCustomHeader { */ class GetRouteInfoRequestHeader : public RoutingCommandCustomHeader { public: - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; }; /** @@ -398,9 +398,9 @@ class PopMessageRequestHeader : public RoutingCommandCustomHeader { public: friend class Decoder; - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; const std::string& consumerGroup() const { return consumer_group_; } @@ -460,9 +460,9 @@ class PopMessageRequestHeader : public RoutingCommandCustomHeader { */ class PopMessageResponseHeader : public CommandCustomHeader { public: - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; // This function is for testing only. int64_t popTimeForTest() const { return pop_time_; } @@ -517,9 +517,9 @@ class PopMessageResponseHeader : public CommandCustomHeader { */ class AckMessageRequestHeader : public RoutingCommandCustomHeader { public: - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; absl::string_view consumerGroup() const { return consumer_group_; } @@ -559,9 +559,9 @@ class AckMessageRequestHeader : public RoutingCommandCustomHeader { */ class UnregisterClientRequestHeader : public CommandCustomHeader { public: - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; void clientId(absl::string_view client_id) { client_id_ = std::string(client_id.data(), client_id.length()); @@ -592,9 +592,9 @@ class UnregisterClientRequestHeader : public CommandCustomHeader { */ class GetConsumerListByGroupRequestHeader : public CommandCustomHeader { public: - void encode(ProtobufWkt::Value& root) override; + void encode(Protobuf::Value& root) override; - void decode(const ProtobufWkt::Value& ext_fields) override; + void decode(const Protobuf::Value& ext_fields) override; void consumerGroup(absl::string_view consumer_group) { consumer_group_ = std::string(consumer_group.data(), consumer_group.length()); @@ -611,7 +611,7 @@ class GetConsumerListByGroupRequestHeader : public CommandCustomHeader { */ class GetConsumerListByGroupResponseBody { public: - void encode(ProtobufWkt::Struct& root); + void encode(Protobuf::Struct& root); void add(absl::string_view consumer_id) { consumer_id_list_.emplace_back(consumer_id.data(), consumer_id.length()); @@ -626,13 +626,13 @@ class GetConsumerListByGroupResponseBody { */ class HeartbeatData : public Logger::Loggable { public: - bool decode(ProtobufWkt::Struct& doc); + bool decode(Protobuf::Struct& doc); const std::string& clientId() const { return client_id_; } const std::vector& consumerGroups() const { return consumer_groups_; } - void encode(ProtobufWkt::Struct& root); + void encode(Protobuf::Struct& root); void clientId(absl::string_view client_id) { client_id_ = std::string(client_id.data(), client_id.size()); diff --git a/contrib/rocketmq_proxy/filters/network/source/topic_route.cc b/contrib/rocketmq_proxy/filters/network/source/topic_route.cc index 7336ec97d17cc..756cccdc243a9 100644 --- a/contrib/rocketmq_proxy/filters/network/source/topic_route.cc +++ b/contrib/rocketmq_proxy/filters/network/source/topic_route.cc @@ -5,42 +5,42 @@ namespace Extensions { namespace NetworkFilters { namespace RocketmqProxy { -void QueueData::encode(ProtobufWkt::Struct& data_struct) { +void QueueData::encode(Protobuf::Struct& data_struct) { auto* fields = data_struct.mutable_fields(); - ProtobufWkt::Value broker_name_v; + Protobuf::Value broker_name_v; broker_name_v.set_string_value(broker_name_); (*fields)["brokerName"] = broker_name_v; - ProtobufWkt::Value read_queue_num_v; + Protobuf::Value read_queue_num_v; read_queue_num_v.set_number_value(read_queue_nums_); (*fields)["readQueueNums"] = read_queue_num_v; - ProtobufWkt::Value write_queue_num_v; + Protobuf::Value write_queue_num_v; write_queue_num_v.set_number_value(write_queue_nums_); (*fields)["writeQueueNums"] = write_queue_num_v; - ProtobufWkt::Value perm_v; + Protobuf::Value perm_v; perm_v.set_number_value(perm_); (*fields)["perm"] = perm_v; } -void BrokerData::encode(ProtobufWkt::Struct& data_struct) { +void BrokerData::encode(Protobuf::Struct& data_struct) { auto& members = *(data_struct.mutable_fields()); - ProtobufWkt::Value cluster_v; + Protobuf::Value cluster_v; cluster_v.set_string_value(cluster_); members["cluster"] = cluster_v; - ProtobufWkt::Value broker_name_v; + Protobuf::Value broker_name_v; broker_name_v.set_string_value(broker_name_); members["brokerName"] = broker_name_v; if (!broker_addrs_.empty()) { - ProtobufWkt::Value brokerAddrsNode; + Protobuf::Value brokerAddrsNode; auto& brokerAddrsMembers = *(brokerAddrsNode.mutable_struct_value()->mutable_fields()); for (auto& entry : broker_addrs_) { - ProtobufWkt::Value address_v; + Protobuf::Value address_v; address_v.set_string_value(entry.second); brokerAddrsMembers[std::to_string(entry.first)] = address_v; } @@ -48,11 +48,11 @@ void BrokerData::encode(ProtobufWkt::Struct& data_struct) { } } -void TopicRouteData::encode(ProtobufWkt::Struct& data_struct) { +void TopicRouteData::encode(Protobuf::Struct& data_struct) { auto* fields = data_struct.mutable_fields(); if (!queue_data_.empty()) { - ProtobufWkt::ListValue queue_data_list_v; + Protobuf::ListValue queue_data_list_v; for (auto& queueData : queue_data_) { queueData.encode(data_struct); queue_data_list_v.add_values()->mutable_struct_value()->CopyFrom(data_struct); @@ -61,7 +61,7 @@ void TopicRouteData::encode(ProtobufWkt::Struct& data_struct) { } if (!broker_data_.empty()) { - ProtobufWkt::ListValue broker_data_list_v; + Protobuf::ListValue broker_data_list_v; for (auto& brokerData : broker_data_) { brokerData.encode(data_struct); broker_data_list_v.add_values()->mutable_struct_value()->CopyFrom(data_struct); diff --git a/contrib/rocketmq_proxy/filters/network/source/topic_route.h b/contrib/rocketmq_proxy/filters/network/source/topic_route.h index 6d5622e24366b..b7f4968ddc1af 100644 --- a/contrib/rocketmq_proxy/filters/network/source/topic_route.h +++ b/contrib/rocketmq_proxy/filters/network/source/topic_route.h @@ -18,7 +18,7 @@ class QueueData { : broker_name_(broker_name), read_queue_nums_(read_queue_num), write_queue_nums_(write_queue_num), perm_(perm) {} - void encode(ProtobufWkt::Struct& data_struct); + void encode(Protobuf::Struct& data_struct); const std::string& brokerName() const { return broker_name_; } @@ -41,7 +41,7 @@ class BrokerData { absl::node_hash_map&& broker_addrs) : cluster_(cluster), broker_name_(broker_name), broker_addrs_(broker_addrs) {} - void encode(ProtobufWkt::Struct& data_struct); + void encode(Protobuf::Struct& data_struct); const std::string& cluster() const { return cluster_; } @@ -57,7 +57,7 @@ class BrokerData { class TopicRouteData { public: - void encode(ProtobufWkt::Struct& data_struct); + void encode(Protobuf::Struct& data_struct); TopicRouteData() = default; diff --git a/contrib/rocketmq_proxy/filters/network/test/active_message_test.cc b/contrib/rocketmq_proxy/filters/network/test/active_message_test.cc index 6ed1e33541b01..a75dd942cc240 100644 --- a/contrib/rocketmq_proxy/filters/network/test/active_message_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/active_message_test.cc @@ -172,7 +172,7 @@ TEST_F(ActiveMessageTest, RecordPopRouteInfo) { auto host_description = new NiceMock(); auto metadata = std::make_shared(); - ProtobufWkt::Struct topic_route_data; + Protobuf::Struct topic_route_data; auto* fields = topic_route_data.mutable_fields(); std::string broker_name = "broker-a"; @@ -184,7 +184,7 @@ TEST_F(ActiveMessageTest, RecordPopRouteInfo) { (*fields)[RocketmqConstants::get().BrokerName] = ValueUtil::stringValue(broker_name); (*fields)[RocketmqConstants::get().BrokerId] = ValueUtil::numberValue(broker_id); (*fields)[RocketmqConstants::get().Perm] = ValueUtil::numberValue(6); - metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( NetworkFilterNames::get().RocketmqProxy, topic_route_data)); EXPECT_CALL(*host_description, metadata()).WillRepeatedly(Return(metadata)); diff --git a/contrib/rocketmq_proxy/filters/network/test/codec_test.cc b/contrib/rocketmq_proxy/filters/network/test/codec_test.cc index 9a8d471078a7c..f84b0cdafb6af 100644 --- a/contrib/rocketmq_proxy/filters/network/test/codec_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/codec_test.cc @@ -519,7 +519,7 @@ TEST_F(RocketmqCodecTest, EncodeResponseSendMessageSuccess) { Decoder::FRAME_LENGTH_FIELD_SIZE + Decoder::FRAME_HEADER_LENGTH_FIELD_SIZE; response_buffer.copyOut(frame_header_content_offset, header_length, header_data.get()); std::string header_json(header_data.get(), header_length); - ProtobufWkt::Struct doc; + Protobuf::Struct doc; MessageUtil::loadFromJson(header_json, doc); const auto& members = doc.fields(); diff --git a/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc b/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc index 967365507026b..2cbd77621f6f9 100644 --- a/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc @@ -366,7 +366,7 @@ stat_prefix: test initializeFilter(yaml); auto metadata = std::make_shared(); - ProtobufWkt::Struct topic_route_data; + Protobuf::Struct topic_route_data; auto* fields = topic_route_data.mutable_fields(); (*fields)[RocketmqConstants::get().ReadQueueNum] = ValueUtil::numberValue(4); (*fields)[RocketmqConstants::get().WriteQueueNum] = ValueUtil::numberValue(4); @@ -374,7 +374,7 @@ stat_prefix: test (*fields)[RocketmqConstants::get().BrokerName] = ValueUtil::stringValue("broker-a"); (*fields)[RocketmqConstants::get().BrokerId] = ValueUtil::numberValue(0); (*fields)[RocketmqConstants::get().Perm] = ValueUtil::numberValue(6); - metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( NetworkFilterNames::get().RocketmqProxy, topic_route_data)); host_->metadata(metadata); initializeCluster(); @@ -465,7 +465,7 @@ develop_mode: true initializeFilter(yaml); auto metadata = std::make_shared(); - ProtobufWkt::Struct topic_route_data; + Protobuf::Struct topic_route_data; auto* fields = topic_route_data.mutable_fields(); (*fields)[RocketmqConstants::get().ReadQueueNum] = ValueUtil::numberValue(4); (*fields)[RocketmqConstants::get().WriteQueueNum] = ValueUtil::numberValue(4); @@ -473,7 +473,7 @@ develop_mode: true (*fields)[RocketmqConstants::get().BrokerName] = ValueUtil::stringValue("broker-a"); (*fields)[RocketmqConstants::get().BrokerId] = ValueUtil::numberValue(0); (*fields)[RocketmqConstants::get().Perm] = ValueUtil::numberValue(6); - metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( NetworkFilterNames::get().RocketmqProxy, topic_route_data)); host_->metadata(metadata); initializeCluster(); diff --git a/contrib/rocketmq_proxy/filters/network/test/protocol_test.cc b/contrib/rocketmq_proxy/filters/network/test/protocol_test.cc index 495eb74671463..3c1de1fe298a0 100644 --- a/contrib/rocketmq_proxy/filters/network/test/protocol_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/protocol_test.cc @@ -21,7 +21,7 @@ TEST_F(UnregisterClientRequestHeaderTest, Encode) { request_header.producerGroup(producer_group_); request_header.consumerGroup(consumer_group_); - ProtobufWkt::Value doc; + Protobuf::Value doc; request_header.encode(doc); const auto& members = doc.struct_value().fields(); @@ -40,7 +40,7 @@ TEST_F(UnregisterClientRequestHeaderTest, Decode) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); UnregisterClientRequestHeader unregister_client_request_header; unregister_client_request_header.decode(doc); @@ -54,7 +54,7 @@ TEST(GetConsumerListByGroupResponseBodyTest, Encode) { response_body.add("localhost@1"); response_body.add("localhost@2"); - ProtobufWkt::Struct doc; + Protobuf::Struct doc; response_body.encode(doc); const auto& members = doc.fields(); @@ -79,7 +79,7 @@ TEST_F(AckMessageRequestHeaderTest, Encode) { ack_header.extraInfo(extra_info); ack_header.offset(offset); - ProtobufWkt::Value doc; + Protobuf::Value doc; ack_header.encode(doc); const auto& members = doc.struct_value().fields(); @@ -111,7 +111,7 @@ TEST_F(AckMessageRequestHeaderTest, Decode) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); AckMessageRequestHeader ack_header; @@ -134,7 +134,7 @@ TEST_F(AckMessageRequestHeaderTest, DecodeNumSerializedAsString) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); AckMessageRequestHeader ack_header; @@ -174,7 +174,7 @@ TEST_F(PopMessageRequestHeaderTest, Encode) { pop_request_header.expType(exp_type); pop_request_header.exp(exp); - ProtobufWkt::Value doc; + Protobuf::Value doc; pop_request_header.encode(doc); const auto& members = doc.struct_value().fields(); @@ -226,7 +226,7 @@ TEST_F(PopMessageRequestHeaderTest, Decode) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); PopMessageRequestHeader pop_request_header; pop_request_header.decode(doc); @@ -259,7 +259,7 @@ TEST_F(PopMessageRequestHeaderTest, DecodeNumSerializedAsString) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); PopMessageRequestHeader pop_request_header; pop_request_header.decode(doc); @@ -298,7 +298,7 @@ TEST_F(PopMessageResponseHeaderTest, Encode) { pop_response_header.msgOffsetInfo(msg_offset_info); pop_response_header.orderCountInfo(order_count_info); - ProtobufWkt::Value doc; + Protobuf::Value doc; pop_response_header.encode(doc); const auto& members = doc.struct_value().fields(); @@ -333,7 +333,7 @@ TEST_F(PopMessageResponseHeaderTest, Decode) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); PopMessageResponseHeader header; @@ -362,7 +362,7 @@ TEST_F(PopMessageResponseHeaderTest, DecodeNumSerializedAsString) { } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); PopMessageResponseHeader header; @@ -388,7 +388,7 @@ TEST_F(SendMessageResponseHeaderTest, Encode) { response_header_.queueId(1); response_header_.queueOffset(100); response_header_.transactionId("TX_01"); - ProtobufWkt::Value doc; + Protobuf::Value doc; response_header_.encode(doc); const auto& members = doc.struct_value().fields(); @@ -412,7 +412,7 @@ TEST_F(SendMessageResponseHeaderTest, Decode) { "transactionId": "TX_1" } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); response_header_.decode(doc); EXPECT_STREQ("abc", response_header_.msgId().c_str()); @@ -430,7 +430,7 @@ TEST_F(SendMessageResponseHeaderTest, DecodeNumSerializedAsString) { "transactionId": "TX_1" } )EOF"; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); response_header_.decode(doc); EXPECT_STREQ("abc", response_header_.msgId().c_str()); @@ -443,7 +443,7 @@ class SendMessageRequestHeaderTest : public testing::Test {}; TEST_F(SendMessageRequestHeaderTest, EncodeDefault) { SendMessageRequestHeader header; - ProtobufWkt::Value doc; + Protobuf::Value doc; header.encode(doc); const auto& members = doc.struct_value().fields(); EXPECT_TRUE(members.contains("producerGroup")); @@ -468,7 +468,7 @@ TEST_F(SendMessageRequestHeaderTest, EncodeOptional) { header.unitMode(true); header.batch(true); header.maxReconsumeTimes(32); - ProtobufWkt::Value doc; + Protobuf::Value doc; header.encode(doc); const auto& members = doc.struct_value().fields(); EXPECT_TRUE(members.contains("producerGroup")); @@ -495,7 +495,7 @@ TEST_F(SendMessageRequestHeaderTest, EncodeOptional) { TEST_F(SendMessageRequestHeaderTest, EncodeDefaultV2) { SendMessageRequestHeader header; header.version(SendMessageRequestVersion::V2); - ProtobufWkt::Value doc; + Protobuf::Value doc; header.encode(doc); const auto& members = doc.struct_value().fields(); EXPECT_TRUE(members.contains("a")); @@ -521,7 +521,7 @@ TEST_F(SendMessageRequestHeaderTest, EncodeOptionalV2) { header.batch(true); header.maxReconsumeTimes(32); header.version(SendMessageRequestVersion::V2); - ProtobufWkt::Value doc; + Protobuf::Value doc; header.encode(doc); const auto& members = doc.struct_value().fields(); @@ -549,7 +549,7 @@ TEST_F(SendMessageRequestHeaderTest, EncodeOptionalV2) { TEST_F(SendMessageRequestHeaderTest, EncodeV3) { SendMessageRequestHeader header; header.version(SendMessageRequestVersion::V3); - ProtobufWkt::Value doc; + Protobuf::Value doc; header.encode(doc); } @@ -571,7 +571,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV1) { )EOF"; SendMessageRequestHeader header; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.decode(doc); EXPECT_STREQ("FooBar", header.topic().c_str()); @@ -609,7 +609,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV1Optional) { )EOF"; SendMessageRequestHeader header; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.decode(doc); EXPECT_STREQ("FooBar", header.topic().c_str()); @@ -647,7 +647,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV1OptionalNumSerializedAsString) { )EOF"; SendMessageRequestHeader header; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.decode(doc); EXPECT_STREQ("FooBar", header.topic().c_str()); @@ -684,7 +684,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV2) { SendMessageRequestHeader header; header.version(SendMessageRequestVersion::V2); - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.decode(doc); EXPECT_STREQ("FooBar", header.topic().c_str()); @@ -723,7 +723,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV2Optional) { SendMessageRequestHeader header; header.version(SendMessageRequestVersion::V2); - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.decode(doc); EXPECT_STREQ("FooBar", header.topic().c_str()); @@ -762,7 +762,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV2OptionalNumSerializedAsString) { SendMessageRequestHeader header; header.version(SendMessageRequestVersion::V2); - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.decode(doc); EXPECT_STREQ("FooBar", header.topic().c_str()); @@ -798,7 +798,7 @@ TEST_F(SendMessageRequestHeaderTest, DecodeV3) { )EOF"; SendMessageRequestHeader header; - ProtobufWkt::Value doc; + Protobuf::Value doc; MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); header.version(SendMessageRequestVersion::V3); header.decode(doc); @@ -845,7 +845,7 @@ TEST_F(HeartbeatDataTest, Decoding) { const char* consumerGroup = "please_rename_unique_group_name_4"; HeartbeatData heart_beat_data; - ProtobufWkt::Struct doc; + Protobuf::Struct doc; MessageUtil::loadFromJson(json, doc); heart_beat_data.decode(doc); @@ -885,14 +885,14 @@ TEST_F(HeartbeatDataTest, DecodeClientIdMissing) { } )EOF"; - ProtobufWkt::Struct doc; + Protobuf::Struct doc; MessageUtil::loadFromJson(json, doc); EXPECT_FALSE(data_.decode(doc)); } TEST_F(HeartbeatDataTest, Encode) { data_.clientId("CID_01"); - ProtobufWkt::Struct doc; + Protobuf::Struct doc; data_.encode(doc); const auto& members = doc.fields(); EXPECT_TRUE(members.contains("clientID")); diff --git a/contrib/rocketmq_proxy/filters/network/test/route_matcher_test.cc b/contrib/rocketmq_proxy/filters/network/test/route_matcher_test.cc index f078f6d437538..8304972293ad9 100644 --- a/contrib/rocketmq_proxy/filters/network/test/route_matcher_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/route_matcher_test.cc @@ -59,7 +59,7 @@ name: default_route const std::vector& mmc = criteria->metadataMatchCriteria(); - ProtobufWkt::Value v1; + Protobuf::Value v1; v1.set_string_value("v1"); HashedValue hv1(v1); diff --git a/contrib/rocketmq_proxy/filters/network/test/topic_route_test.cc b/contrib/rocketmq_proxy/filters/network/test/topic_route_test.cc index 2a067b6a25bfd..59b0699d181af 100644 --- a/contrib/rocketmq_proxy/filters/network/test/topic_route_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/topic_route_test.cc @@ -11,7 +11,7 @@ namespace RocketmqProxy { TEST(TopicRouteTest, Serialization) { QueueData queue_data("broker-a", 8, 8, 6); - ProtobufWkt::Struct doc; + Protobuf::Struct doc; queue_data.encode(doc); const auto& members = doc.fields(); @@ -33,7 +33,7 @@ TEST(BrokerDataTest, Serialization) { std::string broker_name("broker-a"); BrokerData broker_data(cluster, broker_name, std::move(broker_addrs)); - ProtobufWkt::Struct doc; + Protobuf::Struct doc; broker_data.encode(doc); const auto& members = doc.fields(); @@ -61,7 +61,7 @@ TEST(TopicRouteDataTest, Serialization) { topic_route_data.brokerData().emplace_back( BrokerData(cluster, broker_name, std::move(broker_addrs))); } - ProtobufWkt::Struct doc; + Protobuf::Struct doc; EXPECT_NO_THROW(topic_route_data.encode(doc)); MessageUtil::getJsonStringFromMessageOrError(doc); } diff --git a/contrib/sip_proxy/filters/network/test/mocks.cc b/contrib/sip_proxy/filters/network/test/mocks.cc index 488254734f1b9..aa3e32fe42a90 100644 --- a/contrib/sip_proxy/filters/network/test/mocks.cc +++ b/contrib/sip_proxy/filters/network/test/mocks.cc @@ -12,9 +12,9 @@ using testing::ReturnRef; namespace Envoy { -// Provide a specialization for ProtobufWkt::Struct (for MockFilterConfigFactory) +// Provide a specialization for Protobuf::Struct (for MockFilterConfigFactory) template <> -void MessageUtil::validate(const ProtobufWkt::Struct&, ProtobufMessage::ValidationVisitor&, bool) {} +void MessageUtil::validate(const Protobuf::Struct&, ProtobufMessage::ValidationVisitor&, bool) {} namespace Extensions { namespace NetworkFilters { @@ -68,7 +68,7 @@ FilterFactoryCb MockFilterConfigFactory::createFilterFactoryFromProto( Server::Configuration::FactoryContext& context) { UNREFERENCED_PARAMETER(context); - config_struct_ = dynamic_cast(proto_config); + config_struct_ = dynamic_cast(proto_config); config_stat_prefix_ = stats_prefix; return [this](FilterChainFactoryCallbacks& callbacks) -> void { diff --git a/contrib/sip_proxy/filters/network/test/mocks.h b/contrib/sip_proxy/filters/network/test/mocks.h index 39287e245e8b7..6e5b19bef3e2e 100644 --- a/contrib/sip_proxy/filters/network/test/mocks.h +++ b/contrib/sip_proxy/filters/network/test/mocks.h @@ -153,12 +153,12 @@ class MockFilterConfigFactory : public NamedSipFilterConfigFactory { Server::Configuration::FactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return name_; } - ProtobufWkt::Struct config_struct_; + Protobuf::Struct config_struct_; std::string config_stat_prefix_; private: diff --git a/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc b/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc index 2791f35000bc1..fc61e9b0c0c22 100644 --- a/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc +++ b/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc @@ -62,7 +62,7 @@ void UdpTapSink::UdpTapSinkHandle::setStreamedTraceDataAndSubmit( // Set data from original trace to new trace. dst_streamed_trace.set_trace_id(src_streamed_trace.trace_id()); - ProtobufWkt::Timestamp* dst_ts = dst_streamed_trace.mutable_event()->mutable_timestamp(); + Protobuf::Timestamp* dst_ts = dst_streamed_trace.mutable_event()->mutable_timestamp(); dst_ts->CopyFrom(src_streamed_trace.event().timestamp()); dst_ts->set_nanos(dst_ts->nanos() + new_trace_cnt); diff --git a/contrib/vcl/source/BUILD b/contrib/vcl/source/BUILD index d8bea84a9a50b..3aed2b35c825f 100644 --- a/contrib/vcl/source/BUILD +++ b/contrib/vcl/source/BUILD @@ -85,9 +85,9 @@ envoy_cmake( "libvlibmemoryclient.a", ], postfix_script = """ - mkdir -p $INSTALLDIR/lib/external $INSTALLDIR/include/external \ - && find . -name "*.a" | xargs -I{} cp -a {} $INSTALLDIR/lib/ \ - && find . -name "*.h" ! -name config.h | xargs -I{} cp -a {} $INSTALLDIR/include + mkdir -p $$INSTALLDIR/lib/external $$INSTALLDIR/include/external \ + && find . -name "*.a" | xargs -I{} cp -a {} $$INSTALLDIR/lib/ \ + && find . -name "*.h" ! -name config.h | xargs -I{} cp -a {} $$INSTALLDIR/include """, tags = [ "cpu:16", diff --git a/distribution/docker/BUILD b/distribution/docker/BUILD new file mode 100644 index 0000000000000..bd89a60245c4b --- /dev/null +++ b/distribution/docker/BUILD @@ -0,0 +1,7 @@ +licenses(["notice"]) # Apache 2 + +exports_files([ + "Dockerfile-envoy", + "docker-entrypoint.sh", + "build.sh", +]) diff --git a/ci/Dockerfile-envoy b/distribution/docker/Dockerfile-envoy similarity index 93% rename from ci/Dockerfile-envoy rename to distribution/docker/Dockerfile-envoy index 1386990b4617e..3f3375f8ab870 100644 --- a/ci/Dockerfile-envoy +++ b/distribution/docker/Dockerfile-envoy @@ -1,11 +1,11 @@ ARG BUILD_OS=ubuntu ARG BUILD_TAG=22.04 -ARG BUILD_SHA=59ccd419c0dc0edf9e3bff1a3b2b073ea15a2ce4bc45ce7c989278b225b09af3 +ARG BUILD_SHA=1aa979d85661c488ce030ac292876cf6ed04535d3a237e49f61542d8e5de5ae0 ARG ENVOY_VRP_BASE_IMAGE=envoy-base FROM scratch AS binary -COPY ci/docker-entrypoint.sh / +COPY distribution/docker/docker-entrypoint.sh / ADD configs/envoyproxy_io_proxy.yaml /etc/envoy/envoy.yaml # See https://github.com/docker/buildx/issues/510 for why this _must_ be this way ARG TARGETPLATFORM @@ -59,7 +59,7 @@ COPY --chown=0:0 --chmod=755 \ # STAGE: envoy-distroless -FROM gcr.io/distroless/base-nossl-debian12:nonroot@sha256:fa7b50f111719aaf5f7435383b6d05f12277f3ce9514bc0a62759374a04d6bae AS envoy-distroless +FROM gcr.io/distroless/base-nossl-debian12:nonroot@sha256:8981b63f968e829d21351ea9d28cc21127e5f034707f1d8483d2993d9577be0b AS envoy-distroless EXPOSE 10000 ENTRYPOINT ["/usr/local/bin/envoy"] CMD ["-c", "/etc/envoy/envoy.yaml"] diff --git a/distribution/docker/README.md b/distribution/docker/README.md new file mode 100644 index 0000000000000..be8af131aca5a --- /dev/null +++ b/distribution/docker/README.md @@ -0,0 +1,35 @@ +# Envoy Docker Distribution + +This directory contains the Docker build configuration for Envoy container images. + +## Files + +- `Dockerfile-envoy` - Main Dockerfile for building Envoy container images +- `buildd.sh` - Script for building Docker images in CI +- `docker-entrypoint.sh` - Entrypoint script for Envoy containers + +## Usage + +The Docker build is typically invoked through the main CI script: + +```bash +./ci/do_ci.sh docker +``` + +This will build Docker images for multiple platforms and variants including: +- Standard Envoy image +- Debug image +- Contrib image (with additional extensions) +- Distroless image +- Google VRP image +- Tools image + +## Development + +For local development, you can build images directly using the build.sh script: + +```bash +DOCKER_CI_DRYRUN=1 ./distribution/docker/build.sh +``` + +This will show what commands would be executed without actually building images. diff --git a/distribution/docker/build.sh b/distribution/docker/build.sh new file mode 100755 index 0000000000000..11167615f2df2 --- /dev/null +++ b/distribution/docker/build.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash + +# Do not ever set -x here, it is a security hazard as it will place the credentials below in the +# CI logs. +set -e + +# Workaround for https://github.com/envoyproxy/envoy/issues/26634 +DOCKER_BUILD_TIMEOUT="${DOCKER_BUILD_TIMEOUT:-500}" + +# Allow single platform builds via DOCKER_BUILD_PLATFORM +if [[ -n "$DOCKER_BUILD_PLATFORM" ]]; then + DOCKER_PLATFORM="${DOCKER_BUILD_PLATFORM}" +else + DOCKER_PLATFORM="${DOCKER_PLATFORM:-linux/arm64,linux/amd64}" +fi + +if [[ -n "$DOCKER_CI_DRYRUN" ]]; then + CI_SHA1="${CI_SHA1:-MOCKSHA}" +fi + +DEV_VERSION_REGEX="-dev$" +if [[ -z "$ENVOY_VERSION" ]]; then + ENVOY_VERSION="$(cat VERSION.txt)" +fi + +if [[ "$ENVOY_VERSION" =~ $DEV_VERSION_REGEX ]]; then + # Dev version + IMAGE_POSTFIX="-dev" + IMAGE_NAME="${CI_SHA1}" +else + # Non-dev version + IMAGE_POSTFIX="" + IMAGE_NAME="v${ENVOY_VERSION}" +fi + +if [[ -n "$DOCKER_LOAD_IMAGES" ]]; then + LOAD_IMAGES=1 +fi + +ENVOY_OCI_DIR="${ENVOY_OCI_DIR:-${BUILD_DIR:-.}/build_images}" + +# This prefix is altered for the private security images on setec builds. +DOCKER_IMAGE_PREFIX="${DOCKER_IMAGE_PREFIX:-envoyproxy/envoy}" +if [[ -z "$DOCKER_CI_DRYRUN" ]]; then + mkdir -p "${ENVOY_OCI_DIR}" +fi + +# Setting environments for buildx tools + +config_env() { + BUILDKIT_VERSION=$(grep '^FROM moby/buildkit:' ci/Dockerfile-buildkit | cut -d ':' -f2) + echo ">> BUILDX: install ${BUILDKIT_VERSION}" + + if [[ "${DOCKER_PLATFORM}" == *","* ]]; then + echo "> docker run --rm --privileged tonistiigi/binfmt --install all" + docker run --rm --privileged tonistiigi/binfmt:qemu-v7.0.0 --install all + fi + + echo "> docker buildx rm envoy-builder 2> /dev/null || :" + echo "> docker buildx create --use --name envoy-builder --platform ${DOCKER_PLATFORM}" + + # Remove older build instance + docker buildx rm envoy-builder 2> /dev/null || : + docker buildx create --use --name envoy-builder --platform "${DOCKER_PLATFORM}" --driver-opt "image=moby/buildkit:${BUILDKIT_VERSION}" +} + +BUILD_TYPES=("" "-debug" "-contrib" "-contrib-debug" "-distroless" "-tools") + +if [[ "$DOCKER_PLATFORM" == "linux/amd64" ]]; then + BUILD_TYPES+=("-google-vrp") +fi + +# Configure docker-buildx tools +BUILD_COMMAND=("buildx" "build") +config_env + +image_tag_name () { + local build_type="$1" image_name="$2" image_tag + parts=() + if [[ -n "$build_type" ]]; then + parts+=("${build_type:1}") + fi + if [[ -n ${IMAGE_POSTFIX:1} ]]; then + parts+=("${IMAGE_POSTFIX:1}") + fi + if [[ -z "$image_name" ]]; then + parts+=("$IMAGE_NAME") + elif [[ "$image_name" != "latest" ]]; then + parts+=("$image_name") + fi + image_tag=$(IFS=- ; echo "${parts[*]}") + echo -n "${DOCKER_IMAGE_PREFIX}:${image_tag}" +} + +build_args() { + local build_type=$1 target + + target="${build_type/-debug/}" + target="${target/-contrib/}" + printf ' -f distribution/docker/Dockerfile-envoy --target %s' "envoy${target}" + + if [[ "${build_type}" == *-contrib* ]]; then + printf ' --build-arg ENVOY_BINARY=envoy-contrib' + fi + + if [[ "${build_type}" == *-debug ]]; then + printf ' --build-arg ENVOY_BINARY_PREFIX=dbg/' + fi +} + +use_builder() { + if [[ "${DOCKER_PLATFORM}" != *","* ]]; then + return + fi + echo ">> BUILDX: use envoy-builder" + echo "> docker buildx use envoy-builder" + + if [[ -n "$DOCKER_CI_DRYRUN" ]]; then + return + fi + docker buildx use envoy-builder +} + +build_image () { + local image_type="$1" platform docker_build_args _args args=() docker_build_args docker_image_tarball build_tag action platform size + + action="BUILD" + use_builder "${image_type}" + _args=$(build_args "${image_type}") + read -ra args <<<"$_args" + platform="$DOCKER_PLATFORM" + build_tag="$(image_tag_name "${image_type}")" + docker_image_tarball="${ENVOY_OCI_DIR}/envoy${image_type}.tar" + + # `--sbom` and `--provenance` args added for skopeo 1.5.0 compat, + # can probably be removed for later versions. + args+=( + "--sbom=false" + "--provenance=false") + if [[ -n "$LOAD_IMAGES" ]]; then + action="BUILD+LOAD" + args+=("--load") + else + if [[ "$platform" != *","* ]]; then + arch="$(echo "$platform" | cut -d/ -f2)" + docker_image_tarball="${ENVOY_OCI_DIR}/envoy${image_type}.${arch}.tar" + fi + action="BUILD+OCI" + args+=("-o" "type=oci,dest=${docker_image_tarball}") + fi + + docker_build_args=( + "${BUILD_COMMAND[@]}" + "--platform" "${platform}" + "${args[@]}" + -t "${build_tag}" + .) + echo ">> ${action}: ${build_tag}" + echo "> docker ${docker_build_args[*]}" + + timeout "$DOCKER_BUILD_TIMEOUT" docker "${docker_build_args[@]}" || { + if [[ "$?" == 124 ]]; then + echo "Docker build timed out ..." >&2 + else + echo "Docker build errored ..." >&2 + fi + sleep 5 + echo "trying again ..." >&2 + docker "${docker_build_args[@]}" + } + if [[ -n "$LOAD_IMAGES" || ! -f "${docker_image_tarball}" ]]; then + return + fi + size=$(du -h "${docker_image_tarball}" | cut -f1) + echo ">> OCI tarball created: ${docker_image_tarball} (${size})" +} + +do_docker_ci () { + local build_type + echo "Docker build configuration:" + echo " Platform(s): ${DOCKER_PLATFORM}" + echo " Output directory: ${ENVOY_OCI_DIR}" + if [[ -n "$DOCKER_FORCE_OCI_OUTPUT" ]]; then + echo " Output format: OCI tarballs only" + fi + echo + for build_type in "${BUILD_TYPES[@]}"; do + build_image "$build_type" + done +} + +do_docker_ci diff --git a/ci/docker-entrypoint.sh b/distribution/docker/docker-entrypoint.sh similarity index 100% rename from ci/docker-entrypoint.sh rename to distribution/docker/docker-entrypoint.sh diff --git a/docs/BUILD b/docs/BUILD index c47696b7d0dc7..2e317b672a110 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -22,6 +22,7 @@ filegroup( "root/operations/_include/traffic_tapping_*.yaml", "root/configuration/http/http_filters/_include/checksum_filter.yaml", "root/configuration/http/http_filters/_include/dns-cache-circuit-breaker.yaml", + "root/configuration/http/http_filters/_include/http-cache-configuration-fs.yaml", "root/configuration/other_features/_include/dlb.yaml", "root/configuration/other_features/_include/hyperscan_matcher.yaml", "root/configuration/other_features/_include/hyperscan_matcher_multiple.yaml", diff --git a/docs/inventories/v1.31/objects.inv b/docs/inventories/v1.31/objects.inv index b65d7aa97c45d..054d6f7874152 100644 Binary files a/docs/inventories/v1.31/objects.inv and b/docs/inventories/v1.31/objects.inv differ diff --git a/docs/inventories/v1.32/objects.inv b/docs/inventories/v1.32/objects.inv index 57c54bd292cc3..51e7493391e9d 100644 Binary files a/docs/inventories/v1.32/objects.inv and b/docs/inventories/v1.32/objects.inv differ diff --git a/docs/inventories/v1.33/objects.inv b/docs/inventories/v1.33/objects.inv index f5ec3fff47eaa..bbc5af55c0153 100644 Binary files a/docs/inventories/v1.33/objects.inv and b/docs/inventories/v1.33/objects.inv differ diff --git a/docs/inventories/v1.34/objects.inv b/docs/inventories/v1.34/objects.inv index 41d63ef51a4ab..10fedeb95fbe5 100644 Binary files a/docs/inventories/v1.34/objects.inv and b/docs/inventories/v1.34/objects.inv differ diff --git a/docs/inventories/v1.35/objects.inv b/docs/inventories/v1.35/objects.inv new file mode 100644 index 0000000000000..6f3bd041b5b31 Binary files /dev/null and b/docs/inventories/v1.35/objects.inv differ diff --git a/docs/root/_static/reverse_connection_concept.png b/docs/root/_static/reverse_connection_concept.png new file mode 100644 index 0000000000000..e967fec213fa2 Binary files /dev/null and b/docs/root/_static/reverse_connection_concept.png differ diff --git a/docs/root/_static/reverse_connection_workflow.png b/docs/root/_static/reverse_connection_workflow.png new file mode 100644 index 0000000000000..c47e5f5590519 Binary files /dev/null and b/docs/root/_static/reverse_connection_workflow.png differ diff --git a/docs/root/api-v3/bootstrap/bootstrap.rst b/docs/root/api-v3/bootstrap/bootstrap.rst index 4c454d0180976..37670bc877d06 100644 --- a/docs/root/api-v3/bootstrap/bootstrap.rst +++ b/docs/root/api-v3/bootstrap/bootstrap.rst @@ -7,6 +7,8 @@ Bootstrap ../config/bootstrap/v3/bootstrap.proto ../extensions/bootstrap/internal_listener/v3/internal_listener.proto + ../extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.proto + ../extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.proto ../config/metrics/v3/metrics_service.proto ../config/overload/v3/overload.proto ../config/ratelimit/v3/rls.proto diff --git a/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst b/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst index 87790ac145ec8..c6ad6353bab4b 100644 --- a/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst +++ b/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst @@ -1,7 +1,7 @@ OpenTelemetry Resource Detectors ================================ -Resource detectors that can be configured with the OpenTelemetry Tracer: +Resource detectors that can be configured with the OpenTelemetry Tracer and the OpenTelemetry StatSink: .. toctree:: :glob: diff --git a/docs/root/configuration/advanced/well_known_dynamic_metadata.rst b/docs/root/configuration/advanced/well_known_dynamic_metadata.rst index 8b21caa3941a4..88c1a3f9440a5 100644 --- a/docs/root/configuration/advanced/well_known_dynamic_metadata.rst +++ b/docs/root/configuration/advanced/well_known_dynamic_metadata.rst @@ -32,6 +32,7 @@ The following Envoy filters can be configured to consume dynamic metadata emitte ` * :ref:`RateLimit Filter limit override ` * :ref:`Original destination listener filter ` +* :ref:`TLS Inspector listener filter ` .. _shared_dynamic_metadata: diff --git a/docs/root/configuration/http/http_conn_man/headers.rst b/docs/root/configuration/http/http_conn_man/headers.rst index 7c9be4c62459c..a84f992d4c9d7 100644 --- a/docs/root/configuration/http/http_conn_man/headers.rst +++ b/docs/root/configuration/http/http_conn_man/headers.rst @@ -637,6 +637,28 @@ The ``x-amzn-trace-id`` HTTP header is used by the AWS X-Ray tracer in Envoy. Th parent ID and sampling decision are added to HTTP requests in the tracing header. See more on AWS X-Ray tracing `here `__. +.. _config_http_conn_man_headers_traceparent: + +traceparent +----------- + +The ``traceparent`` HTTP header is used for W3C trace context propagation. It contains version, trace ID, +parent ID, and trace flags in a standardized format. This header is supported by the Zipkin tracer when +``trace_context_option`` is set to ``USE_B3_WITH_W3C_PROPAGATION``. In this mode, the tracer will extract +from W3C headers as fallback when B3 headers are not present, and inject both B3 and W3C headers for +upstream requests. See more on W3C Trace Context `here `__. + +.. _config_http_conn_man_headers_tracestate: + +tracestate +---------- + +The ``tracestate`` HTTP header is used for W3C trace context propagation. It carries vendor-specific trace +identification data as a set of name/value pairs. This header is supported by the Zipkin tracer when +``trace_context_option`` is set to ``USE_B3_WITH_W3C_PROPAGATION``. In this mode, the tracer will extract +from W3C headers as fallback when B3 headers are not present, and inject both B3 and W3C headers for +upstream requests. See more on W3C Trace Context `here `__. + .. _config_http_conn_man_headers_custom_request_headers: Custom request/response headers @@ -680,7 +702,7 @@ headers are modified before the request is sent upstream and the response is not .. attention:: - The following legacy header formatters are still supported, but will be deprecated in the future. + The following legacy header formatters are deprecated and will be removed soon. The equivalent information can be accessed using indicated substitutes. ``%DYNAMIC_METADATA(["namespace", "key", ...])%`` diff --git a/docs/root/configuration/http/http_filters/_include/http-cache-configuration-fs.yaml b/docs/root/configuration/http/http_filters/_include/http-cache-configuration-fs.yaml new file mode 100644 index 0000000000000..78977ee5f58fb --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/http-cache-configuration-fs.yaml @@ -0,0 +1,68 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: AUTO + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: "/service/1" + route: + cluster: service1 + - match: + prefix: "/service/2" + route: + cluster: service2 + http_filters: + - name: envoy.filters.http.cache + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.cache.v3.CacheConfig + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.cache.file_system_http_cache.v3.FileSystemHttpCacheConfig + manager_config: + thread_pool: + thread_count: 2 + cache_path: /var/cache/envoy + max_cache_size_bytes: 1073741824 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: service1 + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: service1 + port_value: 8000 + - name: service2 + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service2 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: service2 + port_value: 8000 diff --git a/docs/root/configuration/http/http_filters/_include/lua-filter.yaml b/docs/root/configuration/http/http_filters/_include/lua-filter.yaml index 2bce99f380732..8f4bff5b31c88 100644 --- a/docs/root/configuration/http/http_filters/_include/lua-filter.yaml +++ b/docs/root/configuration/http/http_filters/_include/lua-filter.yaml @@ -17,19 +17,26 @@ static_resources: virtual_hosts: - name: local_service domains: ["*"] + metadata: + filter_metadata: + lua-custom-name: + foo: vh-bar + baz: + - vh-bad + - vh-baz routes: - match: prefix: "/" route: host_rewrite_literal: upstream.com cluster: upstream_com - metadata: - filter_metadata: - lua-custom-name: - foo: bar - baz: - - bad - - baz + metadata: + filter_metadata: + lua-custom-name: + foo: bar + baz: + - bad + - baz http_filters: - name: lua-custom-name typed_config: diff --git a/docs/root/configuration/http/http_filters/_include/set-metadata-basic-static.yaml b/docs/root/configuration/http/http_filters/_include/set-metadata-basic-static.yaml new file mode 100644 index 0000000000000..11af094b0018b --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/set-metadata-basic-static.yaml @@ -0,0 +1,42 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "OK" + http_filters: + - name: envoy.filters.http.set_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Config + metadata: + - metadata_namespace: envoy.lb + value: + version: "v1.2.3" + environment: "production" + features: + - "feature_a" + - "feature_b" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/docs/root/configuration/http/http_filters/_include/set-metadata-multiple-entries.yaml b/docs/root/configuration/http/http_filters/_include/set-metadata-multiple-entries.yaml new file mode 100644 index 0000000000000..9ace86188df0e --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/set-metadata-multiple-entries.yaml @@ -0,0 +1,47 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "OK" + http_filters: + - name: envoy.filters.http.set_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Config + metadata: + # Service identification metadata + - metadata_namespace: envoy.lb + allow_overwrite: true + value: + service: "user-service" + version: "v2.1.0" + # Request routing metadata + - metadata_namespace: envoy.filters.http.fault + allow_overwrite: true + value: + upstream_cluster: "backend" + retry_policy: "aggressive" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/docs/root/configuration/http/http_filters/_include/set-metadata-overwrite-control.yaml b/docs/root/configuration/http/http_filters/_include/set-metadata-overwrite-control.yaml new file mode 100644 index 0000000000000..88d7866f44cd9 --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/set-metadata-overwrite-control.yaml @@ -0,0 +1,52 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "OK" + http_filters: + - name: envoy.filters.http.set_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Config + metadata: + # First entry - will be set initially + - metadata_namespace: test.namespace + value: + counter: 1 + list: ["first"] + # Second entry - will be ignored without allow_overwrite + - metadata_namespace: test.namespace + value: + counter: 2 + list: ["second"] + # Third entry - will merge with allow_overwrite: true + - metadata_namespace: test.namespace + allow_overwrite: true + value: + counter: 3 + list: ["third"] + new_field: "added" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/docs/root/configuration/http/http_filters/_include/set-metadata-typed-configuration.yaml b/docs/root/configuration/http/http_filters/_include/set-metadata-typed-configuration.yaml new file mode 100644 index 0000000000000..a5df2b984552b --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/set-metadata-typed-configuration.yaml @@ -0,0 +1,42 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "OK" + http_filters: + - name: envoy.filters.http.set_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Config + metadata: + - metadata_namespace: custom.typed + allow_overwrite: true + typed_value: + "@type": type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Config + metadata_namespace: nested_namespace + value: + custom_field: "typed_value" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/docs/root/configuration/http/http_filters/cache_filter.rst b/docs/root/configuration/http/http_filters/cache_filter.rst index 68deed12bb0a3..1527e0e1693f1 100644 --- a/docs/root/configuration/http/http_filters/cache_filter.rst +++ b/docs/root/configuration/http/http_filters/cache_filter.rst @@ -36,7 +36,15 @@ For HTTP Responses: HTTP Cache delegates the actual storage of HTTP responses to implementations of the ``HttpCache`` interface. These implementations can cover all points on the spectrum of persistence, performance, and distribution, from local RAM caches to globally distributed persistent caches. They can be fully custom caches, or wrappers/adapters around local or remote open-source or proprietary caches. -Currently the only available cache storage implementation is :ref:`SimpleHTTPCache `. +Built-in cache storage backends include :ref:`SimpleHttpCacheConfig ` (in-memory) and :ref:`FileSystemHttpCacheConfig ` (persistent; LRU). + +Architecture and extension points +--------------------------------- + +Envoy’s HTTP caching is split into: + +* **HTTP Cache filter** (extension name ``envoy.filters.http.cache``, category ``envoy.filters.http``) — configured via ``CacheConfig`` to apply HTTP caching semantics. +* **Cache storage backends** (extension category ``envoy.http.cache``) — the filter delegates object storage/retrieval to a backend, selected via a nested ``typed_config`` in ``CacheConfig``. Example configuration --------------------- @@ -50,7 +58,26 @@ Example filter configuration with a ``SimpleHttpCache`` cache implementation: :lineno-start: 29 :caption: :download:`http-cache-configuration.yaml <_include/http-cache-configuration.yaml>` +Example filter configuration with a ``FileSystemHttpCache`` cache implementation: + +.. literalinclude:: _include/http-cache-configuration-fs.yaml + :language: yaml + :start-at: http_filters: + :end-before: envoy.filters.http.router + :linenos: + :lineno-match: + :caption: :download:`http-cache-configuration-fs.yaml <_include/http-cache-configuration-fs.yaml>` + .. seealso:: :ref:`Envoy Cache Sandbox ` Learn more about the Envoy Cache filter in the step by step sandbox. + + :ref:`HTTP Cache filter (proto file) ` + ``CacheConfig`` API reference. + + :ref:`In-memory storage backend ` + ``SimpleHttpCacheConfig`` API reference. + + :ref:`Persistent on-disk storage backend ` + Docs page for File System Http Cache; links to ``FileSystemHttpCacheConfig`` API reference. diff --git a/docs/root/configuration/http/http_filters/composite_filter.rst b/docs/root/configuration/http/http_filters/composite_filter.rst index 2f2851e8864de..5c07663308fb4 100644 --- a/docs/root/configuration/http/http_filters/composite_filter.rst +++ b/docs/root/configuration/http/http_filters/composite_filter.rst @@ -9,10 +9,10 @@ or filter configurations to be selected based on the incoming request, allowing configuration that could become prohibitive when making use of per route configurations (e.g. because the cardinality would cause a route table explosion). -The filter does not do any kind of buffering, and as a result it must be able to instantiate the -filter it will delegate to before it receives any callbacks that it needs to delegate. Because of -this, in order to delegate all the data to the specified filter, the decision must be made based -on just the request headers. +The filter does not do any kind of buffering, and as a result it will only +delegate callbacks received during or after the phase which instantiates the +delegated filter. In order to delegate all the data to the specified filter, +the decision must be made based on just the request headers. Delegation can fail if the filter factory attempted to use a callback not supported by the composite filter. In either case, the ``.composite.delegation_error`` stat will be diff --git a/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst b/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst index cd61aaf6885b1..34090f4d0069e 100644 --- a/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst +++ b/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst @@ -155,5 +155,5 @@ namespace. :header: Name, Type, Description :widths: 1, 1, 2 - rq_pending_open, Gauge, Whether the requests circuit breaker is closed (0) or open (1) - rq_pending_remaining, Gauge, Number of remaining requests until the circuit breaker opens + rq_pending_open, Gauge, Whether the requests circuit breaker is closed (0) or open (1). + rq_pending_remaining, Gauge, Number of remaining requests until the circuit breaker opens. diff --git a/docs/root/configuration/http/http_filters/geoip_filter.rst b/docs/root/configuration/http/http_filters/geoip_filter.rst index 14340f611c7e1..3732f42518fad 100644 --- a/docs/root/configuration/http/http_filters/geoip_filter.rst +++ b/docs/root/configuration/http/http_filters/geoip_filter.rst @@ -78,5 +78,6 @@ per geolocation database type (rooted at ``.maxmind.``). Database t ``.lookup_error``, Counter, Total number of errors that occured during lookups for a given geolocation database file. ``.db_reload_success``, Counter, Total number of times when the geolocation database file was reloaded successfully. ``.db_reload_error``, Counter, Total number of times when the geolocation database file failed to reload. + ``.db_build_epoch``, Gauge, The build timestamp of the geolocation database file represented as a Unix epoch value. diff --git a/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst b/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst index 276780083d584..0119dd9926f34 100644 --- a/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst +++ b/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst @@ -20,6 +20,51 @@ A typical use case for this filter is to dynamically match requests with load ba subsets. For this, a given header's value would be extracted and attached to the request as dynamic metadata which would then be used to match a subset of endpoints. +Statistics +---------- + +The filter can optionally emit statistics when the :ref:`stat_prefix ` field is configured. + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.header_to_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config + stat_prefix: header_converter + request_rules: + - header: x-version + on_header_present: + metadata_namespace: envoy.lb + key: version + type: STRING + +This configuration would emit statistics such as: + +- ``http_filter_name.header_converter.request_rules_processed`` +- ``http_filter_name.header_converter.request_metadata_added`` +- ``http_filter_name.header_converter.response_rules_processed`` +- ``http_filter_name.header_converter.response_metadata_added`` +- ``http_filter_name.header_converter.request_header_not_found`` + +When ``stat_prefix`` is not configured, no statistics are emitted. + +These statistics are rooted at *http_filter_name.* with the following counters: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + request_rules_processed, Counter, Total number of request rules processed + response_rules_processed, Counter, Total number of response rules processed + request_metadata_added, Counter, Total number of metadata entries successfully added from request headers + response_metadata_added, Counter, Total number of metadata entries successfully added from response headers + request_header_not_found, Counter, Total number of times expected request headers were missing + response_header_not_found, Counter, Total number of times expected response headers were missing + base64_decode_failed, Counter, Total number of times Base64 decoding failed + header_value_too_long, Counter, Total number of times header values exceeded the maximum length + regex_substitution_failed, Counter, Total number of times regex substitution resulted in empty values + Example ------- @@ -78,8 +123,3 @@ Note that this filter also supports per route configuration: This can be used to either override the global configuration or if the global configuration is empty (no rules), it can be used to only enable the filter at a per route level. - -Statistics ----------- - -Currently, this filter generates no statistics. diff --git a/docs/root/configuration/http/http_filters/lua_filter.rst b/docs/root/configuration/http/http_filters/lua_filter.rst index b4b7160ada9ba..194317f9b3be3 100644 --- a/docs/root/configuration/http/http_filters/lua_filter.rst +++ b/docs/root/configuration/http/http_filters/lua_filter.rst @@ -54,8 +54,8 @@ A simple example of configuring the Lua HTTP filter that contains only :ref:`def .. literalinclude:: _include/lua-filter.yaml :language: yaml - :lines: 34-46 - :lineno-start: 34 + :lines: 41-53 + :lineno-start: 41 :linenos: :caption: :download:`lua-filter.yaml <_include/lua-filter.yaml>` @@ -482,12 +482,20 @@ If no entry could be found by the filter config name, then the filter canonical i.e. ``envoy.filters.http.lua`` will be used as an alternative. Note that this downgrade will be deprecated in the future. +.. note:: + + This method will be deprecated in the future. In order to access route configuration, + consider using :ref:`route object's metadata() ` instead, + which provides more consistent behavior. **Important**: route object's ``metadata()`` requires + metadata to be configured under the exact filter name and does not fall back to the + canonical name ``envoy.filters.http.lua``. + Below is an example of a ``metadata`` in a :ref:`route entry `. .. literalinclude:: _include/lua-filter.yaml :language: yaml - :lines: 26-32 - :lineno-start: 26 + :lines: 33-39 + :lineno-start: 33 :linenos: :caption: :download:`lua-filter.yaml <_include/lua-filter.yaml>` @@ -678,6 +686,38 @@ since epoch. ``resolution`` is an optional enum parameter to indicate the resolu Supported resolutions are ``EnvoyTimestampResolution.MILLISECOND`` and ``EnvoyTimestampResolution.MICROSECOND``. The default resolution is millisecond if ``resolution`` is not set. +.. _config_http_filters_lua_stream_handle_api_virtual_host: + +``virtualHost()`` +^^^^^^^^^^^^^^^^^ + +.. code-block:: lua + + local virtual_host = handle:virtualHost() + +Returns a virtual host object that provides access to the virtual host configuration. This method always returns +a valid object, even when the request does not match any configured virtual host. However, if no virtual host +matches, calling methods on the returned object will return ``nil`` or, in the case of the ``metadata()`` method, +an empty metadata object. + +Returns a :ref:`virtual host object `. + +.. _config_http_filters_lua_stream_handle_api_route: + +``route()`` +^^^^^^^^^^^ + +.. code-block:: lua + + local route = handle:route() + +Returns a route object that provides access to the route configuration. This method always returns +a valid object, even when the request does not match any configured route. However, if no route +matches, calling methods on the returned object will return ``nil`` or, in the case of the ``metadata()`` method, +an empty metadata object. + +Returns a :ref:`route object `. + .. _config_http_filters_lua_header_wrapper: Header object API @@ -1020,6 +1060,37 @@ Returns dynamic typed metadata for a given filter name. This provides type-safe end end +``filterState()`` +^^^^^^^^^^^^^^^^^ + +.. code-block:: lua + + streamInfo:filterState() + +Returns a :ref:`filter state object ` that provides access to objects stored by filters during request processing. + +Filter state contains data shared between filters, such as routing decisions, authentication results, rate limiting state, and other processing information. + +Example usage: + +.. code-block:: lua + + function envoy_on_request(request_handle) + local filter_state = request_handle:streamInfo():filterState() + + -- Get authentication result + local auth_result = filter_state:get("auth.result") + if auth_result then + request_handle:headers():add("x-auth-result", auth_result) + end + + -- Check rate limiting decision + local rate_limit_remaining = filter_state:get("rate_limit.remaining") + if rate_limit_remaining and rate_limit_remaining < 10 then + request_handle:headers():add("x-rate-limit-warning", "low") + end + end + ``downstreamSslConnection()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1190,6 +1261,59 @@ its keys can only be ``string`` or ``numeric``. Iterates through every ``dynamicMetadata`` entry. ``key`` is a string that supplies a ``dynamicMetadata`` key. ``value`` is a ``dynamicMetadata`` entry value. +.. _config_http_filters_lua_stream_info_filter_state_wrapper: + +Filter state object API +------------------------ + +.. include:: ../../../_include/lua_common.rst + +``get()`` +^^^^^^^^^ + +.. code-block:: lua + + filterState:get(objectName) + filterState:get(objectName, fieldName) + +Gets a filter state object by name with optional field access. ``objectName`` is a string that specifies the name of the filter state object to retrieve. ``fieldName`` is an optional string that specifies a field name for objects that support field access. + +Returns the filter state value as a string. Returns ``nil`` if the object does not exist, cannot be serialized, or if the specified field doesn't exist. + +Objects that support field access can have specific fields retrieved using the optional second parameter. + +.. code-block:: lua + + function envoy_on_request(request_handle) + local filter_state = request_handle:streamInfo():filterState() + + -- All values returned as strings + local auth_token = filter_state:get("auth.token") + if auth_token then + request_handle:headers():add("x-auth-token", auth_token) + end + + -- Boolean-like string values + local is_authenticated = filter_state:get("auth.authenticated") + if is_authenticated == "true" then + request_handle:headers():add("x-authenticated", "yes") + end + + -- Access specific fields from objects that support field access + local user_name = filter_state:get("user.info", "name") + if user_name then + request_handle:headers():add("x-user-name", user_name) + end + + local user_id_str = filter_state:get("user.info", "id") + if user_id_str then + local user_id = tonumber(user_id_str) + if user_id and user_id > 1000 then + request_handle:headers():add("x-premium-user", "true") + end + end + end + .. _config_http_filters_lua_connection_wrapper: Connection object API @@ -1492,3 +1616,63 @@ field or if the field can't be converted to a UTF8 string. Returns the string representation of O fields (as a table) from the X.509 name. Returns an empty table if there is no such field or if the field can't be converted to a UTF8 string. + +.. _config_http_filters_lua_virtual_host_wrapper: + +Virtual host object API +----------------------- + +.. include:: ../../../_include/lua_common.rst + +``metadata()`` +^^^^^^^^^^^^^^ + +.. code-block:: lua + + local metadata = virtual_host:metadata() + +Returns the virtual host metadata. Note that the metadata should be specified +under the :ref:`filter config name +`. + +Below is an example of a ``metadata`` in a :ref:`route entry `. + +.. literalinclude:: _include/lua-filter.yaml + :language: yaml + :lines: 20-26 + :lineno-start: 20 + :linenos: + :caption: :download:`lua-filter.yaml <_include/lua-filter.yaml>` + +Returns a :ref:`metadata object `. + +.. _config_http_filters_lua_route_wrapper: + +Route object API +---------------- + +.. include:: ../../../_include/lua_common.rst + +.. _config_http_filters_lua_route_wrapper_metadata: + +``metadata()`` +^^^^^^^^^^^^^^ + +.. code-block:: lua + + local metadata = route:metadata() + +Returns the route metadata. Note that the metadata should be specified +under the :ref:`filter config name +`. + +Below is an example of a ``metadata`` in a :ref:`route entry `. + +.. literalinclude:: _include/lua-filter.yaml + :language: yaml + :lines: 33-39 + :lineno-start: 33 + :linenos: + :caption: :download:`lua-filter.yaml <_include/lua-filter.yaml>` + +Returns a :ref:`metadata object `. diff --git a/docs/root/configuration/http/http_filters/router_filter.rst b/docs/root/configuration/http/http_filters/router_filter.rst index 90f2517c09e6c..15c817bea1fee 100644 --- a/docs/root/configuration/http/http_filters/router_filter.rst +++ b/docs/root/configuration/http/http_filters/router_filter.rst @@ -83,8 +83,8 @@ can be specified using a ',' delimited list. The supported policies are: request, including any retries that take place. gateway-error - This policy is similar to the *5xx* policy but will only retry requests that result in a 502, 503, - or 504. + This policy is similar to the *5xx* policy but will attempt a retry if the upstream server responds + with 502, 503, or 504 response code, or does not respond at all (disconnect/reset/read timeout). reset Envoy will attempt a retry if the upstream server does not respond at all (disconnect/reset/read timeout.) diff --git a/docs/root/configuration/http/http_filters/set_metadata_filter.rst b/docs/root/configuration/http/http_filters/set_metadata_filter.rst index fde69d5a50317..49634980e1d09 100644 --- a/docs/root/configuration/http/http_filters/set_metadata_filter.rst +++ b/docs/root/configuration/http/http_filters/set_metadata_filter.rst @@ -6,16 +6,43 @@ Set Metadata * This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Config``. * :ref:`v3 API reference ` -This filters adds or updates dynamic metadata with static data. +The Set Metadata filter adds or updates dynamic metadata with static or dynamically formatted data. +This filter is useful for attaching contextual information to requests that can be consumed by other +filters, used for load balancing decisions, included in access logs, or utilized for routing decisions. -Dynamic metadata values are updated with the following rules. If a key does not exist, it is copied into the current metadata. If the key exists, then following rules will be used: +The filter supports both untyped metadata (using ``google.protobuf.Struct``) and typed metadata +(using ``google.protobuf.Any``). Dynamic metadata values are updated according to specific merge +rules that vary based on the value type and the ``allow_overwrite`` configuration. -* if :ref:`typed metadata value ` is used, it will overwrite existing values iff :ref:`allow_overwrite ` is set to true, otherwise nothing is done. -* if :ref:`untyped metadata value ` is used and ``allow_overwrite`` is set to true, or if deprecated :ref:`value ` field is used, the values are updated with the following scheme: - - existing value with different type: the existing value is replaced. - - scalar values (null, string, number, boolean): the existing value is replaced. - - lists: new values are appended to the current list. - - structures: recursively apply this scheme. +Common use cases include: + +* Tagging requests with environment or service information for downstream processing. +* Adding routing hints for load balancer subset selection. +* Enriching access logs with additional context. +* Storing computed values for use by subsequent filters in the chain. + +Configuration +------------- + +The filter can be configured with multiple metadata entries, each targeting a specific namespace. +Each entry can contain either static values or use the deprecated legacy configuration format. + +Metadata Merge Rules +-------------------- + +Dynamic metadata values are updated with the following rules. If a key does not exist, it is copied into the current metadata. If the key exists, the following rules apply: + +* **Typed metadata values**: When :ref:`typed_value ` is used, it will overwrite existing values if and only if :ref:`allow_overwrite ` is set to ``true``. Otherwise, the operation is skipped. + +* **Untyped metadata values**: When :ref:`value ` is used and ``allow_overwrite`` is set to ``true``, or when the deprecated :ref:`value ` field is used, values are merged using the following scheme: + + - **Different type values**: The existing value is replaced entirely. + - **Scalar values** (null, string, number, boolean): The existing value is replaced. + - **Lists**: New values are appended to the existing list. + - **Structures**: The merge rules are applied recursively to nested structures. + +Merge Example +^^^^^^^^^^^^^ For instance, if the namespace already contains this structure: @@ -48,15 +75,72 @@ After applying this filter, the namespace will contain: tag0: 1 tag1: 1 +Configuration Examples +---------------------- + +Basic Static Metadata +^^^^^^^^^^^^^^^^^^^^^^ + +A simple configuration that adds static metadata to the ``envoy.lb`` namespace: + +.. literalinclude:: _include/set-metadata-basic-static.yaml + :language: yaml + :lines: 29-39 + :lineno-start: 29 + :linenos: + :caption: :download:`set-metadata-basic-static.yaml <_include/set-metadata-basic-static.yaml>` + +Multiple Metadata Entries +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configuration with multiple metadata entries targeting different namespaces: + +.. literalinclude:: _include/set-metadata-multiple-entries.yaml + :language: yaml + :lines: 29-44 + :lineno-start: 29 + :linenos: + :caption: :download:`set-metadata-multiple-entries.yaml <_include/set-metadata-multiple-entries.yaml>` + +Typed Metadata Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configuration using typed metadata with ``google.protobuf.Any``: + +.. literalinclude:: _include/set-metadata-typed-configuration.yaml + :language: yaml + :lines: 29-39 + :lineno-start: 29 + :linenos: + :caption: :download:`set-metadata-typed-configuration.yaml <_include/set-metadata-typed-configuration.yaml>` + +Overwrite Control +^^^^^^^^^^^^^^^^^ + +Configuration demonstrating overwrite control behavior: + +.. literalinclude:: _include/set-metadata-overwrite-control.yaml + :language: yaml + :lines: 29-49 + :lineno-start: 29 + :linenos: + :caption: :download:`set-metadata-overwrite-control.yaml <_include/set-metadata-overwrite-control.yaml>` + +.. note:: + + In the above example, the final metadata will contain: + ``counter: 3``, ``list: ["first", "third"]``, and ``new_field: "added"``. + The second entry is ignored because ``allow_overwrite`` is not set. + Statistics ---------- -The ``set_metadata`` filter outputs statistics in the ``http..set_metadata.`` namespace. The :ref:`stat prefix -` comes from the -owning HTTP connection manager. +The Set Metadata filter outputs statistics in the ``http..set_metadata.`` namespace. +The :ref:`stat prefix ` +comes from the owning HTTP connection manager. .. csv-table:: :header: Name, Type, Description :widths: 1, 1, 2 - overwrite_denied, Counter, Total number of denied attempts to overwrite an existing metadata value + overwrite_denied, Counter, Total number of denied attempts to overwrite an existing metadata value when ``allow_overwrite`` is ``false`` diff --git a/docs/root/configuration/listeners/listener_filters/tls_inspector.rst b/docs/root/configuration/listeners/listener_filters/tls_inspector.rst index c933678c144cd..4bd8988af986c 100644 --- a/docs/root/configuration/listeners/listener_filters/tls_inspector.rst +++ b/docs/root/configuration/listeners/listener_filters/tls_inspector.rst @@ -81,4 +81,12 @@ This filter has a statistics tree rooted at *tls_inspector* with the following s If the connection terminates early nothing is recorded if we didn't have sufficient bytes for either of the cases above. +.. _config_listener_filters_tls_inspector_dynamic_metadata: + +Dynamic Metadata +---------------- + +If the filter fails to detect TLS it will populate dynamic metadata under the key +`envoy.filters.listener.tls_inspector` indicating the reason (eg. ``ClientHello`` too +large or not detected at all). diff --git a/docs/root/configuration/listeners/listeners.rst b/docs/root/configuration/listeners/listeners.rst index 5e4cc6b22c5e4..ede3c56d2d3d9 100644 --- a/docs/root/configuration/listeners/listeners.rst +++ b/docs/root/configuration/listeners/listeners.rst @@ -13,3 +13,4 @@ Listeners network_filters/network_filters udp_filters/udp_filters lds + network_namespace_matching diff --git a/docs/root/configuration/listeners/network_filters/_include/redis-aws-iam-auth.yaml b/docs/root/configuration/listeners/network_filters/_include/redis-aws-iam-auth.yaml new file mode 100644 index 0000000000000..d22ef696f4e36 --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/_include/redis-aws-iam-auth.yaml @@ -0,0 +1,41 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: 0.0.0.0 + port_value: 6379 + filter_chains: + - filters: + - name: envoy.filters.network.redis_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy + stat_prefix: egress_redis + settings: + op_timeout: 5s + prefix_routes: + catch_all_route: + cluster: redis_cluster + clusters: + - name: redis_cluster + connect_timeout: 1s + type: STRICT_DNS + load_assignment: + cluster_name: redis_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: testcache-7dh4z9.serverless.apse2.cache.amazonaws.com + port_value: 6379 + typed_extension_protocol_options: + envoy.filters.network.redis_proxy: + "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProtocolOptions + auth_username: + inline_string: test + aws_iam: + region: ap-southeast-2 + service_name: elasticache + cache_name: testcache + expiration_time: 900s diff --git a/docs/root/configuration/listeners/network_filters/_include/redis-dns-lookups.yaml b/docs/root/configuration/listeners/network_filters/_include/redis-dns-lookups.yaml new file mode 100644 index 0000000000000..3e7222852f217 --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/_include/redis-dns-lookups.yaml @@ -0,0 +1,36 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: 0.0.0.0 + port_value: 6379 + filter_chains: + - filters: + - name: envoy.filters.network.redis_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy + stat_prefix: redis_stats + prefix_routes: + catch_all_route: + cluster: redis_cluster + settings: + op_timeout: 5s + enable_redirection: true + dns_cache_config: + name: dns_cache_for_redis + dns_lookup_family: V4_ONLY + max_hosts: 100 + clusters: + - name: redis_cluster + connect_timeout: 1s + type: STRICT_DNS + load_assignment: + cluster_name: redis_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: redis.example.com + port_value: 6379 diff --git a/docs/root/configuration/listeners/network_filters/_include/redis-fault-injection.yaml b/docs/root/configuration/listeners/network_filters/_include/redis-fault-injection.yaml new file mode 100644 index 0000000000000..1f12bfd48f9d0 --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/_include/redis-fault-injection.yaml @@ -0,0 +1,47 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: 0.0.0.0 + port_value: 6379 + filter_chains: + - filters: + - name: envoy.filters.network.redis_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy + stat_prefix: redis_stats + prefix_routes: + catch_all_route: + cluster: redis_cluster + settings: + op_timeout: 5s + faults: + - fault_type: ERROR + fault_enabled: + default_value: + numerator: 10 + denominator: HUNDRED + runtime_key: "bogus_key" + commands: + - GET + - fault_type: DELAY + fault_enabled: + default_value: + numerator: 10 + denominator: HUNDRED + runtime_key: "bogus_key" + delay: 2s + clusters: + - name: redis_cluster + connect_timeout: 1s + type: STRICT_DNS + load_assignment: + cluster_name: redis_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: redis.example.com + port_value: 6379 diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index f838839fbdbba..1cadbdc33bb6f 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -23,6 +23,7 @@ filters. kafka_mesh_filter local_rate_limit_filter mongo_proxy_filter + reverse_tunnel_filter mysql_proxy_filter postgres_proxy_filter rate_limit_filter diff --git a/docs/root/configuration/listeners/network_filters/redis_proxy_filter.rst b/docs/root/configuration/listeners/network_filters/redis_proxy_filter.rst index 52f5e0beafba4..fa987bb2edf6e 100644 --- a/docs/root/configuration/listeners/network_filters/redis_proxy_filter.rst +++ b/docs/root/configuration/listeners/network_filters/redis_proxy_filter.rst @@ -99,24 +99,12 @@ or runtime key are set). Example configuration: -.. code-block:: yaml - - faults: - - fault_type: ERROR - fault_enabled: - default_value: - numerator: 10 - denominator: HUNDRED - runtime_key: "bogus_key" - commands: - - GET - - fault_type: DELAY - fault_enabled: - default_value: - numerator: 10 - denominator: HUNDRED - runtime_key: "bogus_key" - delay: 2s +.. literalinclude:: _include/redis-fault-injection.yaml + :language: yaml + :lines: 19-34 + :linenos: + :lineno-start: 19 + :caption: :download:`redis-fault-injection.yaml <_include/redis-fault-injection.yaml>` This creates two faults- an error, applying only to GET commands at 10%, and a delay, applying to all commands at 10%. This means that 20% of GET commands will have a fault applied, as discussed earlier. @@ -126,21 +114,12 @@ DNS lookups on redirections As noted in the :ref:`architecture overview `, when Envoy sees a MOVED or ASK response containing a hostname it will not perform a DNS lookup and instead bubble up the error to the client. The following configuration example enables DNS lookups on such responses to avoid the client error and have Envoy itself perform the redirection: -.. code-block:: yaml - - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy - stat_prefix: redis_stats - prefix_routes: - catch_all_route: - cluster: cluster_0 - settings: - op_timeout: 5 - enable_redirection: true - dns_cache_config: - name: dns_cache_for_redis - dns_lookup_family: V4_ONLY - max_hosts: 100 +.. literalinclude:: _include/redis-dns-lookups.yaml + :language: yaml + :lines: 11-23 + :linenos: + :lineno-start: 11 + :caption: :download:`redis-dns-lookups.yaml <_include/redis-dns-lookups.yaml>` .. _config_network_filters_redis_proxy_aws_iam: @@ -160,40 +139,9 @@ The `service_name` should be `elasticache` for an Amazon ElastiCache cache in va matches the service which is added to the IAM Policy for the associated IAM principal being used to make the connection. For example, `service_name: memorydb` matches an AWS IAM Policy containing the Action `memorydb:Connect`, and that policy must be attached to the IAM principal being used by envoy. -.. code-block:: yaml - - filter_chains: - - filters: - - name: envoy.filters.network.redis_proxy - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy - stat_prefix: egress_redis - settings: - op_timeout: 5s - prefix_routes: - catch_all_route: - cluster: redis_cluster - clusters: - - name: redis_cluster - connect_timeout: 1s - type: strict_dns - load_assignment: - cluster_name: redis_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: testcache-7dh4z9.serverless.apse2.cache.amazonaws.com - port_value: 6379 - typed_extension_protocol_options: - envoy.filters.network.redis_proxy: - "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProtocolOptions - auth_username: - inline_string: test - aws_iam: - region: ap-southeast-2 - service_name: elasticache - cache_name: testcache - expiration_time: 900s - +.. literalinclude:: _include/redis-aws-iam-auth.yaml + :language: yaml + :lines: 8-41 + :linenos: + :lineno-start: 8 + :caption: :download:`redis-aws-iam-auth.yaml <_include/redis-aws-iam-auth.yaml>` diff --git a/docs/root/configuration/listeners/network_filters/reverse_tunnel_filter.rst b/docs/root/configuration/listeners/network_filters/reverse_tunnel_filter.rst new file mode 100644 index 0000000000000..435d6a00e29af --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/reverse_tunnel_filter.rst @@ -0,0 +1,16 @@ +.. _config_network_filters_reverse_tunnel: + +Reverse tunnel +============== + +The reverse tunnel network filter accepts or rejects reverse connection requests by parsing +HTTP/1.1 requests with Node ID, Cluster ID, and Tenant ID headers and optionally validating these +values using the Envoy filter state. + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel``. +* :ref:`v3 API reference ` + +Configuration notes: + +- **HTTP method**: ``request_method`` uses :ref:`RequestMethod `. If not specified, it defaults to ``GET``. +- In this version, the filter does not perform additional request validation against filter state or metadata. diff --git a/docs/root/configuration/listeners/network_filters/sni_dynamic_forward_proxy_filter.rst b/docs/root/configuration/listeners/network_filters/sni_dynamic_forward_proxy_filter.rst index 4d64134b3c8ea..b81a83733aab3 100644 --- a/docs/root/configuration/listeners/network_filters/sni_dynamic_forward_proxy_filter.rst +++ b/docs/root/configuration/listeners/network_filters/sni_dynamic_forward_proxy_filter.rst @@ -39,3 +39,33 @@ by setting a per-connection state object under the key ``envoy.upstream.dynamic_ objects are set, they take precedence over the SNI value and default port. In case that the overridden port is out of the valid port range, the overriding value will be ignored and the default port configured will be used. See the implementation for the details. + +Statistics +---------- + +The SNI dynamic forward proxy DNS cache outputs statistics in the ``dns_cache..`` +namespace. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + dns_query_attempt, Counter, Number of DNS query attempts. + dns_query_success, Counter, Number of DNS query successes. + dns_query_failure, Counter, Number of DNS query failures. + dns_query_timeout, Counter, Number of DNS query :ref:`timeouts `. + host_address_changed, Counter, Number of DNS queries that resulted in a host address change. + host_added, Counter, Number of hosts that have been added to the cache. + host_removed, Counter, Number of hosts that have been removed from the cache. + num_hosts, Gauge, Number of hosts that are currently in the cache. + dns_rq_pending_overflow, Counter, Number of DNS pending request overflow. + +The dynamic forward proxy DNS cache circuit breakers output statistics in the ``dns_cache..circuit_breakers`` +namespace. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + rq_pending_open, Gauge, Whether the requests circuit breaker is closed (0) or open (1). + rq_pending_remaining, Gauge, Number of remaining requests until the circuit breaker opens. diff --git a/docs/root/configuration/listeners/network_namespace_matching.rst b/docs/root/configuration/listeners/network_namespace_matching.rst new file mode 100644 index 0000000000000..0f0ac5cd938fc --- /dev/null +++ b/docs/root/configuration/listeners/network_namespace_matching.rst @@ -0,0 +1,259 @@ +.. _config_listeners_network_namespace_matching: + +Network Namespace Matching +========================== + +Envoy supports routing connections to different filter chains based on the network namespace +of the listener address. This feature is particularly useful in containerized environments +where different services or environments are isolated using Linux network namespaces. + +.. attention:: + + Network namespace matching is only supported on Linux systems. On other platforms, + the network namespace input will always return an empty value, causing connections + to use the default filter chain. + +Overview +-------- + +Network namespace matching allows you to: + +* Route traffic from different network namespaces to different filter chains within a single listener. +* Implement multi-tenant architectures where each tenant has its own network namespace. +* Provide environment-specific routing like production, staging, or development based on namespace isolation. +* Maintain separate configurations for different containerized services. + +The network namespace is determined by the ``network_namespace_filepath`` field in the +:ref:`SocketAddress ` configuration of the listener. + +Configuration +------------- + +Network namespace matching is configured using the :ref:`filter_chain_matcher +` field with the +``envoy.matching.inputs.network_namespace`` input. + +Basic Configuration +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + listeners: + - name: ns_aware_listener + address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/development" + additional_addresses: + - address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/staging" + - address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/production" + + filter_chain_matcher: + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.NetworkNamespaceInput + exact_match_map: + map: + "/var/run/netns/production": + action: + name: production_chain + "/var/run/netns/staging": + action: + name: staging_chain + "/var/run/netns/development": + action: + name: development_chain + + filter_chains: + - name: production_chain + # Production-specific filters + - name: staging_chain + # Staging-specific filters + - name: staging_chain + # Development-specific filters + + default_filter_chain: + # Default chain for unknown namespaces + +Multiple Listeners Approach +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alternatively, you can create separate listeners for each network namespace: + +.. code-block:: yaml + + listeners: + - name: production_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/production" + filter_chains: + - # Production-specific configuration + + - name: staging_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 # Same port, different namespace + network_namespace_filepath: "/var/run/netns/staging" + filter_chains: + - # Staging-specific configuration + +Input Behavior +-------------- + +The ``envoy.matching.inputs.network_namespace`` input: + +* Returns the network namespace filepath as a string when available. +* Returns an empty value (no match) when: + + * No network namespace is configured for the listener address. + * The network namespace filepath is empty. + * Running on non-Linux platforms where network namespaces are not supported. + * The address type doesn't support network namespaces (e.g., Unix domain sockets). + +* Always returns the namespace of the listener's local address, not the client's namespace. + +Matching Strategies +------------------- + +Exact Match +~~~~~~~~~~~ + +Use exact string matching for specific namespaces: + +.. code-block:: yaml + + exact_match_map: + map: + "/var/run/netns/production": { action: { name: "prod_chain" } } + "/var/run/netns/staging": { action: { name: "staging_chain" } } + +Prefix Match +~~~~~~~~~~~~ + +Use prefix matching for namespace hierarchies: + +.. code-block:: yaml + + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + prefix_match_map: + map: + "/var/run/netns/prod": { action: { name: "production_chain" } } + "/var/run/netns/dev": { action: { name: "development_chain" } } + +Complex Matching +~~~~~~~~~~~~~~~~ + +Combine with other inputs for sophisticated routing: + +.. code-block:: yaml + + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + exact_match_map: + map: + "/var/run/netns/production": + matcher: + matcher_tree: + input: + name: envoy.matching.inputs.destination_port + exact_match_map: + map: + "8080": { action: { name: "prod_http_chain" } } + "8443": { action: { name: "prod_https_chain" } } + +Use Cases +--------- + +Multi-Tenant Architecture +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Route requests from different tenants to isolated backend services: + +.. code-block:: yaml + + filter_chain_matcher: + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + exact_match_map: + map: + "/var/run/netns/tenant_a": + action: { name: "tenant_a_chain" } + "/var/run/netns/tenant_b": + action: { name: "tenant_b_chain" } + +Environment Isolation +~~~~~~~~~~~~~~~~~~~~~ + +Separate production, staging, and development traffic: + +.. code-block:: yaml + + filter_chain_matcher: + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + exact_match_map: + map: + "/var/run/netns/production": + action: { name: "production_chain" } + "/var/run/netns/staging": + action: { name: "staging_chain" } + "/var/run/netns/development": + action: { name: "development_chain" } + +Service Mesh Integration +~~~~~~~~~~~~~~~~~~~~~~~~ + +Route traffic based on service identity encoded in network namespaces: + +.. code-block:: yaml + + filter_chain_matcher: + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + prefix_match_map: + map: + "/var/run/netns/service-": + matcher: + # Further routing based on service name extracted from namespace + +Statistics +---------- + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + "listener..filter_chain_selected.", Counter, "Total number of connections routed to the specified filter chain." + "listener..no_filter_chain_match", Counter, "Total number of connections that did not match any filter chain." + "listener_manager.listener_create_success", Counter, "Total number of successfully created listeners." + "listener_manager.listener_create_failure", Counter, "Total number of listener creation failures." + +Example Configuration +--------------------- + +See :repo:`network_namespace_matching_example.yaml ` +for a complete example configuration demonstrating various network namespace matching scenarios. diff --git a/docs/root/configuration/listeners/network_namespace_matching_example.yaml b/docs/root/configuration/listeners/network_namespace_matching_example.yaml new file mode 100644 index 0000000000000..f160c02ad0178 --- /dev/null +++ b/docs/root/configuration/listeners/network_namespace_matching_example.yaml @@ -0,0 +1,243 @@ +# Example configuration demonstrating network namespace-based filter chain matching +# This configuration shows how to route traffic to different filter chains based on +# the network namespace of the listener address. +static_resources: + listeners: + - name: multi_namespace_listener + # This listener will bind to all three network namespaces on the same IP:port. + # Individual addresses can specify different namespaces. + address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/development" + additional_addresses: + - address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/staging" + - address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8080 + network_namespace_filepath: "/var/run/netns/production" + + # Use filter_chain_matcher to route based on network namespace. + filter_chain_matcher: + matcher_tree: + input: + name: envoy.matching.inputs.network_namespace + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.NetworkNamespaceInput + exact_match_map: + map: + # Route traffic from namespace 'production' to production filter chain. + "/var/run/netns/production": + action: + name: production_chain + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: production_chain + # Route traffic from namespace 'staging' to staging filter chain. + "/var/run/netns/staging": + action: + name: staging_chain + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: staging_chain + # Route traffic from namespace 'development' to development filter chain. + "/var/run/netns/development": + action: + name: development_chain + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: development_chain + # Define filter chains for Production environment. + filter_chains: + - name: production_chain + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: production_hcm + route_config: + name: production_routes + virtual_hosts: + - name: production_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: production_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Define filter chains for Staging environment. + - name: staging_chain + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: staging_hcm + route_config: + name: staging_routes + virtual_hosts: + - name: staging_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: staging_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Define filter chains for Development environment. + - name: development_chain + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: development_hcm + route_config: + name: development_routes + virtual_hosts: + - name: development_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: development_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Default filter chain for connections from unknown namespaces or non-Linux platforms. + default_filter_chain: + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: default_hcm + route_config: + name: default_routes + virtual_hosts: + - name: default_service + domains: ["*"] + routes: + - match: + prefix: "/" + direct_response: + status: 503 + body: + inline_string: "Service not available from this network namespace" + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Multiple listeners with different network namespaces. + - name: production_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 + network_namespace_filepath: "/var/run/netns/production" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: production_dedicated + route_config: + name: production_dedicated_routes + virtual_hosts: + - name: production_dedicated_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: production_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + - name: staging_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 # Same port, different namespace + network_namespace_filepath: "/var/run/netns/staging" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: staging_dedicated + route_config: + name: staging_dedicated_routes + virtual_hosts: + - name: staging_dedicated_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: staging_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: production_cluster + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: production_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: production-backend + port_value: 80 + + - name: staging_cluster + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: staging_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: staging-backend + port_value: 80 + + - name: development_cluster + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: development_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: development-backend + port_value: 80 diff --git a/docs/root/configuration/observability/access_log/usage.rst b/docs/root/configuration/observability/access_log/usage.rst index dcda517865165..f46144b0fb577 100644 --- a/docs/root/configuration/observability/access_log/usage.rst +++ b/docs/root/configuration/observability/access_log/usage.rst @@ -351,6 +351,13 @@ The following command operators are supported: UDP Total number of HTTP header bytes sent to the upstream stream, For UDP tunneling flows. Not supported for non-tunneling. +%UPSTREAM_DECOMPRESSED_HEADER_BYTES_SENT% + HTTP + Number of decompressed header bytes sent to the upstream by the http stream. + + TCP/UDP + Not implemented (0). + %UPSTREAM_HEADER_BYTES_RECEIVED% HTTP Number of header bytes received from the upstream by the http stream. @@ -361,6 +368,14 @@ The following command operators are supported: UDP Total number of HTTP header bytes received from the upstream stream, For UDP tunneling flows. Not supported for non-tunneling. +%UPSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED% + HTTP + Number of decompressed header bytes received from the upstream by the http stream. + + TCP/UDP + Not implemented (0). + + %DOWNSTREAM_WIRE_BYTES_SENT% HTTP Total number of bytes sent to the downstream by the http stream. @@ -388,6 +403,13 @@ The following command operators are supported: TCP/UDP Not implemented (0). +%DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_SENT% + HTTP + Number of decompressed header bytes sent to the downstream by the http stream. + + TCP/UDP + Not implemented (0). + %DOWNSTREAM_HEADER_BYTES_RECEIVED% HTTP Number of header bytes received from the downstream by the http stream. @@ -397,6 +419,13 @@ The following command operators are supported: Renders a numeric value in typed JSON logs. +%DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED% + HTTP + Number of decompressed header bytes received from the downstream by the http stream. + + TCP/UDP + Not implemented (0). + .. _config_access_log_format_duration: %DURATION% diff --git a/docs/root/configuration/observability/statistics.rst b/docs/root/configuration/observability/statistics.rst index 930414a929581..fb01aaadab0d2 100644 --- a/docs/root/configuration/observability/statistics.rst +++ b/docs/root/configuration/observability/statistics.rst @@ -31,6 +31,7 @@ Server related statistics are rooted at *server.* with following statistics: initialization_time_ms, Histogram, Total time taken for Envoy initialization in milliseconds. This is the time from server start-up until the worker threads are ready to accept new connections debug_assertion_failures, Counter, Number of debug assertion failures detected in a release build if compiled with ``--define log_debug_assert_in_release=enabled`` or zero otherwise envoy_bug_failures, Counter, Number of envoy bug failures detected in a release build. File or report the issue if this increments as this may be serious. + envoy_notifications, Counter, Number of envoy notifications detected. File or report the issue if this increments as this may be serious. Please include logs from the ``notification`` component at the debug level. See :ref:`command line option --component-log-level ` for details. static_unknown_fields, Counter, Number of messages in static configuration with unknown fields dynamic_unknown_fields, Counter, Number of messages in dynamic configuration with unknown fields wip_protos, Counter, Number of messages and fields marked as work-in-progress being used diff --git a/docs/root/configuration/operations/overload_manager/overload_manager.rst b/docs/root/configuration/operations/overload_manager/overload_manager.rst index 9f7229b02db1a..86a7deaf7557f 100644 --- a/docs/root/configuration/operations/overload_manager/overload_manager.rst +++ b/docs/root/configuration/operations/overload_manager/overload_manager.rst @@ -218,6 +218,14 @@ The following core load shed points are supported: rejected by this load shed point and there is no available capacity to serve the downstream request, the downstream request will fail. + * - envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch + - Envoy will send a ``GOAWAY`` while processing HTTP2 requests at the codec + level AND immediately force close the downstream connection. If both this and + ``http2_server_go_away_on_dispatch`` are configured and shouldShedLoad() + returns true for both, this takes precedence. This is a disruptive action + (causes downstream connections to ungracefully close) that should only be + used with a very high threshold (if at all). + .. _config_overload_manager_reducing_timeouts: Reducing timeouts diff --git a/docs/root/configuration/operations/tools/router_check.rst b/docs/root/configuration/operations/tools/router_check.rst index a8eb4013e6dd4..b02522e30c8b6 100644 --- a/docs/root/configuration/operations/tools/router_check.rst +++ b/docs/root/configuration/operations/tools/router_check.rst @@ -47,6 +47,49 @@ This test case asserts that GET requests to ``api.lyft.com/api/locations`` are r validate: cluster_name: instant-server +Dynamic metadata example +------------------------ + +This test case demonstrates how to test routes that use dynamic metadata matchers. The test sets dynamic metadata +and verifies that the route with the matching dynamic metadata condition is selected. + +.. code-block:: yaml + + tests: + - test_name: dynamic_metadata_test + input: + authority: api.lyft.com + path: /example + method: GET + dynamic_metadata: + - metadata_namespace: example.meta + value: + foo: bar + validate: + cluster_name: cluster2 + virtual_host_name: default + +The corresponding route configuration would need to include a dynamic metadata matcher: + +.. code-block:: yaml + + virtual_hosts: + - name: default + domains: + - 'api.lyft.com' + routes: + - route: + cluster: cluster2 + match: + path: /example + dynamic_metadata: + - filter: example.meta + path: + - key: foo + value: + string_match: + exact: bar + Available test parameters ------------------------- @@ -68,6 +111,11 @@ Available test parameters additional_response_headers: - key: ... value: ... + dynamic_metadata: + - metadata_namespace: ... + value: ... + typed_value: ... + allow_overwrite: ... validate: cluster_name: ... virtual_cluster_name: ... @@ -137,6 +185,23 @@ input value *(required, string)* The value of the header field to add. + dynamic_metadata + *(optional, array)* Dynamic metadata to be added to the request as input for route determination. + This allows testing routes that use :ref:`dynamic metadata matchers `. + Each metadata entry follows the :ref:`set_metadata filter schema `. + + metadata_namespace + *(required, string)* The namespace for the metadata (e.g., "example.meta"). + + value + *(optional, object)* The metadata value as a JSON object (e.g., {"foo": "bar"}). + + typed_value + *(optional, object)* The typed metadata value (alternative to value). + + allow_overwrite + *(optional, boolean)* Whether to allow overwriting existing metadata. Defaults to false. + validate *(required, object)* The validate object specifies the returned route parameters to match. At least one test parameter must be specified. Use "" (empty string) to indicate that no return value is expected. diff --git a/docs/root/configuration/other_features/other_features.rst b/docs/root/configuration/other_features/other_features.rst index ada8a284812d7..672bcf3069a19 100644 --- a/docs/root/configuration/other_features/other_features.rst +++ b/docs/root/configuration/other_features/other_features.rst @@ -8,6 +8,7 @@ Other features hyperscan internal_listener rate_limit + reverse_connection io_uring vcl wasm diff --git a/docs/root/configuration/other_features/reverse_tunnel.rst b/docs/root/configuration/other_features/reverse_tunnel.rst new file mode 100644 index 0000000000000..c16ba8005f9f7 --- /dev/null +++ b/docs/root/configuration/other_features/reverse_tunnel.rst @@ -0,0 +1,411 @@ +.. _config_reverse_connection: + +Reverse Tunnels +=============== + +Envoy supports reverse tunnels that enable establishing persistent connections from downstream Envoy instances +to upstream Envoy instances without requiring the upstream to be directly reachable from the downstream. +This feature is particularly useful in scenarios where downstream instances are behind NATs, firewalls, +or in private networks, and need to initiate connections to upstream instances in public networks or cloud environments. + +Reverse tunnels work by having the downstream Envoy initiate TCP connections to upstream Envoy instances +and keep them alive for reuse. These connections are established using a handshake protocol and can be +used for forwarding traffic from services behind upstream Envoy to downstream services behind the downstream Envoy. + +.. _config_reverse_tunnel_bootstrap: + +Reverse tunnels require the following extensions: + +1. **Downstream socket interface**: Registered as a bootstrap extension on initiator envoy to initiate and maintain reverse tunnels. +2. **Upstream socket interface**: Registered as a bootstrap extension on responder envoy to accept and manage reverse tunnels. +3. **Reverse tunnel network filter**: On responder Envoy to accept reverse tunnel requests. +4. **Reverse connection cluster**: Added on responder Envoy for each downstream envoy node that needs to be reached through reverse tunnels. + +.. _config_reverse_tunnel_configuration_files: + +Configuration Files +------------------- + +For practical examples and working configurations, see: + +* :repo:`Initiator Envoy configuration `: Configuration for the initiator Envoy (downstream) +* :repo:`Responder Envoy configuration `: Configuration for the responder Envoy (upstream) + +.. _config_reverse_tunnel_initiator: + +Initiator Configuration (Downstream Envoy) +------------------------------------------- + +The initiator Envoy (downstream) requires the following configuration components to establish reverse tunnels: + +Downstream Socket Interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. validated-code-block:: yaml + :type-name: envoy.config.bootstrap.v3.Bootstrap + + bootstrap_extensions: + - name: envoy.bootstrap.reverse_tunnel.downstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" + +This extension enables the initiator to initiate and manage reverse tunnels to the responder Envoy. + +Reverse Tunnel Listener +~~~~~~~~~~~~~~~~~~~~~~~~ + +The reverse tunnel listener triggers the reverse connection initiation to the upstream Envoy instance and encodes identity metadata for the local Envoy. It also contains the route configuration for downstream services reachable via reverse tunnels. + +.. validated-code-block:: yaml + :type-name: envoy.config.listener.v3.Listener + + name: reverse_conn_listener + address: + socket_address: + # Format: rc://src_node_id:src_cluster_id:src_tenant_id@remote_cluster:connection_count + address: "rc://downstream-node:downstream-cluster:downstream-tenant@upstream-cluster:1" + port_value: 0 + # Use custom resolver that can parse reverse connection metadata + resolver_name: "envoy.resolvers.reverse_connection" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_conn_listener + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/downstream_service' + route: + cluster: downstream-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + +The special ``rc://`` address format encodes: + +* ``src_node_id``: "downstream-node" - Unique identifier for this downstream node +* ``src_cluster_id``: "downstream-cluster" - Cluster name of the downstream Envoy +* ``src_tenant_id``: "downstream-tenant" - Tenant identifier +* ``remote_cluster``: "upstream-cluster" - Name of the upstream cluster to connect to +* ``connection_count``: "1" - Number of reverse connections to establish + +The 'downstream-service' cluster is the service behind initiator envoy that will be accessed via reverse tunnels from behind the responder envoy. + +.. validated-code-block:: yaml + :type-name: envoy.config.cluster.v3.Cluster + + name: downstream-service + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: downstream-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: downstream-service + port_value: 80 + +Upstream Cluster +~~~~~~~~~~~~~~~~~ + +Each upstream envoy to which reverse tunnels should be established needs to be configured with a cluster, added via CDS. + +.. validated-code-block:: yaml + :type-name: envoy.config.cluster.v3.Cluster + + name: upstream-cluster + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: upstream-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: upstream-envoy # Responder Envoy address + port_value: 9000 # Port where responder listens for reverse tunnel requests + +Multiple Cluster Support +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To initiate reverse tunnels to multiple upstream clusters, each such cluster needs to be configured under an additional address section. + +.. validated-code-block:: yaml + :type-name: envoy.config.listener.v3.Listener + + name: multi_cluster_listener + address: + socket_address: + address: "rc://node-1:downstream-cluster:tenant-a@cluster-a:2" + port_value: 0 + additional_addresses: + - address: + socket_address: + address: "rc://node-1:downstream-cluster:tenant-a@cluster-b:3" + port_value: 0 + filter_chains: + - filters: + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcp + cluster: dynamic_cluster + +This configuration establishes: + +* 2 connections to ``cluster-a`` +* 3 connections to ``cluster-b`` + +TLS Configuration (Optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For secure reverse tunnel establishment, add a TLS context to the upstream cluster: + +.. validated-code-block:: yaml + :type-name: envoy.config.cluster.v3.Cluster + + name: upstream-cluster + type: STRICT_DNS + connect_timeout: 30s + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + tls_certificates: + - certificate_chain: + filename: "/etc/ssl/certs/client-cert.pem" + private_key: + filename: "/etc/ssl/private/client-key.pem" + validation_context: + filename: "/etc/ssl/certs/ca-cert.pem" + verify_certificate_spki: + - "NdQcW/8B5PcygH/5tnDNXeA2WS/2JzV3K1PKz7xQlKo=" + alpn_protocols: ["h2", "http/1.1"] + sni: upstream-envoy.example.com + +This configuration enables mTLS authentication between the downstream and upstream Envoys. + +.. _config_reverse_tunnel_responder: + +Responder Configuration (Upstream Envoy) +----------------------------------------- + +The responder Envoy (upstream) requires the following configuration components to accept reverse tunnels: + +Bootstrap Extension for Socket Interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. validated-code-block:: yaml + :type-name: envoy.config.bootstrap.v3.Bootstrap + + bootstrap_extensions: + - name: envoy.bootstrap.reverse_tunnel.upstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3.UpstreamReverseConnectionSocketInterface + stat_prefix: "upstream_reverse_connection" + +This extension enables the responder to accept and manage reverse connections from initiator Envoys. + +Reverse Tunnel Network Filter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The reverse tunnel network filter implements the reverse tunnel handshake protocol and accepts or rejects reverse tunnel requests: + +.. validated-code-block:: yaml + :type-name: envoy.config.listener.v3.Listener + + name: rev_conn_api_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 9000 # Port where initiator will connect for tunnel establishment + filter_chains: + - filters: + - name: envoy.filters.network.reverse_tunnel + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel + ping_interval: 2s + +The ``envoy.filters.network.reverse_tunnel`` network filter handles the reverse tunnel handshake protocol and connection acceptance. + +.. _config_reverse_connection_handshake: + +Handshake Protocol +~~~~~~~~~~~~~~~~~~ + +Reverse tunnels use a handshake protocol to establish authenticated connections between +downstream and upstream Envoy instances. The handshake has the following steps: + +1. **Connection Initiation**: Initiator Envoy initiates TCP connections to each host of the upstream cluster, +and writes the handshake request on it over HTTP. +2. **Identity Exchange**: The downstream Envoy's reverse tunnel handshake contains identity information (node ID, cluster ID, tenant ID) sent as HTTP headers. The reverse tunnel network filter expects the following headers: + + * ``x-envoy-reverse-tunnel-node-id``: Unique identifier for the downstream node (e.g., "on-prem-node") + * ``x-envoy-reverse-tunnel-cluster-id``: Cluster name of the downstream Envoy (e.g., "on-prem") + * ``x-envoy-reverse-tunnel-tenant-id``: Tenant identifier for multi-tenant deployments (e.g., "on-prem") + + These identify values are obtained from the reverse tunnel listener address and the headers are automatically added by the reverse tunnel downstream socket interface during the handshake process. +3. **Validation/Authentication**: The upstream Envoy performs the following validation checks on receiving the handshake request: + + * **HTTP Method Validation**: Verifies the request method matches the configured method (defaults to ``GET``) + * **HTTP Path Validation**: Verifies the request path matches the configured path (defaults to ``/reverse_connections/request``) + * **Required Headers Validation**: Ensures all three required identity headers are present: + + - ``x-envoy-reverse-tunnel-node-id`` + - ``x-envoy-reverse-tunnel-cluster-id`` + - ``x-envoy-reverse-tunnel-tenant-id`` + + If any validation fails, the request is rejected with appropriate HTTP error codes (404 for method/path mismatch, 400 for missing headers). +4. **Connection Establishment**: Post a successful handshake, the upstream Envoy stores the TCP socket mapped to the downstream node ID. + +.. _config_reverse_connection_cluster: + +Reverse Connection Cluster +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each downstream node reachable from upstream Envoy via reverse connections needs to be configured with a reverse connection cluster. When a data request arrives at the upstream Envoy, this cluster uses cached "reverse connections" instead of creating new forward connections. + +.. code-block:: yaml + :type-name: envoy.config.cluster.v3.Cluster + + name: reverse_connection_cluster + connect_timeout: 200s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + http_header_names: + - x-remote-node-id # Should be set to "downstream-node" + - x-dst-cluster-uuid # Should be set to "downstream-cluster" + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} # HTTP/2 required for reverse connections + +The reverse connection cluster configuration specifies: + +* **Load balancing policy**: ``CLUSTER_PROVIDED`` allows the custom cluster to manage load balancing +* **Header Resolution Strategy**: The cluster follows a tiered approach to identify the target downstream node: + + 1. **Configured Headers**: First checks for headers specified in ``http_header_names`` configuration. If not configured, defaults to ``x-envoy-dst-node-uuid`` and ``x-envoy-dst-cluster-uuid`` + 2. **Host Header**: If no configured headers are found, extracts UUID from the Host header in format ``.tcpproxy.envoy.remote:`` + 3. **SNI (Server Name Indication)**: If Host header extraction fails, extracts UUID from SNI in format ``.tcpproxy.envoy.remote`` +* **Protocol**: Only HTTP/2 is supported for reverse connections +* **Host Reuse**: Once a host is created for a specific downstream node ID, it is cached and reused for all subsequent requests to that node. Each such request is multiplexed as a new stream on the existing HTTP/2 connection. + + +Egress Listener for Data Traffic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add an egress listener on upstream envoy that accepts data traffic and routes it to the reverse connection cluster. + +.. validated-code-block:: yaml + :type-name: envoy.config.listener.v3.Listener + + name: egress_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 8085 # Port for sending requests to initiator services + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + route_config: + virtual_hosts: + - name: backend + domains: ["*"] + routes: + - match: + prefix: "/downstream_service" + route: + cluster: reverse_connection_cluster # Routes to initiator via reverse tunnel + +.. _config_reverse_connection_stats: + +Statistics +---------- + +The reverse tunnel extensions emit the following statistics: + +**Reverse Tunnel Filter:** + +The reverse tunnel network filter emits handshake-related statistics with the prefix ``reverse_tunnel.handshake.``: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + reverse_tunnel.handshake.parse_error, Counter, Number of handshake requests with missing required headers + reverse_tunnel.handshake.accepted, Counter, Number of successfully accepted reverse tunnel connections + reverse_tunnel.handshake.rejected, Counter, Number of rejected reverse tunnel connections + +**Downstream Socket Interface:** + +The downstream reverse tunnel extension emits both host-level and cluster-level statistics for connection states. The stat names follow the pattern: + +- Host-level: ``.host..`` +- Cluster-level: ``.cluster..`` + +Where ```` can be one of: + +.. csv-table:: + :header: State, Type, Description + :widths: 1, 1, 2 + + connecting, Gauge, Number of connections currently being established + connected, Gauge, Number of successfully established connections + failed, Gauge, Number of failed connection attempts + recovered, Gauge, Number of connections that recovered from failure + backoff, Gauge, Number of hosts currently in backoff state + cannot_connect, Gauge, Number of connection attempts that could not be initiated + unknown, Gauge, Number of connections in unknown state (fallback) + +For example, with ``stat_prefix: "downstream_rc"``: +- ``downstream_rc.host.192.168.1.1.connecting`` - connections being established to host 192.168.1.1 +- ``downstream_rc.cluster.upstream-cluster.connected`` - established connections to upstream-cluster + +**Upstream Socket Interface:** + +The upstream reverse tunnel extension emits node-level and cluster-level statistics for accepted connections. The stat names follow the pattern: + +- Node-level: ``reverse_connections.nodes.`` +- Cluster-level: ``reverse_connections.clusters.`` + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + reverse_connections.nodes., Gauge, Number of active connections from downstream node + reverse_connections.clusters., Gauge, Number of active connections from downstream cluster + +For example: +- ``reverse_connections.nodes.node-1`` - active connections from downstream node "node-1" +- ``reverse_connections.clusters.downstream-cluster`` - active connections from downstream cluster "downstream-cluster" + +.. _config_reverse_connection_security: + +Security Considerations +----------------------- + +Reverse tunnels should be used with appropriate security measures: + +* **Authentication**: Implement proper authentication mechanisms for handshake validation as part of the reverse tunnel handshake protocol. +* **Authorization**: Validate that downstream nodes are authorized to connect to upstream clusters. +* **TLS**: TLS can be configured for each upstream cluster reverse tunnels are established to. + diff --git a/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst b/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst index 1a6426c522ed8..4303a5edd34b6 100644 --- a/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst +++ b/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst @@ -82,6 +82,7 @@ Every cluster has a statistics tree rooted at *cluster..* with the followi upstream_rq_pending_overflow, Counter, Total requests that overflowed connection pool or requests (mainly for HTTP/2 and above) circuit breaking and were failed upstream_rq_pending_failure_eject, Counter, Total requests that were failed due to a connection pool connection failure or remote connection termination upstream_rq_pending_active, Gauge, Total active requests pending a connection pool connection + upstream_rq_per_cx, Histogram, Number of requests handled per upstream connection for all HTTP protocols upstream_rq_cancelled, Counter, Total requests cancelled before obtaining a connection pool connection upstream_rq_maintenance_mode, Counter, Total requests that resulted in an immediate 503 due to :ref:`maintenance mode` upstream_rq_timeout, Counter, Total requests that timed out waiting for a response diff --git a/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst b/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst index 695b8b80edf1a..9e39057fabd53 100644 --- a/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst +++ b/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst @@ -40,13 +40,13 @@ logic for a specific key. Incoming config metadata (via xDS) is converted to class objects at config load time. Filters can then obtain a typed variant of the metadata at runtime (per request or connection), thereby eliminating the need for filters to repeatedly convert from -``ProtobufWkt::Struct`` to some internal object during request/connection +``Protobuf::Struct`` to some internal object during request/connection processing. For example, a filter that desires to have a convenience wrapper class over an opaque metadata with key ``xxx.service.policy`` in ``ClusterInfo`` could register a factory ``ServicePolicyFactory`` that inherits from -``ClusterTypedMetadataFactory``. The factory translates the ``ProtobufWkt::Struct`` +``ClusterTypedMetadataFactory``. The factory translates the ``Protobuf::Struct`` into an instance of ``ServicePolicy`` class (inherited from ``FilterState::Object``). When a ``Cluster`` is created, the associated ``ServicePolicy`` instance will be created and cached. Note that typed diff --git a/docs/root/intro/arch_overview/advanced/dynamic_modules.rst b/docs/root/intro/arch_overview/advanced/dynamic_modules.rst index 55d6a3aa487be..0d56fb54d6804 100644 --- a/docs/root/intro/arch_overview/advanced/dynamic_modules.rst +++ b/docs/root/intro/arch_overview/advanced/dynamic_modules.rst @@ -5,8 +5,7 @@ Dynamic modules .. attention:: - The dynamic modules feature is experimental and is currently under active development. - + The dynamic modules feature is currently under active development. Capabilities will be expanded over time and it still lacks some features that are available in other extension mechanisms. We are looking for feedback from the community to improve the feature. diff --git a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst index d930df331dfe8..a20991336f420 100644 --- a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst +++ b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst @@ -49,6 +49,7 @@ These input functions are available for matching TCP connections and HTTP reques * :ref:`Direct source IP `. * :ref:`Source type `. * :ref:`Server name `. +* :ref:`Network namespace `. * :ref:`Filter state `. These input functions are available for matching TCP connections: @@ -80,9 +81,9 @@ Custom Matching Algorithms In addition to the built-in exact and prefix matchers, these custom matchers are available in some contexts: -.. _extension_envoy.matching.custom_matchers.trie_matcher: +.. _extension_envoy.matching.custom_matchers.ip_range_matcher: -* :ref:`Trie-based IP matcher ` applies to network inputs. +* :ref:`IP range matcher ` applies to network inputs. .. _extension_envoy.matching.custom_matchers.domain_matcher: diff --git a/docs/root/intro/arch_overview/observability/tracing.rst b/docs/root/intro/arch_overview/observability/tracing.rst index 9d5fb69374c03..3e5afe4aa5b8d 100644 --- a/docs/root/intro/arch_overview/observability/tracing.rst +++ b/docs/root/intro/arch_overview/observability/tracing.rst @@ -100,6 +100,19 @@ Alternatively the trace context can be manually propagated by the service: request. In addition, the single :ref:`config_http_conn_man_headers_b3` header propagation format is supported, which is a more compressed format. + The Zipkin tracer can optionally be configured to support both B3 and W3C trace context formats + for improved interoperability. This is controlled by the + :ref:`trace_context_option ` configuration option. + When set to ``USE_B3_WITH_W3C_PROPAGATION``, the tracer will: + + - For downstream requests: Extract trace context from B3 headers first, fallback to W3C trace headers + (:ref:`traceparent ` and + :ref:`tracestate `) when B3 headers are not present. + - For upstream requests: Inject both B3 and W3C trace headers to maximize compatibility. + + This option is disabled by default (``USE_B3``) to maintain backward compatibility, where only + B3 headers are used for both extraction and injection. + * When using the Datadog tracer, Envoy relies on the service to propagate the Datadog-specific HTTP headers ( :ref:`config_http_conn_man_headers_x-datadog-trace-id`, diff --git a/docs/root/intro/arch_overview/operations/hot_restart.rst b/docs/root/intro/arch_overview/operations/hot_restart.rst index 6c98220cf6562..8a6f09eabafe7 100644 --- a/docs/root/intro/arch_overview/operations/hot_restart.rst +++ b/docs/root/intro/arch_overview/operations/hot_restart.rst @@ -6,8 +6,9 @@ Hot restart Ease of operation is one of the primary goals of Envoy. In addition to robust statistics and a local administration interface, Envoy has the ability to “hot” or “live” restart itself. This means that Envoy can fully reload itself (both code and configuration) without dropping existing connections -during the :ref:`drain process `. The hot restart functionality has the -following general architecture: +during the :ref:`drain process `. However, existing connections are not +transferred to the new envoy process: they must complete during the drain process or be terminated. +The hot restart functionality has the following general architecture: * The two active processes communicate with each other over unix domain sockets using a basic RPC protocol. All counters are sent from the old process to the new process over the unix domain, and @@ -21,8 +22,12 @@ following general architecture: * During the draining phase, the old process attempts to gracefully close existing connections. How this is done depends on the configured filters. The drain time is configurable via the :option:`--drain-time-s` option and as more time passes draining becomes more aggressive. -* After drain sequence, the new Envoy process tells the old Envoy process to shut itself down. - This time is configurable via the :option:`--parent-shutdown-time-s` option. +* Later, usually after the drain sequence, the new Envoy process tells the old Envoy process to shut + itself down. This time is configurable via the :option:`--parent-shutdown-time-s` option. Note + that the `--parent-shutdown-time-s` option is independent of the `--drain-time-s` value, and so + the parent shutdown time should be set to a larger value. +* Any remaining connections to the old envoy process are closed. The hot restart functionality + does not transfer existing connections to the new process. * Envoy’s hot restart support was designed so that it will work correctly even if the new Envoy process and the old Envoy process are running inside different containers. Communication between the processes takes place only using unix domain sockets. diff --git a/docs/root/intro/arch_overview/other_protocols/redis.rst b/docs/root/intro/arch_overview/other_protocols/redis.rst index 1192ac0ba6f80..42f851309a653 100644 --- a/docs/root/intro/arch_overview/other_protocols/redis.rst +++ b/docs/root/intro/arch_overview/other_protocols/redis.rst @@ -170,12 +170,21 @@ For details on each command's usage see the official TTL, Generic TYPE, Generic UNLINK, Generic + COPY, Generic + RENAME, Generic + RENAMENX, Generic + SORT, Generic + SORT_RO, Generic GEOADD, Geo GEODIST, Geo GEOHASH, Geo GEOPOS, Geo GEORADIUS_RO, Geo GEORADIUSBYMEMBER_RO, Geo + GEOSEARCH, Geo + GEOSEARCHSTORE, Geospatial + GEORADIUS, Geospatial + GEORADIUSBYMEMBER, Geospatial HDEL, Hash HEXISTS, Hash HGET, Hash @@ -191,8 +200,10 @@ For details on each command's usage see the official HSETNX, Hash HSTRLEN, Hash HVALS, Hash + HRANDFIELD, Hash PFADD, HyperLogLog PFCOUNT, HyperLogLog + PFMERGE, HyperLogLog LINDEX, List LINSERT, List LLEN, List @@ -203,6 +214,8 @@ For details on each command's usage see the official LREM, List LSET, List LTRIM, List + LPOS, List + RPOPLPUSH, List MULTI, Transaction RPOP, List RPUSH, List @@ -219,6 +232,14 @@ For details on each command's usage see the official SREM, Set SCAN, Generic SSCAN, Set + SDIFF, Set + SDIFFSTORE, Set + SINTER, Set + SINTERSTORE, Set + SMISMEMBER, Set + SMOVE, Set + SUNION, Set + SUNIONSTORE, Set WATCH, String UNWATCH, String ZADD, Sorted Set @@ -242,6 +263,15 @@ For details on each command's usage see the official ZPOPMAX, Sorted Set ZSCAN, Sorted Set ZSCORE, Sorted Set + ZDIFF, Sorted Set + ZDIFFSTORE, Sorted Set + ZINTER, Sorted Set + ZINTERSTORE, Sorted Set + ZMSCORE, Sorted Set + ZRANDMEMBER, Sorted Set + ZRANGESTORE, Sorted Set + ZUNION, Sorted Set + ZUNIONSTORE, Sorted Set APPEND, String BITCOUNT, String BITFIELD, String @@ -251,12 +281,14 @@ For details on each command's usage see the official GET, String GETBIT, String GETDEL, String + GETEX, String GETRANGE, String GETSET, String INCR, String INCRBY, String INCRBYFLOAT, String INFO, Server + ROLE, Server MGET, String MSET, String PSETEX, String @@ -266,6 +298,8 @@ For details on each command's usage see the official SETNX, String SETRANGE, String STRLEN, String + MSETNX, String + SUBSTR, String XACK, Stream XADD, Stream XAUTOCLAIM, Stream @@ -286,6 +320,7 @@ For details on each command's usage see the official BF.MEXISTS, Bloom BF.RESERVE, Bloom BF.SCANDUMP, Bloom + BITOP, Bitmap Failure modes ------------- diff --git a/docs/root/operations/admin.rst b/docs/root/operations/admin.rst index 09e77e7c5e3f8..059bbb83b2a07 100644 --- a/docs/root/operations/admin.rst +++ b/docs/root/operations/admin.rst @@ -177,7 +177,7 @@ modify different aspects of the server: .. http:get:: /config_dump?mask={} Specify a subset of fields that you would like to be returned. The mask is parsed as a - ``ProtobufWkt::FieldMask`` and applied to each top level dump such as + ``Protobuf::FieldMask`` and applied to each top level dump such as :ref:`BootstrapConfigDump ` and :ref:`ClustersConfigDump `. This behavior changes if both resource and mask query parameters are specified. See @@ -225,7 +225,7 @@ modify different aspects of the server: When both resource and mask query parameters are specified, the mask is applied to every element in the desired repeated field so that only a subset of fields are returned. The mask is parsed - as a ``ProtobufWkt::FieldMask``. + as a ``Protobuf::FieldMask``. For example, get the names of all active dynamic clusters with ``/config_dump?resource=dynamic_active_clusters&mask=cluster.name`` @@ -288,7 +288,7 @@ modify different aspects of the server: .. http:get:: /init_dump?mask={} When mask query parameters is specified, the mask value is the desired component to dump unready targets. - The mask is parsed as a ``ProtobufWkt::FieldMask``. + The mask is parsed as a ``Protobuf::FieldMask``. For example, get the unready targets of all listeners with ``/init_dump?mask=listener`` diff --git a/docs/versions.yaml b/docs/versions.yaml index 2f24bc759d9e0..44adf4d0f9317 100644 --- a/docs/versions.yaml +++ b/docs/versions.yaml @@ -24,7 +24,8 @@ "1.28": 1.28.7 "1.29": 1.29.12 "1.30": 1.30.11 -"1.31": 1.31.9 -"1.32": 1.32.7 -"1.33": 1.33.4 -"1.34": 1.34.2 +"1.31": 1.31.10 +"1.32": 1.32.12 +"1.33": 1.33.9 +"1.34": 1.34.7 +"1.35": 1.35.3 diff --git a/envoy/access_log/access_log_config.h b/envoy/access_log/access_log_config.h index aca39ccbe5d90..cfe53571282e4 100644 --- a/envoy/access_log/access_log_config.h +++ b/envoy/access_log/access_log_config.h @@ -26,7 +26,7 @@ class ExtensionFilterFactory : public Config::TypedFactory { * @return an instance of extension filter implementation from a config proto. */ virtual FilterPtr createFilter(const envoy::config::accesslog::v3::ExtensionFilter& config, - Server::Configuration::FactoryContext& context) PURE; + Server::Configuration::GenericFactoryContext& context) PURE; std::string category() const override { return "envoy.access_loggers.extension_filters"; } }; @@ -50,7 +50,7 @@ class AccessLogInstanceFactory : public Config::TypedFactory { */ virtual AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) PURE; std::string category() const override { return "envoy.access_loggers"; } diff --git a/envoy/buffer/buffer.h b/envoy/buffer/buffer.h index bd53cade2a8d0..0e0a7f7b2ab8f 100644 --- a/envoy/buffer/buffer.h +++ b/envoy/buffer/buffer.h @@ -506,13 +506,13 @@ class Instance { * If set to non-zero, overflow callbacks will be called if the * buffered data exceeds watermark * overflow_multiplier. */ - virtual void setWatermarks(uint32_t watermark, uint32_t overflow_multiplier = 0) PURE; + virtual void setWatermarks(uint64_t watermark, uint32_t overflow_multiplier = 0) PURE; /** * Returns the configured high watermark. A return value of 0 indicates that watermark * functionality is disabled. */ - virtual uint32_t highWatermark() const PURE; + virtual uint64_t highWatermark() const PURE; /** * Determine if the buffer watermark trigger condition is currently set. The watermark trigger is * set when the buffer size exceeds the configured high watermark and is cleared once the buffer diff --git a/envoy/common/conn_pool.h b/envoy/common/conn_pool.h index 8fd7f1061bf95..14c5b05c7c1c0 100644 --- a/envoy/common/conn_pool.h +++ b/envoy/common/conn_pool.h @@ -49,6 +49,9 @@ enum class DrainBehavior { // all new streams take place on a new connection. For example, when a health check failure // occurs. DrainExistingConnections, + // Same as DrainExistingConnections, but only drains connections unable to migrate to a different + // network on mobile. + DrainExistingNonMigratableConnections, }; /** diff --git a/envoy/config/config_validator.h b/envoy/config/config_validator.h index 285995934a8f3..158bce05add20 100644 --- a/envoy/config/config_validator.h +++ b/envoy/config/config_validator.h @@ -60,7 +60,7 @@ class ConfigValidatorFactory : public Config::TypedFactory { * Creates a ConfigValidator using the given config. */ virtual ConfigValidatorPtr - createConfigValidator(const ProtobufWkt::Any& config, + createConfigValidator(const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor) PURE; std::string category() const override { return "envoy.config.validators"; } diff --git a/envoy/config/grpc_mux.h b/envoy/config/grpc_mux.h index 690b5463d93ac..d6408e1c907c6 100644 --- a/envoy/config/grpc_mux.h +++ b/envoy/config/grpc_mux.h @@ -120,8 +120,8 @@ class GrpcMux { * Updates the current gRPC-Mux object to use a new gRPC client, and config. */ virtual absl::Status - updateMuxSource(Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope& scope, + updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) PURE; }; diff --git a/envoy/config/subscription.h b/envoy/config/subscription.h index 6ca9dc2403b57..bb835a607db53 100644 --- a/envoy/config/subscription.h +++ b/envoy/config/subscription.h @@ -75,11 +75,11 @@ class OpaqueResourceDecoder { virtual ~OpaqueResourceDecoder() = default; /** - * @param resource some opaque resource (ProtobufWkt::Any). + * @param resource some opaque resource (Protobuf::Any). * @return ProtobufTypes::MessagePtr decoded protobuf message in the opaque resource, e.g. the * RouteConfiguration for an Any containing envoy.config.route.v3.RouteConfiguration. */ - virtual ProtobufTypes::MessagePtr decodeResource(const ProtobufWkt::Any& resource) PURE; + virtual ProtobufTypes::MessagePtr decodeResource(const Protobuf::Any& resource) PURE; /** * @param resource some opaque resource (Protobuf::Message). @@ -166,7 +166,7 @@ class UntypedConfigUpdateCallbacks { * is accepted. Accepted configurations have their version_info reflected in subsequent * requests. */ - virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) PURE; /** diff --git a/envoy/config/subscription_factory.h b/envoy/config/subscription_factory.h index c9977e7773e93..9fb51f6c4a823 100644 --- a/envoy/config/subscription_factory.h +++ b/envoy/config/subscription_factory.h @@ -140,8 +140,8 @@ class MuxFactory : public Config::UntypedFactory { std::string category() const override { return "envoy.config_mux"; } virtual void shutdownAll() PURE; virtual std::shared_ptr - create(std::unique_ptr&& async_client, - std::unique_ptr&& async_failover_client, + create(std::shared_ptr&& async_client, + std::shared_ptr&& async_failover_client, Event::Dispatcher& dispatcher, Random::RandomGenerator& random, Stats::Scope& scope, const envoy::config::core::v3::ApiConfigSource& ads_config, const LocalInfo::LocalInfo& local_info, diff --git a/envoy/config/typed_metadata.h b/envoy/config/typed_metadata.h index 059c563c782e9..bd93e0d0dbf22 100644 --- a/envoy/config/typed_metadata.h +++ b/envoy/config/typed_metadata.h @@ -63,7 +63,7 @@ class TypedMetadataFactory : public UntypedFactory { * @throw EnvoyException if the parsing can't be done. */ virtual std::unique_ptr - parse(const ProtobufWkt::Struct& data) const PURE; + parse(const Protobuf::Struct& data) const PURE; /** * Convert the google.protobuf.Any into an instance of TypedMetadata::Object. @@ -73,8 +73,7 @@ class TypedMetadataFactory : public UntypedFactory { * one doesn't implement parse() method. * @throw EnvoyException if the parsing can't be done. */ - virtual std::unique_ptr - parse(const ProtobufWkt::Any& data) const PURE; + virtual std::unique_ptr parse(const Protobuf::Any& data) const PURE; std::string category() const override { return "envoy.typed_metadata"; } }; diff --git a/envoy/config/xds_config_tracker.h b/envoy/config/xds_config_tracker.h index 5f0b3ddd740aa..d4bacc586c6b3 100644 --- a/envoy/config/xds_config_tracker.h +++ b/envoy/config/xds_config_tracker.h @@ -99,7 +99,7 @@ class XdsConfigTrackerFactory : public Config::TypedFactory { * Creates an XdsConfigTracker using the given config. */ virtual XdsConfigTrackerPtr - createXdsConfigTracker(const ProtobufWkt::Any& config, + createXdsConfigTracker(const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor, Api::Api& api, Event::Dispatcher& dispatcher) PURE; diff --git a/envoy/config/xds_manager.h b/envoy/config/xds_manager.h index 0207341f036f4..d1457e974cb62 100644 --- a/envoy/config/xds_manager.h +++ b/envoy/config/xds_manager.h @@ -77,6 +77,32 @@ class XdsManager { absl::string_view type_url, Stats::Scope& scope, SubscriptionCallbacks& callbacks, OpaqueResourceDecoderSharedPtr resource_decoder, const SubscriptionOptions& options) PURE; + /** + * Pause discovery requests for a given API type on all ADS types (both xdstp-based and "old" + * ADS). This is useful, for example, when we're processing an update for LDS or CDS and don't + * want a flood of updates for RDS or EDS respectively. Discovery requests may later be resumed + * with after the returned ScopedResume object is destroyed. + * @param type_url type URL corresponding to xDS API, e.g. + * type.googleapis.com/envoy.config.cluster.v3.Cluster. + * + * @return a ScopedResume object, which when destructed, resumes the paused discovery requests. + * A discovery request will be sent if one would have been sent during the pause. + */ + ABSL_MUST_USE_RESULT virtual ScopedResume pause(const std::string& type_url) PURE; + + /** + * Pause discovery requests for given API types on all ADS types (both xdstp-based and "old" ADS). + * This is useful, for example, when we're processing an update for LDS or CDS and don't want a + * flood of updates for RDS or EDS respectively. Discovery requests may later be resumed with + * after the returned ScopedResume object is destroyed. + * @param type_urls type URLs corresponding to xDS API, e.g. + * type.googleapis.com/envoy.config.cluster.v3.Cluster. + * + * @return a ScopedResume object, which when destructed, resumes the paused discovery requests. + * A discovery request will be sent if one would have been sent during the pause. + */ + ABSL_MUST_USE_RESULT virtual ScopedResume pause(const std::vector& type_urls) PURE; + /** * Shuts down the xDS-Manager and all the configured connections to the config * servers. diff --git a/envoy/config/xds_resources_delegate.h b/envoy/config/xds_resources_delegate.h index ca58cb05f1ce3..407c9059426ce 100644 --- a/envoy/config/xds_resources_delegate.h +++ b/envoy/config/xds_resources_delegate.h @@ -104,7 +104,7 @@ class XdsResourcesDelegateFactory : public Config::TypedFactory { * @return The created XdsResourcesDelegate instance */ virtual XdsResourcesDelegatePtr - createXdsResourcesDelegate(const ProtobufWkt::Any& config, + createXdsResourcesDelegate(const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor, Api::Api& api, Event::Dispatcher& dispatcher) PURE; diff --git a/envoy/event/scaled_timer.h b/envoy/event/scaled_timer.h index a307df73fded1..5e17869a01874 100644 --- a/envoy/event/scaled_timer.h +++ b/envoy/event/scaled_timer.h @@ -83,6 +83,9 @@ enum class ScaledTimerType { // The max time an HTTP connection to a downstream client can be connected at all. This // corresponds to the HTTP_DOWNSTREAM_CONNECTION_MAX TimerType in overload.proto. HttpDownstreamMaxConnectionTimeout, + // The max time the downstream codec will wait to flush an ended response stream. This corresponds + // to HTTP_DOWNSTREAM_STREAM_FLUSH TimerType in overload.proto. + HttpDownstreamStreamFlush, }; using ScaledTimerTypeMap = absl::flat_hash_map; diff --git a/envoy/formatter/http_formatter_context.h b/envoy/formatter/http_formatter_context.h index cdee09339cb84..8422a52db81ae 100644 --- a/envoy/formatter/http_formatter_context.h +++ b/envoy/formatter/http_formatter_context.h @@ -132,6 +132,15 @@ class HttpFormatterContext { */ AccessLogType accessLogType() const; + /** + * Set or overwrite the active span. + * @param active_span supplies the active span. + */ + HttpFormatterContext& setActiveSpan(const Tracing::Span& active_span) { + active_span_ = &active_span; + return *this; + } + /** * @return const Tracing::Span& the active span. */ @@ -163,7 +172,7 @@ class HttpFormatterContext { const Http::RequestHeaderMap* request_headers_{}; const Http::ResponseHeaderMap* response_headers_{}; const Http::ResponseTrailerMap* response_trailers_{}; - absl::string_view local_reply_body_{}; + absl::string_view local_reply_body_; AccessLogType log_type_{AccessLogType::NotSet}; const Tracing::Span* active_span_ = nullptr; OptRef extension_; diff --git a/envoy/formatter/substitution_formatter_base.h b/envoy/formatter/substitution_formatter_base.h index c9bd79aaaa6b8..115c89d92c144 100644 --- a/envoy/formatter/substitution_formatter_base.h +++ b/envoy/formatter/substitution_formatter_base.h @@ -55,10 +55,10 @@ class FormatterProvider { * Format the value with the given context and stream info. * @param context supplies the formatter context. * @param stream_info supplies the stream info. - * @return ProtobufWkt::Value containing a single value extracted from the given + * @return Protobuf::Value containing a single value extracted from the given * context and stream info. */ - virtual ProtobufWkt::Value + virtual Protobuf::Value formatValueWithContext(const Context& context, const StreamInfo::StreamInfo& stream_info) const PURE; }; diff --git a/envoy/grpc/async_client.h b/envoy/grpc/async_client.h index 2c4a2c1901ae6..0584da32c6f26 100644 --- a/envoy/grpc/async_client.h +++ b/envoy/grpc/async_client.h @@ -34,6 +34,21 @@ class AsyncRequest { * Returns the underlying stream info. */ virtual const StreamInfo::StreamInfo& streamInfo() const PURE; + + /** + * Detach the pending request. This is used for the case where we send a side + * request but never cancel it even if the related downstream main request is + * completed. + * + * This will will clean up all context associated with downstream request like + * downstream stream info, parent tracing span, and so on, to avoid potential + * dangling references. + * + * NOTE: the callbacks that registered to take the response will be kept to do + * some clean up or operations when response arrives. The caller is responsible + * for ensuring that the callbacks have enough lifetime. + */ + virtual void detach() PURE; }; /** diff --git a/envoy/http/BUILD b/envoy/http/BUILD index 68572e841b0f2..cb2b1a03695c9 100644 --- a/envoy/http/BUILD +++ b/envoy/http/BUILD @@ -40,10 +40,17 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "codec_runtime_overrides", + hdrs = ["codec_runtime_overrides.h"], + deps = ["@com_google_absl//absl/strings"], +) + envoy_cc_library( name = "codec_interface", hdrs = ["codec.h"], deps = [ + ":codec_runtime_overrides", ":header_map_interface", ":metadata_interface", ":protocol_interface", diff --git a/envoy/http/async_client.h b/envoy/http/async_client.h index 9afbc3f394336..3155e5af60323 100644 --- a/envoy/http/async_client.h +++ b/envoy/http/async_client.h @@ -343,8 +343,8 @@ class AsyncClient { // The retry policy can be set as either a proto or Router::RetryPolicy but // not both. If both formats of the options are set, the more recent call // will overwrite the older one. - StreamOptions& setRetryPolicy(const Router::RetryPolicy& p) { - parsed_retry_policy = &p; + StreamOptions& setRetryPolicy(Router::RetryPolicyConstSharedPtr p) { + parsed_retry_policy = std::move(p); retry_policy = absl::nullopt; return *this; } @@ -432,7 +432,7 @@ class AsyncClient { absl::optional buffer_limit_; absl::optional retry_policy; - const Router::RetryPolicy* parsed_retry_policy{nullptr}; + Router::RetryPolicyConstSharedPtr parsed_retry_policy; Router::FilterConfigSharedPtr filter_config_; diff --git a/envoy/http/codec.h b/envoy/http/codec.h index a00db67b8b390..3c23ea86b2636 100644 --- a/envoy/http/codec.h +++ b/envoy/http/codec.h @@ -10,6 +10,7 @@ #include "envoy/common/optref.h" #include "envoy/common/pure.h" #include "envoy/grpc/status.h" +#include "envoy/http/codec_runtime_overrides.h" #include "envoy/http/header_formatter.h" #include "envoy/http/header_map.h" #include "envoy/http/metadata_interface.h" @@ -37,20 +38,6 @@ namespace Http3 { struct CodecStats; } -// Legacy default value of 60K is safely under both codec default limits. -static constexpr uint32_t DEFAULT_MAX_REQUEST_HEADERS_KB = 60; -// Default maximum number of headers. -static constexpr uint32_t DEFAULT_MAX_HEADERS_COUNT = 100; - -constexpr absl::string_view MaxRequestHeadersCountOverrideKey = - "envoy.reloadable_features.max_request_headers_count"; -constexpr absl::string_view MaxResponseHeadersCountOverrideKey = - "envoy.reloadable_features.max_response_headers_count"; -constexpr absl::string_view MaxRequestHeadersSizeOverrideKey = - "envoy.reloadable_features.max_request_headers_size_kb"; -constexpr absl::string_view MaxResponseHeadersSizeOverrideKey = - "envoy.reloadable_features.max_response_headers_size_kb"; - class Stream; class RequestDecoder; diff --git a/envoy/http/codec_runtime_overrides.h b/envoy/http/codec_runtime_overrides.h new file mode 100644 index 0000000000000..e75e4d5992e2f --- /dev/null +++ b/envoy/http/codec_runtime_overrides.h @@ -0,0 +1,23 @@ +#pragma once + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Http { + +// Legacy default value of 60K is safely under both codec default limits. +static constexpr uint32_t DEFAULT_MAX_REQUEST_HEADERS_KB = 60; +// Default maximum number of headers. +static constexpr uint32_t DEFAULT_MAX_HEADERS_COUNT = 100; + +constexpr absl::string_view MaxRequestHeadersCountOverrideKey = + "envoy.reloadable_features.max_request_headers_count"; +constexpr absl::string_view MaxResponseHeadersCountOverrideKey = + "envoy.reloadable_features.max_response_headers_count"; +constexpr absl::string_view MaxRequestHeadersSizeOverrideKey = + "envoy.reloadable_features.max_request_headers_size_kb"; +constexpr absl::string_view MaxResponseHeadersSizeOverrideKey = + "envoy.reloadable_features.max_response_headers_size_kb"; + +} // namespace Http +} // namespace Envoy diff --git a/envoy/http/codes.h b/envoy/http/codes.h index 5da40a9ff137e..11393ab521a98 100644 --- a/envoy/http/codes.h +++ b/envoy/http/codes.h @@ -74,7 +74,9 @@ enum class Code : uint16_t { InsufficientStorage = 507, LoopDetected = 508, NotExtended = 510, - NetworkAuthenticationRequired = 511 + NetworkAuthenticationRequired = 511, + // 512-599 are unassigned server error codes. + LastUnassignedServerErrorCode = 599 // clang-format on }; diff --git a/envoy/http/filter.h b/envoy/http/filter.h index 91d59de40b3e7..1295ddfcc13e8 100644 --- a/envoy/http/filter.h +++ b/envoy/http/filter.h @@ -246,6 +246,14 @@ class UpstreamStreamFilterCallbacks { virtual bool pausedForWebsocketUpgrade() const PURE; virtual void setPausedForWebsocketUpgrade(bool value) PURE; + // Disable the route timeout after websocket upgrade completes successfully. + // This should only be used by the upstream codec filter. + virtual void disableRouteTimeoutForWebsocketUpgrade() PURE; + + // Disable per-try timeouts after websocket upgrade completes successfully. + // This should only be used by the upstream codec filter. + virtual void disablePerTryTimeoutForWebsocketUpgrade() PURE; + // Return the upstreamStreamOptions for this stream. virtual const Http::ConnectionPool::Instance::StreamOptions& upstreamStreamOptions() const PURE; @@ -766,7 +774,7 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { * * @param limit supplies the desired buffer limit. */ - virtual void setDecoderBufferLimit(uint32_t limit) PURE; + virtual void setDecoderBufferLimit(uint64_t limit) PURE; /** * This routine returns the current buffer limit for decoder filters. Filters should abide by @@ -775,7 +783,7 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { * * @return the buffer limit the filter should apply. */ - virtual uint32_t decoderBufferLimit() PURE; + virtual uint64_t decoderBufferLimit() PURE; /** * @return the account, if any, used by this stream. diff --git a/envoy/matcher/matcher.h b/envoy/matcher/matcher.h index 223ed538e5952..531f99cf15bc4 100644 --- a/envoy/matcher/matcher.h +++ b/envoy/matcher/matcher.h @@ -91,22 +91,20 @@ class Action { } }; -using ActionPtr = std::unique_ptr; -using ActionFactoryCb = std::function; +using ActionConstSharedPtr = std::shared_ptr; template class ActionFactory : public Config::TypedFactory { public: - virtual ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, - ActionFactoryContext& action_factory_context, - ProtobufMessage::ValidationVisitor& validation_visitor) PURE; + virtual ActionConstSharedPtr + createAction(const Protobuf::Message& config, ActionFactoryContext& action_factory_context, + ProtobufMessage::ValidationVisitor& validation_visitor) PURE; std::string category() const override { return "envoy.matching.action"; } }; // On match, we either return the action to perform or another match tree to match against. template struct OnMatch { - const ActionFactoryCb action_cb_; + const ActionConstSharedPtr action_; const MatchTreeSharedPtr matcher_; bool keep_matching_{}; }; @@ -130,30 +128,39 @@ template class OnMatchFactory { // - The match could not be completed due to lack of data (isInsufficientData() will return true.) // - The match was completed, no match found (isNoMatch() will return true.) // - The match was completed, match found (isMatch() will return true, action() will return the -// ActionFactoryCb.) +// ActionConstSharedPtr.) struct MatchResult { public: - MatchResult(ActionFactoryCb cb) : result_(std::move(cb)) {} + MatchResult(ActionConstSharedPtr cb) : result_(std::move(cb)) {} static MatchResult noMatch() { return MatchResult(NoMatch{}); } static MatchResult insufficientData() { return MatchResult(InsufficientData{}); } bool isInsufficientData() const { return absl::holds_alternative(result_); } bool isComplete() const { return !isInsufficientData(); } bool isNoMatch() const { return absl::holds_alternative(result_); } - bool isMatch() const { return absl::holds_alternative(result_); } - ActionFactoryCb actionFactory() const { return absl::get(result_); } - ActionPtr action() const { return actionFactory()(); } + bool isMatch() const { return absl::holds_alternative(result_); } + const ActionConstSharedPtr& action() const { + ASSERT(isMatch()); + return absl::get(result_); + } + // Returns the action by move. The caller must ensure that the MatchResult is not used after + // this call. + ActionConstSharedPtr actionByMove() { + ASSERT(isMatch()); + return absl::get(std::move(result_)); + } private: struct InsufficientData {}; struct NoMatch {}; - using Result = absl::variant; + using Result = absl::variant; Result result_; MatchResult(NoMatch) : result_(NoMatch{}) {} MatchResult(InsufficientData) : result_(InsufficientData{}) {} }; // Callback to execute against skipped matches' actions. -using SkippedMatchCb = std::function; +using SkippedMatchCb = std::function; + /** * MatchTree provides the interface for performing matches against the data provided by DataType. */ @@ -187,19 +194,19 @@ template class MatchTree { // Parent result's keep_matching skips the nested result. if (on_match->keep_matching_ && nested_result.isMatch()) { if (skipped_match_cb) { - skipped_match_cb(nested_result.actionFactory()); + skipped_match_cb(nested_result.action()); } return MatchResult::noMatch(); } return nested_result; } - if (on_match->action_cb_ && on_match->keep_matching_) { + if (on_match->action_ && on_match->keep_matching_) { if (skipped_match_cb) { - skipped_match_cb(on_match->action_cb_); + skipped_match_cb(on_match->action_); } return MatchResult::noMatch(); } - return MatchResult{on_match->action_cb_}; + return MatchResult{on_match->action_}; } }; diff --git a/envoy/network/address.h b/envoy/network/address.h index 0b2d22fd607f4..c7a9c51b8c5fa 100644 --- a/envoy/network/address.h +++ b/envoy/network/address.h @@ -100,6 +100,46 @@ class Ip { */ virtual bool isUnicastAddress() const PURE; + /** + * Determines whether the address is a link-local address. For IPv6, the prefix is fe80::/10. For + * IPv4, the prefix is 169.254.0.0/16. + * + * See https://datatracker.ietf.org/doc/html/rfc3513#section-2.4 for details. + * + * @return true if the address is a link-local address, false otherwise. + */ + virtual bool isLinkLocalAddress() const PURE; + + /** + * Determines whether the address is a Unique Local Address. Applies to IPv6 addresses only, where + * the prefix is fc00::/7. + * + * See https://datatracker.ietf.org/doc/html/rfc4193 for details. + * + * @return true if the address is a Unique Local Address, false otherwise. + */ + virtual bool isUniqueLocalAddress() const PURE; + + /** + * Determines whether the address is a Site-Local Address. Applies to IPv6 addresses only, where + * the prefix is fec0::/10. + * + * See https://datatracker.ietf.org/doc/html/rfc3513#section-2.4 for details. + * + * @return true if the address is a Site-Local Address, false otherwise. + */ + virtual bool isSiteLocalAddress() const PURE; + + /** + * Determines whether the address is a Teredo address. Applies to IPv6 addresses only, where the + * prefix is 2001:0000::/32. + * + * See https://datatracker.ietf.org/doc/html/rfc4380 for details. + * + * @return true if the address is a Teredo address, false otherwise. + */ + virtual bool isTeredoAddress() const PURE; + /** * @return Ipv4 address data IFF version() == IpVersion::v4, otherwise nullptr. */ diff --git a/envoy/network/connection.h b/envoy/network/connection.h index 0ae30475b9dc5..c8a2593ec8ade 100644 --- a/envoy/network/connection.h +++ b/envoy/network/connection.h @@ -342,6 +342,11 @@ class Connection : public Event::DeferredDeletable, */ virtual bool aboveHighWatermark() const PURE; + /** + * @return const ConnectionSocketPtr& reference to the socket from current connection. + */ + virtual const ConnectionSocketPtr& getSocket() const PURE; + /** * Get the socket options set on this connection. */ diff --git a/envoy/network/filter.h b/envoy/network/filter.h index 8684036d18b35..44226e7fa540d 100644 --- a/envoy/network/filter.h +++ b/envoy/network/filter.h @@ -355,7 +355,7 @@ class ListenerFilterCallbacks { * @param value the struct to set on the namespace. A merge will be performed with new values for * the same key overriding existing. */ - virtual void setDynamicMetadata(const std::string& name, const ProtobufWkt::Struct& value) PURE; + virtual void setDynamicMetadata(const std::string& name, const Protobuf::Struct& value) PURE; /** * @param name the namespace used in the metadata in reverse DNS format, for example: @@ -363,7 +363,7 @@ class ListenerFilterCallbacks { * @param value of type protobuf any to set on the namespace. A merge will be performed with new * values for the same key overriding existing. */ - virtual void setDynamicTypedMetadata(const std::string& name, const ProtobufWkt::Any& value) PURE; + virtual void setDynamicTypedMetadata(const std::string& name, const Protobuf::Any& value) PURE; /** * @return const envoy::config::core::v3::Metadata& the dynamic metadata associated with this @@ -440,6 +440,12 @@ class ListenerFilter { */ virtual FilterStatus onData(Network::ListenerFilterBuffer& buffer) PURE; + /** + * Called when the connection is closed. Only the current filter that has stopped filter + * chain iteration will get the callback. + */ + virtual void onClose() {}; + /** * Return the size of data the filter want to inspect from the connection. * The size can be increased after filter need to inspect more data. diff --git a/envoy/router/router.h b/envoy/router/router.h index 363f40acf668e..ea2b1fae6a6fd 100644 --- a/envoy/router/router.h +++ b/envoy/router/router.h @@ -57,6 +57,7 @@ class ResponseEntry { * @param stream_info holds additional information about the request. */ virtual void finalizeResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const PURE; /** @@ -197,6 +198,9 @@ class ResetHeaderParser { using ResetHeaderParserSharedPtr = std::shared_ptr; +class RetryPolicy; +using RetryPolicyConstSharedPtr = std::shared_ptr; + /** * Route level retry policy. */ @@ -677,14 +681,20 @@ class VirtualHost { virtual bool includeIsTimeoutRetryHeader() const PURE; /** - * @return uint32_t any route cap on bytes which should be buffered for shadowing or retries. - * This is an upper bound so does not necessarily reflect the bytes which will be buffered - * as other limits may apply. - * If a per route limit exists, it takes precedence over this configuration. - * Unlike some other buffer limits, 0 here indicates buffering should not be performed - * rather than no limit applies. + * @return uint64_t the maximum bytes which should be buffered for request bodies. This enables + * buffering larger request bodies beyond the connection buffer limit for use cases + * with large payloads, shadowing, or retries. + * + * This method consolidates the functionality of the previous + * per_request_buffer_limit_bytes and request_body_buffer_limit fields. It supports both + * legacy configurations using per_request_buffer_limit_bytes and new configurations using + * request_body_buffer_limit. + * + * If neither is set, falls back to connection buffer limits. Unlike some other buffer + * limits, 0 here indicates buffering should not be performed rather than no limit + * applies. */ - virtual uint32_t retryShadowBufferLimit() const PURE; + virtual uint64_t requestBodyBufferLimit() const PURE; /** * This is a helper to get the route's per-filter config if it exists, up along the config @@ -784,12 +794,12 @@ class MetadataMatchCriteria { * Creates a new MetadataMatchCriteria, merging existing * metadata criteria with the provided criteria. The result criteria is the * combination of both sets of criteria, with those from the metadata_matches - * ProtobufWkt::Struct taking precedence. + * Protobuf::Struct taking precedence. * @param metadata_matches supplies the new criteria. * @return MetadataMatchCriteriaConstPtr the result criteria. */ virtual MetadataMatchCriteriaConstPtr - mergeMatchCriteria(const ProtobufWkt::Struct& metadata_matches) const PURE; + mergeMatchCriteria(const Protobuf::Struct& metadata_matches) const PURE; /** * Creates a new MetadataMatchCriteria with criteria vector reduced to given names @@ -935,6 +945,7 @@ class RouteEntry : public ResponseEntry { * or x-envoy-original-host header if host rewritten. */ virtual void finalizeRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info, bool keep_original_host_or_path) const PURE; @@ -975,7 +986,7 @@ class RouteEntry : public ResponseEntry { * @return const RetryPolicy& the retry policy for the route. All routes have a retry policy even * if it is empty and does not allow retries. */ - virtual const RetryPolicy& retryPolicy() const PURE; + virtual const RetryPolicyConstSharedPtr& retryPolicy() const PURE; /** * @return const InternalRedirectPolicy& the internal redirect policy for the route. All routes @@ -995,13 +1006,20 @@ class RouteEntry : public ResponseEntry { virtual const PathRewriterSharedPtr& pathRewriter() const PURE; /** - * @return uint32_t any route cap on bytes which should be buffered for shadowing or retries. - * This is an upper bound so does not necessarily reflect the bytes which will be buffered - * as other limits may apply. - * Unlike some other buffer limits, 0 here indicates buffering should not be performed - * rather than no limit applies. + * @return uint64_t the maximum bytes which should be buffered for request bodies. This enables + * buffering larger request bodies beyond the connection buffer limit for use cases + * with large payloads, shadowing, or retries. + * + * This method consolidates the functionality of the previous + * per_request_buffer_limit_bytes and request_body_buffer_limit fields. It supports both + * legacy configurations using per_request_buffer_limit_bytes and new configurations using + * request_body_buffer_limit. + * + * If neither is set, falls back to connection buffer limits. Unlike some other buffer + * limits, 0 here indicates buffering should not be performed rather than no limit + * applies. */ - virtual uint32_t retryShadowBufferLimit() const PURE; + virtual uint64_t requestBodyBufferLimit() const PURE; /** * @return const std::vector& the shadow policies for the route. The vector is empty @@ -1020,6 +1038,12 @@ class RouteEntry : public ResponseEntry { */ virtual absl::optional idleTimeout() const PURE; + /** + * @return optional the route's flush timeout. Zero indicates a + * disabled idle timeout, while nullopt indicates deference to the global timeout. + */ + virtual absl::optional flushTimeout() const PURE; + /** * @return true if new style max_stream_duration config should be used over the old style. */ diff --git a/envoy/router/router_filter_interface.h b/envoy/router/router_filter_interface.h index 34c306afc860b..f0cb02eddb7d8 100644 --- a/envoy/router/router_filter_interface.h +++ b/envoy/router/router_filter_interface.h @@ -104,6 +104,18 @@ class RouterFilterInterface { */ virtual void onStreamMaxDurationReached(UpstreamRequest& upstream_request) PURE; + /* + * This will be called to set up the route timeout early for websocket upgrades. + * This ensures the timeout is active during the upgrade negotiation phase. + */ + virtual void setupRouteTimeoutForWebsocketUpgrade() PURE; + + /* + * This will be called to disable the route timeout after websocket upgrade completes. + * This prevents the timeout from firing after successful upgrade. + */ + virtual void disableRouteTimeoutForWebsocketUpgrade() PURE; + /* * @returns the Router filter's StreamDecoderFilterCallbacks. */ diff --git a/envoy/server/configuration.h b/envoy/server/configuration.h index 4c18b49054270..9da04d1ac7012 100644 --- a/envoy/server/configuration.h +++ b/envoy/server/configuration.h @@ -89,6 +89,11 @@ class StatsConfig { * @return true if deferred creation of stats is enabled. */ virtual bool enableDeferredCreationStats() const PURE; + + /** + * @return uint32_t a multiple of the flush interval to perform stats eviction, or 0 if disabled. + */ + virtual uint32_t evictOnFlush() const PURE; }; /** diff --git a/envoy/server/overload/load_shed_point.h b/envoy/server/overload/load_shed_point.h index 9434aac1c9058..d25bae4b5f56f 100644 --- a/envoy/server/overload/load_shed_point.h +++ b/envoy/server/overload/load_shed_point.h @@ -27,6 +27,11 @@ class LoadShedPointNameValues { const std::string H2ServerGoAwayOnDispatch = "envoy.load_shed_points.http2_server_go_away_on_dispatch"; + // Envoy will send a GOAWAY and immediately close the connection while processing HTTP2 requests + // at the codec level. + const std::string H2ServerGoAwayAndCloseOnDispatch = + "envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch"; + // Envoy will close the connections before creating codec if Envoy is under pressure, // typically memory. This happens once geting data from the connection. const std::string HcmCodecCreation = "envoy.load_shed_points.hcm_ondata_creating_codec"; diff --git a/envoy/ssl/BUILD b/envoy/ssl/BUILD index cc840247281c4..fa337b2f15bb2 100644 --- a/envoy/ssl/BUILD +++ b/envoy/ssl/BUILD @@ -76,6 +76,7 @@ envoy_cc_library( envoy_cc_library( name = "ssl_socket_extended_info_interface", hdrs = ["ssl_socket_extended_info.h"], + deps = [":handshaker_interface"], ) envoy_cc_library( diff --git a/envoy/ssl/ssl_socket_extended_info.h b/envoy/ssl/ssl_socket_extended_info.h index 192f395c204e0..228cb937edf04 100644 --- a/envoy/ssl/ssl_socket_extended_info.h +++ b/envoy/ssl/ssl_socket_extended_info.h @@ -7,6 +7,7 @@ #include "envoy/common/pure.h" #include "envoy/event/dispatcher.h" +#include "envoy/ssl/handshaker.h" namespace Envoy { namespace Ssl { diff --git a/envoy/stats/histogram.h b/envoy/stats/histogram.h index 227ae3c01993d..d6c6a9a61f699 100644 --- a/envoy/stats/histogram.h +++ b/envoy/stats/histogram.h @@ -24,6 +24,13 @@ class HistogramSettings { * @return The buckets for the histogram. Each value is an upper bound of a bucket. */ virtual ConstSupportedBuckets& buckets(absl::string_view stat_name) const PURE; + + /** + * Number of bins to pre-allocate per each thread instance (times 2 for active/passive + * version of the histogram). + * @return An optional override for the number of bins. + */ + virtual absl::optional bins(absl::string_view stat_name) const PURE; }; using HistogramSettingsConstPtr = std::unique_ptr; diff --git a/envoy/stats/scope.h b/envoy/stats/scope.h index 1116281d93026..e76582b34c430 100644 --- a/envoy/stats/scope.h +++ b/envoy/stats/scope.h @@ -71,8 +71,10 @@ class Scope : public std::enable_shared_from_this { * See also scopeFromStatName, which is preferred. * * @param name supplies the scope's namespace prefix. + * @param evictable whether unused metrics can be deleted from the scope caches. This requires + * that the metrics are not stored by reference. */ - virtual ScopeSharedPtr createScope(const std::string& name) PURE; + virtual ScopeSharedPtr createScope(const std::string& name, bool evictable = false) PURE; /** * Allocate a new scope. NOTE: The implementation should correctly handle overlapping scopes @@ -80,8 +82,10 @@ class Scope : public std::enable_shared_from_this { * gracefully swapped in while an old scope with the same name is being destroyed. * * @param name supplies the scope's namespace prefix. + * @param evictable whether unused metrics can be deleted from the scope caches. This requires + * that the metrics are not stored by reference. */ - virtual ScopeSharedPtr scopeFromStatName(StatName name) PURE; + virtual ScopeSharedPtr scopeFromStatName(StatName name, bool evictable = false) PURE; /** * Creates a Counter from the stat name. Tag extraction will be performed on the name. diff --git a/envoy/stats/sink.h b/envoy/stats/sink.h index a56ec2bb27575..6c7029d484230 100644 --- a/envoy/stats/sink.h +++ b/envoy/stats/sink.h @@ -83,8 +83,6 @@ class SinkPredicates { /* * @return true if @param histogram needs to be flushed to sinks. - * Note that this is used only if runtime flag envoy.reloadable_features.enable_include_histograms - * (which is false by default) is set to true. */ virtual bool includeHistogram(const Histogram& histogram) PURE; }; diff --git a/envoy/stats/stats.h b/envoy/stats/stats.h index 5d5b1f58c80cb..a8307baf836dd 100644 --- a/envoy/stats/stats.h +++ b/envoy/stats/stats.h @@ -93,6 +93,11 @@ class Metric : public RefcountInterface { */ virtual bool used() const PURE; + /** + * Clear any indicator on whether this metric has been updated. + */ + virtual void markUnused() PURE; + /** * Indicates whether this metric is hidden. */ diff --git a/envoy/stats/store.h b/envoy/stats/store.h index ff1c6a08c53e0..306cef4c5a9b0 100644 --- a/envoy/stats/store.h +++ b/envoy/stats/store.h @@ -117,6 +117,11 @@ class Store { virtual void forEachHistogram(SizeFn f_size, StatFn f_stat) const PURE; virtual void forEachScope(SizeFn f_size, StatFn f_stat) const PURE; + /** + * Delete unused metrics from all the evictable scope caches, and mark the rest as unused. + */ + virtual void evictUnused() PURE; + /** * @return a null counter that will ignore increments and always return 0. */ @@ -172,7 +177,9 @@ class Store { /** * @return a scope of the given name. */ - ScopeSharedPtr createScope(const std::string& name) { return rootScope()->createScope(name); } + ScopeSharedPtr createScope(const std::string& name, bool evictable = false) { + return rootScope()->createScope(name, evictable); + } /** * Extracts tags from the name and appends them to the provided StatNameTagVector. diff --git a/envoy/stream_info/filter_state.h b/envoy/stream_info/filter_state.h index f43ffe00eb3b9..55bc1f21e945f 100644 --- a/envoy/stream_info/filter_state.h +++ b/envoy/stream_info/filter_state.h @@ -93,7 +93,7 @@ class FilterState { /** * @return Protobuf::MessagePtr an unique pointer to the proto serialization of the filter - * state. If returned message type is ProtobufWkt::Any it will be directly used in protobuf + * state. If returned message type is Protobuf::Any it will be directly used in protobuf * logging. nullptr if the filter state cannot be serialized or serialization is not supported. */ virtual ProtobufTypes::MessagePtr serializeAsProto() const { return nullptr; } diff --git a/envoy/stream_info/stream_info.h b/envoy/stream_info/stream_info.h index b96b4c460479b..f4ded9b4a7181 100644 --- a/envoy/stream_info/stream_info.h +++ b/envoy/stream_info/stream_info.h @@ -438,9 +438,17 @@ struct BytesMeter { uint64_t wireBytesReceived() const { return wire_bytes_received_; } uint64_t headerBytesSent() const { return header_bytes_sent_; } uint64_t headerBytesReceived() const { return header_bytes_received_; } + uint64_t decompressedHeaderBytesSent() const { return decompressed_header_bytes_sent_; } + uint64_t decompressedHeaderBytesReceived() const { return decompressed_header_bytes_received_; } void addHeaderBytesSent(uint64_t added_bytes) { header_bytes_sent_ += added_bytes; } void addHeaderBytesReceived(uint64_t added_bytes) { header_bytes_received_ += added_bytes; } + void addDecompressedHeaderBytesSent(uint64_t added_bytes) { + decompressed_header_bytes_sent_ += added_bytes; + } + void addDecompressedHeaderBytesReceived(uint64_t added_bytes) { + decompressed_header_bytes_received_ += added_bytes; + } void addWireBytesSent(uint64_t added_bytes) { wire_bytes_sent_ += added_bytes; } void addWireBytesReceived(uint64_t added_bytes) { wire_bytes_received_ += added_bytes; } @@ -448,6 +456,8 @@ struct BytesMeter { SystemTime snapshot_time; uint64_t header_bytes_sent{}; uint64_t header_bytes_received{}; + uint64_t decompressed_header_bytes_sent{}; + uint64_t decompressed_header_bytes_received{}; uint64_t wire_bytes_sent{}; uint64_t wire_bytes_received{}; }; @@ -457,6 +467,10 @@ struct BytesMeter { downstream_periodic_logging_bytes_snapshot_->snapshot_time = snapshot_time; downstream_periodic_logging_bytes_snapshot_->header_bytes_sent = header_bytes_sent_; downstream_periodic_logging_bytes_snapshot_->header_bytes_received = header_bytes_received_; + downstream_periodic_logging_bytes_snapshot_->decompressed_header_bytes_sent = + decompressed_header_bytes_sent_; + downstream_periodic_logging_bytes_snapshot_->decompressed_header_bytes_received = + decompressed_header_bytes_received_; downstream_periodic_logging_bytes_snapshot_->wire_bytes_sent = wire_bytes_sent_; downstream_periodic_logging_bytes_snapshot_->wire_bytes_received = wire_bytes_received_; } @@ -466,6 +480,10 @@ struct BytesMeter { upstream_periodic_logging_bytes_snapshot_->snapshot_time = snapshot_time; upstream_periodic_logging_bytes_snapshot_->header_bytes_sent = header_bytes_sent_; upstream_periodic_logging_bytes_snapshot_->header_bytes_received = header_bytes_received_; + upstream_periodic_logging_bytes_snapshot_->decompressed_header_bytes_sent = + decompressed_header_bytes_sent_; + upstream_periodic_logging_bytes_snapshot_->decompressed_header_bytes_received = + decompressed_header_bytes_received_; upstream_periodic_logging_bytes_snapshot_->wire_bytes_sent = wire_bytes_sent_; upstream_periodic_logging_bytes_snapshot_->wire_bytes_received = wire_bytes_received_; } @@ -493,6 +511,8 @@ struct BytesMeter { // Accumulate existing bytes. header_bytes_sent_ += existing.header_bytes_sent_; header_bytes_received_ += existing.header_bytes_received_; + decompressed_header_bytes_sent_ += existing.decompressed_header_bytes_sent_; + decompressed_header_bytes_received_ += existing.decompressed_header_bytes_received_; wire_bytes_sent_ += existing.wire_bytes_sent_; wire_bytes_received_ += existing.wire_bytes_received_; } @@ -500,6 +520,8 @@ struct BytesMeter { private: uint64_t header_bytes_sent_{}; uint64_t header_bytes_received_{}; + uint64_t decompressed_header_bytes_sent_{}; + uint64_t decompressed_header_bytes_received_{}; uint64_t wire_bytes_sent_{}; uint64_t wire_bytes_received_{}; std::unique_ptr downstream_periodic_logging_bytes_snapshot_; @@ -863,14 +885,14 @@ class StreamInfo { * @param value the struct to set on the namespace. A merge will be performed with new values for * the same key overriding existing. */ - virtual void setDynamicMetadata(const std::string& name, const ProtobufWkt::Struct& value) PURE; + virtual void setDynamicMetadata(const std::string& name, const Protobuf::Struct& value) PURE; /** * @param name the namespace used in the metadata in reverse DNS format, for example: * envoy.test.my_filter. * @param value of type protobuf any to set on the namespace. */ - virtual void setDynamicTypedMetadata(const std::string& name, const ProtobufWkt::Any& value) PURE; + virtual void setDynamicTypedMetadata(const std::string& name, const Protobuf::Any& value) PURE; /** * Object on which filters can share data on a per-request basis. For singleton data objects, only diff --git a/envoy/tcp/upstream.h b/envoy/tcp/upstream.h index b201d2e153a71..4fdc258f6c122 100644 --- a/envoy/tcp/upstream.h +++ b/envoy/tcp/upstream.h @@ -4,6 +4,7 @@ #include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.h" #include "envoy/http/filter.h" #include "envoy/http/header_evaluator.h" +#include "envoy/http/request_id_extension.h" #include "envoy/stream_info/stream_info.h" #include "envoy/tcp/conn_pool.h" #include "envoy/upstream/upstream.h" @@ -42,6 +43,9 @@ class TunnelingConfigHelper { // The evaluator to add additional HTTP request headers to the upstream request. virtual Envoy::Http::HeaderEvaluator& headerEvaluator() const PURE; + // The request ID extension used for generation/validation when tunneling. + virtual const Envoy::Http::RequestIDExtensionSharedPtr& requestIDExtension() const PURE; + // Save HTTP response headers to the downstream filter state. virtual void propagateResponseHeaders(Http::ResponseHeaderMapPtr&& headers, diff --git a/envoy/tracing/trace_config.h b/envoy/tracing/trace_config.h index c556aa592f387..d593771372f2a 100644 --- a/envoy/tracing/trace_config.h +++ b/envoy/tracing/trace_config.h @@ -27,9 +27,13 @@ class Config { virtual bool spawnUpstreamSpan() const PURE; /** - * @return custom tags to be attached to the active span. + * @return modify the span. For example, set custom tags from configuration or + * make other modifications. + * This method MUST be called at most ONLY once per span before the span is + * finished. + * @param span the span to modify. */ - virtual const CustomTagMap* customTags() const PURE; + virtual void modifySpan(Span& span) const PURE; /** * @return true if spans should be annotated with more detailed information. @@ -43,66 +47,5 @@ class Config { virtual uint32_t maxPathTagLength() const PURE; }; -/** - * Route or connection manager level tracing configuration. - */ -class TracingConfig { -public: - virtual ~TracingConfig() = default; - - /** - * This method returns the client sampling percentage. - * @return the client sampling percentage - */ - virtual const envoy::type::v3::FractionalPercent& getClientSampling() const PURE; - - /** - * This method returns the random sampling percentage. - * @return the random sampling percentage - */ - virtual const envoy::type::v3::FractionalPercent& getRandomSampling() const PURE; - - /** - * This method returns the overall sampling percentage. - * @return the overall sampling percentage - */ - virtual const envoy::type::v3::FractionalPercent& getOverallSampling() const PURE; - - /** - * This method returns the tracing custom tags. - * @return the tracing custom tags. - */ - virtual const Tracing::CustomTagMap& getCustomTags() const PURE; -}; - -/** - * Connection manager tracing configuration. - */ -class ConnectionManagerTracingConfig : public TracingConfig { -public: - /** - * @return operation name for tracing, e.g., ingress. - */ - virtual OperationName operationName() const PURE; - - /** - * @return create separated child span for upstream request if true. - */ - virtual bool spawnUpstreamSpan() const PURE; - - /** - * @return true if spans should be annotated with more detailed information. - */ - virtual bool verbose() const PURE; - - /** - * @return the maximum length allowed for paths in the extracted HttpUrl tag. This is only used - * for HTTP protocol tracing. - */ - virtual uint32_t maxPathTagLength() const PURE; -}; - -using ConnectionManagerTracingConfigPtr = std::unique_ptr; - } // namespace Tracing } // namespace Envoy diff --git a/envoy/tracing/trace_driver.h b/envoy/tracing/trace_driver.h index 94a7ca27050e3..4e5e9617c6ae3 100644 --- a/envoy/tracing/trace_driver.h +++ b/envoy/tracing/trace_driver.h @@ -113,6 +113,23 @@ class Span { */ virtual void setSampled(bool sampled) PURE; + /** + * When the startSpan() of tracer is called, the Envoy tracing decision is passed to the + * tracer to help determine whether the span should be sampled. + * + * But note that the tracer may have its own sampling decision logic (e.g. custom sampler, + * external tracing context, etc.), and it may not use the Envoy tracing decision at all, + * then the desicion may be ignored by the tracer. + * + * The method is used to return whether the Envoy tracing decision is used by the tracer + * or not. + * + * When the Envoy tracing decision is refreshed becase route refresh or other reasons, if + * the Envoy tracing decision is used by the tracer, the sampled value will be updated + * by the HTTP connection manager based on the new Envoy tracing decision. + */ + virtual bool useLocalDecision() const PURE; + /** * Retrieve a key's value from the span's baggage. * This baggage data could've been set by this span or any parent spans. diff --git a/envoy/upstream/cluster_manager.h b/envoy/upstream/cluster_manager.h index 09e373ef555c8..f1aeab218bc8b 100644 --- a/envoy/upstream/cluster_manager.h +++ b/envoy/upstream/cluster_manager.h @@ -46,6 +46,13 @@ class EnvoyQuicNetworkObserverRegistry; } // namespace Quic +namespace Config { +// TODO(adisuissa): This forward declaration is needed because OD-CDS code is +// part of the Envoy::Upstream namespace but should be eventually moved to the +// Envoy::Config namespace (next to the XdsManager). +class XdsManager; +} // namespace Config + namespace Upstream { /** @@ -496,7 +503,8 @@ class ClusterManager { * @param predicate supplies the optional drain connections host predicate. If not supplied, all * hosts are drained. */ - virtual void drainConnections(DrainConnectionsHostPredicate predicate) PURE; + virtual void drainConnections(DrainConnectionsHostPredicate predicate, + ConnectionPool::DrainBehavior drain_behavior) PURE; /** * Check if the cluster is active and statically configured, and if not, return an error @@ -512,12 +520,15 @@ class ClusterManager { * @param validation_visitor * @return OdCdsApiHandlePtr the ODCDS handle. */ - + // TODO(adisuissa): once the xDS-TP config-sources are fully supported, the + // `odcds_config` parameter should become optional, and the comment above + // should be updated. using OdCdsCreationFunction = std::function>( const envoy::config::core::v3::ConfigSource& odcds_config, - OptRef odcds_resources_locator, ClusterManager& cm, - MissingClusterNotifier& notifier, Stats::Scope& scope, - ProtobufMessage::ValidationVisitor& validation_visitor)>; + OptRef odcds_resources_locator, + Config::XdsManager& xds_manager, ClusterManager& cm, MissingClusterNotifier& notifier, + Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::ServerFactoryContext& server_factory_context)>; virtual absl::StatusOr allocateOdCdsApi(OdCdsCreationFunction creation_function, @@ -630,7 +641,8 @@ class ClusterManagerFactory { */ virtual absl::StatusOr createCds(const envoy::config::core::v3::ConfigSource& cds_config, - const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm) PURE; + const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm, + bool support_multi_ads_sources) PURE; }; /** diff --git a/envoy/upstream/health_check_event_sink.h b/envoy/upstream/health_check_event_sink.h index 9f82edda1458b..6dc3fdb4acff0 100644 --- a/envoy/upstream/health_check_event_sink.h +++ b/envoy/upstream/health_check_event_sink.h @@ -30,7 +30,7 @@ class HealthCheckEventSinkFactory : public Config::TypedFactory { * Creates an HealthCheckEventSink using the given config. */ virtual HealthCheckEventSinkPtr - createHealthCheckEventSink(const ProtobufWkt::Any& config, + createHealthCheckEventSink(const Protobuf::Any& config, Server::Configuration::HealthCheckerFactoryContext& context) PURE; std::string category() const override { return "envoy.health_check.event_sinks"; } diff --git a/envoy/upstream/load_balancer.h b/envoy/upstream/load_balancer.h index ae252e524f3b6..65db38b85d586 100644 --- a/envoy/upstream/load_balancer.h +++ b/envoy/upstream/load_balancer.h @@ -333,6 +333,14 @@ using ThreadAwareLoadBalancerPtr = std::unique_ptr; class LoadBalancerConfig { public: virtual ~LoadBalancerConfig() = default; + + /** + * Optional method to allow a load balancer to validate endpoints before they're applied. If an + * error is returned from this method, the endpoints are rejected. If this method does not return + * an error, the load balancer must be able to use these endpoints in an update from the priority + * set. + */ + virtual absl::Status validateEndpoints(const PriorityState&) const { return absl::OkStatus(); } }; using LoadBalancerConfigPtr = std::unique_ptr; diff --git a/envoy/upstream/upstream.h b/envoy/upstream/upstream.h index 74ade9ff0d06b..7389dc26cd68e 100644 --- a/envoy/upstream/upstream.h +++ b/envoy/upstream/upstream.h @@ -474,18 +474,6 @@ class HostSet { */ virtual LocalityWeightsConstSharedPtr localityWeights() const PURE; - /** - * @return next locality index to route to if performing locality weighted balancing - * against healthy hosts. - */ - virtual absl::optional chooseHealthyLocality() PURE; - - /** - * @return next locality index to route to if performing locality weighted balancing - * against degraded hosts. - */ - virtual absl::optional chooseDegradedLocality() PURE; - /** * @return uint32_t the priority of this host set. */ @@ -511,32 +499,36 @@ using HostSetPtr = std::unique_ptr; class PrioritySet { public: using MemberUpdateCb = - std::function; + std::function; - using PriorityUpdateCb = std::function; + using PriorityUpdateCb = std::function; virtual ~PrioritySet() = default; /** - * Install a callback that will be invoked when any of the HostSets in the PrioritySet changes. - * hosts_added and hosts_removed will only be populated when a host is added or completely removed - * from the PrioritySet. - * This includes when a new HostSet is created. + * Install a callback that will be invoked when anything on any host in the PrioritySet is + * changed. + * + * hosts_added and hosts_removed will only be populated when a host is added or + * completely removed from the PrioritySet. This includes when a new HostSet is created. * * @param callback supplies the callback to invoke. - * @return Common::CallbackHandlePtr a handle which can be used to unregister the callback. + * @return Common::CallbackHandlePtr a handle which unregisters the callback upon its destruction. */ ABSL_MUST_USE_RESULT virtual Common::CallbackHandlePtr addMemberUpdateCb(MemberUpdateCb callback) const PURE; /** - * Install a callback that will be invoked when a host set changes. Triggers when any change - * happens to the hosts within the host set. If hosts are added/removed from the host set, the - * added/removed hosts will be passed to the callback. + * Install a callback that will be invoked when a host changes. Triggers when any change + * happens to the hosts within that priority, and is invoked once for each priority that has a + * change. + * + * If hosts are added/removed from the host set, the added/removed hosts will be passed to + * the callback. * * @param callback supplies the callback to invoke. - * @return Common::CallbackHandlePtr a handle which can be used to unregister the callback. + * @return Common::CallbackHandlePtr a handle which unregisters the callback upon its destruction. */ ABSL_MUST_USE_RESULT virtual Common::CallbackHandlePtr addPriorityUpdateCb(PriorityUpdateCb callback) const PURE; @@ -574,7 +566,6 @@ class PrioritySet { * @param locality_weights supplies a map from locality to associated weight. * @param hosts_added supplies the hosts added since the last update. * @param hosts_removed supplies the hosts removed since the last update. - * @param seed a random number to initialize the locality load-balancing algorithm. * @param weighted_priority_health if present, overwrites the current weighted_priority_health. * @param overprovisioning_factor if present, overwrites the current overprovisioning_factor. * @param cross_priority_host_map read only cross-priority host map which is created in the main @@ -583,7 +574,7 @@ class PrioritySet { virtual void updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - uint64_t seed, absl::optional weighted_priority_health, + absl::optional weighted_priority_health, absl::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map = nullptr) PURE; @@ -607,7 +598,7 @@ class PrioritySet { virtual void updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - uint64_t seed, absl::optional weighted_priority_health, + absl::optional weighted_priority_health, absl::optional overprovisioning_factor) PURE; }; @@ -747,7 +738,8 @@ class PrioritySet { GAUGE(upstream_rq_active, Accumulate) \ GAUGE(upstream_rq_pending_active, Accumulate) \ HISTOGRAM(upstream_cx_connect_ms, Milliseconds) \ - HISTOGRAM(upstream_cx_length_ms, Milliseconds) + HISTOGRAM(upstream_cx_length_ms, Milliseconds) \ + HISTOGRAM(upstream_rq_per_cx, Unspecified) /** * All cluster load report stats. These are only use for EDS load reporting and not sent to the diff --git a/examples/reverse_connection/Dockerfile.xds b/examples/reverse_connection/Dockerfile.xds new file mode 100644 index 0000000000000..4631d2a23ae7e --- /dev/null +++ b/examples/reverse_connection/Dockerfile.xds @@ -0,0 +1,150 @@ +FROM ubuntu:20.04 + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +WORKDIR /app + +# Install Python and pip +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies +RUN pip3 install requests pyyaml + +# Create a simple xDS server script +RUN echo '#!/usr/bin/env python3\n\ +import json\n\ +import time\n\ +import threading\n\ +import http.server\n\ +import socketserver\n\ +import logging\n\ +\n\ +logging.basicConfig(level=logging.INFO)\n\ +logger = logging.getLogger(__name__)\n\ +\n\ +class XDSServer:\n\ + def __init__(self):\n\ + self.listeners = {}\n\ + self.version = 1\n\ + self._lock = threading.Lock()\n\ + self.server = None\n\ + \n\ + def start(self, port):\n\ + class XDSHandler(http.server.BaseHTTPRequestHandler):\n\ + def do_POST(self):\n\ + if self.path == "/v3/discovery:listeners":\n\ + content_length = int(self.headers["Content-Length"])\n\ + post_data = self.rfile.read(content_length)\n\ + response_data = self.server.xds_server.handle_lds_request(post_data)\n\ + self.send_response(200)\n\ + self.send_header("Content-type", "application/json")\n\ + self.end_headers()\n\ + self.wfile.write(response_data.encode())\n\ + elif self.path == "/add_listener":\n\ + content_length = int(self.headers["Content-Length"])\n\ + post_data = self.rfile.read(content_length)\n\ + data = json.loads(post_data.decode())\n\ + self.server.xds_server.add_listener(data["name"], data["config"])\n\ + self.send_response(200)\n\ + self.send_header("Content-type", "application/json")\n\ + self.end_headers()\n\ + self.wfile.write(json.dumps({"status": "success"}).encode())\n\ + elif self.path == "/remove_listener":\n\ + content_length = int(self.headers["Content-Length"])\n\ + post_data = self.rfile.read(content_length)\n\ + data = json.loads(post_data.decode())\n\ + success = self.server.xds_server.remove_listener(data["name"])\n\ + if success:\n\ + self.send_response(200)\n\ + self.send_header("Content-type", "application/json")\n\ + self.end_headers()\n\ + self.wfile.write(json.dumps({"status": "success"}).encode())\n\ + else:\n\ + self.send_response(404)\n\ + self.send_header("Content-type", "application/json")\n\ + self.end_headers()\n\ + self.wfile.write(json.dumps({"status": "not_found"}).encode())\n\ + elif self.path == "/state":\n\ + state = self.server.xds_server.get_state()\n\ + self.send_response(200)\n\ + self.send_header("Content-type", "application/json")\n\ + self.end_headers()\n\ + self.wfile.write(json.dumps(state).encode())\n\ + else:\n\ + self.send_response(404)\n\ + self.end_headers()\n\ + \n\ + def log_message(self, format, *args):\n\ + pass\n\ + \n\ + class XDSServer(socketserver.TCPServer):\n\ + def __init__(self, server_address, RequestHandlerClass, xds_server):\n\ + self.xds_server = xds_server\n\ + super().__init__(server_address, RequestHandlerClass)\n\ + \n\ + self.server = XDSServer(("0.0.0.0", port), XDSHandler, self)\n\ + self.server_thread = threading.Thread(target=self.server.serve_forever)\n\ + self.server_thread.daemon = True\n\ + self.server_thread.start()\n\ + logger.info(f"xDS server started on port {port}")\n\ + \n\ + def handle_lds_request(self, request_data):\n\ + with self._lock:\n\ + response = {\n\ + "version_info": str(self.version),\n\ + "resources": [],\n\ + "type_url": "type.googleapis.com/envoy.config.listener.v3.Listener"\n\ + }\n\ + for listener_name, listener_config in self.listeners.items():\n\ + # Wrap the listener config in a proper Any message\n\ + wrapped_config = {\n\ + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",\n\ + **listener_config\n\ + }\n\ + response["resources"].append(wrapped_config)\n\ + return json.dumps(response)\n\ + \n\ + def add_listener(self, listener_name, listener_config):\n\ + with self._lock:\n\ + self.listeners[listener_name] = listener_config\n\ + self.version += 1\n\ + logger.info(f"Added listener {listener_name}, version {self.version}")\n\ + \n\ + def remove_listener(self, listener_name):\n\ + with self._lock:\n\ + if listener_name in self.listeners:\n\ + del self.listeners[listener_name]\n\ + self.version += 1\n\ + logger.info(f"Removed listener {listener_name}, version {self.version}")\n\ + return True\n\ + return False\n\ +\n\ + def get_state(self):\n\ + with self._lock:\n\ + return {\n\ + "version": self.version,\n\ + "listeners": list(self.listeners.keys())\n\ + }\n\ +\n\ +if __name__ == "__main__":\n\ + xds_server = XDSServer()\n\ + xds_server.start(18000)\n\ + try:\n\ + while True:\n\ + time.sleep(1)\n\ + except KeyboardInterrupt:\n\ + print("Shutting down xDS server...")\n\ +' > /app/xds_server.py + +# Make the script executable +RUN chmod +x /app/xds_server.py + +# Expose the xDS server port +EXPOSE 18000 + +# Run the xDS server +CMD ["python3", "/app/xds_server.py"] \ No newline at end of file diff --git a/examples/reverse_connection/README.md b/examples/reverse_connection/README.md new file mode 100644 index 0000000000000..482a199b9b1c4 --- /dev/null +++ b/examples/reverse_connection/README.md @@ -0,0 +1,67 @@ +# Running the Sandbox for reverse tunnels + +## Steps to run sandbox + +1. Build envoy with reverse tunnels feature: + - ```./ci/run_envoy_docker.sh './ci/do_ci.sh bazel.release.server_only'``` +2. Build envoy docker image: + - ```docker build -f ci/Dockerfile-envoy-image -t envoy:latest .``` +3. Launch test containers. + - ```docker-compose -f examples/reverse_connection/docker-compose.yaml up``` + + **Note**: The docker-compose maps the following ports: + - **downstream-envoy**: Host port 9000 → Container port 9000 (reverse connection API) + - **upstream-envoy**: Host port 9001 → Container port 9000 (reverse connection API) + +4. The reverse example configuration in initiator-envoy.yaml initiates reverse tunnels to upstream envoy using a custom address resolver. The configuration includes: + + ```yaml + # Bootstrap extension for reverse tunnel functionality + bootstrap_extensions: + - name: envoy.bootstrap.reverse_tunnel.downstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" + + # Reverse connection listener with custom address format + - name: reverse_conn_listener + address: + socket_address: + # Format: rc://src_node_id:src_cluster_id:src_tenant_id@remote_cluster:connection_count + address: "rc://downstream-node:downstream-cluster:downstream-tenant@upstream-cluster:1" + port_value: 0 + resolver_name: "envoy.resolvers.reverse_connection" + ``` + +5. Test reverse tunnel: + - Perform http request for the service behind downstream envoy, to upstream-envoy. This request will be sent + over a reverse tunnel. + + ```bash + [basundhara.c@basundhara-c envoy-examples]$ curl -H "x-remote-node-id: downstream-node" -H "x-dst-cluster-uuid: downstream-cluster" http://localhost:8085/downstream_service -v + * Trying ::1... + * TCP_NODELAY set + * Connected to localhost (::1) port 8085 (#0) + > GET /downstream_service HTTP/1.1 + > Host: localhost:8085 + > User-Agent: curl/7.61.1 + > Accept: */* + > x-remote-node-id: downstream-node + > x-dst-cluster-uuid: downstream-cluster + > + < HTTP/1.1 200 OK + < server: envoy + < date: Thu, 25 Sep 2025 21:25:38 GMT + < content-type: text/plain + < content-length: 159 + < expires: Thu, 25 Sep 2025 21:25:37 GMT + < cache-control: no-cache + < x-envoy-upstream-service-time: 13 + < + Server address: 172.27.0.3:80 + Server name: b490f264caf9 + Date: 25/Sep/2025:21:25:38 +0000 + URI: /downstream_service + Request ID: 41807e3cd1f6a0b601597b80f7e51513 + * Connection #0 to host localhost left intact + ``` \ No newline at end of file diff --git a/examples/reverse_connection/backup/cloud-envoy-grpc-enhanced.yaml b/examples/reverse_connection/backup/cloud-envoy-grpc-enhanced.yaml new file mode 100644 index 0000000000000..79930b9d2a44c --- /dev/null +++ b/examples/reverse_connection/backup/cloud-envoy-grpc-enhanced.yaml @@ -0,0 +1,147 @@ +--- +node: + id: cloud-node + cluster: cloud +static_resources: + listeners: + # Enhanced listener for both HTTP and gRPC reverse tunnel handshake requests + - name: reverse_tunnel_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 9000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_tunnel + codec_type: AUTO + route_config: + name: reverse_tunnel_route + virtual_hosts: + - name: reverse_tunnel_service + domains: ["*"] + routes: + # gRPC reverse tunnel handshake service + - match: + prefix: "/envoy.service.reverse_tunnel.v3.ReverseTunnelHandshakeService" + headers: + - name: "content-type" + string_match: + exact: "application/grpc" + route: + cluster: local_service + timeout: 30s + # Legacy HTTP reverse tunnel handshake for backward compatibility + - match: + prefix: "/reverse_connections" + route: + cluster: local_service + timeout: 30s + # Generic route for other services + - match: + prefix: "/" + route: + cluster: local_service + http_filters: + # Enhanced reverse connection filter with gRPC support + - name: envoy.filters.http.reverse_conn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.reverse_conn.v3.ReverseConn + ping_interval: 2 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Enable HTTP/2 for gRPC support + http2_protocol_options: {} + + # Listener that will route the downstream request to the reverse connection cluster + - name: egress_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 8085 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + codec_type: AUTO + route_config: + name: egress_route + virtual_hosts: + - name: reverse_service + domains: ["*"] + routes: + - match: + prefix: "/on_prem_service" + route: + cluster: reverse_connection_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Cluster used to write requests to cached sockets + clusters: + - name: reverse_connection_cluster + connect_timeout: 200s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + # The following headers are expected in downstream requests + # to be sent over reverse connections + http_header_names: + - x-remote-node-id # Should be set to the node ID of the downstream envoy node, ie., on-prem-node + - x-dst-cluster-uuid # Should be set to the cluster ID of the downstream envoy node, ie., on-prem + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + # Right the moment, reverse connections are supported over HTTP/2 only + http2_protocol_options: {} + + # Local service cluster for handling gRPC and HTTP handshake requests + - name: local_service + type: STATIC + lb_policy: ROUND_ROBIN + # Enable HTTP/2 for gRPC support + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: local_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 0.0.0.0 + port_value: 9000 + +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8888 + +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 + +# Enable reverse connection bootstrap extension for upstream (acceptor) +bootstrap_extensions: +- name: envoy.bootstrap.reverse_connection.upstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3.UpstreamReverseConnectionSocketInterface + stat_prefix: "upstream_reverse_connection" diff --git a/examples/reverse_connection/backup/cloud-envoy-grpc.yaml b/examples/reverse_connection/backup/cloud-envoy-grpc.yaml new file mode 100644 index 0000000000000..74f0fe153c404 --- /dev/null +++ b/examples/reverse_connection/backup/cloud-envoy-grpc.yaml @@ -0,0 +1,112 @@ +--- +node: + id: cloud-node + cluster: cloud +static_resources: + listeners: + # Listener for both HTTP and gRPC reverse tunnel handshake requests + - name: reverse_tunnel_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 9000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_tunnel + codec_type: AUTO + route_config: + name: reverse_tunnel_route + virtual_hosts: + - name: reverse_tunnel_service + domains: ["*"] + routes: + # gRPC reverse tunnel handshake + - match: + prefix: "/envoy.service.reverse_tunnel.v3.ReverseTunnelHandshakeService" + headers: + - name: "content-type" + string_match: + exact: "application/grpc" + route: + cluster: local_service + timeout: 30s + # Legacy HTTP reverse tunnel handshake + - match: + prefix: "/reverse_connections" + route: + cluster: local_service + timeout: 30s + http_filters: + # Enhanced reverse connection filter with gRPC support + - name: reverse_conn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.reverse_conn.v3.ReverseConn + ping_interval: 2 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + http2_protocol_options: {} + + # Listener that will route the downstream request to the reverse connection cluster + - name: egress_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8085 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + codec_type: AUTO + route_config: + name: egress_route + virtual_hosts: + - name: reverse_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: reverse_connection_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: local_service + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: local_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 9000 + + - name: reverse_connection_cluster + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: reverse_connection_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 + +bootstrap_extensions: + - name: envoy.bootstrap.reverse_connection.upstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3.UpstreamReverseConnectionSocketInterface + stat_prefix: "upstream_reverse_connection" \ No newline at end of file diff --git a/examples/reverse_connection/backup/on-prem-envoy-custom-resolver-grpc.yaml b/examples/reverse_connection/backup/on-prem-envoy-custom-resolver-grpc.yaml new file mode 100644 index 0000000000000..5e5bb69a891a3 --- /dev/null +++ b/examples/reverse_connection/backup/on-prem-envoy-custom-resolver-grpc.yaml @@ -0,0 +1,169 @@ +--- +node: + id: on-prem-node + cluster: on-prem + +# Enable reverse connection bootstrap extension with gRPC configuration +bootstrap_extensions: +- name: envoy.bootstrap.reverse_connection.downstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" + src_cluster_id: "on-prem" + src_node_id: "on-prem-node" + src_tenant_id: "on-prem" + # gRPC handshake configuration (without grpc_service - cluster comes from listener) + grpc_service_config: + handshake_timeout: 10s + max_retries: 3 + retry_base_interval: 0.15s + retry_max_interval: 5s + initial_metadata: + - key: "x-service-version" + value: "grpc-v1" + - key: "x-envoy-node-id" + value: "on-prem-node" + +static_resources: + listeners: + # Services reverse conn APIs + - name: rev_conn_api_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 9001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: rev_conn_api + codec_type: AUTO + route_config: + name: rev_conn_api_route + virtual_hosts: [] + http_filters: + - name: envoy.filters.http.reverse_conn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.reverse_conn.v3.ReverseConn + ping_interval: 30 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Forwards incoming http requests to backend + - name: ingress_http_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 6060 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: ingress_http_route + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/on_prem_service' + route: + cluster: on-prem-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Initiates reverse connections to cloud using custom resolver AND gRPC handshake + - name: reverse_conn_listener + listener_filters_timeout: 0s + listener_filters: + # Filter that responds to keepalives on reverse connection sockets + - name: envoy.filters.listener.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.reverse_connection.v3.ReverseConnection + ping_wait_timeout: 120 + # Use custom address with reverse connection metadata encoded in URL format + address: + socket_address: + # This encodes: src_node_id=on-prem-node, src_cluster_id=on-prem, src_tenant_id=on-prem + # and remote clusters: cloud with 10 connections (gRPC will be used automatically from bootstrap config) + address: "rc://on-prem-node:on-prem:on-prem@cloud:10" + port_value: 0 + # Use custom resolver that can parse reverse connection metadata + resolver_name: "envoy.resolvers.reverse_connection" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_conn_listener + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/on_prem_service' + route: + cluster: on-prem-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Cluster designating cloud-envoy (must support HTTP/2 for gRPC) + clusters: + - name: cloud + type: STRICT_DNS + connect_timeout: 30s + # Enable HTTP/2 for gRPC support + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: cloud + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: cloud-envoy # Container name of cloud-envoy in docker-compose + port_value: 9000 # Port where cloud-envoy's reverse_tunnel_listener listens + + # Backend HTTP service behind onprem which + # we will access via reverse connections + - name: on-prem-service + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: on-prem-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: on-prem-service + port_value: 80 + +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8888 + +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 diff --git a/examples/reverse_connection/backup/on-prem-envoy-grpc.yaml b/examples/reverse_connection/backup/on-prem-envoy-grpc.yaml new file mode 100644 index 0000000000000..42aeb028c74c4 --- /dev/null +++ b/examples/reverse_connection/backup/on-prem-envoy-grpc.yaml @@ -0,0 +1,112 @@ +--- +node: + id: on-prem-node + cluster: on-prem +static_resources: + listeners: + # Frontend listener accepting requests + - name: frontend_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: frontend_http + codec_type: AUTO + route_config: + name: frontend_route + virtual_hosts: + - name: frontend_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: backend_service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + # Cloud cluster for reverse tunnel handshakes (gRPC) + - name: cloud + type: STATIC + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: cloud + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 9000 + + # Backend service cluster + - name: backend_service + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: backend_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 + +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + address: 127.0.0.1 + port_value: 8879 + +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 + +# Enable reverse connection bootstrap extension with gRPC client +bootstrap_extensions: + - name: envoy.bootstrap.reverse_connection.downstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3.DownstreamReverseConnectionSocketInterface + src_cluster_id: "on-prem" + src_node_id: "on-prem-node" + src_tenant_id: "tenant-1" + remote_cluster_to_conn_count: + - cluster_name: "cloud" + reverse_connection_count: 2 + # gRPC handshake configuration + grpc_service_config: + grpc_service: + envoy_grpc: + cluster_name: cloud + timeout: 10s + retry_policy: + retry_back_off: + base_interval: 0.15s + max_interval: 5s + num_retries: 3 + handshake_timeout: 10s + max_retries: 3 + retry_base_interval: 0.15s + retry_max_interval: 5s + initial_metadata: + - key: "x-service-version" + value: "grpc-v1" + - key: "x-envoy-node-id" + value: "on-prem-node" \ No newline at end of file diff --git a/examples/reverse_connection/backup/test_grpc_handshake.sh b/examples/reverse_connection/backup/test_grpc_handshake.sh new file mode 100755 index 0000000000000..14be4e7b75001 --- /dev/null +++ b/examples/reverse_connection/backup/test_grpc_handshake.sh @@ -0,0 +1,213 @@ +#!/bin/bash + +# Test script for gRPC reverse tunnel handshake demonstration +# This script automates the testing process for the new gRPC-based handshake + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENVOY_BINARY="${ENVOY_BINARY:-bazel-bin/source/exe/envoy-static}" + +echo "🚀 gRPC Reverse Tunnel Handshake Test" +echo "======================================" + +# Check if envoy binary exists +if [ ! -f "${ENVOY_BINARY}" ]; then + echo "❌ Error: Envoy binary not found at ${ENVOY_BINARY}" + echo " Please build Envoy first: bazel build //source/exe:envoy-static" + echo " Or set ENVOY_BINARY environment variable to the correct path" + exit 1 +fi + +# Kill any existing envoy processes +echo "🧹 Cleaning up existing processes..." +pkill -f "envoy-static.*cloud-envoy" || true +pkill -f "envoy-static.*on-prem-envoy" || true +sleep 2 + +# Start Python backend server in background +echo "🐍 Starting Python backend server..." +cd "${SCRIPT_DIR}/../" +python3 -c " +import http.server +import socketserver +import json +import urllib.parse +from datetime import datetime + +class BackendHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + + response = { + 'message': 'Hello from on-premises backend service!', + 'timestamp': datetime.now().isoformat(), + 'path': self.path, + 'method': 'GET', + 'headers': dict(self.headers) + } + + self.wfile.write(json.dumps(response, indent=2).encode()) + + def log_message(self, format, *args): + print(f'[BACKEND] {format % args}') + +PORT = 3000 +with socketserver.TCPServer(('', PORT), BackendHandler) as httpd: + print(f'Backend server running on port {PORT}') + httpd.serve_forever() +" & +BACKEND_PID=$! +echo " Backend server started with PID: ${BACKEND_PID}" + +# Wait for backend to start +sleep 2 + +# Start Cloud Envoy (Acceptor) +echo "☁️ Starting Cloud Envoy (gRPC Acceptor)..." +cd "${SCRIPT_DIR}/../../" +"${ENVOY_BINARY}" \ + -c examples/reverse_connection_socket_interface/cloud-envoy-grpc.yaml \ + --concurrency 1 --use-dynamic-base-id -l trace \ + > /tmp/cloud-envoy.log 2>&1 & +CLOUD_PID=$! +echo " Cloud Envoy started with PID: ${CLOUD_PID}" + +# Wait for Cloud Envoy to start +echo " Waiting for Cloud Envoy to initialize..." +sleep 5 + +# Check if Cloud Envoy started successfully +if ! kill -0 $CLOUD_PID 2>/dev/null; then + echo "❌ Cloud Envoy failed to start. Check logs:" + tail -20 /tmp/cloud-envoy.log + kill $BACKEND_PID 2>/dev/null || true + exit 1 +fi + +# Start On-Premises Envoy (Initiator) +echo "🏢 Starting On-Premises Envoy (gRPC Initiator)..." +"${ENVOY_BINARY}" \ + -c examples/reverse_connection_socket_interface/on-prem-envoy-grpc.yaml \ + --concurrency 1 --use-dynamic-base-id -l trace \ + > /tmp/on-prem-envoy.log 2>&1 & +ONPREM_PID=$! +echo " On-Premises Envoy started with PID: ${ONPREM_PID}" + +# Wait for On-Premises Envoy to start and establish connections +echo " Waiting for gRPC handshake to complete..." +sleep 10 + +# Check if On-Premises Envoy started successfully +if ! kill -0 $ONPREM_PID 2>/dev/null; then + echo "❌ On-Premises Envoy failed to start. Check logs:" + tail -20 /tmp/on-prem-envoy.log + kill $CLOUD_PID $BACKEND_PID 2>/dev/null || true + exit 1 +fi + +# Test the end-to-end flow +echo "🧪 Testing end-to-end reverse tunnel flow..." +echo " Sending test request via reverse tunnel..." + +HTTP_RESPONSE=$(curl -s -w "%{http_code}" \ + -H "x-remote-node-id: on-prem-node" \ + -H "x-dst-cluster-uuid: on-prem" \ + http://localhost:8085/on_prem_service) + +HTTP_STATUS="${HTTP_RESPONSE: -3}" +RESPONSE_BODY="${HTTP_RESPONSE%???}" + +echo " HTTP Status: ${HTTP_STATUS}" + +if [ "${HTTP_STATUS}" = "200" ]; then + echo "✅ SUCCESS: Reverse tunnel working correctly!" + echo " Response received:" + echo "${RESPONSE_BODY}" | jq . 2>/dev/null || echo "${RESPONSE_BODY}" +else + echo "❌ FAILED: Unexpected HTTP status: ${HTTP_STATUS}" + echo " Response: ${RESPONSE_BODY}" +fi + +# Check logs for gRPC handshake evidence +echo "" +echo "📋 Checking logs for gRPC handshake evidence..." + +echo " Cloud Envoy (Acceptor) logs:" +if grep -q "EstablishTunnel gRPC request" /tmp/cloud-envoy.log; then + echo " ✅ Found gRPC handshake requests in Cloud Envoy logs" + grep "EstablishTunnel gRPC request" /tmp/cloud-envoy.log | tail -3 +else + echo " ⚠️ No gRPC handshake requests found in Cloud Envoy logs" +fi + +echo "" +echo " On-Premises Envoy (Initiator) logs:" +if grep -q "gRPC reverse tunnel handshake" /tmp/on-prem-envoy.log; then + echo " ✅ Found gRPC handshake initiation in On-Premises Envoy logs" + grep "gRPC reverse tunnel handshake" /tmp/on-prem-envoy.log | tail -3 +else + echo " ⚠️ No gRPC handshake initiation found in On-Premises Envoy logs" +fi + +# Performance test +echo "" +echo "🏃 Performance test (10 requests)..." +start_time=$(date +%s%N) +for i in {1..10}; do + curl -s -H "x-remote-node-id: on-prem-node" \ + -H "x-dst-cluster-uuid: on-prem" \ + http://localhost:8085/on_prem_service > /dev/null +done +end_time=$(date +%s%N) +duration_ms=$(( (end_time - start_time) / 1000000 )) +avg_latency_ms=$(( duration_ms / 10 )) + +echo " Total time: ${duration_ms}ms" +echo " Average latency: ${avg_latency_ms}ms per request" + +# Cleanup function +cleanup() { + echo "" + echo "🧹 Cleaning up..." + kill $ONPREM_PID $CLOUD_PID $BACKEND_PID 2>/dev/null || true + sleep 2 + + echo " Log files available at:" + echo " - Cloud Envoy: /tmp/cloud-envoy.log" + echo " - On-Premises Envoy: /tmp/on-prem-envoy.log" + echo "" + echo " To view detailed logs, run:" + echo " tail -f /tmp/cloud-envoy.log" + echo " tail -f /tmp/on-prem-envoy.log" +} + +# Set trap for cleanup +trap cleanup EXIT INT TERM + +echo "" +echo "🎉 Test completed! Press Ctrl+C to cleanup and exit." +echo " Envoys will continue running for further testing..." + +# Keep the script running +while true; do + sleep 10 + + # Check if processes are still running + if ! kill -0 $CLOUD_PID 2>/dev/null; then + echo "❌ Cloud Envoy died unexpectedly" + break + fi + + if ! kill -0 $ONPREM_PID 2>/dev/null; then + echo "❌ On-Premises Envoy died unexpectedly" + break + fi + + if ! kill -0 $BACKEND_PID 2>/dev/null; then + echo "❌ Backend server died unexpectedly" + break + fi +done \ No newline at end of file diff --git a/examples/reverse_connection/backup/test_logs.txt b/examples/reverse_connection/backup/test_logs.txt new file mode 100644 index 0000000000000..1ea2c4207d8f6 --- /dev/null +++ b/examples/reverse_connection/backup/test_logs.txt @@ -0,0 +1,52 @@ +#0 building with "default" instance using docker driver + +#1 [xds-server internal] load .dockerignore +#1 transferring context: 2B done +#1 DONE 0.0s + +#2 [xds-server internal] load build definition from Dockerfile.xds +#2 transferring dockerfile: 6.20kB done +#2 DONE 0.0s + +#3 [xds-server internal] load metadata for docker.io/library/ubuntu:20.04 +#3 DONE 0.0s + +#4 [xds-server 1/6] FROM docker.io/library/ubuntu:20.04 +#4 DONE 0.0s + +#5 [xds-server 3/6] RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* +#5 CACHED + +#6 [xds-server 4/6] RUN pip3 install requests pyyaml +#6 CACHED + +#7 [xds-server 2/6] WORKDIR /app +#7 CACHED + +#8 [xds-server 5/6] RUN echo '#!/usr/bin/env python3\nimport json\nimport time\nimport threading\nimport http.server\nimport socketserver\nimport logging\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\nclass XDSServer:\n def __init__(self):\n self.listeners = {}\n self.version = 1\n self._lock = threading.Lock()\n self.server = None\n \n def start(self, port):\n class XDSHandler(http.server.BaseHTTPRequestHandler):\n def do_POST(self):\n if self.path == "/v3/discovery:listeners":\n content_length = int(self.headers["Content-Length"])\n post_data = self.rfile.read(content_length)\n response_data = self.server.xds_server.handle_lds_request(post_data)\n self.send_response(200)\n self.send_header("Content-type", "application/json")\n self.end_headers()\n self.wfile.write(response_data.encode())\n elif self.path == "/add_listener":\n content_length = int(self.headers["Content-Length"])\n post_data = self.rfile.read(content_length)\n data = json.loads(post_data.decode())\n self.server.xds_server.add_listener(data["name"], data["config"])\n self.send_response(200)\n self.send_header("Content-type", "application/json")\n self.end_headers()\n self.wfile.write(json.dumps({"status": "success"}).encode())\n elif self.path == "/remove_listener":\n content_length = int(self.headers["Content-Length"])\n post_data = self.rfile.read(content_length)\n data = json.loads(post_data.decode())\n success = self.server.xds_server.remove_listener(data["name"])\n if success:\n self.send_response(200)\n self.send_header("Content-type", "application/json")\n self.end_headers()\n self.wfile.write(json.dumps({"status": "success"}).encode())\n else:\n self.send_response(404)\n self.send_header("Content-type", "application/json")\n self.end_headers()\n self.wfile.write(json.dumps({"status": "not_found"}).encode())\n elif self.path == "/state":\n state = self.server.xds_server.get_state()\n self.send_response(200)\n self.send_header("Content-type", "application/json")\n self.end_headers()\n self.wfile.write(json.dumps(state).encode())\n else:\n self.send_response(404)\n self.end_headers()\n \n def log_message(self, format, *args):\n pass\n \n class XDSServer(socketserver.TCPServer):\n def __init__(self, server_address, RequestHandlerClass, xds_server):\n self.xds_server = xds_server\n super().__init__(server_address, RequestHandlerClass)\n \n self.server = XDSServer(("0.0.0.0", port), XDSHandler, self)\n self.server_thread = threading.Thread(target=self.server.serve_forever)\n self.server_thread.daemon = True\n self.server_thread.start()\n logger.info(f"xDS server started on port {port}")\n \n def handle_lds_request(self, request_data):\n with self._lock:\n response = {\n "version_info": str(self.version),\n "resources": [],\n "type_url": "type.googleapis.com/envoy.config.listener.v3.Listener"\n }\n for listener_name, listener_config in self.listeners.items():\n wrapped_config = {\n "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",\n **listener_config\n }\n response["resources"].append(wrapped_config)\n return json.dumps(response)\n \n def add_listener(self, listener_name, listener_config):\n with self._lock:\n self.listeners[listener_name] = listener_config\n self.version += 1\n logger.info(f"Added listener {listener_name}, version {self.version}")\n \n def remove_listener(self, listener_name):\n with self._lock:\n if listener_name in self.listeners:\n del self.listeners[listener_name]\n self.version += 1\n logger.info(f"Removed listener {listener_name}, version {self.version}")\n return True\n return False\n\n def get_state(self):\n with self._lock:\n return {\n "version": self.version,\n "listeners": list(self.listeners.keys())\n }\n\nif __name__ == "__main__":\n xds_server = XDSServer()\n xds_server.start(18000)\n try:\n while True:\n time.sleep(1)\n except KeyboardInterrupt:\n print("Shutting down xDS server...")\n' > /app/xds_server.py +#8 CACHED + +#9 [xds-server 6/6] RUN chmod +x /app/xds_server.py +#9 CACHED + +#10 [xds-server] exporting to image +#10 exporting layers done +#10 writing image sha256:cc765ad92907541f91a22ed321919acbeafeb018ce196b355b62788d3344fb2f done +#10 naming to docker.io/library/tmp0sqi5eqk-xds-server done +#10 DONE 0.0s +Attaching to cloud-envoy-1, on-prem-envoy-1, on-prem-service-1, xds-server-1 +on-prem-service-1 | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration +on-prem-service-1 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ +on-prem-service-1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh +on-prem-service-1 | 10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf is not a file or does not exist +on-prem-service-1 | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh +on-prem-service-1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh +on-prem-service-1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh +on-prem-service-1 | /docker-entrypoint.sh: Configuration complete; ready for start up +cloud-envoy-1 | [2025-07-26T00:52:10.565Z] "GET /ready HTTP/1.1" 200 - 0 5 3 - "172.26.0.1" "python-requests/2.27.1" "-" "localhost:8889" "-" +on-prem-envoy-1 | [2025-07-26T00:52:10.573Z] "GET /ready HTTP/1.1" 200 - 0 5 2 - "172.26.0.1" "python-requests/2.27.1" "-" "localhost:8888" "-" +on-prem-service-1 | 172.26.0.1 - - [26/Jul/2025:00:52:17 +0000] "GET /on_prem_service HTTP/1.1" 200 156 "-" "python-requests/2.27.1" "-" +cloud-envoy-1 exited with code 0 +cloud-envoy-1 exited with code 0 +cloud-envoy-1 | [2025-07-26T00:52:38.503Z] "GET /ready HTTP/1.1" 200 - 0 5 3 - "172.26.0.1" "python-requests/2.27.1" "-" "localhost:8889" "-" +on-prem-envoy-1 exited with code 139 diff --git a/examples/reverse_connection/docker-compose.yaml b/examples/reverse_connection/docker-compose.yaml new file mode 100644 index 0000000000000..1d069bcf64571 --- /dev/null +++ b/examples/reverse_connection/docker-compose.yaml @@ -0,0 +1,55 @@ +version: '2' +services: + + xds-server: + build: + context: . + dockerfile: Dockerfile.xds + ports: + - "18000:18000" + networks: + - envoy-network + + downstream-envoy: + image: debug/envoy:latest + volumes: + - ./initiator-envoy.yaml:/etc/downstream-envoy.yaml + command: envoy -c /etc/downstream-envoy.yaml --concurrency 1 -l trace --drain-time-s 3 + ports: + # Admin interface + - "8888:8888" + # Reverse connection API listener + - "9000:9000" + # Ingress HTTP listener + - "6060:6060" + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - envoy-network + depends_on: + - xds-server + - downstream-service + + downstream-service: + image: nginxdemos/hello:plain-text + networks: + - envoy-network + + upstream-envoy: + image: debug/envoy:latest + volumes: + - ./responder-envoy.yaml:/etc/upstream-envoy.yaml + command: envoy -c /etc/upstream-envoy.yaml --concurrency 1 -l trace --drain-time-s 3 + ports: + # Admin interface + - "8889:8888" + # Reverse connection API listener + - "9001:9000" + # Egress listener + - "8085:8085" + networks: + - envoy-network + +networks: + envoy-network: + driver: bridge \ No newline at end of file diff --git a/examples/reverse_connection/docs/LIFE_OF_A_REQUEST.md b/examples/reverse_connection/docs/LIFE_OF_A_REQUEST.md new file mode 100644 index 0000000000000..821644fcc63ca --- /dev/null +++ b/examples/reverse_connection/docs/LIFE_OF_A_REQUEST.md @@ -0,0 +1,80 @@ +# Life of a Request + +This document describes the complete lifecycle of a request through the reverse connection system, from initial request to final response. + +## Overview Diagram + +![Life of a Request Diagram](https://lucid.app/lucidchart/c369b0df-436b-4dbb-b32b-03d2412c1db2/edit?invitationId=inv_1aa0e150-484c-4b43-9799-afb3d61289e1&page=IjcZ1O4Q5TdRF#) + +## Request Lifecycle + +### 1. Request with Custom Headers + +The request begins with custom headers that identify the target downstream cluster and node: + +```http +GET /api/data HTTP/1.1 +Host: reverse-connection-cluster +x-dst-cluster-uuid: cluster-123 +x-remote-node-id: node-456 +``` + +### 2. Cluster Processing + +The request lands on a `reverse_connection` cluster configured in Envoy: + +```yaml + clusters: + - name: reverse_connection_cluster + connect_timeout: 2s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3alpha.RevConClusterConfig + # The following headers are expected in downstream requests + # to be sent over reverse connections + http_header_names: + - x-remote-node-id # Should be set to the node ID of the downstream envoy node, ie., on-prem-node + - x-dst-cluster-uuid # Should be set to the cluster ID of the downstream envoy node, ie., on-prem +``` + +The cluster extracts the custom headers and creates a host with the appropriate metadata. This is important because when the subsequent requests arrive with the same node header, the same host will be re-used, thereby re-using the tcp connection pool and the cached socket. + +### 3. Host Address Creation + +The host is created with a custom address that returns the `UpstreamReverseSocketInterface` in its `socketInterface()` method: + +### 4. Thread-Local Socket Creation + +The `socket()` function of `UpstreamReverseSocketInterface` is called thread-locally: + + +### 5. Socket Selection from Thread-Local SocketManager + +The `UpstreamReverseSocketInterface` picks an available socket from the thread-local `SocketManager`: + + +### 6. Cluster -> Node Mapping + +The `SocketManager` maintains two key mappings: + +- **Cluster → Node Mapping**: `cluster_id -> set` - tracks which nodes belong to each cluster +- **Node → Socket Mapping**: `node_id -> cached_socket` - stores the actual cached socket for each node + +**Request Routing Logic:** +- **Node-specific requests**: Use `x-remote-node-id` header to route to specific node socket +- **Cluster requests**: Use `x-dst-cluster-uuid` header to randomly select from available nodes in that cluster + +### 7. UpstreamReverseConnectionIOHandle Creation + +The `UpstreamReverseSocketInterface` returns an `UpstreamReverseConnectionIOHandle` that wraps the cached socket: + + +### 8. Connection Creation and Usage + +A connection is created with the `UpstreamReverseConnectionIOHandle` and used for the request: + + +## References +- [Reverse Connection Design Document](https://docs.google.com/document/d/1rH91TgPX7JbTcWYacCpavY4hiA1ce1yQE4mN3XZSewo/edit?tab=t.0) \ No newline at end of file diff --git a/examples/reverse_connection/docs/REVERSE_CONN_INITIATION.md b/examples/reverse_connection/docs/REVERSE_CONN_INITIATION.md new file mode 100644 index 0000000000000..1601a6fdcc8b8 --- /dev/null +++ b/examples/reverse_connection/docs/REVERSE_CONN_INITIATION.md @@ -0,0 +1,134 @@ +# Reverse Connection Listener: Using Custom Resolver + +This document explains how reverse connection listeners are configured and how the custom resolver parses reverse connection metadata to initiate reverse tunnels. + +## Overview + +Reverse connection listeners use a custom address resolver to parse metadata encoded in socket addresses and establish reverse TCP connections. The use of listeners makes reverse tunnel initiation dynamically configurable via LDS updates so that reverse tunnels to clusters can be set up on demand and torn down. It also eases cleanup, which can be done by just draining the listener. + +## Architecture Diagram + +The following diagram shows how multiple listeners with reverse connection metadata are processed: + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Envoy Configuration │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ Listener 1 │ │ Listener 2 │ │ +│ │ │ │ │ │ +│ │ address: │ │ address: │ │ +│ │ "rc://node1: │ │ "rc://node2: │ │ +│ │ cluster1: │ │ cluster2: │ │ +│ │ tenant1@cloud:2" │ │ tenant2@cloud:1" │ │ +│ │ │ │ │ │ +│ │ resolver_name: │ │ resolver_name: │ │ +│ │ "envoy.resolvers. │ │ "envoy.resolvers. │ │ +│ │ reverse_connection"│ │ reverse_connection"│ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ │ │ +│ └───────────┬───────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ReverseConnectionResolver │ │ +│ │ │ │ +│ │ • Detects "rc://" prefix │ │ +│ │ • Parses metadata: node_id, cluster_id, tenant_id, │ │ +│ │ target_cluster, connection_count │ │ +│ │ • Creates ReverseConnectionAddress instances │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ReverseConnectionAddress │ │ +│ │ │ │ +│ │ • Stores parsed configuration │ │ +│ │ • Internal address: "127.0.0.1:0" (for filter chain matching) │ │ +│ │ • Logical name: original "rc://" address │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ListenerFactory │ │ +│ │ │ │ +│ │ • Detects ReverseConnectionAddress │ │ +│ │ • Creates DownstreamReverseSocketInterface │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Worker Threads │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ Worker N │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ • Listener │ │ • Listener │ │ • Listener │ │ • Listener │ │ │ +│ │ │ 1 and 2 │ │ 1 and 2 │ │ 1 and 2 │ │ 1 and 2 │ │ │ +│ │ │ initiate │ │ initiate │ │ initiate │ │ initiate │ │ │ +│ │ │ reverse │ │ reverse │ │ reverse │ │ reverse │ │ │ +│ │ │ tunnels │ │ tunnels │ │ tunnels │ │ tunnels │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## How Reverse Connection Listeners Work + +### 1. Listener Configuration + +Listeners are configured with socket addresses that use the custom resolver and contain reverse connection metadata: + +```yaml +listeners: + - name: reverse_conn_listener_1 + address: + socket_address: + address: "rc://node1:cluster1:tenant1@cloud:2" + port_value: 0 + resolver_name: "envoy.resolvers.reverse_connection" + + - name: reverse_conn_listener_2 + address: + socket_address: + address: "rc://node2:cluster2:tenant2@cloud:1" + port_value: 0 + resolver_name: "envoy.resolvers.reverse_connection" +``` + +### 2. Address Resolution + +The `ReverseConnectionResolver` detects addresses starting with `rc://` and parses the metadata: +- **Format**: `rc://src_node_id:src_cluster_id:src_tenant_id@cluster_name:count` +- **Example**: `rc://node1:cluster1:tenant1@cloud:2` parses to: + - Source node ID: `node1` + - Source cluster ID: `cluster1` + - Source tenant ID: `tenant1` + - Target cluster: `cloud` + - Connection count: `2` + +### 3. ReverseConnectionAddress Creation + +The resolver creates a `ReverseConnectionAddress` instance that: +- Stores the parsed reverse connection configuration +- Uses a dummy `127.0.0.1:0` as the address. This needs to be a valid address for filter chain lookups. +- Maintains the original `rc://` address in `logicalName()` for identification + +### 4. ListenerFactory Processing + +The ListenerFactory detects reverse connection addresses and: +- Creates the appropriate socket interface (DownstreamReverseSocketInterface) + +### 5. Socket Interface Integration + +The DownstreamReverseSocketInterface (described in a separate document) handles: +- Resolving the target cluster to get host addresses +- Initiating TCP connections +- Managing connection pools and health checks +- Triggering listener accept() when connections are ready + +## References + +- [Reverse Connection Design Document](https://docs.google.com/document/d/1rH91TgPX7JbTcWYacCpavY4hiA1ce1yQE4mN3XZSewo/edit?tab=t.0) +- [Architecture Diagram](https://lucid.app/lucidchart/daa06383-79a7-454e-9bd6-f2ec3dbb8c2c/edit?invitationId=inv_481cba23-1629-4e0f-ba16-25b87c07fb94&page=OuV8EpgCLfgFX#) \ No newline at end of file diff --git a/examples/reverse_connection/docs/SOCKET_INTERFACES.md b/examples/reverse_connection/docs/SOCKET_INTERFACES.md new file mode 100644 index 0000000000000..e3e895e5e90af --- /dev/null +++ b/examples/reverse_connection/docs/SOCKET_INTERFACES.md @@ -0,0 +1,245 @@ +# Socket Interfaces + +## Reverse Tunnel Initiator + +This document explains how the ReverseTunnelInitiator works, including thread-local entities and the reverse connection establishment process. + +## Overview + +The ReverseTunnelInitiator manages the initiation of reverse connections from on-premises Envoy instances to cloud-based instances. It uses thread-local storage to manage connection pools and handles the establishment of reverse TCP connections. + +## Sequence Diagram + +The following diagram shows the flow from ListenerFactory to reverse connection establishment: + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Initiator Side │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ ListenerFactory │ │ ReverseTunnel │ │ Worker Thread │ │ +│ │ │ │ Initiator │ │ │ │ +│ │ • detects │───▶│ │───▶│ • socket() called │ │ +│ │ ReverseConn │ │ • registered as │ │ • creates │ │ +│ │ Address │ │ bootstrap ext │ │ ReverseConnIO │ │ +│ │ │ │ • handles socket │ │ Handle │ │ +│ └─────────────────┘ │ creation │ │ │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ReverseConnectionIOHandle │ │ +│ │ │ │ +│ │ • Resolves target cluster to get host addresses │ │ +│ │ • Establishes reverse TCP connections to each host │ │ +│ │ • Triggers listener accept() when connections ready │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ TCP Connection Establishment │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ HTTP Handshake with Metadata │ │ │ +│ │ │ │ │ │ +│ │ │ HTTP POST with reverse connection metadata │ │ │ +│ │ │ ────────────────────────────────────────────────────────── │ │ │ +│ │ │ Response: ACCEPTED/REJECTED │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Upstream Side │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Worker Threads │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Worker 1 │ │ Worker 2 │ │ Worker N │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ +│ │ │ │Reverse Conn │ │ │ │Reverse Conn │ │ │ │Reverse Conn │ │ │ │ +│ │ │ │HTTP Filter │ │ │ │HTTP Filter │ │ │ │HTTP Filter │ │ │ │ +│ │ │ │(Thread-Local)│ │ │ │(Thread-Local)│ │ │ │(Thread-Local)│ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │• Accepts │ │ │ │• Accepts │ │ │ │• Accepts │ │ │ │ +│ │ │ │ incoming │ │ │ │ incoming │ │ │ │ incoming │ │ │ │ +│ │ │ │ reverse │ │ │ │ reverse │ │ │ │ reverse │ │ │ │ +│ │ │ │ connections│ │ │ │ connections│ │ │ │ connections│ │ │ │ +│ │ │ │• Responds │ │ │ │• Responds │ │ │ │• Responds │ │ │ │ +│ │ │ │ ACCEPTED/ │ │ │ │ ACCEPTED/ │ │ │ │ ACCEPTED/ │ │ │ │ +│ │ │ │ REJECTED │ │ │ │ REJECTED │ │ │ │ REJECTED │ │ │ │ +│ │ │ │• Calls │ │ │ │• Calls │ │ │ │• Calls │ │ │ │ +│ │ │ │ Upstream │ │ │ │ Upstream │ │ │ │ Upstream │ │ │ │ +│ │ │ │ Socket │ │ │ │ Socket │ │ │ │ Socket │ │ │ │ +│ │ │ │ Interface │ │ │ │ Interface │ │ │ │ Interface │ │ │ │ +│ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Worker Threads │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Worker 1 │ │ Worker 2 │ │ Worker N │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ +│ │ │ │Reverse Conn │ │ │ │Reverse Conn │ │ │ │Reverse Conn │ │ │ │ +│ │ │ │HTTP Filter │ │ │ │HTTP Filter │ │ │ │HTTP Filter │ │ │ │ +│ │ │ │(Thread-Local)│ │ │ │(Thread-Local)│ │ │ │(Thread-Local)│ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ │• Accepts │ │ │ │• Accepts │ │ │ │• Accepts │ │ │ │ +│ │ │ │ incoming │ │ │ │ incoming │ │ │ │ incoming │ │ │ │ +│ │ │ │ reverse │ │ │ │ reverse │ │ │ │ reverse │ │ │ │ +│ │ │ │ connections│ │ │ │ connections│ │ │ │ connections│ │ │ │ +│ │ │ │• Responds │ │ │ │• Responds │ │ │ │• Responds │ │ │ │ +│ │ │ │ ACCEPTED/ │ │ │ │ ACCEPTED/ │ │ │ │ ACCEPTED/ │ │ │ │ +│ │ │ │ REJECTED │ │ │ │ REJECTED │ │ │ │ REJECTED │ │ │ │ +│ │ │ │• Calls │ │ │ │• Calls │ │ │ │• Calls │ │ │ │ +│ │ │ │ Upstream │ │ │ │ Upstream │ │ │ │ Upstream │ │ │ │ +│ │ │ │ Socket │ │ │ │ Socket │ │ │ │ Socket │ │ │ │ +│ │ │ │ Interface │ │ │ │ Interface │ │ │ │ Interface │ │ │ │ +│ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ UpstreamReverseSocketInterface │ │ +│ │ (Global) │ │ +│ │ │ │ +│ │ • Fetches thread-local SocketManager │ │ +│ │ • Passes accepted sockets to thread-local SocketManager │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Thread-Local SocketManagers │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │SocketManager │ │SocketManager │ │SocketManager │ │ │ +│ │ │(Worker 1) │ │(Worker 2) │ │(Worker N) │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │• Caches │ │• Caches │ │• Caches │ │ │ +│ │ │ connections │ │ connections │ │ connections │ │ │ +│ │ │ per node/ │ │ per node/ │ │ per node/ │ │ │ +│ │ │ cluster │ │ cluster │ │ cluster │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + + +## Connection Establishment Process + +### 1. Socket Interface Registration + +The bootstrap extension registers the custom socket interface: + +```yaml +bootstrap_extensions: +- name: envoy.bootstrap.reverse_connection.downstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3alpha.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" +``` + +### 2. Thread-Local Socket Creation + +When a listener with reverse connection metadata is created: +- ListenerFactory detects ReverseConnectionAddress +- Calls DownstreamReverseSocketInterface::socket() +- Creates ReverseConnectionIOHandle for each worker thread + +### 3. Cluster Resolution and Connection Initiation + +The ReverseConnectionIOHandle: +- Resolves target cluster to get cluster -> host address mapping +- Establishes reverse TCP connections to each host in the cluster +- Initiates the reverse connection handshake; writes a HTTP POST with the +reverse connection request message on the established TCP connection +- upstream replies with ACCEPTED/REJECTED + +### 4. Socket Management + +- Once the reverse connection is accepted by upstream, downstream triggers listener accept() mechanism + +## Waking up accept() on connection establishment + +The reverse connection system uses a trigger pipe mechanism to wake up the `accept()` method when a reverse connection is successfully established. This allows the listener to process established connections as they become available. + +### Trigger Pipe Mechanism + +The system uses a pipe with two file descriptors: +- `trigger_pipe_read_fd_`: Read end of the pipe, monitored by `accept()` +- `trigger_pipe_write_fd_`: Write end of the pipe, used to signal connection establishment + +### Connection Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Connection Establishment Flow │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ RCConnection │ │ onConnectionDone() │ │ Trigger Pipe │ │ +│ │ Wrapper │ │ │ │ │ │ +│ │ │ │ • Connection │ │ • Write 1 byte │ │ +│ │ • HTTP handshake│───▶│ established │───▶│ to write_fd │ │ +│ │ • Success │ │ • Push connection │ │ • Triggers EPOLL │ │ +│ │ response │ │ to queue │ │ event │ │ +│ └─────────────────┘ └─────────────────────┘ └─────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ accept() Method │ │ +│ │ │ │ +│ │ • Blocks on read() from trigger_pipe_read_fd_ │ │ +│ │ • Receives 1 byte when connection ready │ │ +│ │ • Pops connection from established_connections_ queue │ │ +│ │ • Extracts file descriptor from connection │ │ +│ │ • Wraps FD in new IoHandle and returns it │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Filter Chain Processing │ │ +│ │ │ │ +│ │ • IoHandle passes through regular filter chain │ │ +│ │ • Uses FD of previously established connection │ │ +│ │ • Normal Envoy connection processing │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Implementation Details + +1. **Connection Establishment**: When a reverse connection handshake succeeds, `onConnectionDone()` is called +2. **Queue Management**: The established connection is pushed to `established_connections_` queue +3. **Trigger Signal**: A single byte is written to `trigger_pipe_write_fd_` to signal readiness +4. **EPOLL Event**: This triggers an EPOLL event that wakes up the event loop +5. **accept() Processing**: The `accept()` method reads the byte from `trigger_pipe_read_fd_` +6. **Connection Retrieval**: The method pops the connection from the queue and extracts its file descriptor +7. **IoHandle Creation**: A new `IoSocketHandleImpl` is created with the connection's file descriptor +8. **Filter Chain**: The IoHandle is returned and processed through the normal Envoy filter chain + +This allows us to cleanly cache a previously established connection. + +## Reverse Tunnel Acceptor + +The ReverseTunnelAcceptor manages accepted reverse connections on the cloud side. It uses thread-local socket managers to maintain connection caches and mappings. + +### Thread-Local Socket Management + +Each worker thread has its own socket manager that: +- **Node Caching**: Maintains `node_id -> cached_sockets` mapping for connection retrieval +- **Cluster Mapping**: Stores `cluster_id -> node_ids` mappings. This is used to return cached sockets for different nodes in a load balanced fashion for requests intended to a specific downstream cluster. + +## References + +- [Reverse Connection Design Document](https://docs.google.com/document/d/1rH91TgPX7JbTcWYacCpavY4hiA1ce1yQE4mN3XZSewo/edit?tab=t.0) diff --git a/examples/reverse_connection/initiator-envoy.yaml b/examples/reverse_connection/initiator-envoy.yaml new file mode 100644 index 0000000000000..03c05037f1a9e --- /dev/null +++ b/examples/reverse_connection/initiator-envoy.yaml @@ -0,0 +1,91 @@ +--- +node: + id: downstream-node + cluster: downstream-cluster + +# Enable reverse connection bootstrap extension which registers the custom resolver +bootstrap_extensions: +- name: envoy.bootstrap.reverse_tunnel.downstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" + +static_resources: + listeners: + # Initiates reverse connections to upstream using custom resolver + - name: reverse_conn_listener + listener_filters_timeout: 0s + listener_filters: + # Use custom address with reverse connection metadata encoded in URL format + address: + socket_address: + # This encodes: src_node_id=downstream-node, src_cluster_id=downstream, src_tenant_id=downstream + # and remote clusters: upstream with 1 connection + address: "rc://downstream-node:downstream-cluster:downstream-tenant@upstream-cluster:1" + port_value: 0 + # Use custom resolver that can parse reverse connection metadata + resolver_name: "envoy.resolvers.reverse_connection" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_conn_listener + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/downstream_service' + route: + cluster: downstream-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Cluster designating upstream-envoy + clusters: + - name: upstream-cluster + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: upstream-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: upstream-envoy # Container name of upstream-envoy in docker-compose + port_value: 9000 # Port where upstream-envoy's rev_conn_api_listener listens + + # Backend HTTP service behind downstream which + # we will access via reverse connections + - name: downstream-service + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: downstream-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: downstream-service + port_value: 80 + +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8888 + +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 \ No newline at end of file diff --git a/examples/reverse_connection/requirements.txt b/examples/reverse_connection/requirements.txt new file mode 100644 index 0000000000000..faee1f6065431 --- /dev/null +++ b/examples/reverse_connection/requirements.txt @@ -0,0 +1,5 @@ +requests>=2.25.0 +PyYAML>=5.4.0 +grpcio>=1.43.0 +grpcio-tools>=1.43.0 +protobuf>=3.19.0 \ No newline at end of file diff --git a/examples/reverse_connection/responder-envoy.yaml b/examples/reverse_connection/responder-envoy.yaml new file mode 100644 index 0000000000000..8b73234256f39 --- /dev/null +++ b/examples/reverse_connection/responder-envoy.yaml @@ -0,0 +1,83 @@ +--- +node: + id: upstream-node + cluster: upstream-cluster +static_resources: + listeners: + # Accepts reverse tunnel requests + - name: rev_conn_api_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 9000 + filter_chains: + - filters: + - name: envoy.filters.network.reverse_tunnel + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel + ping_interval: 2s + + # Listener that will route the downstream request to the reverse connection cluster + - name: egress_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 8085 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: "/downstream_service" + route: + cluster: reverse_connection_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Cluster used to write requests to cached sockets + clusters: + - name: reverse_connection_cluster + connect_timeout: 200s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + # The following headers are expected in downstream requests + # to be sent over reverse connections + http_header_names: + - x-remote-node-id # Should be set to the node ID of the downstream envoy node, ie., downstream-node + - x-dst-cluster-uuid # Should be set to the cluster ID of the downstream envoy node, ie., downstream + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + # Right the moment, reverse connections are supported over HTTP/2 only + http2_protocol_options: {} +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + address: 0.0.0.0 + port_value: 8888 +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 + envoy.reloadable_features.reverse_conn_force_local_reply: true +# Enable reverse connection bootstrap extension +bootstrap_extensions: +- name: envoy.bootstrap.reverse_tunnel.upstream_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3.UpstreamReverseConnectionSocketInterface + stat_prefix: "upstream_reverse_connection" \ No newline at end of file diff --git a/examples/reverse_connection/test_reverse_connections.py b/examples/reverse_connection/test_reverse_connections.py new file mode 100644 index 0000000000000..31cf6f1fc6d9b --- /dev/null +++ b/examples/reverse_connection/test_reverse_connections.py @@ -0,0 +1,701 @@ +#!/usr/bin/env python3 +""" +Test script for reverse connection socket interface functionality. + +This script: +1. Starts two Envoy instances (downstream and upstream) using Docker Compose +2. Starts the backend service (downstream-service) +3. Initially starts downstream without the reverse_conn_listener (removed from config) +4. Verifies reverse connections are not established by checking the upstream API +5. Adds the reverse_conn_listener to downstream via xDS +6. Verifies reverse connections are established +7. Tests request routing through reverse connections +8. Stops and restarts upstream Envoy to test connection recovery +9. Verifies reverse connections are re-established +""" + +import json +import time +import subprocess +import requests +import yaml +import tempfile +import os +import signal +import sys +import logging +from typing import Optional + +# Configuration +CONFIG = { + # File paths - use absolute paths based on script location + 'script_dir': + os.path.dirname(os.path.abspath(__file__)), + 'docker_compose_file': + os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docker-compose.yaml'), + 'downstream_config_file': + os.path.join(os.path.dirname(os.path.abspath(__file__)), 'initiator-envoy.yaml'), + 'upstream_config_file': + os.path.join(os.path.dirname(os.path.abspath(__file__)), 'responder-envoy.yaml'), + + # Ports + 'upstream_admin_port': + 8889, + 'upstream_api_port': + 9001, + 'upstream_egress_port': + 8085, + 'downstream_admin_port': + 8888, + 'xds_server_port': + 18000, # Port for our xDS server + + # Container names + 'upstream_container': + 'upstream-envoy', + 'downstream_container': + 'downstream-envoy', + + # Timeouts + 'envoy_startup_timeout': + 30, + 'docker_startup_delay': + 10, +} + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + + +class ReverseConnectionTester: + + def __init__(self): + self.docker_compose_process: Optional[subprocess.Popen] = None + self.temp_dir = tempfile.mkdtemp() + self.docker_compose_dir = CONFIG['script_dir'] + self.current_compose_file = None # Track which compose file is being used + self.current_compose_cwd = None # Track which directory to run from + + def create_downstream_config_with_xds(self) -> str: + """Create downstream Envoy config with xDS for dynamic listener management.""" + # Load the original config + with open(CONFIG['downstream_config_file'], 'r') as f: + config = yaml.safe_load(f) + + # Remove the reverse_conn_listener (will be added via xDS) + listeners = config['static_resources']['listeners'] + config['static_resources']['listeners'] = [ + listener for listener in listeners if listener['name'] != 'reverse_conn_listener' + ] + + # Update the downstream-service cluster to point to downstream-service container + for cluster in config['static_resources']['clusters']: + if cluster['name'] == 'downstream-service': + cluster['load_assignment']['endpoints'][0]['lb_endpoints'][0]['endpoint'][ + 'address']['socket_address']['address'] = 'downstream-service' + cluster['load_assignment']['endpoints'][0]['lb_endpoints'][0]['endpoint'][ + 'address']['socket_address']['port_value'] = 80 + + # Update the upstream-cluster cluster to point to upstream-envoy container + for cluster in config['static_resources']['clusters']: + if cluster['name'] == 'upstream-cluster': + cluster['load_assignment']['endpoints'][0]['lb_endpoints'][0]['endpoint'][ + 'address']['socket_address']['address'] = 'upstream-envoy' + cluster['load_assignment']['endpoints'][0]['lb_endpoints'][0]['endpoint'][ + 'address']['socket_address']['port_value'] = 9000 + + # Add xDS cluster for dynamic configuration + config['static_resources']['clusters'].append({ + 'name': 'xds_cluster', + 'type': 'STRICT_DNS', + 'connect_timeout': '30s', + 'load_assignment': { + 'cluster_name': + 'xds_cluster', + 'endpoints': [{ + 'lb_endpoints': [{ + 'endpoint': { + 'address': { + 'socket_address': { + 'address': 'xds-server', + 'port_value': CONFIG['xds_server_port'] + } + } + } + }] + }] + }, + 'dns_lookup_family': 'V4_ONLY' + }) + + # Add dynamic resources configuration + config['dynamic_resources'] = { + 'lds_config': { + 'resource_api_version': 'V3', + 'api_config_source': { + 'api_type': 'REST', + 'transport_api_version': 'V3', + 'cluster_names': ['xds_cluster'], + 'refresh_delay': '1s' + } + } + } + + config_file = os.path.join(self.temp_dir, "downstream-envoy-with-xds.yaml") + with open(config_file, 'w') as f: + yaml.dump(config, f, default_flow_style=False) + + return config_file + + def start_docker_compose(self, downstream_config: str = None) -> bool: + """Start Docker Compose services.""" + logger.info("Starting Docker Compose services") + + # Create a temporary docker-compose file with the custom downstream config if provided + if downstream_config: + # Copy the original docker-compose file and modify it + with open(CONFIG['docker_compose_file'], 'r') as f: + compose_config = yaml.safe_load(f) + + # Update the downstream-envoy service to use the custom config + compose_config['services']['downstream-envoy']['volumes'] = [ + f"{downstream_config}:/etc/downstream-envoy.yaml" + ] + + # Copy responder-envoy.yaml to temp directory and update the path + import shutil + temp_upstream_config = os.path.join(self.temp_dir, "responder-envoy.yaml") + shutil.copy(CONFIG['upstream_config_file'], temp_upstream_config) + compose_config['services']['upstream-envoy']['volumes'] = [ + f"{temp_upstream_config}:/etc/upstream-envoy.yaml" + ] + + # Copy Dockerfile.xds to temp directory + dockerfile_xds = os.path.join(CONFIG['script_dir'], "Dockerfile.xds") + temp_dockerfile_xds = os.path.join(self.temp_dir, "Dockerfile.xds") + shutil.copy(dockerfile_xds, temp_dockerfile_xds) + + temp_compose_file = os.path.join(self.temp_dir, "docker-compose.yaml") + with open(temp_compose_file, 'w') as f: + yaml.dump(compose_config, f, default_flow_style=False) + + compose_file = temp_compose_file + else: + compose_file = CONFIG['docker_compose_file'] + + # Start docker-compose in background with logs visible + cmd = ["docker-compose", "-f", compose_file, "up"] + + # If using a temporary compose file, run from temp directory, otherwise from docker_compose_dir + if downstream_config: + # Run from temp directory where both files are located + self.docker_compose_process = subprocess.Popen( + cmd, cwd=self.temp_dir, universal_newlines=True) + self.current_compose_file = compose_file + self.current_compose_cwd = self.temp_dir + else: + # Run from original directory + self.docker_compose_process = subprocess.Popen( + cmd, cwd=self.docker_compose_dir, universal_newlines=True) + self.current_compose_file = compose_file + self.current_compose_cwd = self.docker_compose_dir + + # Wait a moment for containers to be ready + time.sleep(CONFIG['docker_startup_delay']) + + # Check if process is still running + if self.docker_compose_process.poll() is not None: + logger.error("Docker Compose failed to start") + return False + + return True + + def stop_docker_compose(self) -> bool: + """Stop Docker Compose services.""" + logger.info("Stopping Docker Compose services") + + cmd = ["docker-compose", "-f", "docker-compose.yaml", "down"] + + process = subprocess.Popen(cmd, cwd=self.docker_compose_dir, universal_newlines=True) + + process.wait() + return process.returncode == 0 + + def wait_for_envoy_ready(self, admin_port: int, name: str, timeout: int = 30) -> bool: + """Wait for Envoy to be ready by checking admin endpoint.""" + start_time = time.time() + while time.time() - start_time < timeout: + try: + response = requests.get(f"http://localhost:{admin_port}/ready", timeout=1) + if response.status_code == 200: + logger.info(f"{name} Envoy is ready") + return True + except requests.exceptions.RequestException: + pass + + time.sleep(1) + + logger.error(f"{name} Envoy failed to start within {timeout} seconds") + return False + + def check_reverse_connections(self, api_port: int) -> bool: + """Check if reverse connections are established by calling the upstream API.""" + try: + # Check the reverse connections API on port 9001 (upstream-envoy's rev_conn_api_listener) + response = requests.get(f"http://localhost:{api_port}/reverse_connections", timeout=5) + if response.status_code == 200: + data = response.json() + logger.info(f"Reverse connections state: {data}") + + # Check if downstream is connected + if "connected" in data and "downstream-node" in data["connected"]: + logger.info("Reverse connections are established") + return True + else: + logger.info("Reverse connections are not established") + return False + else: + logger.error(f"Failed to get reverse connections state: {response.status_code}") + logger.error(f"Response: {response.text}") + return False + except requests.exceptions.RequestException as e: + logger.error(f"Error checking reverse connections: {e}") + return False + except json.JSONDecodeError as e: + logger.error(f"Error parsing JSON response: {e}") + logger.error(f"Response text: {response.text}") + return False + + def test_reverse_connection_request(self, port: int) -> bool: + """Test sending a request through reverse connection.""" + try: + headers = {"x-remote-node-id": "downstream-node", "x-dst-cluster-uuid": "downstream"} + # Use port 8085 (upstream-envoy's egress_listener) as specified in docker-compose + response = requests.get( + f"http://localhost:{port}/downstream_service", headers=headers, timeout=10) + + if response.status_code == 200: + logger.info(f"Reverse connection request successful: {response.text[:100]}...") + return True + else: + logger.error(f"Reverse connection request failed: {response.status_code}") + return False + except requests.exceptions.RequestException as e: + logger.error(f"Error testing reverse connection request: {e}") + return False + + def get_reverse_conn_listener_config(self) -> dict: + """Get the reverse_conn_listener configuration.""" + # Load the original config to extract the reverse_conn_listener + with open(CONFIG['downstream_config_file'], 'r') as f: + config = yaml.safe_load(f) + + # Find the reverse_conn_listener + for listener in config['static_resources']['listeners']: + if listener['name'] == 'reverse_conn_listener': + return listener + + raise Exception("reverse_conn_listener not found in config") + + def add_reverse_conn_listener_via_xds(self) -> bool: + """Add reverse_conn_listener via xDS.""" + logger.info("Adding reverse_conn_listener via xDS") + + try: + # Get the reverse_conn_listener configuration + listener_config = self.get_reverse_conn_listener_config() + + # Send request to xDS server running in Docker + import requests + response = requests.post( + f"http://localhost:{CONFIG['xds_server_port']}/add_listener", + json={ + 'name': 'reverse_conn_listener', + 'config': listener_config + }, + timeout=10) + + if response.status_code == 200: + logger.info("✓ reverse_conn_listener added via xDS") + return True + else: + logger.error(f"Failed to add listener via xDS: {response.status_code}") + return False + + except Exception as e: + logger.error(f"Failed to add reverse_conn_listener via xDS: {e}") + return False + + def remove_reverse_conn_listener_via_xds(self) -> bool: + """Remove reverse_conn_listener via xDS.""" + logger.info("Removing reverse_conn_listener via xDS") + + try: + # Send request to xDS server running in Docker + import requests + response = requests.post( + f"http://localhost:{CONFIG['xds_server_port']}/remove_listener", + json={'name': 'reverse_conn_listener'}, + timeout=10) + + if response.status_code == 200: + logger.info("✓ reverse_conn_listener removed via xDS") + return True + else: + logger.error(f"Failed to remove listener via xDS: {response.status_code}") + return False + + except Exception as e: + logger.error(f"Failed to remove reverse_conn_listener via xDS: {e}") + return False + + def get_container_name(self, service_name: str) -> str: + """Get the actual container name for a service, handling Docker Compose suffixes.""" + try: + result = subprocess.run( + ['docker', 'ps', '--filter', f'name={service_name}', '--format', '{{.Names}}'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=10) + if result.returncode == 0 and result.stdout.strip(): + container_name = result.stdout.strip() + logger.info(f"Found container name for service {service_name}: {container_name}") + return container_name + else: + logger.error( + f"Failed to find container for service {service_name}: {result.stderr}") + return service_name # Fallback to service name + except Exception as e: + logger.error(f"Error finding container name for {service_name}: {e}") + return service_name # Fallback to service name + + def check_container_network_status(self) -> bool: + """Check the network status of containers to help debug DNS issues.""" + logger.info("Checking container network status") + try: + # Check if containers are running and their network info + cmd = [ + 'docker', 'ps', '--filter', 'name=envoy', '--format', + 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' + ] + + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=10) + + if result.returncode == 0: + logger.info("Container status:") + logger.info(result.stdout) + else: + logger.error(f"Failed to get container status: {result.stderr}") + + # Check network info for the envoy-network + cmd = ['docker', 'network', 'inspect', 'envoy-network'] + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=10) + + if result.returncode == 0: + logger.info("Network info:") + logger.info(result.stdout) + else: + logger.error(f"Failed to get network info: {result.stderr}") + + return True + except Exception as e: + logger.error(f"Error checking container network status: {e}") + return False + + def check_network_connectivity(self) -> bool: + """Check network connectivity from downstream container to upstream container.""" + logger.info("Checking network connectivity from downstream to upstream container") + try: + # First check container network status + self.check_container_network_status() + + # Get the downstream container name + on_prem_container = self.get_container_name(CONFIG['downstream_container']) + + # Test DNS resolution first + logger.info("Testing DNS resolution...") + dns_cmd = ['docker', 'exec', on_prem_container, 'sh', '-c', 'nslookup upstream-envoy'] + + dns_result = subprocess.run( + dns_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=15) + + logger.info(f"DNS resolution result: {dns_result.stdout}") + if dns_result.stderr: + logger.error(f"DNS resolution error: {dns_result.stderr}") + + # Test ping connectivity + logger.info("Testing ping connectivity...") + ping_cmd = ['docker', 'exec', on_prem_container, 'sh', '-c', 'ping -c 1 upstream-envoy'] + + ping_result = subprocess.run( + ping_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=15) + + logger.info(f"Ping result: {ping_result.stdout}") + if ping_result.stderr: + logger.error(f"Ping error: {ping_result.stderr}") + + # Test TCP connectivity to the specific port + logger.info("Testing TCP connectivity to upstream-envoy:9000...") + tcp_cmd = ['docker', 'exec', on_prem_container, 'sh', '-c', 'nc -z upstream-envoy 9000'] + + tcp_result = subprocess.run( + tcp_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=15) + + logger.info(f"TCP connectivity result: {tcp_result.stdout}") + if tcp_result.stderr: + logger.error(f"TCP connectivity error: {tcp_result.stderr}") + + # Consider it successful if at least DNS resolution works + if dns_result.returncode == 0: + logger.info("✓ DNS resolution is working") + return True + else: + logger.error("✗ DNS resolution failed") + return False + + except Exception as e: + logger.error(f"Error checking network connectivity: {e}") + return False + + def start_upstream_envoy(self) -> bool: + """Start the upstream Envoy container.""" + logger.info("Starting upstream Envoy container") + try: + # Use the same docker-compose file and directory that was used in start_docker_compose + # This ensures the container is started with the same configuration + if self.current_compose_file and self.current_compose_cwd: + logger.info(f"Using stored compose file: {self.current_compose_file}") + logger.info(f"Using stored compose directory: {self.current_compose_cwd}") + compose_file = self.current_compose_file + compose_cwd = self.current_compose_cwd + else: + logger.warn("No stored compose file found, using default") + compose_file = CONFIG['docker_compose_file'] + compose_cwd = self.docker_compose_dir + + logger.info( + "Using docker-compose up to start upstream-envoy with consistent network config") + result = subprocess.run( + ['docker-compose', '-f', compose_file, 'up', '-d', CONFIG['upstream_container']], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=60, + cwd=compose_cwd) + + if result.returncode == 0: + logger.info("✓ Cloud Envoy container started") + + # Add a small delay to ensure network is properly established + logger.info("Waiting for network to be established...") + time.sleep(3) + + # Check network connectivity + if not self.check_network_connectivity(): + logger.warn("Network connectivity check failed, but continuing...") + + # Wait for upstream Envoy to be ready + if not self.wait_for_envoy_ready(CONFIG['upstream_admin_port'], "upstream", + CONFIG['envoy_startup_timeout']): + logger.error("Cloud Envoy failed to become ready after restart") + return False + logger.info("✓ Cloud Envoy is ready after restart") + return True + else: + logger.error(f"Failed to start upstream Envoy: {result.stderr}") + return False + except Exception as e: + logger.error(f"Error starting upstream Envoy: {e}") + return False + + def stop_upstream_envoy(self) -> bool: + """Stop the upstream Envoy container.""" + logger.info("Stopping upstream Envoy container") + try: + container_name = self.get_container_name(CONFIG['upstream_container']) + result = subprocess.run(['docker', 'stop', container_name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=30) + if result.returncode == 0: + logger.info("✓ Cloud Envoy container stopped") + return True + else: + logger.error(f"Failed to stop upstream Envoy: {result.stderr}") + return False + except Exception as e: + logger.error(f"Error stopping upstream Envoy: {e}") + return False + + def run_test(self): + """Run the complete reverse connection test.""" + try: + logger.info("Starting reverse connection test") + + # Step 0: Start Docker Compose services with xDS config + downstream_config_with_xds = self.create_downstream_config_with_xds() + if not self.start_docker_compose(downstream_config_with_xds): + raise Exception("Failed to start Docker Compose services") + + # Step 1: Wait for Envoy instances to be ready + if not self.wait_for_envoy_ready(CONFIG['upstream_admin_port'], "upstream", + CONFIG['envoy_startup_timeout']): + raise Exception("Upstream Envoy failed to start") + + if not self.wait_for_envoy_ready(CONFIG['downstream_admin_port'], "downstream", + CONFIG['envoy_startup_timeout']): + raise Exception("Downstream Envoy failed to start") + + # Step 2: Verify reverse connections are NOT established + logger.info("Verifying reverse connections are NOT established") + time.sleep(5) # Give some time for any potential connections + if self.check_reverse_connections( + CONFIG['upstream_api_port']): # upstream-envoy's API port + raise Exception( + "Reverse connections should not be established without reverse_conn_listener") + logger.info("✓ Reverse connections are correctly not established") + + # Step 3: Add reverse_conn_listener to downstream via xDS + logger.info("Adding reverse_conn_listener to downstream via xDS") + if not self.add_reverse_conn_listener_via_xds(): + raise Exception("Failed to add reverse_conn_listener via xDS") + + # Step 4: Wait for reverse connections to be established + logger.info("Waiting for reverse connections to be established") + max_wait = 60 + start_time = time.time() + while time.time() - start_time < max_wait: + if self.check_reverse_connections( + CONFIG['upstream_api_port']): # upstream-envoy's API port + logger.info("✓ Reverse connections are established") + break + logger.info("Waiting for reverse connections to be established") + time.sleep(1) + else: + raise Exception("Reverse connections failed to establish within timeout") + + # Step 5: Test request through reverse connection + logger.info("Testing request through reverse connection") + if not self.test_reverse_connection_request( + CONFIG['upstream_egress_port']): # upstream-envoy's egress port + raise Exception("Reverse connection request failed") + logger.info("✓ Reverse connection request successful") + + # Step 6: Stop upstream Envoy and verify reverse connections are down + logger.info("Step 6: Stopping upstream Envoy to test connection recovery") + if not self.stop_upstream_envoy(): + raise Exception("Failed to stop upstream Envoy") + + # Verify reverse connections are down + logger.info("Verifying reverse connections are down after stopping upstream Envoy") + time.sleep(2) # Give some time for connections to be detected as down + if self.check_reverse_connections(CONFIG['upstream_api_port']): + logger.warn("Reverse connections still appear active after stopping upstream Envoy") + else: + logger.info( + "✓ Reverse connections are correctly down after stopping upstream Envoy") + + # Step 7: Wait for > drain timer (3s) and then start upstream Envoy + logger.info("Step 7: Waiting for drain timer (3s) before starting upstream Envoy") + time.sleep(15) # Wait more than the reverse conn retry timer for the connections + # to be drained. + + logger.info("Starting upstream Envoy to test reverse connection re-establishment") + if not self.start_upstream_envoy(): + raise Exception("Failed to start upstream Envoy") + + # Step 8: Verify reverse connections are re-established + logger.info("Step 8: Verifying reverse connections are re-established") + max_wait = 60 + start_time = time.time() + while time.time() - start_time < max_wait: + if self.check_reverse_connections(CONFIG['upstream_api_port']): + logger.info( + "✓ Reverse connections are re-established after upstream Envoy restart") + break + logger.info("Waiting for reverse connections to be re-established") + time.sleep(1) + else: + raise Exception("Reverse connections failed to re-establish within timeout") + + # # Step 10: Remove reverse_conn_listener from downstream via xDS + logger.info("Removing reverse_conn_listener from downstream via xDS") + if not self.remove_reverse_conn_listener_via_xds(): + raise Exception("Failed to remove reverse_conn_listener via xDS") + + # # Step 11: Verify reverse connections are torn down + logger.info("Verifying reverse connections are torn down") + time.sleep(10) # Wait for connections to be torn down + if self.check_reverse_connections( + CONFIG['upstream_api_port']): # upstream-envoy's API port + raise Exception("Reverse connections should be torn down after removing listener") + logger.info("✓ Reverse connections are correctly torn down") + + logger.info("Test completed successfully!") + return True + + except Exception as e: + logger.error(f"Test failed: {e}") + return False + finally: + self.cleanup() + + def cleanup(self): + """Clean up processes and temporary files.""" + logger.info("Cleaning up") + + # Stop Docker Compose services + if self.docker_compose_process: + self.docker_compose_process.terminate() + self.docker_compose_process.wait() + + self.stop_docker_compose() + + # Clean up temp directory + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + + +def main(): + """Main function.""" + tester = ReverseConnectionTester() + + # Handle Ctrl+C gracefully + def signal_handler(sig, frame): + logger.info("Received interrupt signal, cleaning up...") + tester.cleanup() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + + success = tester.run_test() + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/examples/reverse_connection_macos_config/cloud-envoy.yaml b/examples/reverse_connection_macos_config/cloud-envoy.yaml new file mode 100644 index 0000000000000..dfca37d4a5a43 --- /dev/null +++ b/examples/reverse_connection_macos_config/cloud-envoy.yaml @@ -0,0 +1,102 @@ +--- +node: + id: cloud-node + cluster: cloud +static_resources: + listeners: + # Services reverse conn APIs + - name: rev_conn_api_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 9000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: rev_conn_api + # Any dummy route config works + route_config: + virtual_hosts: + - name: rev_conn_api_route + domains: + - "*" + routes: + - match: + prefix: '/on_prem_service' + route: + cluster: reverse_connection_cluster + http_filters: + # Filter that services reverse conn APIs + - name: envoy.filters.http.reverse_conn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.reverse_conn.v3.ReverseConn + ping_interval: 30 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Listener that will route the downstream request to the reverse connection cluster + - name: egress_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8085 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: "/on_prem_service" + route: + cluster: reverse_connection_cluster + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + # Cluster used to write requests to cached sockets + clusters: + - name: reverse_connection_cluster + connect_timeout: 200s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + # The following headers are expected in downstream requests + # to be sent over reverse connections + http_header_names: + - x-remote-node-id # Should be set to the node ID of the downstream envoy node, ie., on-prem-node + - x-dst-cluster-uuid # Should be set to the cluster ID of the downstream envoy node, ie., on-prem + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + # Right the moment, reverse connections are supported over HTTP/2 only + http2_protocol_options: {} +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + address: 127.0.0.1 + port_value: 8878 +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 +# Enable reverse connection bootstrap extension +bootstrap_extensions: +- name: envoy.bootstrap.reverse_connection.upstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3.UpstreamReverseConnectionSocketInterface + stat_prefix: "upstream_reverse_connection" \ No newline at end of file diff --git a/examples/reverse_connection_macos_config/on-prem-envoy-custom-resolver.yaml b/examples/reverse_connection_macos_config/on-prem-envoy-custom-resolver.yaml new file mode 100644 index 0000000000000..71ec377b8885c --- /dev/null +++ b/examples/reverse_connection_macos_config/on-prem-envoy-custom-resolver.yaml @@ -0,0 +1,149 @@ +--- +node: + id: on-prem-node + cluster: on-prem + +# Enable reverse connection bootstrap extension which registers the custom resolver +bootstrap_extensions: +- name: envoy.bootstrap.reverse_connection.downstream_reverse_connection_socket_interface + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_connection_socket_interface.v3.DownstreamReverseConnectionSocketInterface + stat_prefix: "downstream_reverse_connection" + +static_resources: + listeners: + # Services reverse conn APIs + - name: rev_conn_api_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 9001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: rev_conn_api + codec_type: AUTO + route_config: + name: rev_conn_api_route + virtual_hosts: [] + http_filters: + - name: envoy.filters.http.reverse_conn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.reverse_conn.v3.ReverseConn + ping_interval: 30 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Forwards incoming http requests to backend + - name: ingress_http_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 6060 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: ingress_http_route + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/on_prem_service' + route: + cluster: on-prem-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Initiates reverse connections to cloud using custom resolver + - name: reverse_conn_listener + listener_filters_timeout: 0s + listener_filters: + # Filter that responds to keepalives on reverse connection sockets + - name: envoy.filters.listener.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.reverse_connection.v3.ReverseConnection + ping_wait_timeout: 120 + # Use custom address with reverse connection metadata encoded in URL format + address: + socket_address: + # This encodes: src_node_id=on-prem-node, src_cluster_id=on-prem, src_tenant_id=on-prem + # and remote clusters: cloud with 1 connection + address: "rc://on-prem-node:on-prem:on-prem@cloud:10" + port_value: 0 + # Use custom resolver that can parse reverse connection metadata + resolver_name: "envoy.resolvers.reverse_connection" + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: reverse_conn_listener + route_config: + virtual_hosts: + - name: backend + domains: + - "*" + routes: + - match: + prefix: '/on_prem_service' + route: + cluster: on-prem-service + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # Cluster designating cloud-envoy + clusters: + - name: cloud + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: cloud + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 # Container name of cloud-envoy in docker-compose + port_value: 9000 # Port where cloud-envoy's rev_conn_api_listener listens + + # Backend HTTP service behind onprem which + # we will access via reverse connections + - name: on-prem-service + type: STRICT_DNS + connect_timeout: 30s + load_assignment: + cluster_name: on-prem-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 7070 + +admin: + access_log_path: "/dev/stdout" + address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 8888 + +layered_runtime: + layers: + - name: layer + static_layer: + re2.max_program_size.error_level: 1000 \ No newline at end of file diff --git a/go.mod b/go.mod index fc0002fc3a35c..026f85cabedcb 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/envoyproxy/envoy -go 1.22 +go 1.23 -require google.golang.org/protobuf v1.36.6 +require google.golang.org/protobuf v1.36.9 require github.com/google/go-cmp v0.5.9 // indirect diff --git a/go.sum b/go.sum index ce42a4aeef356..7633c53456409 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= diff --git a/mobile/.bazelrc b/mobile/.bazelrc index 5c5131790e249..833394cf13ed3 100644 --- a/mobile/.bazelrc +++ b/mobile/.bazelrc @@ -203,15 +203,6 @@ test:mobile-remote-ci-linux-tsan --test_env=ENVOY_IP_TEST_VERSIONS=v4only ############################################################################# # Clang environment variables (keep in sync with //third_party/rbe_configs) # Coverage environment variables (keep in sync with //third_party/rbe_configs) -build:mobile-ci-linux-coverage --action_env=GCOV=/opt/llvm/bin/llvm-profdata -build:mobile-ci-linux-coverage --test_env=GCOV=/opt/llvm/bin/llvm-profdata -build:mobile-ci-linux-coverage --repo_env=GCOV=/opt/llvm/bin/llvm-profdata -build:mobile-ci-linux-coverage --action_env=BAZEL_LLVM_COV=/opt/llvm/bin/llvm-cov -build:mobile-ci-linux-coverage --test_env=BAZEL_LLVM_COV=/opt/llvm/bin/llvm-cov -build:mobile-ci-linux-coverage --repo_env=BAZEL_LLVM_COV=/opt/llvm/bin/llvm-cov -build:mobile-ci-linux-coverage --action_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1 -build:mobile-ci-linux-coverage --test_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1 -build:mobile-ci-linux-coverage --repo_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1 build:mobile-ci-linux-coverage --build_tests_only build:mobile-ci-linux-coverage --@envoy//tools/coverage:config=@envoy_mobile//test:coverage_config diff --git a/mobile/WORKSPACE b/mobile/WORKSPACE index e03a4865991c6..4f69835631999 100644 --- a/mobile/WORKSPACE +++ b/mobile/WORKSPACE @@ -56,7 +56,7 @@ envoy_dependencies() load("@envoy//bazel:repositories_extra.bzl", "envoy_dependencies_extra") -envoy_dependencies_extra(ignore_root_user_error=True) +envoy_dependencies_extra(ignore_root_user_error = True) load("@envoy//bazel:python_dependencies.bzl", "envoy_python_dependencies") diff --git a/mobile/bazel/android_artifacts.bzl b/mobile/bazel/android_artifacts.bzl index d2e63cfbf39d1..f2885d18a586f 100644 --- a/mobile/bazel/android_artifacts.bzl +++ b/mobile/bazel/android_artifacts.bzl @@ -308,7 +308,7 @@ def _manifest(package_name): package="{}" > """.format(package_name) diff --git a/mobile/bazel/test_manifest.xml b/mobile/bazel/test_manifest.xml index 2176226b2013d..da03dbd55d9f5 100644 --- a/mobile/bazel/test_manifest.xml +++ b/mobile/bazel/test_manifest.xml @@ -7,6 +7,6 @@ diff --git a/mobile/docs/BUILD b/mobile/docs/BUILD index c32bcffcba352..b0bfdd30f2532 100644 --- a/mobile/docs/BUILD +++ b/mobile/docs/BUILD @@ -3,6 +3,7 @@ load("@envoy//bazel:envoy_build_system.bzl", "envoy_mobile_package") load("@envoy//tools/python:namespace.bzl", "envoy_py_namespace") load("@rules_pkg//pkg:mappings.bzl", "pkg_filegroup", "pkg_files") load("@rules_pkg//pkg:pkg.bzl", "pkg_tar") +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") licenses(["notice"]) # Apache 2 diff --git a/mobile/examples/java/hello_world/AndroidManifest.xml b/mobile/examples/java/hello_world/AndroidManifest.xml index 3bcd50ef8bf2c..761c180a60c7c 100644 --- a/mobile/examples/java/hello_world/AndroidManifest.xml +++ b/mobile/examples/java/hello_world/AndroidManifest.xml @@ -8,7 +8,7 @@ diff --git a/mobile/library/cc/engine_builder.cc b/mobile/library/cc/engine_builder.cc index ab5f5049f42e5..15d1d436210da 100644 --- a/mobile/library/cc/engine_builder.cc +++ b/mobile/library/cc/engine_builder.cc @@ -308,7 +308,7 @@ EngineBuilder& EngineBuilder::addNativeFilter(std::string name, std::string type } EngineBuilder& EngineBuilder::addNativeFilter(const std::string& name, - const ProtobufWkt::Any& typed_config) { + const Protobuf::Any& typed_config) { native_filter_chain_.push_back(NativeFilterConfig(name, typed_config)); return *this; } @@ -324,7 +324,7 @@ std::string EngineBuilder::nativeNameToConfig(absl::string_view name) { proto_config.set_platform_filter_name(name); std::string ret; proto_config.SerializeToString(&ret); - ProtobufWkt::Any any_config; + Protobuf::Any any_config; any_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.platform_bridge.PlatformBridge"); any_config.set_value(ret); @@ -833,7 +833,7 @@ std::unique_ptr EngineBuilder::generate auto* node = bootstrap->mutable_node(); node->set_id("envoy-mobile"); node->set_cluster("envoy-mobile"); - ProtobufWkt::Struct& metadata = *node->mutable_metadata(); + Protobuf::Struct& metadata = *node->mutable_metadata(); (*metadata.mutable_fields())["app_id"].set_string_value(app_id_); (*metadata.mutable_fields())["app_version"].set_string_value(app_version_); (*metadata.mutable_fields())["device_os"].set_string_value(device_os_); @@ -841,17 +841,16 @@ std::unique_ptr EngineBuilder::generate // Set up runtime. auto* runtime = bootstrap->mutable_layered_runtime()->add_layers(); runtime->set_name("static_layer_0"); - ProtobufWkt::Struct envoy_layer; - ProtobufWkt::Struct& runtime_values = + Protobuf::Struct envoy_layer; + Protobuf::Struct& runtime_values = *(*envoy_layer.mutable_fields())["envoy"].mutable_struct_value(); - ProtobufWkt::Struct& reloadable_features = + Protobuf::Struct& reloadable_features = *(*runtime_values.mutable_fields())["reloadable_features"].mutable_struct_value(); - (*reloadable_features.mutable_fields())["prefer_quic_client_udp_gro"].set_bool_value(true); for (auto& guard_and_value : runtime_guards_) { (*reloadable_features.mutable_fields())[guard_and_value.first].set_bool_value( guard_and_value.second); } - ProtobufWkt::Struct& restart_features = + Protobuf::Struct& restart_features = *(*runtime_values.mutable_fields())["restart_features"].mutable_struct_value(); (*runtime_values.mutable_fields())["disallow_global_stats"].set_bool_value(true); (*runtime_values.mutable_fields())["enable_dfp_dns_trace"].set_bool_value(true); @@ -859,7 +858,7 @@ std::unique_ptr EngineBuilder::generate (*restart_features.mutable_fields())[guard_and_value.first].set_bool_value( guard_and_value.second); } - ProtobufWkt::Struct& overload_values = + Protobuf::Struct& overload_values = *(*envoy_layer.mutable_fields())["overload"].mutable_struct_value(); (*overload_values.mutable_fields())["global_downstream_max_connections"].set_string_value( "4294967295"); diff --git a/mobile/library/cc/engine_builder.h b/mobile/library/cc/engine_builder.h index 724c2f2421a19..95f2f76b3960e 100644 --- a/mobile/library/cc/engine_builder.h +++ b/mobile/library/cc/engine_builder.h @@ -87,7 +87,7 @@ class EngineBuilder { // E.g. addDnsPreresolveHost(std::string host, uint32_t port); EngineBuilder& addDnsPreresolveHostnames(const std::vector& hostnames); EngineBuilder& addNativeFilter(std::string name, std::string typed_config); - EngineBuilder& addNativeFilter(const std::string& name, const ProtobufWkt::Any& typed_config); + EngineBuilder& addNativeFilter(const std::string& name, const Protobuf::Any& typed_config); EngineBuilder& addPlatformFilter(const std::string& name); // Adds a runtime guard for the `envoy.reloadable_features.`. @@ -140,12 +140,12 @@ class EngineBuilder { NativeFilterConfig(std::string name, std::string typed_config) : name_(std::move(name)), textproto_typed_config_(std::move(typed_config)) {} - NativeFilterConfig(const std::string& name, const ProtobufWkt::Any& typed_config) + NativeFilterConfig(const std::string& name, const Protobuf::Any& typed_config) : name_(name), typed_config_(typed_config) {} std::string name_; std::string textproto_typed_config_{}; - ProtobufWkt::Any typed_config_{}; + Protobuf::Any typed_config_{}; }; Logger::Logger::Levels log_level_ = Logger::Logger::Levels::info; diff --git a/mobile/library/common/internal_engine.cc b/mobile/library/common/internal_engine.cc index 75f09af7bff12..0299f5de802b9 100644 --- a/mobile/library/common/internal_engine.cc +++ b/mobile/library/common/internal_engine.cc @@ -27,6 +27,8 @@ MobileProcessWide& initOnceMobileProcessWide(const OptionsImplBase& options) { Network::Address::InstanceConstSharedPtr ipv6ProbeAddr() { // Use Google DNS IPv6 address for IPv6 probes. + // Same as Chromium: + // https://source.chromium.org/chromium/chromium/src/+/main:net/dns/host_resolver_manager.cc;l=155;drc=7b232da0f22e8cdf555d43c52b6491baeb87f729. CONSTRUCT_ON_FIRST_USE(Network::Address::InstanceConstSharedPtr, new Network::Address::Ipv6Instance("2001:4860:4860::8888", 53)); } @@ -218,6 +220,26 @@ envoy_status_t InternalEngine::main(std::shared_ptr options) { server_->serverFactoryContext(), server_->serverFactoryContext().messageValidationVisitor()); connectivity_manager_ = Network::ConnectivityManagerFactory{generic_context}.get(); + Network::DefaultNetworkChangeCallback cb = + [this](envoy_netconf_t current_configuration_key) { + dispatcher_->post([this, current_configuration_key]() { + if (connectivity_manager_->getConfigurationKey() != current_configuration_key) { + // The default network has changed to a different one. + return; + } + ENVOY_LOG_MISC( + trace, + "Default network state has been changed. Current net configuration key {}", + current_configuration_key); + resetHttpPropertiesAndDrainHosts(probeAndGetLocalAddr(AF_INET6) != nullptr); + if (!disable_dns_refresh_on_network_change_) { + // This call will possibly drain all connections asynchronously. + connectivity_manager_->doRefreshDns(current_configuration_key, + /*drain_connections=*/true); + } + }); + }; + connectivity_manager_->setDefaultNetworkChangeCallback(std::move(cb)); if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.dns_cache_set_ip_version_to_remove")) { if (probeAndGetLocalAddr(AF_INET6) == nullptr) { @@ -373,23 +395,21 @@ void InternalEngine::onDefaultNetworkChanged(int network) { }); } -void InternalEngine::onDefaultNetworkChangedAndroid(ConnectionType /*connection_type*/, - int64_t /*net_id*/) { - ENVOY_LOG_MISC(trace, "Calling the default network changed callback on Android"); +void InternalEngine::onDefaultNetworkChangedAndroid(ConnectionType connection_type, + int64_t net_id) { + connectivity_manager_->onDefaultNetworkChangedAndroid(connection_type, net_id); } -void InternalEngine::onNetworkDisconnectAndroid(int64_t /*net_id*/) { - ENVOY_LOG_MISC(trace, "Calling network disconnect callback on Android"); +void InternalEngine::onNetworkDisconnectAndroid(int64_t net_id) { + connectivity_manager_->onNetworkDisconnectAndroid(net_id); } -void InternalEngine::onNetworkConnectAndroid(ConnectionType /*connection_type*/, - int64_t /*net_id*/) { - ENVOY_LOG_MISC(trace, "Calling network connect callback on Android"); +void InternalEngine::onNetworkConnectAndroid(ConnectionType connection_type, int64_t net_id) { + connectivity_manager_->onNetworkConnectAndroid(connection_type, net_id); } -void InternalEngine::purgeActiveNetworkListAndroid( - const std::vector& /*active_network_ids*/) { - ENVOY_LOG_MISC(trace, "Calling network purge callback on Android"); +void InternalEngine::purgeActiveNetworkListAndroid(const std::vector& active_network_ids) { + connectivity_manager_->purgeActiveNetworkListAndroid(active_network_ids); } void InternalEngine::onDefaultNetworkUnavailable() { @@ -398,8 +418,17 @@ void InternalEngine::onDefaultNetworkUnavailable() { } void InternalEngine::handleNetworkChange(const int network_type, const bool has_ipv6_connectivity) { - envoy_netconf_t configuration = - Network::ConnectivityManagerImpl::setPreferredNetwork(network_type); + envoy_netconf_t configuration = connectivity_manager_->setPreferredNetwork(network_type); + + resetHttpPropertiesAndDrainHosts(has_ipv6_connectivity); + if (!disable_dns_refresh_on_network_change_) { + // Refresh DNS upon network changes. + // This call will possibly drain all connections asynchronously. + connectivity_manager_->refreshDns(configuration, /*drain_connections=*/true); + } +} + +void InternalEngine::resetHttpPropertiesAndDrainHosts(bool has_ipv6_connectivity) { if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.dns_cache_set_ip_version_to_remove") || Runtime::runtimeFeatureEnabled( @@ -415,25 +444,28 @@ void InternalEngine::handleNetworkChange(const int network_type, const bool has_ } Http::HttpServerPropertiesCacheManager& cache_manager = server_->httpServerPropertiesCacheManager(); - - Http::HttpServerPropertiesCacheManager::CacheFn clear_brokenness = - [](Http::HttpServerPropertiesCache& cache) { cache.resetBrokenness(); }; - cache_manager.forEachThreadLocalCache(clear_brokenness); if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_no_tcp_delay")) { - Http::HttpServerPropertiesCacheManager& cache_manager = - server_->httpServerPropertiesCacheManager(); - + // Reset HTTP/3 status for all origins. Http::HttpServerPropertiesCacheManager::CacheFn reset_status = [](Http::HttpServerPropertiesCache& cache) { cache.resetStatus(); }; cache_manager.forEachThreadLocalCache(reset_status); + } else { + // Reset HTTP/3 status only for origins marked as broken. + Http::HttpServerPropertiesCacheManager::CacheFn clear_brokenness = + [](Http::HttpServerPropertiesCache& cache) { cache.resetBrokenness(); }; + cache_manager.forEachThreadLocalCache(clear_brokenness); } - if (!disable_dns_refresh_on_network_change_) { - connectivity_manager_->refreshDns(configuration, /*drain_connections=*/true); - } else if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.drain_pools_on_network_change")) { - ENVOY_LOG_EVENT(debug, "netconf_immediate_drain", "DrainAllHosts"); - connectivity_manager_->clusterManager().drainConnections( - [](const Upstream::Host&) { return true; }); + + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh") || + disable_dns_refresh_on_network_change_) { + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.drain_pools_on_network_change")) { + // Since DNS refreshing is disabled, explicitly drain all non-migratable connections. + ENVOY_LOG_EVENT(debug, "netconf_immediate_drain", "DrainAllHosts"); + getClusterManager().drainConnections( + [](const Upstream::Host&) { return true; }, + Envoy::ConnectionPool::DrainBehavior::DrainExistingNonMigratableConnections); + } } } @@ -542,9 +574,6 @@ void InternalEngine::logInterfaces(absl::string_view event, Network::Address::InstanceConstSharedPtr InternalEngine::probeAndGetLocalAddr(int domain) { // This probing logic is borrowed from Chromium. - // - - // https://source.chromium.org/chromium/chromium/src/+/main:net/dns/host_resolver_manager.cc;l=154-157;drc=7b232da0f22e8cdf555d43c52b6491baeb87f729 - // - // https://source.chromium.org/chromium/chromium/src/+/main:net/dns/host_resolver_manager.cc;l=1467-1488;drc=7b232da0f22e8cdf555d43c52b6491baeb87f729 ENVOY_LOG(trace, "Checking for {} connectivity.", domain == AF_INET6 ? "IPv6" : "IPv4"); const Api::SysCallSocketResult socket_result = @@ -557,18 +586,47 @@ Network::Address::InstanceConstSharedPtr InternalEngine::probeAndGetLocalAddr(in /* socket_v6only= */ domain == AF_INET6, {domain}); Api::SysCallIntResult connect_result = socket_handle.connect(domain == AF_INET6 ? ipv6ProbeAddr() : ipv4ProbeAddr()); - if (connect_result.return_value_ == 0) { - auto address_or_error = socket_handle.localAddress(); - if (!address_or_error.status().ok()) { - ENVOY_LOG(trace, "Local address error: {}", address_or_error.status().message()); + if (connect_result.return_value_ != 0) { + ENVOY_LOG(trace, "No {} connectivity found with errno: {}.", + domain == AF_INET6 ? "IPv6" : "IPv4", connect_result.errno_); + return nullptr; + } + + absl::StatusOr address = socket_handle.localAddress(); + if (!address.status().ok()) { + ENVOY_LOG(trace, "Local address error: {}", address.status().message()); + return nullptr; + } + + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.mobile_ipv6_probe_simple_filtering")) { + if ((*address)->ip() == nullptr) { + ENVOY_LOG(trace, "Local address is not an IP address: {}.", (*address)->asString()); return nullptr; } - ENVOY_LOG(trace, "Found {} connectivity.", domain == AF_INET6 ? "IPv6" : "IPv4"); - return *address_or_error; + if ((*address)->ip()->isLinkLocalAddress()) { + ENVOY_LOG(trace, "Ignoring link-local address: {}.", (*address)->asString()); + return nullptr; + } + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.mobile_ipv6_probe_advanced_filtering")) { + if ((*address)->ip()->isUniqueLocalAddress()) { + ENVOY_LOG(trace, "Ignoring unique-local address: {}.", (*address)->asString()); + return nullptr; + } + if ((*address)->ip()->isSiteLocalAddress()) { + ENVOY_LOG(trace, "Ignoring site-local address: {}.", (*address)->asString()); + return nullptr; + } + if ((*address)->ip()->isTeredoAddress()) { + ENVOY_LOG(trace, "Ignoring teredo address: {}.", (*address)->asString()); + return nullptr; + } + } } - ENVOY_LOG(trace, "No {} connectivity found with errno: {}.", domain == AF_INET6 ? "IPv6" : "IPv4", - connect_result.errno_); - return nullptr; + + ENVOY_LOG(trace, "Found {} connectivity.", domain == AF_INET6 ? "IPv6" : "IPv4"); + return *address; } } // namespace Envoy diff --git a/mobile/library/common/internal_engine.h b/mobile/library/common/internal_engine.h index c6fc839813b24..e90c7a0f53359 100644 --- a/mobile/library/common/internal_engine.h +++ b/mobile/library/common/internal_engine.h @@ -157,8 +157,8 @@ class InternalEngine : public Logger::Loggable { void onNetworkConnectAndroid(ConnectionType connection_type, int64_t net_id); /** - * The callback that gets executed when the device decides that the given list of networks should - * be forgotten. + * The callback that gets executed when the device decides to forget all networks other than the + * given list. */ void purgeActiveNetworkListAndroid(const std::vector& active_network_ids); @@ -228,6 +228,9 @@ class InternalEngine : public Logger::Loggable { // there is no connectivity for the `domain`, a null pointer will be returned. static Network::Address::InstanceConstSharedPtr probeAndGetLocalAddr(int domain); + // Called when it's been determined that the default network has changed. + void resetHttpPropertiesAndDrainHosts(bool has_ipv6_connectivity); + Thread::PosixThreadFactoryPtr thread_factory_; Event::Dispatcher* event_dispatcher_{}; Stats::ScopeSharedPtr client_scope_; @@ -241,7 +244,7 @@ class InternalEngine : public Logger::Loggable { Thread::MutexBasicLockable mutex_; Thread::CondVar cv_; Http::ClientPtr http_client_; - Network::ConnectivityManagerSharedPtr connectivity_manager_; + Network::ConnectivityManagerImplSharedPtr connectivity_manager_; Event::ProvisionalDispatcherPtr dispatcher_; // Used by the cerr logger to ensure logs don't overwrite each other. absl::Mutex log_mutex_; diff --git a/mobile/library/common/logger/logger_delegate.cc b/mobile/library/common/logger/logger_delegate.cc index c34ee275404bc..5faab74eced59 100644 --- a/mobile/library/common/logger/logger_delegate.cc +++ b/mobile/library/common/logger/logger_delegate.cc @@ -44,12 +44,12 @@ DefaultDelegate::~DefaultDelegate() { restoreDelegate(); } // SinkDelegate void DefaultDelegate::log(absl::string_view msg, const spdlog::details::log_msg&) { - absl::MutexLock l(&mutex_); + absl::MutexLock l(mutex_); std::cerr << msg; } void DefaultDelegate::flush() { - absl::MutexLock l(&mutex_); + absl::MutexLock l(mutex_); std::cerr << std::flush; } diff --git a/mobile/library/common/network/BUILD b/mobile/library/common/network/BUILD index 22b2ec7e213a8..82a5d6b384604 100644 --- a/mobile/library/common/network/BUILD +++ b/mobile/library/common/network/BUILD @@ -19,6 +19,7 @@ envoy_cc_library( ":proxy_settings_lib", "//library/common:engine_types_lib", "//library/common/network:src_addr_socket_option_lib", + "//library/common/system:system_helper_lib", "//library/common/types:c_types_lib", "@envoy//envoy/network:socket_interface", "@envoy//envoy/singleton:manager_interface", diff --git a/mobile/library/common/network/connectivity_manager.cc b/mobile/library/common/network/connectivity_manager.cc index 9e1d6e30260be..085f55046ce76 100644 --- a/mobile/library/common/network/connectivity_manager.cc +++ b/mobile/library/common/network/connectivity_manager.cc @@ -2,6 +2,9 @@ #include +#include +#include + #include "envoy/common/platform.h" #include "source/common/api/os_sys_calls_impl.h" @@ -14,8 +17,8 @@ #include "fmt/ostream.h" #include "library/common/network/network_type_socket_option_impl.h" -#include "library/common/network/network_types.h" #include "library/common/network/src_addr_socket_option_impl.h" +#include "library/common/system/system_helper.h" // Used on Linux (requires root/CAP_NET_RAW) #ifdef SO_BINDTODEVICE @@ -75,15 +78,15 @@ constexpr absl::string_view BaseDnsCache = "base_dns_cache"; // The number of faults allowed on a newly-established connection before switching socket mode. constexpr unsigned int InitialFaultThreshold = 1; -// The number of faults allowed on a previously-successful connection (i.e. able to send and receive -// L7 bytes) before switching socket mode. -constexpr unsigned int MaxFaultThreshold = 3; -ConnectivityManagerImpl::NetworkState ConnectivityManagerImpl::network_state_{ - 1, 0, MaxFaultThreshold, SocketMode::DefaultPreferredNetworkMode, Thread::MutexBasicLockable{}}; +ConnectivityManagerImpl::ConnectivityManagerImpl(Upstream::ClusterManager& cluster_manager, + DnsCacheManagerSharedPtr dns_cache_manager) + : cluster_manager_(cluster_manager), dns_cache_manager_(dns_cache_manager) { + initializeNetworkStates(); +} envoy_netconf_t ConnectivityManagerImpl::setPreferredNetwork(int network) { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; // TODO(goaway): Re-enable this guard. There's some concern that this will miss network updates // moving from offline to online states. We should address this then re-enable this guard to @@ -93,14 +96,19 @@ envoy_netconf_t ConnectivityManagerImpl::setPreferredNetwork(int network) { // return network_state_.configuration_key_ - 1; //} - ENVOY_LOG_EVENT(debug, "netconf_network_change", "{}", std::to_string(static_cast(network))); + setPreferredNetworkNoLock(network); + return network_state_.configuration_key_; +} + +void ConnectivityManagerImpl::setPreferredNetworkNoLock(int network_type) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(network_mutex_) { + ENVOY_LOG_EVENT(debug, "netconf_network_change", "network_type changed to {}", + std::to_string(static_cast(network_type))); network_state_.configuration_key_++; - network_state_.network_ = network; + network_state_.network_ = network_type; network_state_.remaining_faults_ = 1; network_state_.socket_mode_ = SocketMode::DefaultPreferredNetworkMode; - - return network_state_.configuration_key_; } void ConnectivityManagerImpl::setProxySettings(ProxySettingsConstSharedPtr new_proxy_settings) { @@ -120,17 +128,17 @@ void ConnectivityManagerImpl::setProxySettings(ProxySettingsConstSharedPtr new_p ProxySettingsConstSharedPtr ConnectivityManagerImpl::getProxySettings() { return proxy_settings_; } int ConnectivityManagerImpl::getPreferredNetwork() { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; return network_state_.network_; } SocketMode ConnectivityManagerImpl::getSocketMode() { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; return network_state_.socket_mode_; } envoy_netconf_t ConnectivityManagerImpl::getConfigurationKey() { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; return network_state_.configuration_key_; } @@ -152,7 +160,7 @@ void ConnectivityManagerImpl::reportNetworkUsage(envoy_netconf_t configuration_k bool configuration_updated = false; { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; // If the configuration_key isn't current, don't do anything. if (configuration_key != network_state_.configuration_key_) { @@ -200,40 +208,56 @@ void ConnectivityManagerImpl::reportNetworkUsage(envoy_netconf_t configuration_k } } -void ConnectivityManagerImpl::onDnsResolutionComplete( +void RefreshDnsWithPostDrainHandler::refreshDnsAndDrainHosts() { + Extensions::Common::DynamicForwardProxy::DnsCacheSharedPtr dns_cache = + dns_cache_manager_->lookUpCacheByName(BaseDnsCache); + if (!dns_cache) { + // There may not be a DNS cache during initialization, but if one is available, it should always + // exist by the time this handler is instantiated from the NetworkConfigurationFilter. + ENVOY_LOG_EVENT(warn, "netconf_dns_cache_missing", "{}", std::string(BaseDnsCache)); + return; + } + if (dns_callbacks_handle_ == nullptr) { + // Register callbacks once, on demand, using the handler as a sentinel. + dns_callbacks_handle_ = dns_cache->addUpdateCallbacks(*this); + } + dns_cache->iterateHostMap( + [&](absl::string_view host, + const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&) { + hosts_to_drain_.emplace(host); + }); + + dns_cache->forceRefreshHosts(); +} + +void RefreshDnsWithPostDrainHandler::onDnsResolutionComplete( const std::string& resolved_host, const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&, Network::DnsResolver::ResolutionStatus) { - if (enable_drain_post_dns_refresh_) { - // Check if the set of hosts pending drain contains the current resolved host. - if (hosts_to_drain_.erase(resolved_host) == 0) { - return; - } - - // We ignore whether DNS resolution has succeeded here. If it failed, we may be offline and - // should probably drain connections. If it succeeds, we may have new DNS entries and so we - // drain connections. It may be possible to refine this logic in the future. - // TODO(goaway): check the set of cached hosts from the last triggered DNS refresh for this - // host, and if present, remove it and trigger connection drain for this host specifically. - ENVOY_LOG_EVENT(debug, "netconf_post_dns_drain_cx", "{}", resolved_host); - - // Pass predicate to only drain connections to the resolved host (for any cluster). - cluster_manager_.drainConnections( - [resolved_host](const Upstream::Host& host) { return host.hostname() == resolved_host; }); + // Check if the set of hosts pending drain contains the current resolved host. + if (hosts_to_drain_.erase(resolved_host) == 0) { + return; } + + // We ignore whether DNS resolution has succeeded here. If it failed, we may be offline and + // should probably drain connections. If it succeeds, we may have new DNS entries and so we + // drain connections. It may be possible to refine this logic in the future. + // TODO(goaway): check the set of cached hosts from the last triggered DNS refresh for this + // host, and if present, remove it and trigger connection drain for this host specifically. + ENVOY_LOG_EVENT(debug, "netconf_post_dns_drain_cx", "{}", resolved_host); + + // Pass predicate to only drain connections to the resolved host (for any cluster). + cluster_manager_.drainConnections( + [resolved_host](const Upstream::Host& host) { return host.hostname() == resolved_host; }, + ConnectionPool::DrainBehavior::DrainExistingConnections); } void ConnectivityManagerImpl::setDrainPostDnsRefreshEnabled(bool enabled) { - enable_drain_post_dns_refresh_ = enabled; if (!enabled) { - hosts_to_drain_.clear(); - } else if (!dns_callbacks_handle_) { - // Register callbacks once, on demand, using the handle as a sentinel. There may not be - // a DNS cache during initialization, but if one is available, it should always exist by the - // time this function is called from the NetworkConfigurationFilter. - if (auto dns_cache = dnsCache()) { - dns_callbacks_handle_ = dns_cache->addUpdateCallbacks(*this); - } + dns_refresh_handler_ = nullptr; + } else if (!dns_refresh_handler_) { + dns_refresh_handler_ = + std::make_unique(dns_cache_manager_, cluster_manager_); } } @@ -244,7 +268,7 @@ void ConnectivityManagerImpl::setInterfaceBindingEnabled(bool enabled) { void ConnectivityManagerImpl::refreshDns(envoy_netconf_t configuration_key, bool drain_connections) { { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; // refreshDns must be queued on Envoy's event loop, whereas network_state_ is updated // synchronously. In the event that multiple refreshes become queued on the event loop, @@ -256,19 +280,19 @@ void ConnectivityManagerImpl::refreshDns(envoy_netconf_t configuration_key, return; } } + doRefreshDns(configuration_key, drain_connections); +} +void ConnectivityManagerImpl::doRefreshDns(envoy_netconf_t configuration_key, + bool drain_connections) { if (auto dns_cache = dnsCache()) { ENVOY_LOG_EVENT(debug, "netconf_refresh_dns", "{}", std::to_string(configuration_key)); - if (drain_connections && enable_drain_post_dns_refresh_) { - dns_cache->iterateHostMap( - [&](absl::string_view host, - const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&) { - hosts_to_drain_.emplace(host); - }); + if (drain_connections && (dns_refresh_handler_ != nullptr)) { + dns_refresh_handler_->refreshDnsAndDrainHosts(); + } else { + dns_cache->forceRefreshHosts(); } - - dns_cache->forceRefreshHosts(); } } @@ -283,7 +307,7 @@ Extensions::Common::DynamicForwardProxy::DnsCacheSharedPtr ConnectivityManagerIm void ConnectivityManagerImpl::resetConnectivityState() { envoy_netconf_t configuration_key; { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; network_state_.network_ = 0; network_state_.remaining_faults_ = 1; network_state_.socket_mode_ = SocketMode::DefaultPreferredNetworkMode; @@ -308,11 +332,16 @@ Socket::OptionsSharedPtr ConnectivityManagerImpl::getUpstreamSocketOptions(int n return getAlternateInterfaceSocketOptions(network); } - // Envoy uses the hash signature of overridden socket options to choose a connection pool. - // Setting a dummy socket option is a hack that allows us to select a different - // connection pool without materially changing the socket configuration. auto options = std::make_shared(); - options->push_back(std::make_shared(network)); + if (!Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh") || + !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.drain_pools_on_network_change")) { + // Envoy uses the hash signature of overridden socket options to choose a connection pool. + // Setting a dummy socket option is a hack that allows us to select a different + // connection pool without materially changing the socket configuration when + // pools are not explicitly drained during network change. + options->push_back(std::make_shared(network)); + } return options; } @@ -359,7 +388,7 @@ ConnectivityManagerImpl::addUpstreamSocketOptions(Socket::OptionsSharedPtr optio SocketMode socket_mode; { - Thread::LockGuard lock{network_state_.mutex_}; + Thread::LockGuard lock{network_mutex_}; configuration_key = network_state_.configuration_key_; network = network_state_.network_; socket_mode = network_state_.socket_mode_; @@ -443,7 +472,148 @@ ConnectivityManagerImpl::enumerateInterfaces([[maybe_unused]] unsigned short fam return pairs; } -ConnectivityManagerSharedPtr ConnectivityManagerFactory::get() { +int connectionTypeToCompoundNetworkType(ConnectionType connection_type) { + int compound_type = 0; + switch (connection_type) { + case ConnectionType::CONNECTION_2G: + compound_type |= (static_cast(NetworkType::WWAN) | static_cast(NetworkType::WWAN_2G)); + break; + case ConnectionType::CONNECTION_3G: + compound_type |= (static_cast(NetworkType::WWAN) | static_cast(NetworkType::WWAN_3G)); + break; + case ConnectionType::CONNECTION_4G: + compound_type |= (static_cast(NetworkType::WWAN) | static_cast(NetworkType::WWAN_4G)); + break; + case ConnectionType::CONNECTION_5G: + compound_type |= (static_cast(NetworkType::WWAN) | static_cast(NetworkType::WWAN_5G)); + break; + case ConnectionType::CONNECTION_WIFI: + case ConnectionType::CONNECTION_ETHERNET: + compound_type |= static_cast(NetworkType::WLAN); + break; + case ConnectionType::CONNECTION_NONE: + break; + case ConnectionType::CONNECTION_BLUETOOTH: + case ConnectionType::CONNECTION_UNKNOWN: + compound_type = static_cast(NetworkType::Generic); + break; + } + return compound_type; +} + +void ConnectivityManagerImpl::onDefaultNetworkChangedAndroid(ConnectionType connection_type, + NetworkHandle net_id) { + bool already_connected{false}; + envoy_netconf_t current_configuration_key{0}; + { + Thread::LockGuard lock{network_mutex_}; + ENVOY_LOG_EVENT(debug, "android_default_network_changed", + "default network changed from {} to {}, new connection_type {}, ", + default_network_handle_, net_id, static_cast(connection_type)); + if (net_id == default_network_handle_) { + return; + } + current_configuration_key = network_state_.configuration_key_; + default_network_handle_ = net_id; + if (connected_networks_.find(net_id) != connected_networks_.end()) { + // Android Lollipop had race conditions where CONNECTIVITY_ACTION intents + // were sent out before the network was actually made the default. + // Delay switching to the new default until Android platform notifies that the network + // connected. + setPreferredNetworkNoLock(connectionTypeToCompoundNetworkType(connection_type)); + current_configuration_key = network_state_.configuration_key_; + already_connected = true; + } + } + if (already_connected) { + if (default_network_change_callback_ != nullptr) { + default_network_change_callback_(current_configuration_key); + } + if (observer_ != nullptr) { + observer_->onNetworkMadeDefault(net_id); + } + } +} + +void ConnectivityManagerImpl::onNetworkDisconnectAndroid(NetworkHandle net_id) { + { + Thread::LockGuard lock{network_mutex_}; + if (net_id == default_network_handle_) { + default_network_handle_ = kInvalidNetworkHandle; + } + if (connected_networks_.erase(net_id) == 0) { + return; + } + } + if (observer_ != nullptr) { + observer_->onNetworkDisconnected(net_id); + } +} + +void ConnectivityManagerImpl::onNetworkConnectAndroid(ConnectionType connection_type, + NetworkHandle net_id) { + bool is_default_network{false}; + envoy_netconf_t current_configuration_key{0}; + { + Thread::LockGuard lock{network_mutex_}; + if (connected_networks_.find(net_id) != connected_networks_.end()) { + return; + } + connected_networks_[net_id] = connection_type; + current_configuration_key = network_state_.configuration_key_; + if (net_id == default_network_handle_) { + // The reported default network finally gets connected. + is_default_network = true; + setPreferredNetworkNoLock(connectionTypeToCompoundNetworkType(connection_type)); + current_configuration_key = network_state_.configuration_key_; + } + } + // Android Lollipop would send many duplicate notifications. + // This was later fixed in Android Marshmallow. + // Deduplicate them here by avoiding sending duplicate notifications. + if (observer_ != nullptr) { + observer_->onNetworkConnected(net_id); + } + if (is_default_network) { + if (default_network_change_callback_ != nullptr) { + default_network_change_callback_(current_configuration_key); + } + if (observer_ != nullptr) { + observer_->onNetworkMadeDefault(net_id); + } + } +} + +void ConnectivityManagerImpl::purgeActiveNetworkListAndroid( + const std::vector& active_network_ids) { + std::vector disconnected_networks; + { + Thread::LockGuard lock{network_mutex_}; + for (auto& i : connected_networks_) { + if (std::find(active_network_ids.begin(), active_network_ids.end(), i.first) == + active_network_ids.end()) { + disconnected_networks.push_back(i.first); + } + } + } + for (auto disconnected_network : disconnected_networks) { + onNetworkDisconnectAndroid(disconnected_network); + } +} + +void ConnectivityManagerImpl::initializeNetworkStates() { + NetworkHandle default_net_id = SystemHelper::getInstance().getDefaultNetworkHandle(); + std::vector> all_connected_networks = + SystemHelper::getInstance().getAllConnectedNetworks(); + + Thread::LockGuard lock{network_mutex_}; + default_network_handle_ = default_net_id; + for (auto& entry : all_connected_networks) { + connected_networks_[entry.first] = entry.second; + } +} + +ConnectivityManagerImplSharedPtr ConnectivityManagerFactory::get() { return context_.serverFactoryContext().singletonManager().getTyped( SINGLETON_MANAGER_REGISTERED_NAME(connectivity_manager), [this] { Envoy::Extensions::Common::DynamicForwardProxy::DnsCacheManagerFactoryImpl diff --git a/mobile/library/common/network/connectivity_manager.h b/mobile/library/common/network/connectivity_manager.h index c6e8074134190..aa3026168eff4 100644 --- a/mobile/library/common/network/connectivity_manager.h +++ b/mobile/library/common/network/connectivity_manager.h @@ -3,6 +3,7 @@ #include #include +#include "envoy/common/optref.h" #include "envoy/network/socket.h" #include "envoy/singleton/manager.h" #include "envoy/upstream/cluster_manager.h" @@ -11,6 +12,7 @@ #include "source/extensions/common/dynamic_forward_proxy/dns_cache_impl.h" #include "library/common/engine_types.h" +#include "library/common/network/network_types.h" #include "library/common/network/proxy_settings.h" #include "library/common/types/c_types.h" @@ -20,11 +22,11 @@ * remain valid/relevant at time of execution. * * Currently, there are two primary circumstances this is used: - * 1. When network type changes, a refreshDNS call will be scheduled on the event dispatcher, along - * with a configuration key of this type. If network type changes again before that refresh - * executes, the refresh is now stale, another refresh task will have been queued, and it should no - * longer execute. The configuration key allows the connectivity_manager to determine if the - * refreshDNS call is representative of current configuration. + * 1. When network type changes, some clean up will be scheduled on the event dispatcher, along + * with a configuration key of this type. If network type changes again before that scheduled clean + * up executes, another clean up will be scheduled, and the old one should no longer execute. The + * configuration key allows the connectivity_manager to determine if the clean up is representative + * of current configuration. * 2. When a request is configured with a certain set of socket options and begins, it is given a * configuration key. The heuristic in reportNetworkUsage relies on characteristics of the * request/response to make future decisions about socket options, but needs to be able to correctly @@ -39,6 +41,9 @@ */ typedef uint16_t envoy_netconf_t; +using NetworkHandle = int64_t; +constexpr NetworkHandle kInvalidNetworkHandle = -1; + namespace Envoy { namespace Network { @@ -59,6 +64,14 @@ enum class SocketMode : int { AlternateBoundInterfaceMode = 1, }; +namespace { + +// The number of faults allowed on a previously-successful connection (i.e. able to send and receive +// L7 bytes) before switching socket mode. +constexpr unsigned int MaxFaultThreshold = 3; + +} // namespace + using DnsCacheManagerSharedPtr = Extensions::Common::DynamicForwardProxy::DnsCacheManagerSharedPtr; using InterfacePair = std::pair; @@ -78,8 +91,7 @@ using InterfacePair = std::pair; + +// Used when draining hosts upon DNS refreshing is desired. +class RefreshDnsWithPostDrainHandler + : public Extensions::Common::DynamicForwardProxy::DnsCache::UpdateCallbacks, + public Logger::Loggable { +public: + RefreshDnsWithPostDrainHandler(DnsCacheManagerSharedPtr dns_cache_manager, + Upstream::ClusterManager& cluster_manager) + : dns_cache_manager_(std::move(dns_cache_manager)), cluster_manager_(cluster_manager) {} + + // Extensions::Common::DynamicForwardProxy::DnsCache::UpdateCallbacks + absl::Status onDnsHostAddOrUpdate( + const std::string& /*host*/, + const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&) override { + return absl::OkStatus(); + } + void onDnsHostRemove(const std::string& /*host*/) override {} + void onDnsResolutionComplete(const std::string& /*host*/, + const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&, + Network::DnsResolver::ResolutionStatus) override; + + // Refresh DNS and drain all hosts upon completion. + // No-op if the default DNS cache in base configuration is not available. + void refreshDnsAndDrainHosts(); + +private: + DnsCacheManagerSharedPtr dns_cache_manager_; + Upstream::ClusterManager& cluster_manager_; + absl::flat_hash_set hosts_to_drain_; + Extensions::Common::DynamicForwardProxy::DnsCache::AddUpdateCallbacksHandlePtr + dns_callbacks_handle_; +}; + +using DefaultNetworkChangeCallback = std::function; + +class NetworkChangeObserver { +public: + virtual ~NetworkChangeObserver() = default; + virtual void onNetworkMadeDefault(NetworkHandle network) PURE; + virtual void onNetworkDisconnected(NetworkHandle network) PURE; + virtual void onNetworkConnected(NetworkHandle network) PURE; }; class ConnectivityManagerImpl : public ConnectivityManager, @@ -204,22 +252,10 @@ class ConnectivityManagerImpl : public ConnectivityManager, * @param network, the OS-preferred network. * @returns configuration key to associate with any related calls. */ - static envoy_netconf_t setPreferredNetwork(int network); + envoy_netconf_t setPreferredNetwork(int network); ConnectivityManagerImpl(Upstream::ClusterManager& cluster_manager, - DnsCacheManagerSharedPtr dns_cache_manager) - : cluster_manager_(cluster_manager), dns_cache_manager_(dns_cache_manager) {} - - // Extensions::Common::DynamicForwardProxy::DnsCache::UpdateCallbacks - absl::Status onDnsHostAddOrUpdate( - const std::string& /*host*/, - const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&) override { - return absl::OkStatus(); - } - void onDnsHostRemove(const std::string& /*host*/) override {} - void onDnsResolutionComplete(const std::string& /*host*/, - const Extensions::Common::DynamicForwardProxy::DnsHostInfoSharedPtr&, - Network::DnsResolver::ResolutionStatus) override; + DnsCacheManagerSharedPtr dns_cache_manager); // ConnectivityManager std::vector enumerateV4Interfaces() override; @@ -236,36 +272,56 @@ class ConnectivityManagerImpl : public ConnectivityManager, void setInterfaceBindingEnabled(bool enabled) override; void refreshDns(envoy_netconf_t configuration_key, bool drain_connections) override; void resetConnectivityState() override; - Socket::OptionsSharedPtr getUpstreamSocketOptions(int network, SocketMode socket_mode) override; envoy_netconf_t addUpstreamSocketOptions(Socket::OptionsSharedPtr options) override; Extensions::Common::DynamicForwardProxy::DnsCacheSharedPtr dnsCache() override; - Upstream::ClusterManager& clusterManager() override { return cluster_manager_; } + + // These interfaces are only used to handle Android network change notifications. + void onDefaultNetworkChangedAndroid(ConnectionType connection_type, NetworkHandle net_id); + void onNetworkDisconnectAndroid(NetworkHandle net_id); + void onNetworkConnectAndroid(ConnectionType connection_type, NetworkHandle net_id); + void purgeActiveNetworkListAndroid(const std::vector& active_network_ids); + void setDefaultNetworkChangeCallback(DefaultNetworkChangeCallback cb) { + default_network_change_callback_ = cb; + } + void setNetworkChangeObserver(NetworkChangeObserver* observer) { observer_ = observer; } + + // Refresh DNS regardless of configuration key change. + void doRefreshDns(envoy_netconf_t configuration_key, bool drain_connections); private: - struct NetworkState { + // The states of the current default network picked by the platform. + struct DefaultNetworkState { // The configuration key is passed through calls dispatched on the run loop to determine if // they're still valid/relevant at time of execution. - envoy_netconf_t configuration_key_ ABSL_GUARDED_BY(mutex_); - int network_ ABSL_GUARDED_BY(mutex_); - uint8_t remaining_faults_ ABSL_GUARDED_BY(mutex_); - SocketMode socket_mode_ ABSL_GUARDED_BY(mutex_); - Thread::MutexBasicLockable mutex_; + envoy_netconf_t configuration_key_; + int network_; + uint8_t remaining_faults_; + SocketMode socket_mode_; }; Socket::OptionsSharedPtr getAlternateInterfaceSocketOptions(int network); InterfacePair getActiveAlternateInterface(int network, unsigned short family); + Socket::OptionsSharedPtr getUpstreamSocketOptions(int network, SocketMode socket_mode); + void setPreferredNetworkNoLock(int network_type) ABSL_EXCLUSIVE_LOCKS_REQUIRED(network_mutex_); + void initializeNetworkStates(); - bool enable_drain_post_dns_refresh_{false}; bool enable_interface_binding_{false}; - absl::flat_hash_set hosts_to_drain_; - Extensions::Common::DynamicForwardProxy::DnsCache::AddUpdateCallbacksHandlePtr - dns_callbacks_handle_{nullptr}; Upstream::ClusterManager& cluster_manager_; + // nullptr if draining hosts after refreshing DNS is disabled via setDrainPostDnsRefreshEnabled(). + std::unique_ptr dns_refresh_handler_; DnsCacheManagerSharedPtr dns_cache_manager_; ProxySettingsConstSharedPtr proxy_settings_; - static NetworkState network_state_; + DefaultNetworkState network_state_ ABSL_GUARDED_BY(network_mutex_){ + 1, 0, MaxFaultThreshold, SocketMode::DefaultPreferredNetworkMode}; + Thread::MutexBasicLockable network_mutex_{}; + // Below states are only populated on Android platform. + NetworkHandle default_network_handle_ ABSL_GUARDED_BY(network_mutex_){kInvalidNetworkHandle}; + absl::flat_hash_map + connected_networks_ ABSL_GUARDED_BY(network_mutex_); + DefaultNetworkChangeCallback default_network_change_callback_; + NetworkChangeObserver* observer_{nullptr}; }; -using ConnectivityManagerSharedPtr = std::shared_ptr; +using ConnectivityManagerImplSharedPtr = std::shared_ptr; /** * Provides access to the singleton ConnectivityManager. @@ -278,7 +334,7 @@ class ConnectivityManagerFactory { /** * @returns singleton ConnectivityManager instance. */ - ConnectivityManagerSharedPtr get(); + ConnectivityManagerImplSharedPtr get(); private: Server::GenericFactoryContextImpl context_; diff --git a/mobile/library/common/system/BUILD b/mobile/library/common/system/BUILD index a8069f9eb773a..d97ca50096cde 100644 --- a/mobile/library/common/system/BUILD +++ b/mobile/library/common/system/BUILD @@ -29,6 +29,7 @@ envoy_cc_library( deps = [ ":default_system_helper_lib", "//library/common/extensions/cert_validator/platform_bridge:c_types_lib", + "//library/common/network:network_types_lib", ] + select({ ":use_android_system_helper": [ "//library/jni:android_jni_utility_lib", diff --git a/mobile/library/common/system/default_system_helper.cc b/mobile/library/common/system/default_system_helper.cc index 15142762e204f..0db0ae1b01b15 100644 --- a/mobile/library/common/system/default_system_helper.cc +++ b/mobile/library/common/system/default_system_helper.cc @@ -16,4 +16,10 @@ DefaultSystemHelper::validateCertificateChain(const std::vector& /* void DefaultSystemHelper::cleanupAfterCertificateValidation() {} +int64_t DefaultSystemHelper::getDefaultNetworkHandle() { return -1; } + +std::vector> DefaultSystemHelper::getAllConnectedNetworks() { + return {}; +} + } // namespace Envoy diff --git a/mobile/library/common/system/default_system_helper.h b/mobile/library/common/system/default_system_helper.h index a9ef1d32e5199..56d80bd25753f 100644 --- a/mobile/library/common/system/default_system_helper.h +++ b/mobile/library/common/system/default_system_helper.h @@ -17,6 +17,8 @@ class DefaultSystemHelper : public SystemHelper { envoy_cert_validation_result validateCertificateChain(const std::vector& certs, absl::string_view hostname) override; void cleanupAfterCertificateValidation() override; + int64_t getDefaultNetworkHandle() override; + std::vector> getAllConnectedNetworks() override; }; } // namespace Envoy diff --git a/mobile/library/common/system/default_system_helper_android.cc b/mobile/library/common/system/default_system_helper_android.cc index 5d205f0945d7f..f678e1cecb7b0 100644 --- a/mobile/library/common/system/default_system_helper_android.cc +++ b/mobile/library/common/system/default_system_helper_android.cc @@ -16,4 +16,10 @@ DefaultSystemHelper::validateCertificateChain(const std::vector& ce void DefaultSystemHelper::cleanupAfterCertificateValidation() { JNI::jvmDetachThread(); } +int64_t DefaultSystemHelper::getDefaultNetworkHandle() { return JNI::getDefaultNetworkHandle(); } + +std::vector> DefaultSystemHelper::getAllConnectedNetworks() { + return JNI::getAllConnectedNetworks(); +} + } // namespace Envoy diff --git a/mobile/library/common/system/default_system_helper_apple.cc b/mobile/library/common/system/default_system_helper_apple.cc index 1facd039d2480..16caed9c22ec4 100644 --- a/mobile/library/common/system/default_system_helper_apple.cc +++ b/mobile/library/common/system/default_system_helper_apple.cc @@ -13,4 +13,10 @@ DefaultSystemHelper::validateCertificateChain(const std::vector& ce void DefaultSystemHelper::cleanupAfterCertificateValidation() {} +int64_t DefaultSystemHelper::getDefaultNetworkHandle() { return -1; } + +std::vector> DefaultSystemHelper::getAllConnectedNetworks() { + return {}; +} + } // namespace Envoy diff --git a/mobile/library/common/system/system_helper.h b/mobile/library/common/system/system_helper.h index d18c6537e49a4..9f9123c6a9011 100644 --- a/mobile/library/common/system/system_helper.h +++ b/mobile/library/common/system/system_helper.h @@ -7,6 +7,7 @@ #include "absl/strings/string_view.h" #include "library/common/extensions/cert_validator/platform_bridge/c_types.h" +#include "library/common/network/network_types.h" namespace Envoy { @@ -39,6 +40,12 @@ class SystemHelper { */ virtual void cleanupAfterCertificateValidation() PURE; + /** + * Invokes platform APIs to retrieve a handle to the current default network. + */ + virtual int64_t getDefaultNetworkHandle() PURE; + + virtual std::vector> getAllConnectedNetworks() PURE; /** * @return a reference to the current SystemHelper instance. */ diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java index 595311f3e72f1..01e905e01d5f0 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java @@ -25,14 +25,18 @@ public class AndroidEngineImpl implements EnvoyEngine { public AndroidEngineImpl(Context context, EnvoyOnEngineRunning runningCallback, EnvoyLogger logger, EnvoyEventTracker eventTracker, Boolean enableProxying, Boolean useNetworkChangeEvent, - Boolean disableDnsRefreshOnNetworkChange) { + Boolean disableDnsRefreshOnNetworkChange, Boolean useV2NetworkMonitor) { this.context = context; this.envoyEngine = new EnvoyEngineImpl(runningCallback, logger, eventTracker, disableDnsRefreshOnNetworkChange); if (ContextUtils.getApplicationContext() == null) { ContextUtils.initApplicationContext(context.getApplicationContext()); } - AndroidNetworkMonitor.load(context, envoyEngine, useNetworkChangeEvent); + if (useV2NetworkMonitor) { + AndroidNetworkMonitorV2.load(context, envoyEngine); + } else { + AndroidNetworkMonitor.load(context, envoyEngine, useNetworkChangeEvent); + } if (enableProxying) { AndroidProxyMonitor.load(context, envoyEngine); } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineManifest.xml b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineManifest.xml index c77e3bede5324..af8b382f5f36a 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineManifest.xml +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineManifest.xml @@ -2,7 +2,7 @@ diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitor.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitor.java index eaba27e9fe345..19e2f26aa31ee 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitor.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitor.java @@ -11,6 +11,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; +import android.os.Build; import android.telephony.TelephonyManager; import androidx.annotation.NonNull; @@ -198,8 +199,10 @@ private AndroidNetworkMonitor(Context context, EnvoyEngine envoyEngine, connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.registerDefaultNetworkCallback( - new DefaultNetworkCallback(envoyEngine, connectivityManager, useNetworkChangeEvent)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.registerDefaultNetworkCallback( + new DefaultNetworkCallback(envoyEngine, connectivityManager, useNetworkChangeEvent)); + } } /** @returns The singleton instance of {@link AndroidNetworkMonitor}. */ diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2.java index 1dc6bcf851ee1..e5921844e3b66 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2.java @@ -1,6 +1,7 @@ package io.envoyproxy.envoymobile.engine; import io.envoyproxy.envoymobile.engine.types.EnvoyConnectionType; +import io.envoyproxy.envoymobile.engine.types.NetworkWithType; import static android.net.ConnectivityManager.TYPE_VPN; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; @@ -9,7 +10,10 @@ import android.Manifest; import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; @@ -172,6 +176,10 @@ public EnvoyConnectionType getEnvoyConnectionType() { private NetworkRequest mNetworkRequest; private NetworkState mNetworkState; private boolean mRegistered = false; + private IntentFilter mIntentFilter; + private Context mApplicationContext; + private BroadcastReceiver mBroadcastReceiver; + private boolean mIgnoreNextBroadcast = false; public static void load(Context context, EnvoyEngine envoyEngine) { if (mInstance != null) { @@ -292,8 +300,7 @@ public long getDefaultNetId() { /** Returns the current default {@link Network}, or {@code null} if disconnected. */ private Network getDefaultNetwork() { - Network defaultNetwork = null; - defaultNetwork = mConnectivityManager.getActiveNetwork(); + Network defaultNetwork = mConnectivityManager.getActiveNetwork(); if (defaultNetwork != null) { return defaultNetwork; } @@ -698,6 +705,22 @@ public void run() { } } + private class ConnectivityBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + runOnThread(new Runnable() { + @Override + public void run() { + if (mIgnoreNextBroadcast) { + mIgnoreNextBroadcast = false; + return; + } + onNetworkStateChangedTo(getDefaultNetworkState(), getDefaultNetId()); + } + }); + } + } + private void onNetworkStateChangedTo(NetworkState networkState, long netId) { assert mNetworkState != null; if (networkState.getEnvoyConnectionType() != mNetworkState.getEnvoyConnectionType() || @@ -710,6 +733,7 @@ private void onNetworkStateChangedTo(NetworkState networkState, long netId) { } private AndroidNetworkMonitorV2(Context context, EnvoyEngine envoyEngine) { + mApplicationContext = context.getApplicationContext(); int permission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_NETWORK_STATE); if (permission == PackageManager.PERMISSION_DENIED) { @@ -726,7 +750,9 @@ private AndroidNetworkMonitorV2(Context context, EnvoyEngine envoyEngine) { (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); mLooper = Looper.myLooper(); mHandler = new Handler(mLooper); - mDefaultNetworkCallback = new DefaultNetworkCallback(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mDefaultNetworkCallback = new DefaultNetworkCallback(); + } mAllNetworksCallback = new AllNetworksCallback(); mNetworkRequest = new NetworkRequest.Builder() .addCapability(NET_CAPABILITY_INTERNET) @@ -734,6 +760,9 @@ private AndroidNetworkMonitorV2(Context context, EnvoyEngine envoyEngine) { .removeCapability(NET_CAPABILITY_NOT_VPN) .build(); mNetworkState = getDefaultNetworkState(); + mIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + // Used when mDefaultNetworkCallback is null. + mBroadcastReceiver = new ConnectivityBroadcastReceiver(); registerNetworkCallbacks(false); } @@ -746,10 +775,23 @@ public void registerNetworkCallbacks(boolean shouldSignalDefaultNetworkChange) { } if (mDefaultNetworkCallback != null) { - try { - mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback); - } catch (RuntimeException e) { - mDefaultNetworkCallback = null; + // This is only reachable for Android O+. + // If registration fails, mDefaultNetworkCallback will be reset. + maybeRegisterDefaultNetworkCallback(); + } + if (mDefaultNetworkCallback == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // When registering for a sticky broadcast, like CONNECTIVITY_ACTION, if + // registerReceiver returns non-null, it means the broadcast was previously issued + // and onReceive() will be immediately called with this previous Intent. Since this + // initial callback doesn't actually indicate a network change, we can ignore it. + mIgnoreNextBroadcast = (mApplicationContext.registerReceiver( + mBroadcastReceiver, mIntentFilter, /*permission*/ null, + mHandler, /*flags*/ 0) != null); + } else { + mIgnoreNextBroadcast = + (mApplicationContext.registerReceiver(mBroadcastReceiver, mIntentFilter, + /*permission*/ null, mHandler) != null); } } mRegistered = true; @@ -757,8 +799,12 @@ public void registerNetworkCallbacks(boolean shouldSignalDefaultNetworkChange) { if (mAllNetworksCallback != null) { mAllNetworksCallback.initializeVpnInPlace(); try { - mConnectivityManager.registerNetworkCallback(mNetworkRequest, mAllNetworksCallback, - mHandler); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mConnectivityManager.registerNetworkCallback(mNetworkRequest, mAllNetworksCallback, + mHandler); + } else { + mConnectivityManager.registerNetworkCallback(mNetworkRequest, mAllNetworksCallback); + } } catch (RuntimeException e) { // If Android thinks this app has used up all available NetworkRequests, don't // bother trying to register any more callbacks as Android will still think @@ -784,6 +830,16 @@ public void registerNetworkCallbacks(boolean shouldSignalDefaultNetworkChange) { } } + // This is guaranteed to be called only for Android O+. + @SuppressLint("NewApi") + private void maybeRegisterDefaultNetworkCallback() { + try { + mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback); + } catch (RuntimeException e) { + mDefaultNetworkCallback = null; + } + } + public void unregisterNetworkCallbacks() { assert onThread(); if (!mRegistered) @@ -794,6 +850,23 @@ public void unregisterNetworkCallbacks() { } if (mDefaultNetworkCallback != null) { mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback); + } else { + mApplicationContext.unregisterReceiver(mBroadcastReceiver); + } + } + + public NetworkWithType[] getAllNetworksAndTypes() { + Network[] filteredNetworks = getAllNetworksFiltered(null); + int size = filteredNetworks.length; + + // Directly create the array with the known size. + NetworkWithType[] networks = new NetworkWithType[size]; + + for (int i = 0; i < size; i++) { + Network network = filteredNetworks[i]; + final EnvoyConnectionType connectionType = getEnvoyConnectionType(network); + networks[i] = new NetworkWithType(network.getNetworkHandle(), connectionType); } + return networks; } } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD b/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD index 89b5f05dabbe3..abb0890c57e8e 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/BUILD @@ -9,8 +9,6 @@ android_library( name = "envoy_engine_lib", srcs = [ "AndroidEngineImpl.java", - "AndroidNetworkMonitor.java", - "AndroidNetworkMonitorV2.java", "AndroidProxyMonitor.java", "UpstreamHttpProtocol.java", ], @@ -21,14 +19,16 @@ android_library( "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", "//library/java/io/envoyproxy/envoymobile/engine/types:envoy_c_types_lib", "//library/java/io/envoyproxy/envoymobile/utilities", + "//library/java/io/envoyproxy/envoymobile/utilities:network_utilities", "@maven//:androidx_annotation_annotation", - "@maven//:androidx_core_core", ], ) android_library( name = "envoy_base_engine_lib", srcs = [ + "AndroidNetworkMonitor.java", + "AndroidNetworkMonitorV2.java", "ByteBuffers.java", "EnvoyConfiguration.java", "EnvoyEngine.java", @@ -52,6 +52,7 @@ android_library( deps = [ "//library/java/io/envoyproxy/envoymobile/engine/types:envoy_c_types_lib", "@maven//:androidx_annotation_annotation", + "@maven//:androidx_core_core", "@maven//:com_google_code_findbugs_jsr305", "@maven//:com_google_protobuf_protobuf_javalite", ], diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index 62caa99458906..828410c815b46 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -305,6 +305,18 @@ public static native Object callCertificateVerificationFromNative(byte[][] certC */ public static native void callClearTestRootCertificateFromNative(); + /** + * Mimic a call to AndroidNetworkLibrary#getDefaultNetworkHandle from native code. + * To be used for testing only. + */ + public static native long callGetDefaultNetworkHandleFromNative(); + + /** + * Mimic a call to AndroidNetworkLibrary#getAllConnectedNetworks from native code. + * To be used for testing only. + */ + public static native long[][] callGetAllConnectedNetworksFromNative(); + /** * Given a filter name, create the proto config for adding the native filter * @@ -341,5 +353,5 @@ public static native long createBootstrap( /** * Returns true if the runtime feature is enabled. */ - public static native boolean isRuntimeFeatureEnabled(String featureName); + public static native boolean runtimeFeatureEnabled(String featureName); } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/types/BUILD b/mobile/library/java/io/envoyproxy/envoymobile/engine/types/BUILD index a28043e1746ae..07e52827bb388 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/types/BUILD +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/types/BUILD @@ -22,6 +22,7 @@ android_library( "EnvoyStatus.java", "EnvoyStreamIntel.java", "EnvoyStringAccessor.java", + "NetworkWithType.java", ], visibility = ["//visibility:public"], ) diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/types/NetworkWithType.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/types/NetworkWithType.java new file mode 100644 index 0000000000000..235730ba508e8 --- /dev/null +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/types/NetworkWithType.java @@ -0,0 +1,16 @@ +package io.envoyproxy.envoymobile.engine.types; + +public class NetworkWithType { + // an opaque handle to a network interface. + private final long netId; + private final EnvoyConnectionType connectionType; + + public NetworkWithType(long netId, EnvoyConnectionType connectionType) { + this.netId = netId; + this.connectionType = connectionType; + } + + public long getNetId() { return netId; } + + public EnvoyConnectionType getConnectionType() { return connectionType; } +} diff --git a/mobile/library/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary.java b/mobile/library/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary.java index 99ac5265f4284..2ee5c88c2ebce 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary.java @@ -23,6 +23,9 @@ import java.nio.charset.StandardCharsets; +import io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2; +import io.envoyproxy.envoymobile.engine.types.NetworkWithType; + /** * This class implements net utilities required by the net component. */ @@ -125,6 +128,32 @@ public static boolean isCleartextTrafficPermitted(String host) { } } + public static long getDefaultNetworkHandle() { + if (io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2.getInstance() == null) { + return -1; + } + return io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2.getInstance().getDefaultNetId(); + } + + public static long[][] getAllConnectedNetworks() { + if (io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2.getInstance() == null) { + return new long[0][0]; + } + NetworkWithType[] networks = + io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2.getInstance() + .getAllNetworksAndTypes(); + if (networks == null || networks.length == 0) { + return new long[0][0]; + } + + long[][] result = new long[networks.length][2]; + for (int i = 0; i < networks.length; i++) { + result[i][0] = networks[i].getNetId(); + result[i][1] = networks[i].getConnectionType().getValue(); + } + return result; + } + /** * Class to wrap FileDescriptor.setInt$() which is hidden and so must be accessed via * reflection. diff --git a/mobile/library/java/io/envoyproxy/envoymobile/utilities/BUILD b/mobile/library/java/io/envoyproxy/envoymobile/utilities/BUILD index 2b51fccee9d1c..677efa1db7b0b 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/utilities/BUILD +++ b/mobile/library/java/io/envoyproxy/envoymobile/utilities/BUILD @@ -8,10 +8,34 @@ envoy_mobile_package() android_library( name = "utilities", - srcs = glob(["*.java"]), + srcs = [ + "ContextUtils.java", + "StatsUtils.java", + "StrictModeContext.java", + "ThreadStatsUid.java", + ], + manifest = "UtilitiesManifest.xml", + visibility = ["//visibility:public"], + deps = [ + artifact("androidx.annotation:annotation"), + ], +) + +android_library( + name = "network_utilities", + srcs = [ + "AndroidCertVerifyResult.java", + "AndroidNetworkLibrary.java", + "CertVerifyStatusAndroid.java", + "FakeX509Util.java", + "X509Util.java", + ], manifest = "UtilitiesManifest.xml", visibility = ["//visibility:public"], deps = [ + ":utilities", + "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", + "//library/java/io/envoyproxy/envoymobile/engine/types:envoy_c_types_lib", artifact("androidx.annotation:annotation"), ], ) diff --git a/mobile/library/java/io/envoyproxy/envoymobile/utilities/UtilitiesManifest.xml b/mobile/library/java/io/envoyproxy/envoymobile/utilities/UtilitiesManifest.xml index 735bcdad9756b..4db455f49c8b4 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/utilities/UtilitiesManifest.xml +++ b/mobile/library/java/io/envoyproxy/envoymobile/utilities/UtilitiesManifest.xml @@ -2,7 +2,7 @@ diff --git a/mobile/library/java/org/chromium/net/ChromiumNetManifest.xml b/mobile/library/java/org/chromium/net/ChromiumNetManifest.xml index bd37d29766181..46a9bb48f66dc 100644 --- a/mobile/library/java/org/chromium/net/ChromiumNetManifest.xml +++ b/mobile/library/java/org/chromium/net/ChromiumNetManifest.xml @@ -2,7 +2,7 @@ diff --git a/mobile/library/java/org/chromium/net/impl/CronvoyManifest.xml b/mobile/library/java/org/chromium/net/impl/CronvoyManifest.xml index 9f12f96b1a7ae..2ec90bc6dc1bd 100644 --- a/mobile/library/java/org/chromium/net/impl/CronvoyManifest.xml +++ b/mobile/library/java/org/chromium/net/impl/CronvoyManifest.xml @@ -2,7 +2,7 @@ diff --git a/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java b/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java index e4b464e5917e4..ee15699fbbea5 100644 --- a/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java +++ b/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java @@ -69,6 +69,7 @@ public class NativeCronvoyEngineBuilderImpl extends CronvoyEngineBuilderImpl { private String mUpstreamTlsSni = ""; private int mH3ConnectionKeepaliveInitialIntervalMilliseconds = 0; private boolean mUseNetworkChangeEvent = false; + private boolean mUseV2NetworkMonitor = false; private final Map mRuntimeGuards = new HashMap<>(); @@ -136,6 +137,11 @@ public NativeCronvoyEngineBuilderImpl setUseNetworkChangeEvent(boolean use) { return this; } + public NativeCronvoyEngineBuilderImpl setUseV2NetworkMonitor(boolean useV2NetworkMonitor) { + mUseV2NetworkMonitor = useV2NetworkMonitor; + return this; + } + /** * Set the DNS minimum refresh time, in seconds, which ensures that we wait to refresh a DNS * entry for at least the minimum refresh time. For example, if the DNS record TTL is 60 seconds @@ -267,7 +273,7 @@ EnvoyEngine createEngine(EnvoyOnEngineRunning onEngineRunning, EnvoyLogger envoy String logLevel) { AndroidEngineImpl engine = new AndroidEngineImpl( getContext(), onEngineRunning, envoyLogger, mEnvoyEventTracker, mEnableProxying, - mUseNetworkChangeEvent, mDisableDnsRefreshOnNetworkChange); + mUseNetworkChangeEvent, mDisableDnsRefreshOnNetworkChange, mUseV2NetworkMonitor); engine.runWithConfig(createEnvoyConfiguration(), logLevel); return engine; } diff --git a/mobile/library/java/org/chromium/net/urlconnection/URLConnectionManifest.xml b/mobile/library/java/org/chromium/net/urlconnection/URLConnectionManifest.xml index 94feae1f3d065..ce017fe44da19 100644 --- a/mobile/library/java/org/chromium/net/urlconnection/URLConnectionManifest.xml +++ b/mobile/library/java/org/chromium/net/urlconnection/URLConnectionManifest.xml @@ -2,6 +2,6 @@ diff --git a/mobile/library/jni/BUILD b/mobile/library/jni/BUILD index 0c0107c303003..5a7dc8865f609 100644 --- a/mobile/library/jni/BUILD +++ b/mobile/library/jni/BUILD @@ -90,7 +90,7 @@ envoy_cc_library( alwayslink = True, ) -# Cert verification related functions which call into AndroidNetworkLibrary. +# Cert verification related functions which call into AndroidNetworkLibrary. And network retrieval functions which call into AndroidNetworkMonitorV2. envoy_cc_library( name = "android_network_utility_lib", srcs = [ @@ -105,6 +105,7 @@ envoy_cc_library( ":jni_utility_lib", "//library/common/bridge:utility_lib", "//library/common/extensions/cert_validator/platform_bridge:c_types_lib", + "//library/common/network:network_types_lib", "//library/common/types:c_types_lib", "@envoy//bazel:boringssl", ], diff --git a/mobile/library/jni/android_network_utility.cc b/mobile/library/jni/android_network_utility.cc index 3f7492a9c6e7c..35c81666e3941 100644 --- a/mobile/library/jni/android_network_utility.cc +++ b/mobile/library/jni/android_network_utility.cc @@ -166,6 +166,63 @@ envoy_cert_validation_result verifyX509CertChain(const std::vector& } } +int64_t getDefaultNetworkHandle() { + JniHelper jni_helper(JniHelper::getThreadLocalEnv()); + jclass jcls_AndroidNetworkLibrary = + jni_helper.findClassFromCache("io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary"); + jmethodID jmid_getDefaultNetworkHandle = jni_helper.getStaticMethodIdFromCache( + jcls_AndroidNetworkLibrary, "getDefaultNetworkHandle", "()J"); + jlong defaultNetwork = + jni_helper.callStaticLongMethod(jcls_AndroidNetworkLibrary, jmid_getDefaultNetworkHandle); + return static_cast(defaultNetwork); +} + +std::vector> getAllConnectedNetworks() { + std::vector> connected_networks; + Envoy::JNI::JniHelper jni_helper(Envoy::JNI::JniHelper::getThreadLocalEnv()); + + // Use a unique_ptr to automatically release the class reference. + jclass jcls_android_network_library = + jni_helper.findClassFromCache("io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary"); + if (jcls_android_network_library == nullptr) { + return connected_networks; + } + + jmethodID jmid_get_all_connected_networks = jni_helper.getStaticMethodIdFromCache( + jcls_android_network_library, "getAllConnectedNetworks", "()[[J"); + if (jmid_get_all_connected_networks == nullptr) { + return connected_networks; + } + + // Call the static Java method to get the long[][] array. + Envoy::JNI::LocalRefUniquePtr java_network_array = + jni_helper.callStaticObjectMethod(jcls_android_network_library, + jmid_get_all_connected_networks); + if (java_network_array == nullptr) { + return connected_networks; + } + + jsize num_networks = jni_helper.getArrayLength(java_network_array.get()); + + for (jsize i = 0; i < num_networks; ++i) { + // Each entry is a jlongArray (long[2]). + Envoy::JNI::LocalRefUniquePtr network_info_array = + jni_helper.getObjectArrayElement(java_network_array.get(), i); + if (network_info_array == nullptr) { + continue; + } + + std::vector network_info; + Envoy::JNI::javaLongArrayToInt64Vector(jni_helper, network_info_array.get(), &network_info); + + if (network_info.size() == 2) { + connected_networks.emplace_back(network_info[0], + static_cast(network_info[1])); + } + } + return connected_networks; +} + void jvmDetachThread() { JniHelper::detachCurrentThread(); } } // namespace JNI diff --git a/mobile/library/jni/android_network_utility.h b/mobile/library/jni/android_network_utility.h index b86ff77081fe7..48dabb3694c8e 100644 --- a/mobile/library/jni/android_network_utility.h +++ b/mobile/library/jni/android_network_utility.h @@ -5,6 +5,7 @@ #include "absl/strings/string_view.h" #include "library/common/extensions/cert_validator/platform_bridge/c_types.h" +#include "library/common/network/network_types.h" #include "library/jni/jni_helper.h" namespace Envoy { @@ -21,6 +22,10 @@ LocalRefUniquePtr callJvmVerifyX509CertChain(JniHelper& jni_helper, envoy_cert_validation_result verifyX509CertChain(const std::vector& certs, absl::string_view hostname); +int64_t getDefaultNetworkHandle(); + +std::vector> getAllConnectedNetworks(); + void jvmDetachThread(); } // namespace JNI diff --git a/mobile/library/jni/jni_impl.cc b/mobile/library/jni/jni_impl.cc index 206aade981d63..2b324a4081513 100644 --- a/mobile/library/jni/jni_impl.cc +++ b/mobile/library/jni/jni_impl.cc @@ -1397,3 +1397,34 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_callClearTestRootCertificateFro jni_helper.callStaticVoidMethod(java_android_network_library_class, java_clear_test_root_certificates_method_id); } + +extern "C" JNIEXPORT jlong JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_callGetDefaultNetworkHandleFromNative(JNIEnv*, + jclass) { + return Envoy::JNI::getDefaultNetworkHandle(); +} + +extern "C" JNIEXPORT jobjectArray JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_callGetAllConnectedNetworksFromNative(JNIEnv* env, + jclass) { + Envoy::JNI::JniHelper jni_helper(env); + std::vector> networks = + Envoy::JNI::getAllConnectedNetworks(); + + jclass long_array_class = env->FindClass("[J"); + jobjectArray result = env->NewObjectArray(networks.size(), long_array_class, nullptr); + + for (size_t i = 0; i < networks.size(); ++i) { + jlongArray network_info_array = env->NewLongArray(2); + if (network_info_array == nullptr) { + return nullptr; + } + jlong network_info[2]; + network_info[0] = networks[i].first; + network_info[1] = static_cast(networks[i].second); + env->SetLongArrayRegion(network_info_array, 0, 2, network_info); + env->SetObjectArrayElement(result, i, network_info_array); + env->DeleteLocalRef(network_info_array); + } + return result; +} diff --git a/mobile/library/jni/jni_init.cc b/mobile/library/jni/jni_init.cc index 024000e9bc850..2e8e834873503 100644 --- a/mobile/library/jni/jni_init.cc +++ b/mobile/library/jni/jni_init.cc @@ -9,20 +9,18 @@ namespace JNI { void initialize(JavaVM* jvm) { JniHelper::initialize(jvm); JniUtility::initCache(); - JniHelper::addToCache( - "io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary", - /* methods= */ {}, - /* static_methods= */ - { - {"isCleartextTrafficPermitted", "(Ljava/lang/String;)Z"}, - {"tagSocket", "(III)V"}, - {"verifyServerCertificates", - "([[B[B[B)Lio/envoyproxy/envoymobile/utilities/AndroidCertVerifyResult;"}, - {"addTestRootCertificate", "([B)V"}, - {"clearTestRootCertificates", "()V"}, - - }, - /* fields= */ {}, /* static_fields= */ {}); + JniHelper::addToCache("io/envoyproxy/envoymobile/utilities/AndroidNetworkLibrary", + /* methods= */ {}, + /* static_methods= */ + {{"isCleartextTrafficPermitted", "(Ljava/lang/String;)Z"}, + {"tagSocket", "(III)V"}, + {"verifyServerCertificates", + "([[B[B[B)Lio/envoyproxy/envoymobile/utilities/AndroidCertVerifyResult;"}, + {"addTestRootCertificate", "([B)V"}, + {"clearTestRootCertificates", "()V"}, + {"getDefaultNetworkHandle", "()J"}, + {"getAllConnectedNetworks", "()[[J"}}, + /* fields= */ {}, /* static_fields= */ {}); JniHelper::addToCache("io/envoyproxy/envoymobile/utilities/AndroidCertVerifyResult", /* methods= */ { diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt index a56b2de2601b5..b3fcb45d46129 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/AndroidEngineBuilder.kt @@ -14,7 +14,8 @@ class AndroidEngineBuilder(context: Context) : EngineBuilder() { eventTracker, enableProxying, /*useNetworkChangeEvent*/ false, - /*disableDnsRefreshOnNetworkChange*/ false + /*disableDnsRefreshOnNetworkChange*/ false, + /*useV2NetworkMonitor*/ false ) } } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD b/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD index fb10726671765..909190cebf272 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/BUILD @@ -39,6 +39,7 @@ kt_android_library( "//library/java/io/envoyproxy/envoymobile/engine:envoy_engine_lib", "//library/java/io/envoyproxy/envoymobile/engine/types:envoy_c_types_lib", "//library/java/io/envoyproxy/envoymobile/utilities", + "//library/java/io/envoyproxy/envoymobile/utilities:network_utilities", ], ) diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/EnvoyManifest.xml b/mobile/library/kotlin/io/envoyproxy/envoymobile/EnvoyManifest.xml index 90e95d8149fb3..9b6186d0801e2 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/EnvoyManifest.xml +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/EnvoyManifest.xml @@ -2,7 +2,7 @@ diff --git a/mobile/test/cc/integration/send_data_test.cc b/mobile/test/cc/integration/send_data_test.cc index 97a519bf2b099..87a4bd73d5eb4 100644 --- a/mobile/test/cc/integration/send_data_test.cc +++ b/mobile/test/cc/integration/send_data_test.cc @@ -15,7 +15,7 @@ TEST(SendDataTest, Success) { auto* request_generic_body_match = assertion.mutable_match_config()->mutable_http_request_generic_body_match(); request_generic_body_match->add_patterns()->set_string_match("request body"); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion"); std::string serialized_assertion; diff --git a/mobile/test/cc/integration/send_headers_test.cc b/mobile/test/cc/integration/send_headers_test.cc index 2f29fc42ea35a..aa035cc3b4318 100644 --- a/mobile/test/cc/integration/send_headers_test.cc +++ b/mobile/test/cc/integration/send_headers_test.cc @@ -23,7 +23,7 @@ TEST(SendHeadersTest, Success) { auto* headers3 = http_request_headers_match->add_headers(); headers3->set_name(":path"); headers3->set_exact_match("/"); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion"); std::string serialized_assertion; diff --git a/mobile/test/cc/integration/send_trailers_test.cc b/mobile/test/cc/integration/send_trailers_test.cc index c3c79e45ca173..2184cf04f5425 100644 --- a/mobile/test/cc/integration/send_trailers_test.cc +++ b/mobile/test/cc/integration/send_trailers_test.cc @@ -17,7 +17,7 @@ TEST(SendTrailersTest, Success) { auto trailer = http_request_trailers_match->add_headers(); trailer->set_name("trailer-key"); trailer->set_exact_match("trailer-value"); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion"); std::string serialized_assertion; diff --git a/mobile/test/cc/unit/envoy_config_test.cc b/mobile/test/cc/unit/envoy_config_test.cc index 94944657768b2..1d2f2db3295c7 100644 --- a/mobile/test/cc/unit/envoy_config_test.cc +++ b/mobile/test/cc/unit/envoy_config_test.cc @@ -107,7 +107,6 @@ TEST(TestConfig, ConfigIsApplied) { "num_timeouts_to_trigger_port_migration { value: 4 }", "idle_network_timeout { seconds: 60 }", "key: \"dns_persistent_cache\" save_interval { seconds: 101 }", - "key: \"prefer_quic_client_udp_gro\" value { bool_value: true }", "key: \"test_feature_false\" value { bool_value: true }", "key: \"device_os\" value { string_value: \"probably-ubuntu-on-CI\" } }", "key: \"app_version\" value { string_value: \"1.2.3\" } }", @@ -462,7 +461,7 @@ TEST(TestConfig, AddNativeFilters) { envoy::extensions::filters::http::buffer::v3::Buffer buffer; buffer.mutable_max_request_bytes()->set_value(5242880); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer"); std::string serialized_buffer; buffer.SerializeToString(&serialized_buffer); diff --git a/mobile/test/common/integration/client_integration_test.cc b/mobile/test/common/integration/client_integration_test.cc index 26abde47daad1..a57df133f2f03 100644 --- a/mobile/test/common/integration/client_integration_test.cc +++ b/mobile/test/common/integration/client_integration_test.cc @@ -103,6 +103,14 @@ class ClientIntegrationTest builder_.enableDnsCache(true, /* save_interval_seconds */ 1); } + // Initialize the connectivity manager with a WIFI default network and another network with + // unknown type. + std::vector> connected_networks{ + {1, ConnectionType::CONNECTION_WIFI}, {2, ConnectionType::CONNECTION_UNKNOWN}}; + EXPECT_CALL(helper_handle_->mock_helper(), getDefaultNetworkHandle()).WillOnce(Return(1)); + EXPECT_CALL(helper_handle_->mock_helper(), getAllConnectedNetworks()) + .WillOnce(Return(connected_networks)); + BaseClientIntegrationTest::initialize(); if (getCodecType() == Http::CodecType::HTTP3) { @@ -362,6 +370,35 @@ TEST_P(ClientIntegrationTest, HandleNetworkChangeEvents) { EXPECT_EQ(4, current_change_event); } +TEST_P(ClientIntegrationTest, HandleNetworkChangeEventsAndroid) { + absl::Notification found_force_dns_refresh; + std::atomic handled_network_change{false}; + auto logger = std::make_unique(); + logger->on_log_ = [&](Logger::Logger::Levels, const std::string& msg) { + if (msg.find("Default network state has been changed. Current net configuration key") != + std::string::npos) { + handled_network_change = true; + } + if (msg.find("beginning DNS cache force refresh") != std::string::npos) { + found_force_dns_refresh.Notify(); + } + }; + builder_.setLogger(std::move(logger)); + builder_.setDisableDnsRefreshOnNetworkChange(false); + + initialize(); + + // A new WIFI network appears and becomes the default network. Even though + // the test is initialized with a WIFI network, this should still have triggred + // a network change event as it has a different network handle. + internalEngine()->onNetworkConnectAndroid(ConnectionType::CONNECTION_WIFI, 123); + internalEngine()->onDefaultNetworkChangedAndroid(ConnectionType::CONNECTION_WIFI, 123); + // The HTTP status reset and DNS refresh should have been posted to the network thread and to be + // handled there. + found_force_dns_refresh.WaitForNotification(); + EXPECT_TRUE(handled_network_change); +} + TEST_P(ClientIntegrationTest, LargeResponse) { initialize(); std::string data(1024 * 32, 'a'); diff --git a/mobile/test/common/internal_engine_test.cc b/mobile/test/common/internal_engine_test.cc index d74493fa9a48e..812fcdec15a2f 100644 --- a/mobile/test/common/internal_engine_test.cc +++ b/mobile/test/common/internal_engine_test.cc @@ -102,6 +102,10 @@ class InternalEngineTest : public testing::Test { helper_handle_ = test::SystemHelperPeer::replaceSystemHelper(); EXPECT_CALL(helper_handle_->mock_helper(), isCleartextPermitted(_)) .WillRepeatedly(Return(true)); + EXPECT_CALL(helper_handle_->mock_helper(), getDefaultNetworkHandle()) + .Times(testing::AtMost(1)) + .WillOnce(Return(-1)); + EXPECT_CALL(helper_handle_->mock_helper(), getAllConnectedNetworks()).Times(testing::AtMost(1)); } envoy_status_t runEngine(const std::unique_ptr& engine, @@ -335,7 +339,7 @@ TEST_F(InternalEngineTest, BasicStream) { envoy::extensions::filters::http::buffer::v3::Buffer buffer; buffer.mutable_max_request_bytes()->set_value(65000); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer"); std::string serialized_buffer; buffer.SerializeToString(&serialized_buffer); diff --git a/mobile/test/common/mocks/common/mocks.h b/mobile/test/common/mocks/common/mocks.h index f6825405b9c7e..383adda3aed38 100644 --- a/mobile/test/common/mocks/common/mocks.h +++ b/mobile/test/common/mocks/common/mocks.h @@ -16,6 +16,8 @@ class MockSystemHelper : public SystemHelper { MOCK_METHOD(envoy_cert_validation_result, validateCertificateChain, (const std::vector& certs, absl::string_view hostname)); MOCK_METHOD(void, cleanupAfterCertificateValidation, ()); + MOCK_METHOD(int64_t, getDefaultNetworkHandle, ()); + MOCK_METHOD((std::vector>), getAllConnectedNetworks, ()); }; // SystemHelperPeer allows the replacement of the SystemHelper singleton diff --git a/mobile/test/common/network/BUILD b/mobile/test/common/network/BUILD index b26d4b163bf15..a40973ef1bed6 100644 --- a/mobile/test/common/network/BUILD +++ b/mobile/test/common/network/BUILD @@ -10,6 +10,7 @@ envoy_cc_test( repository = "@envoy", deps = [ "//library/common/network:connectivity_manager_lib", + "//test/common/mocks/common:common_mocks", "@envoy//test/extensions/common/dynamic_forward_proxy:mocks", "@envoy//test/mocks/upstream:cluster_manager_mocks", ], diff --git a/mobile/test/common/network/connectivity_manager_test.cc b/mobile/test/common/network/connectivity_manager_test.cc index dd086bda6c3b7..2b47ea5ac01e3 100644 --- a/mobile/test/common/network/connectivity_manager_test.cc +++ b/mobile/test/common/network/connectivity_manager_test.cc @@ -1,5 +1,6 @@ #include +#include "test/common/mocks/common/mocks.h" #include "test/extensions/common/dynamic_forward_proxy/mocks.h" #include "test/mocks/upstream/cluster_manager.h" @@ -13,29 +14,55 @@ using testing::Return; namespace Envoy { namespace Network { +class MockNetworkChangeObserver : public NetworkChangeObserver { +public: + MOCK_METHOD(void, onNetworkMadeDefault, (NetworkHandle network), (override)); + MOCK_METHOD(void, onNetworkDisconnected, (NetworkHandle network), (override)); + MOCK_METHOD(void, onNetworkConnected, (NetworkHandle network), (override)); +}; + class ConnectivityManagerTest : public testing::Test { public: ConnectivityManagerTest() : dns_cache_manager_( new NiceMock()), dns_cache_(dns_cache_manager_->dns_cache_), - connectivity_manager_(std::make_shared(cm_, dns_cache_manager_)) { + helper_handle_(test::SystemHelperPeer::replaceSystemHelper()) { + + EXPECT_CALL(helper_handle_->mock_helper(), getDefaultNetworkHandle()).WillOnce(Return(1)); + std::vector> connected_networks{ + {1, ConnectionType::CONNECTION_WIFI}, {2, ConnectionType::CONNECTION_UNKNOWN}}; + EXPECT_CALL(helper_handle_->mock_helper(), getAllConnectedNetworks()) + .WillOnce(Return(connected_networks)); + connectivity_manager_ = std::make_shared(cm_, dns_cache_manager_); ON_CALL(*dns_cache_manager_, lookUpCacheByName(_)).WillByDefault(Return(dns_cache_)); // Toggle network to reset network state. - ConnectivityManagerImpl::setPreferredNetwork(1); - ConnectivityManagerImpl::setPreferredNetwork(2); + connectivity_manager_->setPreferredNetwork(1); + connectivity_manager_->setPreferredNetwork(2); + + // Set up the default network change callback. + auto callback = [&](envoy_netconf_t key) { + EXPECT_EQ(key, connectivity_manager_->getConfigurationKey()); + num_default_network_change_++; + }; + connectivity_manager_->setDefaultNetworkChangeCallback(std::move(callback)); + connectivity_manager_->setNetworkChangeObserver(&observer_); } std::shared_ptr> dns_cache_manager_; std::shared_ptr dns_cache_; NiceMock cm_{}; - ConnectivityManagerSharedPtr connectivity_manager_; + std::unique_ptr helper_handle_; + ConnectivityManagerImplSharedPtr connectivity_manager_; + testing::StrictMock observer_; + // Track callback invocation count. + int num_default_network_change_{0}; }; TEST_F(ConnectivityManagerTest, SetPreferredNetworkWithNewNetworkChangesConfigurationKey) { envoy_netconf_t original_key = connectivity_manager_->getConfigurationKey(); - envoy_netconf_t new_key = ConnectivityManagerImpl::setPreferredNetwork(4); + envoy_netconf_t new_key = connectivity_manager_->setPreferredNetwork(4); EXPECT_NE(original_key, new_key); EXPECT_EQ(new_key, connectivity_manager_->getConfigurationKey()); } @@ -43,7 +70,7 @@ TEST_F(ConnectivityManagerTest, SetPreferredNetworkWithNewNetworkChangesConfigur TEST_F(ConnectivityManagerTest, DISABLED_SetPreferredNetworkWithUnchangedNetworkReturnsStaleConfigurationKey) { envoy_netconf_t original_key = connectivity_manager_->getConfigurationKey(); - envoy_netconf_t stale_key = ConnectivityManagerImpl::setPreferredNetwork(2); + envoy_netconf_t stale_key = connectivity_manager_->setPreferredNetwork(2); EXPECT_NE(original_key, stale_key); EXPECT_EQ(original_key, connectivity_manager_->getConfigurationKey()); } @@ -61,7 +88,14 @@ TEST_F(ConnectivityManagerTest, RefreshDnsForStaleConfigurationDoesntTriggerDnsR } TEST_F(ConnectivityManagerTest, WhenDrainPostDnsRefreshEnabledDrainsPostDnsRefresh) { - EXPECT_CALL(*dns_cache_, addUpdateCallbacks_(Ref(*connectivity_manager_))); + Extensions::Common::DynamicForwardProxy::DnsCache::UpdateCallbacks* dns_completion_callback{ + nullptr}; + EXPECT_CALL(*dns_cache_, addUpdateCallbacks_(_)) + .WillOnce(Invoke([&dns_completion_callback]( + Extensions::Common::DynamicForwardProxy::DnsCache::UpdateCallbacks& cb) { + dns_completion_callback = &cb; + return nullptr; + })); connectivity_manager_->setDrainPostDnsRefreshEnabled(true); auto host_info = std::make_shared(); @@ -77,32 +111,29 @@ TEST_F(ConnectivityManagerTest, WhenDrainPostDnsRefreshEnabledDrainsPostDnsRefre envoy_netconf_t configuration_key = connectivity_manager_->getConfigurationKey(); connectivity_manager_->refreshDns(configuration_key, true); - EXPECT_CALL(cm_, drainConnections(_)); - connectivity_manager_->onDnsResolutionComplete( + EXPECT_CALL(cm_, drainConnections(_, ConnectionPool::DrainBehavior::DrainExistingConnections)); + dns_completion_callback->onDnsResolutionComplete( "cached.example.com", std::make_shared(), Network::DnsResolver::ResolutionStatus::Completed); - connectivity_manager_->onDnsResolutionComplete( + dns_completion_callback->onDnsResolutionComplete( "not-cached.example.com", std::make_shared(), Network::DnsResolver::ResolutionStatus::Completed); - connectivity_manager_->onDnsResolutionComplete( + dns_completion_callback->onDnsResolutionComplete( "not-cached2.example.com", std::make_shared(), Network::DnsResolver::ResolutionStatus::Completed); } TEST_F(ConnectivityManagerTest, WhenDrainPostDnsNotEnabledDoesntDrainPostDnsRefresh) { + EXPECT_CALL(*dns_cache_, addUpdateCallbacks_(_)).Times(0); connectivity_manager_->setDrainPostDnsRefreshEnabled(false); + EXPECT_CALL(*dns_cache_, iterateHostMap(_)).Times(0); EXPECT_CALL(*dns_cache_, forceRefreshHosts()); envoy_netconf_t configuration_key = connectivity_manager_->getConfigurationKey(); connectivity_manager_->refreshDns(configuration_key, true); - - EXPECT_CALL(cm_, drainConnections(_)).Times(0); - connectivity_manager_->onDnsResolutionComplete( - "example.com", std::make_shared(), - Network::DnsResolver::ResolutionStatus::Completed); } TEST_F(ConnectivityManagerTest, @@ -168,7 +199,7 @@ TEST_F(ConnectivityManagerTest, ReportNetworkUsageDisablesOverrideAfterThirdFaul TEST_F(ConnectivityManagerTest, ReportNetworkUsageDisregardsCallsWithStaleConfigurationKey) { envoy_netconf_t stale_key = connectivity_manager_->getConfigurationKey(); - envoy_netconf_t current_key = ConnectivityManagerImpl::setPreferredNetwork(4); + envoy_netconf_t current_key = connectivity_manager_->setPreferredNetwork(4); EXPECT_NE(stale_key, current_key); connectivity_manager_->setInterfaceBindingEnabled(true); @@ -249,14 +280,113 @@ TEST_F(ConnectivityManagerTest, NetworkChangeResultsInDifferentSocketOptionsHash for (const auto& option : *options1) { option->hashKey(hash1); } - ConnectivityManagerImpl::setPreferredNetwork(64); + connectivity_manager_->setPreferredNetwork(64); auto options2 = std::make_shared(); connectivity_manager_->addUpstreamSocketOptions(options2); std::vector hash2; for (const auto& option : *options2) { option->hashKey(hash2); } - EXPECT_NE(hash1, hash2); + if (!Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh") || + !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.drain_pools_on_network_change")) { + EXPECT_NE(hash1, hash2); + } else { + EXPECT_EQ(hash1, hash2); + } +} + +// Verifies that when the platform notifies about the same default network +// again, the signal will be ignored. +TEST_F(ConnectivityManagerTest, DuplicatedSignalOfAndroidNetworkBecomesDefault) { + EXPECT_CALL(observer_, onNetworkMadeDefault(_)).Times(0); + connectivity_manager_->onDefaultNetworkChangedAndroid(ConnectionType::CONNECTION_WIFI, 1); + // The callback should not have been called. + EXPECT_EQ(num_default_network_change_, 0); +} + +// Verifies that when a network is connected and then becomes the default +// default_network_change_callback_ called at the end rather than in the middle. +TEST_F(ConnectivityManagerTest, AndroidNetworkConnectedAndThenBecomesDefault) { + const NetworkHandle net_id = 123; + const auto connection_type = ConnectionType::CONNECTION_WIFI; + + // Simulate a network is connected. + EXPECT_CALL(observer_, onNetworkConnected(net_id)); + connectivity_manager_->onNetworkConnectAndroid(connection_type, net_id); + // The callback should not have been called yet. + EXPECT_EQ(num_default_network_change_, 0); + + // Simulate the connected network now becomes the default. + EXPECT_CALL(observer_, onNetworkMadeDefault(net_id)); + connectivity_manager_->onDefaultNetworkChangedAndroid(connection_type, net_id); + + // Verify the callback was invoked exactly once. + EXPECT_EQ(num_default_network_change_, 1); +} + +// Verifies that when a network becomes the default without becoming connected, +// default_network_change_callback_ is not called. And it should be called once the network is +// connected. +TEST_F(ConnectivityManagerTest, AndroidNetworkBecomesDefaultAndThenConnected) { + const NetworkHandle net_id = 123; + const auto connection_type = ConnectionType::CONNECTION_4G; + const envoy_netconf_t initial_config_key = connectivity_manager_->getConfigurationKey(); + + // Simulate that the network becomes the default. At this point, it is not yet "connected". + connectivity_manager_->onDefaultNetworkChangedAndroid(connection_type, net_id); + + // The callback should not have been called, and the preferred network should not have changed + // yet. + EXPECT_EQ(num_default_network_change_, 0); + EXPECT_EQ(initial_config_key, connectivity_manager_->getConfigurationKey()); + + // Now, simulate the network becoming connected. + // This should trigger the deferred default network callback and update the internal state. + EXPECT_CALL(observer_, onNetworkConnected(net_id)); + EXPECT_CALL(observer_, onNetworkMadeDefault(net_id)); + connectivity_manager_->onNetworkConnectAndroid(connection_type, net_id); + + // Verify the callback was invoked. + EXPECT_EQ(num_default_network_change_, 1); + EXPECT_NE(initial_config_key, connectivity_manager_->getConfigurationKey()); +} + +// Verifies that the observer is notified about a network becoming connected and +// disconnected. +TEST_F(ConnectivityManagerTest, AndroidNetworkConnectedAndThenDisconnected) { + const NetworkHandle net_id = 123; + const auto connection_type = ConnectionType::CONNECTION_WIFI; + + EXPECT_CALL(observer_, onNetworkConnected(net_id)); + // Simulate a network is connected. + connectivity_manager_->onNetworkConnectAndroid(connection_type, net_id); + EXPECT_EQ(num_default_network_change_, 0); + + EXPECT_CALL(observer_, onNetworkDisconnected(net_id)); + connectivity_manager_->onNetworkDisconnectAndroid(net_id); + + // Disconnected network should not be used as the default. + connectivity_manager_->onDefaultNetworkChangedAndroid(connection_type, net_id); + EXPECT_EQ(num_default_network_change_, 0); +} + +// Verifies that the observer is notified about networks becoming disconnected when they are purged. +// But if the network is exempted from purging, observer shouldn't be notified about it being +// disconnected. +TEST_F(ConnectivityManagerTest, AndroidPurgeNetworks) { + + EXPECT_CALL(observer_, onNetworkConnected(_)).Times(3); + connectivity_manager_->onNetworkConnectAndroid(ConnectionType::CONNECTION_WIFI, 123); + connectivity_manager_->onNetworkConnectAndroid(ConnectionType::CONNECTION_4G, 456); + connectivity_manager_->onNetworkConnectAndroid(ConnectionType::CONNECTION_5G, 789); + + // Purge all networks other than the 5G network. + EXPECT_CALL(observer_, onNetworkDisconnected(1)); + EXPECT_CALL(observer_, onNetworkDisconnected(2)); + EXPECT_CALL(observer_, onNetworkDisconnected(123)); + EXPECT_CALL(observer_, onNetworkDisconnected(456)); + connectivity_manager_->purgeActiveNetworkListAndroid({789}); } } // namespace Network diff --git a/mobile/test/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2Test.java b/mobile/test/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2Test.java index 16b80675c42e8..d05cce6a8b6ca 100644 --- a/mobile/test/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2Test.java +++ b/mobile/test/java/io/envoyproxy/envoymobile/engine/AndroidNetworkMonitorV2Test.java @@ -11,6 +11,7 @@ import static org.robolectric.Shadows.shadowOf; import android.content.Context; +import android.content.Intent; import android.Manifest; import android.net.ConnectivityManager; import android.net.NetworkCapabilities; @@ -22,6 +23,7 @@ import androidx.test.rule.GrantPermissionRule; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import android.os.Build; +import android.os.Looper; import org.junit.After; import org.junit.Before; @@ -44,6 +46,8 @@ * Tests functionality of AndroidNetworkMonitorV2 */ @RunWith(RobolectricTestRunner.class) +// Individual tests may override this to test different Android SDK versions. +@Config(sdk = Build.VERSION_CODES.O) public class AndroidNetworkMonitorV2Test { @Rule public GrantPermissionRule mRuntimePermissionRule = @@ -62,7 +66,12 @@ public void setUp() { connectivityManager = androidNetworkMonitor.getConnectivityManager(); networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()); - assertThat(shadowOf(connectivityManager).getNetworkCallbacks()).hasSize(2); + int expectedNetworkCallbackSize = 2; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + expectedNetworkCallbackSize = 1; + } + assertThat(shadowOf(connectivityManager).getNetworkCallbacks()) + .hasSize(expectedNetworkCallbackSize); } @After @@ -246,4 +255,21 @@ public void testOnDefaultNetworkChangedVPN() { .onDefaultNetworkChangedV2(EnvoyConnectionType.CONNECTION_WIFI, activeNetwork.getNetworkHandle()); } + + // Tests that the broadcast receiver is triggered when the default network changes from cell to + // WIFI on Android M. + @Test + @Config(sdk = Build.VERSION_CODES.M) + public void testBroadcastReceiver() { + Network activeNetwork = triggerDefaultNetworkChange(NetworkCapabilities.TRANSPORT_WIFI, + ConnectivityManager.TYPE_WIFI, 0); + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION); + context.sendBroadcast(intent); + // Robolectric doesn't seem to run the receiver in the main looper, so we need to idle it. + shadowOf(Looper.getMainLooper()).idle(); + verify(mockEnvoyEngine) + .onDefaultNetworkChangedV2(EnvoyConnectionType.CONNECTION_WIFI, + activeNetwork.getNetworkHandle()); + } } diff --git a/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt b/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt index c3963a232ad3f..94257304e1822 100644 --- a/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt +++ b/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt @@ -254,9 +254,6 @@ class EnvoyConfigurationTest { // enableDrainPostDnsRefresh = true assertThat(resolvedTemplate).contains("enable_drain_post_dns_refresh: true") - // UDP GRO enabled by default - assertThat(resolvedTemplate).contains("key: \"prefer_quic_client_udp_gro\" value { bool_value: true }") - // enableDNSCache = true assertThat(resolvedTemplate).contains("key: \"dns_persistent_cache\"") // dnsCacheSaveIntervalSeconds = 101 diff --git a/mobile/test/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkTest.java b/mobile/test/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkTest.java new file mode 100644 index 0000000000000..0140329f96ab4 --- /dev/null +++ b/mobile/test/java/io/envoyproxy/envoymobile/utilities/AndroidNetworkTest.java @@ -0,0 +1,114 @@ +package io.envoyproxy.envoymobile.utilities; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.robolectric.Shadows.shadowOf; + +import io.envoyproxy.envoymobile.engine.JniLibrary; +import io.envoyproxy.envoymobile.engine.EnvoyEngine; +import io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2; +import io.envoyproxy.envoymobile.engine.types.EnvoyConnectionType; + +import java.nio.charset.StandardCharsets; + +import android.content.Context; +import android.Manifest; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.Network; +import android.net.NetworkInfo; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.GrantPermissionRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowNetworkInfo; +import org.robolectric.shadows.ShadowNetworkCapabilities; + +@RunWith(RobolectricTestRunner.class) +public final class AndroidNetworkTest { + @Rule + public GrantPermissionRule mRuntimePermissionRule = + GrantPermissionRule.grant(Manifest.permission.ACCESS_NETWORK_STATE); + + private AndroidNetworkMonitorV2 mAndroidNetworkMonitor; + private ConnectivityManager mConnectivityManager; + private final EnvoyEngine mMockEnvoyEngine = mock(EnvoyEngine.class); + + @BeforeClass + public static void beforeClass() { + JniLibrary.loadTestLibrary(); + } + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + if (ContextUtils.getApplicationContext() == null) { + ContextUtils.initApplicationContext(context.getApplicationContext()); + } + AndroidNetworkMonitorV2.load(context, mMockEnvoyEngine); + mAndroidNetworkMonitor = AndroidNetworkMonitorV2.getInstance(); + mConnectivityManager = mAndroidNetworkMonitor.getConnectivityManager(); + } + + @After + public void tearDown() throws Exception { + AndroidNetworkMonitorV2.shutdown(); + } + + @Test + public void testGetDefaultNetworkHandle() { + Network activeNetwork = mConnectivityManager.getActiveNetwork(); + long networkHandle = JniLibrary.callGetDefaultNetworkHandleFromNative(); + assertEquals(activeNetwork.getNetworkHandle(), networkHandle); + + NetworkInfo wifiNetworkInfo = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_WIFI, 0, + true, NetworkInfo.State.CONNECTED); + shadowOf(mConnectivityManager).setActiveNetworkInfo(wifiNetworkInfo); + Network wifiNetwork = mConnectivityManager.getActiveNetwork(); + long wifiNetworkHandle = JniLibrary.callGetDefaultNetworkHandleFromNative(); + assertEquals(wifiNetwork.getNetworkHandle(), wifiNetworkHandle); + } + + @Test + public void testGetAllConnectedNetworks() { + // Make all networks connected to the internet. + Network[] networks = mConnectivityManager.getAllNetworks(); + for (Network network : networks) { + NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities(network); + shadowOf(capabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + NetworkInfo netInfo = mConnectivityManager.getNetworkInfo(network); + shadowOf(netInfo).setConnectionStatus(NetworkInfo.State.CONNECTED); + } + long[][] networkArray = JniLibrary.callGetAllConnectedNetworksFromNative(); + assertEquals(networks.length, networkArray.length); + // The ShadowConnectivityManager should have 2 networks cached, one default WIFI network and + // another cellular one. + Network cellNetwork = null; + for (int i = 0; i < networks.length; ++i) { + assertEquals(networks[i].getNetworkHandle(), networkArray[i][0]); + NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities(networks[i]); + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + assertEquals(EnvoyConnectionType.CONNECTION_WIFI.getValue(), networkArray[i][1]); + } else { + cellNetwork = networks[i]; + assertTrue(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)); + assertEquals(EnvoyConnectionType.CONNECTION_2G.getValue(), networkArray[i][1]); + } + } + + assertNotNull(cellNetwork); + shadowOf(mConnectivityManager).removeNetwork(cellNetwork); + networkArray = JniLibrary.callGetAllConnectedNetworksFromNative(); + assertEquals(1, networkArray.length); + assertEquals(EnvoyConnectionType.CONNECTION_WIFI.getValue(), networkArray[0][1]); + } +} diff --git a/mobile/test/java/io/envoyproxy/envoymobile/utilities/BUILD b/mobile/test/java/io/envoyproxy/envoymobile/utilities/BUILD index c783227d57923..70cc84213d165 100644 --- a/mobile/test/java/io/envoyproxy/envoymobile/utilities/BUILD +++ b/mobile/test/java/io/envoyproxy/envoymobile/utilities/BUILD @@ -24,6 +24,32 @@ envoy_mobile_android_test( "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", "//library/java/io/envoyproxy/envoymobile/engine:envoy_engine_lib", "//library/java/io/envoyproxy/envoymobile/utilities", + "//library/java/io/envoyproxy/envoymobile/utilities:network_utilities", "//library/java/org/chromium/net", ], ) + +envoy_mobile_android_test( + name = "android_network_test", + srcs = [ + "AndroidNetworkTest.java", + ], + native_deps = [ + "//test/jni:libenvoy_jni_with_test_extensions.so", + ] + select({ + "@platforms//os:macos": [ + "//test/jni:libenvoy_jni_with_test_extensions_jnilib", + ], + "//conditions:default": [], + }), + native_lib_name = "envoy_jni_with_test_extensions", + test_class = "io.envoyproxy.envoymobile.utilities.AndroidNetworkTest", + deps = [ + "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", + "//library/java/io/envoyproxy/envoymobile/engine:envoy_engine_lib", + "//library/java/io/envoyproxy/envoymobile/engine/types:envoy_c_types_lib", + "//library/java/io/envoyproxy/envoymobile/utilities", + "//library/java/io/envoyproxy/envoymobile/utilities:network_utilities", + "@maven//:org_robolectric_annotations", + ], +) diff --git a/mobile/test/java/org/chromium/net/CronetHttp3Test.java b/mobile/test/java/org/chromium/net/CronetHttp3Test.java index 0397260e3d58d..27d655ae5a697 100644 --- a/mobile/test/java/org/chromium/net/CronetHttp3Test.java +++ b/mobile/test/java/org/chromium/net/CronetHttp3Test.java @@ -1,33 +1,47 @@ package org.chromium.net; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; - +import static org.junit.Assert.*; +import static org.robolectric.Shadows.shadowOf; +import static com.google.common.truth.Truth.assertThat; + +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.NetworkCapabilities; +import android.net.Network; +import android.net.NetworkInfo; import android.Manifest; +import io.envoyproxy.envoymobile.engine.testing.HttpTestServerFactory; import io.envoyproxy.envoymobile.engine.types.EnvoyNetworkType; -import org.chromium.net.impl.CronvoyUrlRequestContext; +import io.envoyproxy.envoymobile.engine.types.EnvoyConnectionType; +import io.envoyproxy.envoymobile.engine.AndroidNetworkMonitor; +import io.envoyproxy.envoymobile.engine.AndroidNetworkMonitorV2; import io.envoyproxy.envoymobile.engine.EnvoyEngine; +import io.envoyproxy.envoymobile.engine.JniLibrary; +import org.chromium.net.impl.CronvoyUrlRequestContext; import org.chromium.net.impl.CronvoyLogger; +import org.chromium.net.impl.NativeCronvoyEngineBuilderImpl; import androidx.test.core.app.ApplicationProvider; -import org.chromium.net.testing.TestUploadDataProvider; import androidx.test.filters.SmallTest; import androidx.test.rule.GrantPermissionRule; -import org.chromium.net.impl.NativeCronvoyEngineBuilderImpl; import org.chromium.net.testing.CronetTestRule; import org.chromium.net.testing.Feature; +import org.chromium.net.testing.TestUploadDataProvider; import org.chromium.net.testing.TestUrlRequestCallback; -import io.envoyproxy.envoymobile.engine.JniLibrary; import org.junit.After; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import io.envoyproxy.envoymobile.engine.testing.HttpTestServerFactory; + +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowNetwork; +import org.robolectric.shadows.ShadowNetworkInfo; +import org.robolectric.shadows.ShadowNetworkCapabilities; + import java.util.HashMap; import java.util.Map; import java.util.Collections; @@ -63,6 +77,8 @@ public class CronetHttp3Test { private boolean drainOnNetworkChange = false; private boolean resetBrokennessOnNetworkChange = false; private boolean disableDnsRefreshOnNetworkChange = false; + private boolean useAndroidNetworkMonitorV2 = false; + private ConnectivityManager connectivityManager; @BeforeClass public static void loadJniLibrary() { @@ -108,11 +124,29 @@ public void log(int logLevel, String message) { nativeCronetEngineBuilder.setLogger(logger); nativeCronetEngineBuilder.setLogLevel(EnvoyEngine.LogLevel.TRACE); } + if (useAndroidNetworkMonitorV2) { + nativeCronetEngineBuilder.setUseV2NetworkMonitor(useAndroidNetworkMonitorV2); + } // Make sure the handshake will work despite lack of real certs. nativeCronetEngineBuilder.setMockCertVerifierForTesting(); cronvoyEngine = new CronvoyUrlRequestContext(nativeCronetEngineBuilder); // Clear network states in ConnectivityManager. cronvoyEngine.getEnvoyEngine().resetConnectivityState(); + + if (useAndroidNetworkMonitorV2) { + AndroidNetworkMonitorV2 androidNetworkMonitor = AndroidNetworkMonitorV2.getInstance(); + connectivityManager = androidNetworkMonitor.getConnectivityManager(); + // AndroidNetworkMonitorV2 registers 2 NetworkCallbacks. + assertThat(shadowOf(connectivityManager).getNetworkCallbacks()).hasSize(2); + } else { + AndroidNetworkMonitor androidNetworkMonitor = AndroidNetworkMonitor.getInstance(); + connectivityManager = androidNetworkMonitor.getConnectivityManager(); + } + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()); + shadowOf(networkCapabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + // Verifies initial states of ShadowConnectivityManager. + assertTrue(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)); } @After @@ -123,6 +157,9 @@ public void tearDown() throws Exception { if (http3TestServer != null) { http3TestServer.shutdown(); } + if (useAndroidNetworkMonitorV2) { + AndroidNetworkMonitorV2.shutdown(); + } } private TestUrlRequestCallback doBasicGetRequest() { @@ -291,8 +328,11 @@ public void testRetryPostHandshake() throws Exception { @SmallTest @Feature({"Cronet"}) public void networkChangeNoDrains() throws Exception { - // Disable dns refreshment so that the engine will attempt immediate draining. - disableDnsRefreshOnNetworkChange = true; + if (!JniLibrary.runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh")) { + // Disable dns refreshment so that the engine will attempt immediate draining. + disableDnsRefreshOnNetworkChange = true; + } drainOnNetworkChange = false; setUp(printEnvoyLogs); @@ -330,8 +370,11 @@ public void networkChangeNoDrains() throws Exception { @SmallTest @Feature({"Cronet"}) public void networkChangeWithDrains() throws Exception { - // Disable dns refreshment so that the engine will attempt immediate draining. - disableDnsRefreshOnNetworkChange = true; + if (!JniLibrary.runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh")) { + // Disable dns refreshment so that the engine will attempt immediate draining. + disableDnsRefreshOnNetworkChange = true; + } drainOnNetworkChange = true; setUp(printEnvoyLogs); @@ -366,6 +409,250 @@ public void networkChangeWithDrains() throws Exception { assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_destroy: 2")); } + @Test + @SmallTest + @Feature({"Cronet"}) + public void networkChangeMonitorV2FromCellToWifi() throws Exception { + if (!JniLibrary.runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh")) { + // Disable dns refreshment so that the engine will attempt immediate draining. + disableDnsRefreshOnNetworkChange = true; + } + drainOnNetworkChange = true; + useAndroidNetworkMonitorV2 = true; + setUp(printEnvoyLogs); + + // Do the initial handshake dance + doInitialHttp2Request(); + + // Do a HTTP/3 request to establish a connection. + TestUrlRequestCallback getCallback1 = doBasicGetRequest(); + assertEquals(200, getCallback1.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback1.mResponseInfo.getNegotiatedProtocol()); + + // There should be one HTTP/3 connection. + String postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + assertTrue(postStats.contains("cluster.base.upstream_cx_http3_total: 1")); + + // Change from cell to newly connected WIFI network. + NetworkInfo wifiNetworkInfo = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_WIFI, 0, + true, NetworkInfo.State.CONNECTED); + shadowOf(connectivityManager).setActiveNetworkInfo(wifiNetworkInfo); + Network wifiNetwork = connectivityManager.getActiveNetwork(); + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(wifiNetwork); + shadowOf(networkCapabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + shadowOf(networkCapabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + shadowOf(networkCapabilities).addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + shadowOf(connectivityManager).setNetworkCapabilities(wifiNetwork, networkCapabilities); + + // Connected to the new network shouldn't be regarded as switching the default. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + callback.onAvailable(wifiNetwork); + }); + + // Make another request. It should reuse the existing connection because the new network won't + // be regarded as default. + TestUrlRequestCallback getCallback2 = doBasicGetRequest(); + assertEquals(200, getCallback2.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback2.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // The connection count should STILL be 1, proving reuse. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 1")); + // No connections should have been destroyed. + assertFalse(postStats, postStats.contains("cluster.base.upstream_cx_destroy:")); + + // Reported capability change should be regarded as switching the default. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + LinkProperties link = new LinkProperties(); + callback.onLinkPropertiesChanged(wifiNetwork, link); + callback.onCapabilitiesChanged(wifiNetwork, networkCapabilities); + }); + + // Do a 3rd HTTP/3 request. This must create a new connection. + TestUrlRequestCallback getCallback3 = doBasicGetRequest(); + assertEquals(200, getCallback3.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback3.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // Total HTTP/3 connections is now 2 (the original, now destroyed, and the new one). + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 2")); + // The 1st HTTP/3 connection and the TCP connection are both idle now, so they should have been + // closed during draining. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_destroy: 2")); + + // WIFI disconnected, no effect as long as the default network hasn't been switched. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + callback.onLost(wifiNetwork); + }); + + // Do a 4th HTTP/3 request. This should reuse the existing connection. + TestUrlRequestCallback getCallback4 = doBasicGetRequest(); + assertEquals(200, getCallback4.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback4.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // Stats shouldn't have been changed. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 2")); + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_destroy: 2")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void networkChangeMonitorV2FromDisconnectedCellToWifi() throws Exception { + if (!JniLibrary.runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh")) { + // Disable dns refreshment so that the engine will attempt immediate draining. + disableDnsRefreshOnNetworkChange = true; + } + drainOnNetworkChange = true; + useAndroidNetworkMonitorV2 = true; + setUp(printEnvoyLogs); + + // Do the initial handshake dance + doInitialHttp2Request(); + + // Do a HTTP/3 request to establish a connection. + TestUrlRequestCallback getCallback1 = doBasicGetRequest(); + assertEquals(200, getCallback1.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback1.mResponseInfo.getNegotiatedProtocol()); + + // There should be one HTTP/3 connection. + String postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + assertTrue(postStats.contains("cluster.base.upstream_cx_http3_total: 1")); + + // Lost current cellular network. + Network cellNetwork = connectivityManager.getActiveNetwork(); + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + callback.onLost(cellNetwork); + }); + + // Change from the disconnected cell to newly connected WIFI network. + NetworkInfo wifiNetworkInfo = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_WIFI, 0, + true, NetworkInfo.State.CONNECTED); + shadowOf(connectivityManager).setActiveNetworkInfo(wifiNetworkInfo); + Network wifiNetwork = connectivityManager.getActiveNetwork(); + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(wifiNetwork); + shadowOf(networkCapabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + shadowOf(networkCapabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + shadowOf(networkCapabilities).addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + shadowOf(connectivityManager).setNetworkCapabilities(wifiNetwork, networkCapabilities); + + // Connected to the new network shouldn't be regarded as switching the default. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + callback.onAvailable(wifiNetwork); + }); + + // Make another request. It should reuse the existing connection because the new network won't + // be regarded as default. + TestUrlRequestCallback getCallback2 = doBasicGetRequest(); + assertEquals(200, getCallback2.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback2.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // The connection count should STILL be 1, proving reuse. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 1")); + // No connections should have been destroyed. + assertFalse(postStats, postStats.contains("cluster.base.upstream_cx_destroy:")); + + // Reported capability change should be regarded as switching the default. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + LinkProperties link = new LinkProperties(); + callback.onLinkPropertiesChanged(wifiNetwork, link); + callback.onCapabilitiesChanged(wifiNetwork, networkCapabilities); + }); + + // Do a 3rd HTTP/3 request. This must create a new connection. + TestUrlRequestCallback getCallback3 = doBasicGetRequest(); + assertEquals(200, getCallback3.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback3.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // Total HTTP/3 connections is now 2 (the original, now destroyed, and the new one). + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 2")); + // The 1st HTTP/3 connection and the TCP connection are both idle now, so they should have been + // closed during draining. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_destroy: 2")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void networkChangeMonitorV2VpnOnAndOff() throws Exception { + if (!JniLibrary.runtimeFeatureEnabled( + "envoy.reloadable_features.decouple_explicit_drain_pools_and_dns_refresh")) { + // Disable dns refreshment so that the engine will attempt immediate draining. + disableDnsRefreshOnNetworkChange = true; + } + drainOnNetworkChange = true; + useAndroidNetworkMonitorV2 = true; + setUp(printEnvoyLogs); + + // Do the initial handshake dance + doInitialHttp2Request(); + + // Do a HTTP/3 request to establish a connection. + TestUrlRequestCallback getCallback1 = doBasicGetRequest(); + assertEquals(200, getCallback1.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback1.mResponseInfo.getNegotiatedProtocol()); + + // There should be one HTTP/3 connection. + String postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + assertTrue(postStats.contains("cluster.base.upstream_cx_http3_total: 1")); + + // A VPN network becomes available. + NetworkInfo networkInfoVpn = ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_VPN, 0, + true, NetworkInfo.State.CONNECTED); + Network vpnNetwork = ShadowNetwork.newInstance(2); + shadowOf(connectivityManager).addNetwork(vpnNetwork, networkInfoVpn); + NetworkCapabilities capabilities = ShadowNetworkCapabilities.newInstance(); + shadowOf(capabilities).addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + shadowOf(capabilities).addTransportType(NetworkCapabilities.TRANSPORT_VPN); + shadowOf(connectivityManager).setNetworkCapabilities(vpnNetwork, capabilities); + + // As long as VPN is available, it should be regarded as default network and trigger a default + // network change. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + // This should also purge the cellular network. But it's not observable to requests. + callback.onAvailable(vpnNetwork); + }); + + // Do another HTTP/3 request. This should create a new connection as the existing one is + // drained. + TestUrlRequestCallback getCallback2 = doBasicGetRequest(); + assertEquals(200, getCallback2.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback2.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // Total HTTP/3 connections is now 2 (the original, now destroyed, and the new one). + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 2")); + // The 1st HTTP/3 connection and the TCP connection are both idle now, so they should have been + // closed during draining. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_destroy: 2")); + + // The VPN becomes unavailable, the underlying cellular network should be regarded as the + // default. + shadowOf(connectivityManager).getNetworkCallbacks().forEach(callback -> { + callback.onLost(vpnNetwork); + }); + + // Do a 3rd HTTP/3 request. This must create a new connection. + TestUrlRequestCallback getCallback3 = doBasicGetRequest(); + assertEquals(200, getCallback3.mResponseInfo.getHttpStatusCode()); + assertEquals("h3", getCallback3.mResponseInfo.getNegotiatedProtocol()); + + postStats = cronvoyEngine.getEnvoyEngine().dumpStats(); + // Total HTTP/3 connections is now 3. + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_http3_total: 3")); + assertTrue(postStats, postStats.contains("cluster.base.upstream_cx_destroy: 3")); + } + @Test @SmallTest @Feature({"Cronet"}) diff --git a/mobile/test/java/org/chromium/net/testing/BUILD b/mobile/test/java/org/chromium/net/testing/BUILD index fd0adffecd18e..dc30f7ec0feb4 100644 --- a/mobile/test/java/org/chromium/net/testing/BUILD +++ b/mobile/test/java/org/chromium/net/testing/BUILD @@ -37,6 +37,7 @@ android_library( "//library/java/io/envoyproxy/envoymobile/engine:envoy_base_engine_lib", "//library/java/io/envoyproxy/envoymobile/engine:envoy_engine_lib", "//library/java/io/envoyproxy/envoymobile/utilities", + "//library/java/io/envoyproxy/envoymobile/utilities:network_utilities", "//library/java/org/chromium/net", "//library/java/org/chromium/net/impl:cronvoy", "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", diff --git a/mobile/test/jni/jni_utility_test.cc b/mobile/test/jni/jni_utility_test.cc index 9799ddeb3dc9a..1f531d8b9b6ef 100644 --- a/mobile/test/jni/jni_utility_test.cc +++ b/mobile/test/jni/jni_utility_test.cc @@ -25,7 +25,7 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_io_envoyproxy_envoymobile_jni_JniUtilityTest_protoJavaByteArrayConversion(JNIEnv* env, jclass, jbyteArray source) { Envoy::JNI::JniHelper jni_helper(env); - Envoy::ProtobufWkt::Struct s; + Envoy::Protobuf::Struct s; Envoy::JNI::javaByteArrayToProto(jni_helper, source, &s); return Envoy::JNI::protoToJavaByteArray(jni_helper, s).release(); } diff --git a/mobile/test/kotlin/apps/baseline/AndroidManifest.xml b/mobile/test/kotlin/apps/baseline/AndroidManifest.xml index a760b59467071..00ed711cac105 100644 --- a/mobile/test/kotlin/apps/baseline/AndroidManifest.xml +++ b/mobile/test/kotlin/apps/baseline/AndroidManifest.xml @@ -6,7 +6,7 @@ & configs, - Server::Configuration::FactoryContext& context) { + Server::Configuration::GenericFactoryContext& context) { for (const auto& config : configs) { auto filter = FilterFactory::fromProto(config, context); if (filter != nullptr) { @@ -167,11 +167,11 @@ OperatorFilter::OperatorFilter( } OrFilter::OrFilter(const envoy::config::accesslog::v3::OrFilter& config, - Server::Configuration::FactoryContext& context) + Server::Configuration::GenericFactoryContext& context) : OperatorFilter(config.filters(), context) {} AndFilter::AndFilter(const envoy::config::accesslog::v3::AndFilter& config, - Server::Configuration::FactoryContext& context) + Server::Configuration::GenericFactoryContext& context) : OperatorFilter(config.filters(), context) {} bool OrFilter::evaluate(const Formatter::HttpFormatterContext& context, @@ -217,27 +217,27 @@ bool HeaderFilter::evaluate(const Formatter::HttpFormatterContext& context, } ResponseFlagFilter::ResponseFlagFilter( - const envoy::config::accesslog::v3::ResponseFlagFilter& config) - : has_configured_flags_(!config.flags().empty()) { - - // Preallocate the vector to avoid frequent heap allocations. - configured_flags_.resize(StreamInfo::ResponseFlagUtils::responseFlagsVec().size(), false); - for (int i = 0; i < config.flags_size(); i++) { - auto response_flag = StreamInfo::ResponseFlagUtils::toResponseFlag(config.flags(i)); - // The config has been validated. Therefore, every flag in the config will have a mapping. - ASSERT(response_flag.has_value()); - - // The vector is allocated with the size of the response flags vec. Therefore, the index - // should always be valid. - ASSERT(response_flag.value().value() < configured_flags_.size()); - - configured_flags_[response_flag.value().value()] = true; + const envoy::config::accesslog::v3::ResponseFlagFilter& config) { + if (!config.flags().empty()) { + // Preallocate the vector to avoid frequent heap allocations. + configured_flags_.resize(StreamInfo::ResponseFlagUtils::responseFlagsVec().size(), false); + for (int i = 0; i < config.flags_size(); i++) { + auto response_flag = StreamInfo::ResponseFlagUtils::toResponseFlag(config.flags(i)); + // The config has been validated. Therefore, every flag in the config will have a mapping. + ASSERT(response_flag.has_value()); + + // The vector is allocated with the size of the response flags vec. Therefore, the index + // should always be valid. + ASSERT(response_flag.value().value() < configured_flags_.size()); + + configured_flags_[response_flag.value().value()] = true; + } } } bool ResponseFlagFilter::evaluate(const Formatter::HttpFormatterContext&, const StreamInfo::StreamInfo& info) const { - if (has_configured_flags_) { + if (!configured_flags_.empty()) { for (const auto flag : info.responseFlags()) { ASSERT(flag.value() < configured_flags_.size()); if (configured_flags_[flag.value()]) { @@ -292,7 +292,8 @@ bool LogTypeFilter::evaluate(const Formatter::HttpFormatterContext& context, MetadataFilter::MetadataFilter(const envoy::config::accesslog::v3::MetadataFilter& filter_config, Server::Configuration::CommonFactoryContext& context) - : default_match_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(filter_config, match_if_key_not_found, true)), + : present_matcher_(true), + default_match_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(filter_config, match_if_key_not_found, true)), filter_(filter_config.matcher().filter()) { if (filter_config.has_matcher()) { @@ -306,11 +307,6 @@ MetadataFilter::MetadataFilter(const envoy::config::accesslog::v3::MetadataFilte const auto& val = matcher_config.value(); value_matcher_ = Matchers::ValueMatcher::create(val, context); } - - // Matches if the value is present in dynamic metadata - auto present_val = envoy::type::matcher::v3::ValueMatcher(); - present_val.set_present_match(true); - present_matcher_ = Matchers::ValueMatcher::create(present_val, context); } bool MetadataFilter::evaluate(const Formatter::HttpFormatterContext&, @@ -319,7 +315,7 @@ bool MetadataFilter::evaluate(const Formatter::HttpFormatterContext&, Envoy::Config::Metadata::metadataValue(&info.dynamicMetadata(), filter_, path_); // If the key corresponds to a set value in dynamic metadata, return true if the value matches the // the configured 'MetadataMatcher' value and false otherwise - if (present_matcher_->match(value)) { + if (present_matcher_.match(value)) { return value_matcher_ && value_matcher_->match(value); } @@ -330,7 +326,7 @@ bool MetadataFilter::evaluate(const Formatter::HttpFormatterContext&, InstanceSharedPtr AccessLogFactory::fromProto(const envoy::config::accesslog::v3::AccessLog& config, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers) { FilterPtr filter; if (config.has_filter()) { diff --git a/source/common/access_log/access_log_impl.h b/source/common/access_log/access_log_impl.h index e9bd2b1faef36..bbf0264cb5ac3 100644 --- a/source/common/access_log/access_log_impl.h +++ b/source/common/access_log/access_log_impl.h @@ -34,7 +34,7 @@ class FilterFactory { * Read a filter definition from proto and instantiate a concrete filter class. */ static FilterPtr fromProto(const envoy::config::accesslog::v3::AccessLogFilter& config, - Server::Configuration::FactoryContext& context); + Server::Configuration::GenericFactoryContext& context); }; /** @@ -86,7 +86,7 @@ class OperatorFilter : public Filter { public: OperatorFilter( const Protobuf::RepeatedPtrField& configs, - Server::Configuration::FactoryContext& context); + Server::Configuration::GenericFactoryContext& context); protected: std::vector filters_; @@ -98,7 +98,7 @@ class OperatorFilter : public Filter { class AndFilter : public OperatorFilter { public: AndFilter(const envoy::config::accesslog::v3::AndFilter& config, - Server::Configuration::FactoryContext& context); + Server::Configuration::GenericFactoryContext& context); // AccessLog::Filter bool evaluate(const Formatter::HttpFormatterContext& context, @@ -111,7 +111,7 @@ class AndFilter : public OperatorFilter { class OrFilter : public OperatorFilter { public: OrFilter(const envoy::config::accesslog::v3::OrFilter& config, - Server::Configuration::FactoryContext& context); + Server::Configuration::GenericFactoryContext& context); // AccessLog::Filter bool evaluate(const Formatter::HttpFormatterContext& context, @@ -188,7 +188,6 @@ class ResponseFlagFilter : public Filter { const StreamInfo::StreamInfo& info) const override; private: - const bool has_configured_flags_{}; std::vector configured_flags_{}; }; @@ -249,7 +248,7 @@ class MetadataFilter : public Filter { const StreamInfo::StreamInfo& info) const override; private: - Matchers::ValueMatcherConstSharedPtr present_matcher_; + Matchers::PresentMatcher present_matcher_; Matchers::ValueMatcherConstSharedPtr value_matcher_; std::vector path_; @@ -269,7 +268,7 @@ class AccessLogFactory { */ static InstanceSharedPtr fromProto(const envoy::config::accesslog::v3::AccessLog& config, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}); }; diff --git a/source/common/buffer/buffer_impl.h b/source/common/buffer/buffer_impl.h index f9f7f5632401a..72cca4ba9b1a2 100644 --- a/source/common/buffer/buffer_impl.h +++ b/source/common/buffer/buffer_impl.h @@ -699,8 +699,8 @@ class OwnedImpl : public LibEventInstance { // Does not implement watermarking. // TODO(antoniovicente) Implement watermarks by merging the OwnedImpl and WatermarkBuffer // implementations. Also, make high-watermark config a constructor argument. - void setWatermarks(uint32_t, uint32_t) override { ASSERT(false, "watermarks not implemented."); } - uint32_t highWatermark() const override { return 0; } + void setWatermarks(uint64_t, uint32_t) override { ASSERT(false, "watermarks not implemented."); } + uint64_t highWatermark() const override { return 0; } bool highWatermarkTriggered() const override { return false; } /** diff --git a/source/common/buffer/buffer_util.h b/source/common/buffer/buffer_util.h index 7a8e3aef3c07e..2c8c0da05c0ce 100644 --- a/source/common/buffer/buffer_util.h +++ b/source/common/buffer/buffer_util.h @@ -39,7 +39,7 @@ class Util { // generate the string_view, and does not work on all platforms yet. // // The accuracy is checked in buffer_util_test. -#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION >= 14000 +#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION >= 14000 && !defined(__APPLE__) // This version is awkward, and doesn't work on all platforms used in Envoy CI // as of August 2023, but it is the fastest correct option on modern compilers. char buf[100]; diff --git a/source/common/buffer/watermark_buffer.cc b/source/common/buffer/watermark_buffer.cc index bc9d6629c5e69..47bf4d5a706ad 100644 --- a/source/common/buffer/watermark_buffer.cc +++ b/source/common/buffer/watermark_buffer.cc @@ -116,11 +116,10 @@ size_t WatermarkBuffer::addFragments(absl::Span fragmen return total_size_to_write; } -void WatermarkBuffer::setWatermarks(uint32_t high_watermark, +void WatermarkBuffer::setWatermarks(uint64_t high_watermark, uint32_t overflow_watermark_multiplier) { if (overflow_watermark_multiplier > 0 && - (static_cast(overflow_watermark_multiplier) * high_watermark) > - std::numeric_limits::max()) { + (high_watermark > std::numeric_limits::max() / overflow_watermark_multiplier)) { ENVOY_LOG_MISC(debug, "Error setting overflow threshold: overflow_watermark_multiplier * " "high_watermark is overflowing. Disabling overflow watermark."); overflow_watermark_multiplier = 0; diff --git a/source/common/buffer/watermark_buffer.h b/source/common/buffer/watermark_buffer.h index c9de30551a4c0..18c5df1cb0007 100644 --- a/source/common/buffer/watermark_buffer.h +++ b/source/common/buffer/watermark_buffer.h @@ -45,8 +45,8 @@ class WatermarkBuffer : public OwnedImpl { void appendSliceForTest(const void* data, uint64_t size) override; void appendSliceForTest(absl::string_view data) override; - void setWatermarks(uint32_t high_watermark, uint32_t overflow_watermark = 0) override; - uint32_t highWatermark() const override { return high_watermark_; } + void setWatermarks(uint64_t high_watermark, uint32_t overflow_watermark = 0) override; + uint64_t highWatermark() const override { return high_watermark_; } // Returns true if the high watermark callbacks have been called more recently // than the low watermark callbacks. bool highWatermarkTriggered() const override { return above_high_watermark_called_; } @@ -55,6 +55,8 @@ class WatermarkBuffer : public OwnedImpl { virtual void checkHighAndOverflowWatermarks(); virtual void checkLowWatermark(); + uint64_t overflowWatermarkForTestOnly() const { return overflow_watermark_; } + private: void commit(uint64_t length, absl::Span slices, ReservationSlicesOwnerPtr slices_owner) override; @@ -65,9 +67,9 @@ class WatermarkBuffer : public OwnedImpl { // Used for enforcing buffer limits (off by default). If these are set to non-zero by a call to // setWatermarks() the watermark callbacks will be called as described above. - uint32_t high_watermark_{0}; - uint32_t low_watermark_{0}; - uint32_t overflow_watermark_{0}; + uint64_t high_watermark_{0}; + uint64_t low_watermark_{0}; + uint64_t overflow_watermark_{0}; // Tracks the latest state of watermark callbacks. // True between the time above_high_watermark_ has been called until below_low_watermark_ has // been called. diff --git a/source/common/common/BUILD b/source/common/common/BUILD index 9382613088bb3..ebad046d397c7 100644 --- a/source/common/common/BUILD +++ b/source/common/common/BUILD @@ -139,7 +139,7 @@ envoy_basic_cc_library( hdrs = ["fmt.h"], deps = [ "//envoy/common:base_includes", - "@com_github_fmtlib_fmt//:fmtlib", + "@com_github_fmtlib_fmt//:fmt", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", ], @@ -386,6 +386,16 @@ envoy_cc_library( hdrs = ["non_copyable.h"], ) +envoy_cc_library( + name = "notification_lib", + srcs = ["notification.cc"], + hdrs = ["notification.h"], + deps = [ + ":assert_lib", + ":minimal_logger_lib", + ], +) + envoy_cc_library( name = "phantom", hdrs = ["phantom.h"], @@ -479,8 +489,15 @@ envoy_cc_library( ) envoy_cc_library( - name = "trie_lookup_table_lib", - hdrs = ["trie_lookup_table.h"], + name = "radix_tree_lib", + hdrs = ["radix_tree.h"], + deps = [ + ":assert_lib", + "//envoy/common:optref_lib", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:inlined_vector", + "@com_google_absl//absl/strings", + ], ) envoy_cc_library( diff --git a/source/common/common/assert.cc b/source/common/common/assert.cc index 94bda22aa3c3f..d624b1d07cff6 100644 --- a/source/common/common/assert.cc +++ b/source/common/common/assert.cc @@ -45,12 +45,12 @@ class EnvoyBugState { static EnvoyBugState& get() { MUTABLE_CONSTRUCT_ON_FIRST_USE(EnvoyBugState); } void clear() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); counters_.clear(); } uint64_t inc(absl::string_view bug_name) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return ++counters_[bug_name]; } diff --git a/source/common/common/callback_impl.h b/source/common/common/callback_impl.h index 628043e11bdf6..c14aabe63b33a 100644 --- a/source/common/common/callback_impl.h +++ b/source/common/common/callback_impl.h @@ -21,9 +21,9 @@ namespace Common { * * @see ThreadSafeCallbackManager for dealing with callbacks across multiple threads */ -template class CallbackManager { +template class CallbackManager { public: - using Callback = std::function; + using Callback = std::function; /** * Add a callback. @@ -46,12 +46,16 @@ template class CallbackManager { * to change (specifically, it will crash if the next callback in the list is deleted). * @param args supplies the callback arguments. */ - absl::Status runCallbacks(CallbackArgs... args) { + ReturnType runCallbacks(CallbackArgs... args) { for (auto it = callbacks_.cbegin(); it != callbacks_.cend();) { auto current = *(it++); - RETURN_IF_NOT_OK(current->cb_(args...)); + if constexpr (std::is_same_v) { + RETURN_IF_NOT_OK(current->cb_(args...)); + } else { + current->cb_(args...); + } } - return absl::OkStatus(); + return defaultReturn(); } /** @@ -62,12 +66,16 @@ template class CallbackManager { * @param run_with function that is responsible for generating inputs to callbacks. This will be * executed once for each callback. */ - absl::Status runCallbacksWith(std::function(void)> run_with) { + ReturnType runCallbacksWith(std::function(void)> run_with) { for (auto it = callbacks_.cbegin(); it != callbacks_.cend();) { auto cb = *(it++); - RETURN_IF_NOT_OK(std::apply(cb->cb_, run_with())); + if constexpr (std::is_same_v) { + RETURN_IF_NOT_OK(std::apply(cb->cb_, run_with())); + } else { + std::apply(cb->cb_, run_with()); + } } - return absl::OkStatus(); + return defaultReturn(); } size_t size() const noexcept { return callbacks_.size(); } @@ -100,6 +108,15 @@ template class CallbackManager { */ void remove(typename std::list::iterator& it) { callbacks_.erase(it); } + // Templating helper + ReturnType defaultReturn() { + if constexpr (std::is_same_v) { + return absl::OkStatus(); + } else { + return void(); + } + } + std::list callbacks_; // This is a sentinel shared_ptr used for keeping track of whether the manager is still alive. // It is only held by weak reference in the callback holder above. This is used versus having diff --git a/source/common/common/compiler_requirements.h b/source/common/common/compiler_requirements.h index ad14111af9186..912cf0c447d0b 100644 --- a/source/common/common/compiler_requirements.h +++ b/source/common/common/compiler_requirements.h @@ -2,8 +2,8 @@ namespace Envoy { -#if __cplusplus < 201402L -#error "Your compiler does not support C++14. GCC 5+, Clang, or MSVC 2017+ is required." +#if __cplusplus < 202002L +#error "Your compiler does not support C++20. GCC 12+, Clang, or MSVC 2019+ is required." #endif // See: diff --git a/source/common/common/fine_grain_logger.cc b/source/common/common/fine_grain_logger.cc index 734e1328bf78e..7ef40f07ec31e 100644 --- a/source/common/common/fine_grain_logger.cc +++ b/source/common/common/fine_grain_logger.cc @@ -18,9 +18,9 @@ namespace Envoy { class FineGrainLogBasicLockable : public Thread::BasicLockable { public: // BasicLockable - void lock() ABSL_EXCLUSIVE_LOCK_FUNCTION() override { mutex_.Lock(); } - bool tryLock() ABSL_EXCLUSIVE_TRYLOCK_FUNCTION(true) override { return mutex_.TryLock(); } - void unlock() ABSL_UNLOCK_FUNCTION() override { mutex_.Unlock(); } + void lock() ABSL_EXCLUSIVE_LOCK_FUNCTION() override { mutex_.lock(); } + bool tryLock() ABSL_EXCLUSIVE_TRYLOCK_FUNCTION(true) override { return mutex_.try_lock(); } + void unlock() ABSL_UNLOCK_FUNCTION() override { mutex_.unlock(); } private: absl::Mutex mutex_; @@ -28,7 +28,7 @@ class FineGrainLogBasicLockable : public Thread::BasicLockable { SpdLoggerSharedPtr FineGrainLogContext::getFineGrainLogEntry(absl::string_view key) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { - absl::ReaderMutexLock l(&fine_grain_log_lock_); + absl::ReaderMutexLock l(fine_grain_log_lock_); auto it = fine_grain_log_map_->find(key); if (it != fine_grain_log_map_->end()) { return it->second; @@ -37,14 +37,14 @@ SpdLoggerSharedPtr FineGrainLogContext::getFineGrainLogEntry(absl::string_view k } spdlog::level::level_enum FineGrainLogContext::getVerbosityDefaultLevel() const { - absl::ReaderMutexLock l(&fine_grain_log_lock_); + absl::ReaderMutexLock l(fine_grain_log_lock_); return verbosity_default_level_; } void FineGrainLogContext::initFineGrainLogger(const std::string& key, std::atomic& logger) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { - absl::WriterMutexLock l(&fine_grain_log_lock_); + absl::WriterMutexLock l(fine_grain_log_lock_); auto it = fine_grain_log_map_->find(key); spdlog::logger* target; if (it == fine_grain_log_map_->end()) { @@ -57,7 +57,7 @@ void FineGrainLogContext::initFineGrainLogger(const std::string& key, bool FineGrainLogContext::setFineGrainLogger(absl::string_view key, level_enum log_level) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { - absl::ReaderMutexLock l(&fine_grain_log_lock_); + absl::ReaderMutexLock l(fine_grain_log_lock_); auto it = fine_grain_log_map_->find(key); if (it != fine_grain_log_map_->end()) { it->second->set_level(log_level); @@ -70,11 +70,11 @@ void FineGrainLogContext::setDefaultFineGrainLogLevelFormat(spdlog::level::level const std::string& format) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { { - absl::WriterMutexLock wl(&fine_grain_log_lock_); + absl::WriterMutexLock wl(fine_grain_log_lock_); verbosity_default_level_ = level; } - absl::ReaderMutexLock rl(&fine_grain_log_lock_); + absl::ReaderMutexLock rl(fine_grain_log_lock_); for (const auto& [key, logger] : *fine_grain_log_map_) { logger->set_level(getLogLevel(key)); Logger::Utility::setLogFormatForLogger(*logger, format); @@ -82,7 +82,7 @@ void FineGrainLogContext::setDefaultFineGrainLogLevelFormat(spdlog::level::level } std::string FineGrainLogContext::listFineGrainLoggers() ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { - absl::ReaderMutexLock l(&fine_grain_log_lock_); + absl::ReaderMutexLock l(fine_grain_log_lock_); std::string info = absl::StrJoin(*fine_grain_log_map_, "\n", [](std::string* out, const auto& log_pair) { auto level_str_view = spdlog::level::to_string_view(log_pair.second->level()); @@ -94,7 +94,7 @@ std::string FineGrainLogContext::listFineGrainLoggers() ABSL_LOCKS_EXCLUDED(fine void FineGrainLogContext::setAllFineGrainLoggers(spdlog::level::level_enum level) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { - absl::ReaderMutexLock l(&fine_grain_log_lock_); + absl::ReaderMutexLock l(fine_grain_log_lock_); verbosity_update_info_.clear(); for (const auto& it : *fine_grain_log_map_) { it.second->set_level(level); @@ -104,7 +104,7 @@ void FineGrainLogContext::setAllFineGrainLoggers(spdlog::level::level_enum level FineGrainLogLevelMap FineGrainLogContext::getAllFineGrainLogLevelsForTest() ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { FineGrainLogLevelMap log_levels; - absl::ReaderMutexLock l(&fine_grain_log_lock_); + absl::ReaderMutexLock l(fine_grain_log_lock_); for (const auto& it : *fine_grain_log_map_) { log_levels[it.first] = it.second->level(); } @@ -170,7 +170,7 @@ spdlog::logger* FineGrainLogContext::createLogger(const std::string& key) void FineGrainLogContext::updateVerbosityDefaultLevel(level_enum level) { { - absl::WriterMutexLock wl(&fine_grain_log_lock_); + absl::WriterMutexLock wl(fine_grain_log_lock_); verbosity_default_level_ = level; } @@ -179,7 +179,7 @@ void FineGrainLogContext::updateVerbosityDefaultLevel(level_enum level) { void FineGrainLogContext::updateVerbositySetting( const std::vector>& updates) { - absl::WriterMutexLock ul(&fine_grain_log_lock_); + absl::WriterMutexLock ul(fine_grain_log_lock_); verbosity_update_info_.clear(); for (const auto& [glob, level] : updates) { if (level < kLogLevelMin || level > kLogLevelMax) { diff --git a/source/common/common/logger.cc b/source/common/common/logger.cc index 925ad510d3cfe..756ba260a04dc 100644 --- a/source/common/common/logger.cc +++ b/source/common/common/logger.cc @@ -28,15 +28,8 @@ void SinkDelegate::logWithStableName(absl::string_view, absl::string_view, absl: SinkDelegate::~SinkDelegate() { // The previous delegate should have never been set or should have been reset by now via - // restoreDelegate()/restoreTlsDelegate(); + // restoreDelegate(). assert(previous_delegate_ == nullptr); - assert(previous_tls_delegate_ == nullptr); -} - -void SinkDelegate::setTlsDelegate() { - assert(previous_tls_delegate_ == nullptr); - previous_tls_delegate_ = log_sink_->tlsDelegate(); - log_sink_->setTlsDelegate(this); } void SinkDelegate::setDelegate() { @@ -46,13 +39,6 @@ void SinkDelegate::setDelegate() { log_sink_->setDelegate(this); } -void SinkDelegate::restoreTlsDelegate() { - // Ensures stacked allocation of delegates. - assert(log_sink_->tlsDelegate() == this); - log_sink_->setTlsDelegate(previous_tls_delegate_); - previous_tls_delegate_ = nullptr; -} - void SinkDelegate::restoreDelegate() { // Ensures stacked allocation of delegates. assert(log_sink_->delegate() == this); @@ -78,12 +64,12 @@ void StderrSinkDelegate::flush() { } void DelegatingLogSink::set_formatter(std::unique_ptr formatter) { - absl::MutexLock lock(&format_mutex_); + absl::MutexLock lock(format_mutex_); formatter_ = std::move(formatter); } void DelegatingLogSink::log(const spdlog::details::log_msg& msg) { - absl::ReleasableMutexLock lock(&format_mutex_); + absl::ReleasableMutexLock lock(format_mutex_); absl::string_view msg_view = absl::string_view(msg.payload.data(), msg.payload.size()); // This memory buffer must exist in the scope of the entire function, @@ -102,11 +88,6 @@ void DelegatingLogSink::log(const spdlog::details::log_msg& msg) { sink.log(msg_view, msg); } }; - auto* tls_sink = tlsDelegate(); - if (tls_sink != nullptr) { - log_to_sink(*tls_sink); - return; - } // Hold the sink mutex while performing the actual logging. This prevents the sink from being // swapped during an individual log event. @@ -114,7 +95,7 @@ void DelegatingLogSink::log(const spdlog::details::log_msg& msg) { // protection is really only needed in tests. It would be nice to figure out a test-only // mechanism for this that does not require extra locking that we don't explicitly need in the // prod code. - absl::ReaderMutexLock sink_lock(&sink_mutex_); + absl::ReaderMutexLock sink_lock(sink_mutex_); log_to_sink(*sink_); } @@ -139,33 +120,13 @@ DelegatingLogSinkSharedPtr DelegatingLogSink::init() { } void DelegatingLogSink::flush() { - auto* tls_sink = tlsDelegate(); - if (tls_sink != nullptr) { - tls_sink->flush(); - return; - } - absl::ReaderMutexLock lock(&sink_mutex_); + absl::ReaderMutexLock lock(sink_mutex_); sink_->flush(); } -SinkDelegate** DelegatingLogSink::tlsSink() { - static thread_local SinkDelegate* tls_sink = nullptr; - - return &tls_sink; -} - -void DelegatingLogSink::setTlsDelegate(SinkDelegate* sink) { *tlsSink() = sink; } - -SinkDelegate* DelegatingLogSink::tlsDelegate() { return *tlsSink(); } - void DelegatingLogSink::logWithStableName(absl::string_view stable_name, absl::string_view level, absl::string_view component, absl::string_view message) { - auto tls_sink = tlsDelegate(); - if (tls_sink != nullptr) { - tls_sink->logWithStableName(stable_name, level, component, message); - return; - } - absl::ReaderMutexLock sink_lock(&sink_mutex_); + absl::ReaderMutexLock sink_lock(sink_mutex_); sink_->logWithStableName(stable_name, level, component, message); } diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 8db5f52bc52b1..4e9b2f861a56d 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -77,6 +77,7 @@ const static bool should_log = true; FUNCTION(misc) \ FUNCTION(mongo) \ FUNCTION(multi_connection) \ + FUNCTION(notification) \ FUNCTION(oauth2) \ FUNCTION(quic) \ FUNCTION(quic_stream) \ @@ -153,30 +154,21 @@ class SinkDelegate : NonCopyable { virtual void flush() PURE; protected: - // Swap the current thread local log sink delegate for this one. This should be called by the + // Swap the current *global* log sink delegate for this one. This should be called by the // derived class constructor immediately before returning. This is required to match - // restoreTlsDelegate(), otherwise it's possible for the previous delegate to get set in the base + // restoreDelegate(), otherwise it's possible for the previous delegate to get set in the base // class constructor, the derived class constructor throws, and cleanup becomes broken. - void setTlsDelegate(); - - // Swap the current *global* log sink delegate for this one. This behaves as setTlsDelegate, but - // operates on the global log sink instead of the thread local one. void setDelegate(); - // Swap the current thread local log sink (this) for the previous one. This should be called by + // Swap the current *global* log sink (this) for the previous one. This should be called by // the derived class destructor in the body. This is critical as otherwise it's possible for a log // message to get routed to a partially destructed sink. - void restoreTlsDelegate(); - - // Swap the current *global* log sink delegate for the previous one. This behaves as - // restoreTlsDelegate, but operates on the global sink instead of the thread local one. void restoreDelegate(); SinkDelegate* previousDelegate() { return previous_delegate_; } private: SinkDelegate* previous_delegate_{nullptr}; - SinkDelegate* previous_tls_delegate_{nullptr}; DelegatingLogSinkSharedPtr log_sink_; }; @@ -260,9 +252,6 @@ class DelegatingLogSink : public spdlog::sinks::sink { absl::ReaderMutexLock lock(&sink_mutex_); return sink_; } - SinkDelegate** tlsSink(); - void setTlsDelegate(SinkDelegate* sink); - SinkDelegate* tlsDelegate(); SinkDelegate* sink_ ABSL_GUARDED_BY(sink_mutex_){nullptr}; absl::Mutex sink_mutex_; diff --git a/source/common/common/matchers.cc b/source/common/common/matchers.cc index 40072b5445969..46176591d0c96 100644 --- a/source/common/common/matchers.cc +++ b/source/common/common/matchers.cc @@ -44,20 +44,20 @@ ValueMatcher::create(const envoy::type::matcher::v3::ValueMatcher& v, PANIC("unexpected"); } -bool NullMatcher::match(const ProtobufWkt::Value& value) const { - return value.kind_case() == ProtobufWkt::Value::kNullValue; +bool NullMatcher::match(const Protobuf::Value& value) const { + return value.kind_case() == Protobuf::Value::kNullValue; } -bool BoolMatcher::match(const ProtobufWkt::Value& value) const { - return value.kind_case() == ProtobufWkt::Value::kBoolValue && matcher_ == value.bool_value(); +bool BoolMatcher::match(const Protobuf::Value& value) const { + return value.kind_case() == Protobuf::Value::kBoolValue && matcher_ == value.bool_value(); } -bool PresentMatcher::match(const ProtobufWkt::Value& value) const { - return matcher_ && value.kind_case() != ProtobufWkt::Value::KIND_NOT_SET; +bool PresentMatcher::match(const Protobuf::Value& value) const { + return matcher_ && value.kind_case() != Protobuf::Value::KIND_NOT_SET; } -bool DoubleMatcher::match(const ProtobufWkt::Value& value) const { - if (value.kind_case() != ProtobufWkt::Value::kNumberValue) { +bool DoubleMatcher::match(const Protobuf::Value& value) const { + if (value.kind_case() != Protobuf::Value::kNumberValue) { return false; } @@ -81,8 +81,8 @@ ListMatcher::ListMatcher(const envoy::type::matcher::v3::ListMatcher& matcher, oneof_value_matcher_ = ValueMatcher::create(matcher.one_of(), context); } -bool ListMatcher::match(const ProtobufWkt::Value& value) const { - if (value.kind_case() != ProtobufWkt::Value::kListValue) { +bool ListMatcher::match(const Protobuf::Value& value) const { + if (value.kind_case() != Protobuf::Value::kListValue) { return false; } @@ -104,7 +104,7 @@ OrMatcher::OrMatcher(const envoy::type::matcher::v3::OrMatcher& matcher, } } -bool OrMatcher::match(const ProtobufWkt::Value& value) const { +bool OrMatcher::match(const Protobuf::Value& value) const { for (const auto& or_matcher : or_matchers_) { if (or_matcher->match(value)) { return true; diff --git a/source/common/common/matchers.h b/source/common/common/matchers.h index 0fdb15f471c86..651734361fd70 100644 --- a/source/common/common/matchers.h +++ b/source/common/common/matchers.h @@ -36,7 +36,7 @@ class ValueMatcher { /** * Check whether the value is matched to the matcher. */ - virtual bool match(const ProtobufWkt::Value& value) const PURE; + virtual bool match(const Protobuf::Value& value) const PURE; /** * Create the matcher object. @@ -50,14 +50,14 @@ class NullMatcher : public ValueMatcher { /** * Check whether the value is NULL. */ - bool match(const ProtobufWkt::Value& value) const override; + bool match(const Protobuf::Value& value) const override; }; class BoolMatcher : public ValueMatcher { public: BoolMatcher(bool matcher) : matcher_(matcher) {} - bool match(const ProtobufWkt::Value& value) const override; + bool match(const Protobuf::Value& value) const override; private: const bool matcher_; @@ -67,7 +67,7 @@ class PresentMatcher : public ValueMatcher { public: PresentMatcher(bool matcher) : matcher_(matcher) {} - bool match(const ProtobufWkt::Value& value) const override; + bool match(const Protobuf::Value& value) const override; private: const bool matcher_; @@ -77,7 +77,7 @@ class DoubleMatcher : public ValueMatcher { public: DoubleMatcher(const envoy::type::matcher::v3::DoubleMatcher& matcher) : matcher_(matcher) {} - bool match(const ProtobufWkt::Value& value) const override; + bool match(const Protobuf::Value& value) const override; private: const envoy::type::matcher::v3::DoubleMatcher matcher_; @@ -249,8 +249,8 @@ class StringMatcherImpl : public ValueMatcher, public StringMatcher { } // ValueMatcher - bool match(const ProtobufWkt::Value& value) const override { - if (value.kind_case() != ProtobufWkt::Value::kStringValue) { + bool match(const Protobuf::Value& value) const override { + if (value.kind_case() != Protobuf::Value::kStringValue) { return false; } @@ -347,7 +347,7 @@ class ListMatcher : public ValueMatcher { ListMatcher(const envoy::type::matcher::v3::ListMatcher& matcher, Server::Configuration::CommonFactoryContext& context); - bool match(const ProtobufWkt::Value& value) const override; + bool match(const Protobuf::Value& value) const override; private: ValueMatcherConstSharedPtr oneof_value_matcher_; @@ -358,7 +358,7 @@ class OrMatcher : public ValueMatcher { OrMatcher(const envoy::type::matcher::v3::OrMatcher& matcher, Server::Configuration::CommonFactoryContext& context); - bool match(const ProtobufWkt::Value& value) const override; + bool match(const Protobuf::Value& value) const override; private: std::vector or_matchers_; diff --git a/source/common/common/notification.cc b/source/common/common/notification.cc new file mode 100644 index 0000000000000..da1749065acc7 --- /dev/null +++ b/source/common/common/notification.cc @@ -0,0 +1,60 @@ +#include "source/common/common/notification.h" + +#include "source/common/common/assert.h" + +namespace Envoy { +namespace Notification { + +// This class implements the logic for triggering ENVOY_NOTIFICATION logs and actions. +class EnvoyNotificationRegistrationImpl : public Assert::ActionRegistration { +public: + EnvoyNotificationRegistrationImpl(std::function action) + : action_(action) { + next_action_ = envoy_notification_record_action_; + envoy_notification_record_action_ = this; + } + + ~EnvoyNotificationRegistrationImpl() override { + ASSERT(envoy_notification_record_action_ == this); + envoy_notification_record_action_ = next_action_; + } + + void invoke(absl::string_view name) { + action_(name); + if (next_action_) { + next_action_->invoke(name); + } + } + + static void invokeAction(absl::string_view name) { + if (envoy_notification_record_action_ != nullptr) { + envoy_notification_record_action_->invoke(name); + } + } + +private: + std::function action_; + EnvoyNotificationRegistrationImpl* next_action_ = nullptr; + + // Pointer to the first action in the chain or nullptr if no action is currently registered. + static EnvoyNotificationRegistrationImpl* envoy_notification_record_action_; +}; + +EnvoyNotificationRegistrationImpl* + EnvoyNotificationRegistrationImpl::envoy_notification_record_action_ = nullptr; + +Assert::ActionRegistrationPtr +addEnvoyNotificationRecordAction(const std::function& action) { + return std::make_unique(action); +} + +namespace details { + +void invokeEnvoyNotification(absl::string_view name) { + EnvoyNotificationRegistrationImpl::invokeAction(name); +} + +} // namespace details + +} // namespace Notification +} // namespace Envoy diff --git a/source/common/common/notification.h b/source/common/common/notification.h new file mode 100644 index 0000000000000..18df95c2f739f --- /dev/null +++ b/source/common/common/notification.h @@ -0,0 +1,57 @@ +#pragma once + +#include "source/common/common/assert.h" +#include "source/common/common/logger.h" + +namespace Envoy { +namespace Notification { + +/** + * Sets an action to be invoked when an ENVOY_NOTIFICATION is encountered. + * + * This function is not thread-safe; concurrent calls to set the action are not allowed. + * + * The action may be invoked concurrently if two ENVOY_NOTIFICATION in different threads run at the + * same time, so the action must be thread-safe. + * + * The action will be invoked in all build types (debug or release). + * + * @param action The action to take when an envoy bug fails. + * @return A registration object. The registration is removed when the object is destructed. + */ +Assert::ActionRegistrationPtr +addEnvoyNotificationRecordAction(const std::function& action); + +namespace details { +/** + * Invokes the action set by addEnvoyNotificationRecordAction, or does nothing if + * no action has been set. + * + * @param location Unique identifier for the ENVOY_NOTIFICATION. + * + * This should only be called by ENVOY_NOTIFICATION macros in this file. + */ +void invokeEnvoyNotification(absl::string_view name); +} // namespace details + +#define _ENVOY_NOTIFICATION_IMPL(NAME, DETAILS) \ + do { \ + const std::string& details = (DETAILS); \ + ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::notification), debug, \ + "envoy notification: {}.{}{}", NAME, \ + details.empty() ? "" : " Details: ", details); \ + Envoy::Notification::details::invokeEnvoyNotification((NAME)); \ + } while (false) + +/** + * Invoke a notification of a specific condition. In contrast to ENVOY_BUG it does not ASSERT in + * debug builds and as such has no impact on continuous integration or system tests. If a condition + * is met it is logged at the debug verbosity and a stat is incremented. There is no exponential + * backoff, so the notification will be invoked every time the condition is met. As such + * notification handler must have low overhead if the condition is expected to be encountered + * frequently. ENVOY_NOTIFICATION must be called with three arguments for verbose logging. + */ +#define ENVOY_NOTIFICATION(...) PASS_ON(PASS_ON(_ENVOY_NOTIFICATION_IMPL)(__VA_ARGS__)) + +} // namespace Notification +} // namespace Envoy diff --git a/source/common/common/radix_tree.h b/source/common/common/radix_tree.h new file mode 100644 index 0000000000000..a7655f8503d29 --- /dev/null +++ b/source/common/common/radix_tree.h @@ -0,0 +1,321 @@ +#pragma once + +#include +#include + +#include "envoy/common/optref.h" + +#include "source/common/common/assert.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/container/inlined_vector.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" + +namespace Envoy { + +/** + * A radix tree implementation for efficient prefix-based lookups. + * + * Template parameter Value must be default-constructible and moveable. + */ +template class RadixTree { +public: + /** + * Adds an entry to the RadixTree at the given key. + * @param key the key used to add the entry. + * @param value the value to be associated with the key. + * @param overwrite_existing will overwrite the value when the value for a given key already + * exists. + * @return false when a value already exists for the given key and overwrite_existing is false. + */ + bool add(absl::string_view key, Value value, bool overwrite_existing = true) { + // Check if the key already exists. + Value existing; + bool found = root_.findRecursive(key, existing); + + // If a value exists and we shouldn't overwrite, return false. + if (found && !overwrite_existing) { + return false; + } + + root_.insert(key, std::move(value)); + return true; + } + + /** + * Finds the entry associated with the key. + * @param key the key used to find. + * @return the Value associated with the key, or an empty-initialized Value + * if there is no matching key. + */ + Value find(absl::string_view key) const { + Value result; + if (root_.findRecursive(key, result)) { + return result; + } + return Value{}; + } + + /** + * Returns the set of entries that are prefixes of the specified key, longest last. + * Complexity is O(min(longest key prefix, key length)). + * @param key the key used to find. + * @return a vector of values whose keys are a prefix of the specified key, longest last. + */ + absl::InlinedVector findMatchingPrefixes(absl::string_view key) const { + absl::InlinedVector result; + absl::string_view search = key; + const RadixTreeNode* node = &root_; + + // Special case: if searching for empty string, check root node. + if (search.empty()) { + if (hasValue(*node)) { + result.push_back(node->value_); + } + return result; + } + + while (true) { + // Check if current node has a value (is a leaf) and we've consumed some prefix. + if (hasValue(*node)) { + result.push_back(node->value_); + } + + // Check for key exhaustion. + if (search.empty()) { + break; + } + + // Look for an edge. + uint8_t first_char = static_cast(search[0]); + auto child = node->getChild(first_char); + if (!child) { + break; + } + + const RadixTreeNode& child_node = *child; + node = &child_node; + + // Consume the search prefix. + if (search.size() < child->prefix_.size() || + search.substr(0, child->prefix_.size()) != child->prefix_) { + break; + } + // Consume the search prefix. + search = search.substr(child->prefix_.size()); + } + + return result; + } + + /** + * Finds the entry with the longest key that is a prefix of the specified key. + * Complexity is O(min(longest key prefix, key length)). + * @param key the key used to find. + * @return a value whose key is a prefix of the specified key. If there are + * multiple such values, the one with the longest key. If there are + * no keys that are a prefix of the input key, an empty-initialized Value. + */ + Value findLongestPrefix(absl::string_view key) const { + absl::string_view search = key; + const RadixTreeNode* node = &root_; + const RadixTreeNode* last_node_with_value = nullptr; + + while (true) { + // Look for a leaf node. + if (hasValue(*node)) { + last_node_with_value = node; + } + + // Check for key exhaustion. + if (search.empty()) { + break; + } + + // Look for an edge. + uint8_t first_char = static_cast(search[0]); + auto child = node->getChild(first_char); + if (!child) { + break; + } + + const RadixTreeNode& child_node = *child; + node = &child_node; + + // Consume the search prefix. + if (search.size() < child->prefix_.size() || + search.substr(0, child->prefix_.size()) != child->prefix_) { + break; + } + // Consume the search prefix. + search = search.substr(child->prefix_.size()); + } + + // Return the value from the last node that had a value, or empty value if none found. + if (last_node_with_value != nullptr) { + return last_node_with_value->value_; + } + return Value{}; + } + +private: + static constexpr int32_t NoNode = -1; + + /** + * Internal node structure for the radix tree. + */ + struct RadixTreeNode { + std::string prefix_; + Value value_{}; + + // Hash map for O(1) child lookup by first character. + absl::flat_hash_map children_; + + /** + * Insert a key-value pair into this node. + * @param search the remaining search key. + * @param value the value to insert. + */ + void insert(absl::string_view search, Value value) { + // Handle key exhaustion. + if (search.empty()) { + value_ = std::move(value); + return; + } + + // Look for the edge. + uint8_t first_char = static_cast(search[0]); + auto child_it = children_.find(first_char); + + // No edge, create one. + if (child_it == children_.end()) { + // Create a new child node. + RadixTreeNode new_child; + new_child.prefix_ = std::string(search); + new_child.value_ = std::move(value); + + // Add the child to the current node. + children_[first_char] = std::move(new_child); + return; + } + + // Get the child node. + RadixTreeNode& child = child_it->second; + + // Determine longest prefix length of the search key on match. + size_t cpl = commonPrefixLength(search, child.prefix_); + if (cpl == child.prefix_.size()) { + // The search key is longer than the child prefix, continue down. + absl::string_view remaining_search = search.substr(cpl); + child.insert(remaining_search, std::move(value)); + return; + } + + // Split the node. We create a new intermediate node. + RadixTreeNode split_node; + split_node.prefix_ = std::string(search.substr(0, cpl)); + + // Update the child's prefix. + child.prefix_ = std::string(child.prefix_.substr(cpl)); + + // If the search key is exactly the common prefix, set the value on the split node. + if (cpl == search.size()) { + split_node.value_ = std::move(value); + } else { + // Create a new leaf for the current key. + RadixTreeNode new_leaf; + new_leaf.prefix_ = std::string(search.substr(cpl)); + new_leaf.value_ = std::move(value); + split_node.children_[static_cast(new_leaf.prefix_[0])] = std::move(new_leaf); + } + + // Add the child to the split node. + split_node.children_[static_cast(child.prefix_[0])] = std::move(child); + + // Replace the original child with the split node. + children_[first_char] = std::move(split_node); + } + + /** + * Recursive helper for find operation. + * @param search the remaining search key. + * @param result the value to return if found. + * @return true if the key was found, false otherwise. + */ + bool findRecursive(absl::string_view search, Value& result) const { + if (search.empty()) { + if (hasValue(*this)) { + result = value_; + return true; + } + return false; + } + + uint8_t first_char = static_cast(search[0]); + auto child_it = children_.find(first_char); + if (child_it == children_.end()) { + return false; + } + + const RadixTreeNode& child = child_it->second; + + // Check if the child's prefix matches the search. + if (search.size() >= child.prefix_.size() && + search.substr(0, child.prefix_.size()) == child.prefix_) { + absl::string_view new_search = search.substr(child.prefix_.size()); + return child.findRecursive(new_search, result); + } + + return false; + } + + /** + * Get a child node by character key. + * @param char_key the character to look up. + * @return optional reference to the child node. + */ + Envoy::OptRef getChild(uint8_t char_key) const { + auto it = children_.find(char_key); + if (it != children_.end()) { + return {it->second}; + } + return {}; + } + }; + + /** + * Find the longest common prefix between two strings. + * @param a first string. + * @param b second string. + * @return length of the common prefix. + */ + static size_t commonPrefixLength(absl::string_view a, absl::string_view b) { + size_t len = std::min(a.size(), b.size()); + for (size_t i = 0; i < len; i++) { + if (a[i] != b[i]) { + return i; + } + } + return len; + } + + /** + * Check if a node has a value (is a leaf node). + * @param node the node to check. + * @return true if the node has a value. + */ + static bool hasValue(const RadixTreeNode& node) { + // For pointer types, check if the pointer is not null. + if constexpr (std::is_pointer_v) { + return node.value_ != nullptr; + } else { + return static_cast(node.value_); + } + } + + // Root node of the radix tree. + RadixTreeNode root_; +}; + +} // namespace Envoy diff --git a/source/common/common/thread.cc b/source/common/common/thread.cc index cf661693acdf0..a22c1754439f8 100644 --- a/source/common/common/thread.cc +++ b/source/common/common/thread.cc @@ -21,13 +21,13 @@ namespace { // tests more hermetic. struct ThreadIds { bool inMainThread() const { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return main_threads_to_usage_count_.find(std::this_thread::get_id()) != main_threads_to_usage_count_.end(); } bool isMainThreadActive() const { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return !main_threads_to_usage_count_.empty(); } @@ -36,7 +36,7 @@ struct ThreadIds { // Call this from the context of MainThread when it exits. void releaseMainThread() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); auto it = main_threads_to_usage_count_.find(std::this_thread::get_id()); if (!skipAsserts()) { ASSERT(it != main_threads_to_usage_count_.end()); @@ -52,7 +52,7 @@ struct ThreadIds { // Declares current thread as the main one, or verifies that the current // thread matches any previous declarations. void registerMainThread() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); auto it = main_threads_to_usage_count_.find(std::this_thread::get_id()); if (it == main_threads_to_usage_count_.end()) { it = main_threads_to_usage_count_.insert({std::this_thread::get_id(), 0}).first; diff --git a/source/common/common/thread_synchronizer.cc b/source/common/common/thread_synchronizer.cc index 492509ab1fb1f..7e230e6a5d65a 100644 --- a/source/common/common/thread_synchronizer.cc +++ b/source/common/common/thread_synchronizer.cc @@ -10,7 +10,7 @@ void ThreadSynchronizer::enable() { ThreadSynchronizer::SynchronizerEntry& ThreadSynchronizer::getOrCreateEntry(absl::string_view event_name) { - absl::MutexLock lock(&data_->mutex_); + absl::MutexLock lock(data_->mutex_); auto& existing_entry = data_->entries_[event_name]; if (existing_entry == nullptr) { ENVOY_LOG(debug, "thread synchronizer: creating entry: {}", event_name); @@ -21,7 +21,7 @@ ThreadSynchronizer::getOrCreateEntry(absl::string_view event_name) { void ThreadSynchronizer::waitOnWorker(absl::string_view event_name) { SynchronizerEntry& entry = getOrCreateEntry(event_name); - absl::MutexLock lock(&entry.mutex_); + absl::MutexLock lock(entry.mutex_); ENVOY_LOG(debug, "thread synchronizer: waiting on next {}", event_name); ASSERT(!entry.wait_on_); entry.wait_on_ = true; @@ -29,7 +29,7 @@ void ThreadSynchronizer::waitOnWorker(absl::string_view event_name) { void ThreadSynchronizer::syncPointWorker(absl::string_view event_name) { SynchronizerEntry& entry = getOrCreateEntry(event_name); - absl::MutexLock lock(&entry.mutex_); + absl::MutexLock lock(entry.mutex_); // See if we are ignoring waits. If so, just return. if (!entry.wait_on_) { @@ -62,7 +62,7 @@ void ThreadSynchronizer::syncPointWorker(absl::string_view event_name) { void ThreadSynchronizer::barrierOnWorker(absl::string_view event_name) { SynchronizerEntry& entry = getOrCreateEntry(event_name); - absl::MutexLock lock(&entry.mutex_); + absl::MutexLock lock(entry.mutex_); ENVOY_LOG(debug, "thread synchronizer: barrier on {}", event_name); while (!entry.mutex_.AwaitWithTimeout(absl::Condition(&entry.at_barrier_), absl::Seconds(10))) { ENVOY_LOG(warn, "thread synchronizer: barrier on {} stuck for 10 seconds", event_name); @@ -72,7 +72,7 @@ void ThreadSynchronizer::barrierOnWorker(absl::string_view event_name) { void ThreadSynchronizer::signalWorker(absl::string_view event_name) { SynchronizerEntry& entry = getOrCreateEntry(event_name); - absl::MutexLock lock(&entry.mutex_); + absl::MutexLock lock(entry.mutex_); ASSERT(!entry.signaled_); ENVOY_LOG(debug, "thread synchronizer: signaling {}", event_name); entry.signaled_ = true; diff --git a/source/common/common/trie_lookup_table.h b/source/common/common/trie_lookup_table.h deleted file mode 100644 index 5613ac0c56140..0000000000000 --- a/source/common/common/trie_lookup_table.h +++ /dev/null @@ -1,199 +0,0 @@ -#pragma once - -#include - -#include "source/common/common/assert.h" - -#include "absl/strings/string_view.h" - -namespace Envoy { - -/** - * A trie used for faster lookup with lookup time at most equal to the size of the key. - * - * Type of Value must be empty-constructible and moveable, e.g. smart pointers and POD types. - */ -template class TrieLookupTable { - static constexpr int32_t NoNode = -1; - // A TrieNode aims to be a good balance of performant and - // space-efficient, by allocating a vector the size of the range of children - // the node contains. This should be good for most use-cases. - // - // For example, a node with children 'a' and 'z' will contain a vector of - // size 26, containing two values and 24 nulls. A node with only one - // child will contain a vector of size 1. A node with no children will - // contain an empty vector. - // - // Compared to allocating 256 entries for every node, this makes insertions - // a little bit inefficient (especially insertions in reverse order), but - // trie lookups remain O(length-of-longest-matching-prefix) with just a - // couple of very cheap operations extra per step. - // - // By size, having 256 entries for every node makes each node's overhead - // (excluding values) consume 8KB; even a trie containing only a single - // prefix "foobar" consumes 56KB. - // Using ranged vectors like this makes a single prefix "foobar" consume - // less than 20 bytes per node, for a total of less than 0.14KB. - // - // Using indices instead of pointers helps keep the bulk of the data - // localized, and prevents recursive deletion which can provoke a stack - // overflow. - struct TrieNode { - Value value_{}; - // Vector of indices into nodes_, where [0] maps to min_child_key_. - // NoNode will be in any index where there is not a child. - std::vector children_; - uint8_t min_child_key_{0}; - }; - - /** - * Get the index of the node that the branch whose key is `char_key` from the - * node indexed by `current` leads to. - * @param current the index of the node to follow a branch from. - * @param char_key the one-byte key of the branch to be followed. - */ - int32_t getChildIndex(int32_t current, uint8_t char_key) const { - ASSERT(current >= 0 && static_cast(current) < nodes_.size()); - const TrieNode& node = nodes_[current]; - if (node.min_child_key_ > char_key || node.min_child_key_ + node.children_.size() <= char_key) { - return NoNode; - } - return node.children_[char_key - node.min_child_key_]; - } - int32_t getChildIndex(int32_t current, uint8_t char_key) { - return std::as_const(*this).getChildIndex(current, char_key); - } - - /** - * Make the branch whose key is `char_key`, of the node indexed by `current` - * point to node indexed by `child_index`. - * @param current the index of the node whose child is to be updated. - * @param char_key the one-byte key of the branch to be updated. - * @param child_index the index of the node the branch will lead to. - */ - void setChildIndex(int32_t current, uint8_t char_key, int32_t child_index) { - ASSERT(current >= 0 && static_cast(current) < nodes_.size()); - ASSERT(child_index >= 0 && static_cast(child_index) < nodes_.size()); - TrieNode& node = nodes_[current]; - if (node.children_.empty()) { - node.children_.reserve(1); - node.children_.push_back(child_index); - node.min_child_key_ = char_key; - return; - } - if (char_key < node.min_child_key_) { - std::vector new_children; - new_children.reserve(node.min_child_key_ - char_key + node.children_.size()); - new_children.resize(node.min_child_key_ - char_key, NoNode); - std::move(node.children_.begin(), node.children_.end(), std::back_inserter(new_children)); - new_children[0] = child_index; - node.min_child_key_ = char_key; - node.children_ = std::move(new_children); - return; - } - if (char_key >= (node.min_child_key_ + node.children_.size())) { - // Expand the vector forwards. - node.children_.resize(char_key - node.min_child_key_ + 1, NoNode); - // Fall through to "insert" behavior. - } - node.children_[char_key - node.min_child_key_] = child_index; - } - -public: - /** - * Adds an entry to the Trie at the given Key. - * @param key the key used to add the entry. - * @param value the value to be associated with the key. - * @param overwrite_existing will overwrite the value when the value for a given key already - * exists. - * @return false when a value already exists for the given key. - */ - bool add(absl::string_view key, Value value, bool overwrite_existing = true) { - int32_t current = 0; - for (uint8_t c : key) { - int32_t next = getChildIndex(current, c); - if (next == NoNode) { - next = nodes_.size(); - nodes_.emplace_back(); - setChildIndex(current, c, next); - } - current = next; - } - if (nodes_[current].value_ && !overwrite_existing) { - return false; - } - nodes_[current].value_ = std::move(value); - return true; - } - - /** - * Finds the entry associated with the key. - * @param key the key used to find. - * @return the Value associated with the key, or an empty-initialized Value - * if there is no matching key. - */ - Value find(absl::string_view key) const { - int32_t current = 0; - for (uint8_t c : key) { - current = getChildIndex(current, c); - if (current == NoNode) { - return {}; - } - } - return nodes_[current].value_; - } - - /** - * Returns the set of entries that are prefixes of the specified key, longest last. - * Complexity is O(min(longest key prefix, key length)). - * @param key the key used to find. - * @return a vector of values whose keys are a prefix of the specified key, longest last. - */ - absl::InlinedVector findMatchingPrefixes(absl::string_view key) const { - absl::InlinedVector result; - int32_t current = 0; - - for (uint8_t c : key) { - current = getChildIndex(current, c); - - if (current == NoNode) { - return result; - } else if (nodes_[current].value_) { - result.push_back(nodes_[current].value_); - } - } - return result; - } - - /** - * Finds the entry with the longest key that is a prefix of the specified key. - * Complexity is O(min(longest key prefix, key length)). - * @param key the key used to find. - * @return a value whose key is a prefix of the specified key. If there are - * multiple such values, the one with the longest key. If there are - * no keys that are a prefix of the input key, an empty-initialized Value. - */ - Value findLongestPrefix(absl::string_view key) const { - int32_t current = 0; - int32_t result = 0; - - for (uint8_t c : key) { - current = getChildIndex(current, c); - - if (current == NoNode) { - return nodes_[result].value_; - } else if (nodes_[current].value_) { - result = current; - } - } - return nodes_[result].value_; - } - -private: - // Flat representation of the tree - each node has a vector of indices to its - // child nodes. - // Initialized with a single empty node as the root node. - std::vector nodes_ = {TrieNode()}; -}; - -} // namespace Envoy diff --git a/source/common/config/context_provider_impl.h b/source/common/config/context_provider_impl.h index 46156a73ed31e..69200d406ec92 100644 --- a/source/common/config/context_provider_impl.h +++ b/source/common/config/context_provider_impl.h @@ -52,7 +52,7 @@ class ContextProviderImpl : public ContextProvider { const xds::core::v3::ContextParams node_context_; // Map from resource type URL to dynamic context parameters. absl::flat_hash_map dynamic_context_; - mutable Common::CallbackManager update_cb_helper_; + mutable Common::CallbackManager update_cb_helper_; }; } // namespace Config diff --git a/source/common/config/decoded_resource_impl.h b/source/common/config/decoded_resource_impl.h index 384eaca379ad3..fd58b708d5b5c 100644 --- a/source/common/config/decoded_resource_impl.h +++ b/source/common/config/decoded_resource_impl.h @@ -28,7 +28,7 @@ using DecodedResourceImplPtr = std::unique_ptr; class DecodedResourceImpl : public DecodedResource { public: static absl::StatusOr - fromResource(OpaqueResourceDecoder& resource_decoder, const ProtobufWkt::Any& resource, + fromResource(OpaqueResourceDecoder& resource_decoder, const Protobuf::Any& resource, const std::string& version) { if (resource.Is()) { envoy::service::discovery::v3::Resource r; @@ -83,8 +83,8 @@ class DecodedResourceImpl : public DecodedResource { private: DecodedResourceImpl(OpaqueResourceDecoder& resource_decoder, absl::optional name, const Protobuf::RepeatedPtrField& aliases, - const ProtobufWkt::Any& resource, bool has_resource, - const std::string& version, absl::optional ttl, + const Protobuf::Any& resource, bool has_resource, const std::string& version, + absl::optional ttl, const absl::optional& metadata) : resource_(resource_decoder.decodeResource(resource)), has_resource_(has_resource), name_(name ? *name : resource_decoder.resourceName(*resource_)), @@ -108,8 +108,7 @@ struct DecodedResourcesWrapper { DecodedResourcesWrapper() = default; static absl::StatusOr> create(OpaqueResourceDecoder& resource_decoder, - const Protobuf::RepeatedPtrField& resources, - const std::string& version) { + const Protobuf::RepeatedPtrField& resources, const std::string& version) { std::unique_ptr ret = std::make_unique(); for (const auto& resource : resources) { absl::StatusOr resource_or_error = diff --git a/source/common/config/metadata.cc b/source/common/config/metadata.cc index e58d3f2f9847f..3c1621bb3780f 100644 --- a/source/common/config/metadata.cc +++ b/source/common/config/metadata.cc @@ -17,31 +17,31 @@ MetadataKey::MetadataKey(const envoy::type::metadata::v3::MetadataKey& metadata_ } } -const ProtobufWkt::Value& Metadata::metadataValue(const envoy::config::core::v3::Metadata* metadata, - const MetadataKey& metadata_key) { +const Protobuf::Value& Metadata::metadataValue(const envoy::config::core::v3::Metadata* metadata, + const MetadataKey& metadata_key) { return metadataValue(metadata, metadata_key.key_, metadata_key.path_); } -const ProtobufWkt::Value& Metadata::metadataValue(const envoy::config::core::v3::Metadata* metadata, - const std::string& filter, - const std::vector& path) { +const Protobuf::Value& Metadata::metadataValue(const envoy::config::core::v3::Metadata* metadata, + const std::string& filter, + const std::vector& path) { if (!metadata) { - return ProtobufWkt::Value::default_instance(); + return Protobuf::Value::default_instance(); } const auto filter_it = metadata->filter_metadata().find(filter); if (filter_it == metadata->filter_metadata().end()) { - return ProtobufWkt::Value::default_instance(); + return Protobuf::Value::default_instance(); } - const ProtobufWkt::Struct* data_struct = &(filter_it->second); - const ProtobufWkt::Value* val = nullptr; + const Protobuf::Struct* data_struct = &(filter_it->second); + const Protobuf::Value* val = nullptr; // go through path to select sub entries for (const auto& p : path) { if (nullptr == data_struct) { // sub entry not found - return ProtobufWkt::Value::default_instance(); + return Protobuf::Value::default_instance(); } const auto entry_it = data_struct->fields().find(p); if (entry_it == data_struct->fields().end()) { - return ProtobufWkt::Value::default_instance(); + return Protobuf::Value::default_instance(); } val = &(entry_it->second); if (val->has_struct_value()) { @@ -51,21 +51,19 @@ const ProtobufWkt::Value& Metadata::metadataValue(const envoy::config::core::v3: } } if (nullptr == val) { - return ProtobufWkt::Value::default_instance(); + return Protobuf::Value::default_instance(); } return *val; } -const ProtobufWkt::Value& Metadata::metadataValue(const envoy::config::core::v3::Metadata* metadata, - const std::string& filter, - const std::string& key) { +const Protobuf::Value& Metadata::metadataValue(const envoy::config::core::v3::Metadata* metadata, + const std::string& filter, const std::string& key) { const std::vector path{key}; return metadataValue(metadata, filter, path); } -ProtobufWkt::Value& Metadata::mutableMetadataValue(envoy::config::core::v3::Metadata& metadata, - const std::string& filter, - const std::string& key) { +Protobuf::Value& Metadata::mutableMetadataValue(envoy::config::core::v3::Metadata& metadata, + const std::string& filter, const std::string& key) { return (*(*metadata.mutable_filter_metadata())[filter].mutable_fields())[key]; } @@ -79,7 +77,7 @@ bool Metadata::metadataLabelMatch(const LabelSet& label_set, if (filter_it == host_metadata->filter_metadata().end()) { return label_set.empty(); } - const ProtobufWkt::Struct& data_struct = filter_it->second; + const Protobuf::Struct& data_struct = filter_it->second; const auto& fields = data_struct.fields(); for (const auto& kv : label_set) { const auto entry_it = fields.find(kv.first); @@ -87,7 +85,7 @@ bool Metadata::metadataLabelMatch(const LabelSet& label_set, return false; } - if (list_as_any && entry_it->second.kind_case() == ProtobufWkt::Value::kListValue) { + if (list_as_any && entry_it->second.kind_case() == Protobuf::Value::kListValue) { bool any_match = false; for (const auto& v : entry_it->second.list_value().values()) { if (ValueUtil::equal(v, kv.second)) { diff --git a/source/common/config/metadata.h b/source/common/config/metadata.h index ffa24cb19711c..2b4c174348e24 100644 --- a/source/common/config/metadata.h +++ b/source/common/config/metadata.h @@ -42,42 +42,41 @@ class Metadata { * @param metadata reference. * @param filter name. * @param key for filter metadata. - * @return const ProtobufWkt::Value& value if found, empty if not found. + * @return const Protobuf::Value& value if found, empty if not found. */ - static const ProtobufWkt::Value& metadataValue(const envoy::config::core::v3::Metadata* metadata, - const std::string& filter, const std::string& key); + static const Protobuf::Value& metadataValue(const envoy::config::core::v3::Metadata* metadata, + const std::string& filter, const std::string& key); /** * Lookup value by a multi-key path for a given filter in Metadata. If path is empty * will return the empty struct. * @param metadata reference. * @param filter name. * @param path multi-key path. - * @return const ProtobufWkt::Value& value if found, empty if not found. + * @return const Protobuf::Value& value if found, empty if not found. */ - static const ProtobufWkt::Value& metadataValue(const envoy::config::core::v3::Metadata* metadata, - const std::string& filter, - const std::vector& path); + static const Protobuf::Value& metadataValue(const envoy::config::core::v3::Metadata* metadata, + const std::string& filter, + const std::vector& path); /** * Lookup the value by a metadata key from a Metadata. * @param metadata reference. * @param metadata_key with key name and path to retrieve the value. - * @return const ProtobufWkt::Value& value if found, empty if not found. + * @return const Protobuf::Value& value if found, empty if not found. */ - static const ProtobufWkt::Value& metadataValue(const envoy::config::core::v3::Metadata* metadata, - const MetadataKey& metadata_key); + static const Protobuf::Value& metadataValue(const envoy::config::core::v3::Metadata* metadata, + const MetadataKey& metadata_key); /** * Obtain mutable reference to metadata value for a given filter and key. * @param metadata reference. * @param filter name. * @param key for filter metadata. - * @return ProtobufWkt::Value&. A Value message is created if not found. + * @return Protobuf::Value&. A Value message is created if not found. */ - static ProtobufWkt::Value& mutableMetadataValue(envoy::config::core::v3::Metadata& metadata, - const std::string& filter, - const std::string& key); + static Protobuf::Value& mutableMetadataValue(envoy::config::core::v3::Metadata& metadata, + const std::string& filter, const std::string& key); - using LabelSet = std::vector>; + using LabelSet = std::vector>; /** * Returns whether a set of the labels match a particular host's metadata. diff --git a/source/common/config/null_grpc_mux_impl.h b/source/common/config/null_grpc_mux_impl.h index 924e037799f62..a90a4b49e0f39 100644 --- a/source/common/config/null_grpc_mux_impl.h +++ b/source/common/config/null_grpc_mux_impl.h @@ -27,8 +27,8 @@ class NullGrpcMuxImpl : public GrpcMux, ENVOY_BUG(false, "unexpected request for on demand update"); } - absl::Status updateMuxSource(Grpc::RawAsyncClientPtr&&, Grpc::RawAsyncClientPtr&&, Stats::Scope&, - BackOffStrategyPtr&&, + absl::Status updateMuxSource(Grpc::RawAsyncClientSharedPtr&&, Grpc::RawAsyncClientSharedPtr&&, + Stats::Scope&, BackOffStrategyPtr&&, const envoy::config::core::v3::ApiConfigSource&) override { return absl::UnimplementedError(""); } diff --git a/source/common/config/opaque_resource_decoder_impl.h b/source/common/config/opaque_resource_decoder_impl.h index bb0af603f15d1..99677c14dd137 100644 --- a/source/common/config/opaque_resource_decoder_impl.h +++ b/source/common/config/opaque_resource_decoder_impl.h @@ -14,7 +14,7 @@ template class OpaqueResourceDecoderImpl : public Config::Opa : validation_visitor_(validation_visitor), name_field_(name_field) {} // Config::OpaqueResourceDecoder - ProtobufTypes::MessagePtr decodeResource(const ProtobufWkt::Any& resource) override { + ProtobufTypes::MessagePtr decodeResource(const Protobuf::Any& resource) override { auto typed_message = std::make_unique(); // If the Any is a synthetic empty message (e.g. because the resource field was not set in // Resource, this might be empty, so we shouldn't decode. diff --git a/source/common/config/type_to_endpoint.cc b/source/common/config/type_to_endpoint.cc index 6578336813e85..eb96f803cbd90 100644 --- a/source/common/config/type_to_endpoint.cc +++ b/source/common/config/type_to_endpoint.cc @@ -50,7 +50,6 @@ TypeUrlToV3ServiceMap* buildTypeUrlToServiceMap() { for (absl::string_view name : { "envoy.service.route.v3.RouteDiscoveryService", "envoy.service.route.v3.ScopedRoutesDiscoveryService", - "envoy.service.route.v3.ScopedRoutesDiscoveryService", "envoy.service.route.v3.VirtualHostDiscoveryService", "envoy.service.secret.v3.SecretDiscoveryService", "envoy.service.cluster.v3.ClusterDiscoveryService", diff --git a/source/common/config/utility.cc b/source/common/config/utility.cc index feea51edb4398..9bab06ab810b2 100644 --- a/source/common/config/utility.cc +++ b/source/common/config/utility.cc @@ -7,12 +7,14 @@ #include "envoy/config/core/v3/grpc_service.pb.h" #include "envoy/config/endpoint/v3/endpoint.pb.h" #include "envoy/config/endpoint/v3/endpoint_components.pb.h" +#include "envoy/grpc/async_client_manager.h" #include "envoy/stats/scope.h" #include "source/common/common/assert.h" #include "source/common/protobuf/utility.h" #include "absl/status/status.h" +#include "absl/types/optional.h" namespace Envoy { namespace Config { @@ -240,10 +242,10 @@ bool isApiTypeNonAggregated(const envoy::config::core::v3::ApiConfigSource::ApiT } } // namespace -absl::StatusOr Utility::factoryForGrpcApiConfigSource( - Grpc::AsyncClientManager& async_client_manager, - const envoy::config::core::v3::ApiConfigSource& api_config_source, Stats::Scope& scope, - bool skip_cluster_check, int grpc_service_idx, bool xdstp_config_source) { +absl::StatusOr> +Utility::getGrpcConfigFromApiConfigSource( + const envoy::config::core::v3::ApiConfigSource& api_config_source, int grpc_service_idx, + bool xdstp_config_source) { RETURN_IF_NOT_OK(checkApiConfigSourceNames( api_config_source, Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support") ? 2 : 1)); @@ -264,19 +266,32 @@ absl::StatusOr Utility::factoryForGrpcApiConfigSour if (grpc_service_idx >= api_config_source.grpc_services_size()) { // No returned factory in case there's no entry. - return nullptr; + return absl::nullopt; } - envoy::config::core::v3::GrpcService grpc_service; - grpc_service.MergeFrom(api_config_source.grpc_services(grpc_service_idx)); + return Envoy::makeOptRef(api_config_source.grpc_services(grpc_service_idx)); +} + +absl::StatusOr Utility::factoryForGrpcApiConfigSource( + Grpc::AsyncClientManager& async_client_manager, + const envoy::config::core::v3::ApiConfigSource& api_config_source, Stats::Scope& scope, + bool skip_cluster_check, int grpc_service_idx, bool xdstp_config_source) { - return async_client_manager.factoryForGrpcService(grpc_service, scope, skip_cluster_check); + absl::StatusOr> maybe_grpc_service = + getGrpcConfigFromApiConfigSource(api_config_source, grpc_service_idx, xdstp_config_source); + RETURN_IF_NOT_OK(maybe_grpc_service.status()); + + if (!maybe_grpc_service.value().has_value()) { + return nullptr; + } + return async_client_manager.factoryForGrpcService(*maybe_grpc_service.value(), scope, + skip_cluster_check); } -absl::Status Utility::translateOpaqueConfig(const ProtobufWkt::Any& typed_config, +absl::Status Utility::translateOpaqueConfig(const Protobuf::Any& typed_config, ProtobufMessage::ValidationVisitor& validation_visitor, Protobuf::Message& out_proto) { - static const std::string struct_type(ProtobufWkt::Struct::default_instance().GetTypeName()); + static const std::string struct_type(Protobuf::Struct::default_instance().GetTypeName()); static const std::string typed_struct_type( xds::type::v3::TypedStruct::default_instance().GetTypeName()); static const std::string legacy_typed_struct_type( @@ -322,7 +337,7 @@ absl::Status Utility::translateOpaqueConfig(const ProtobufWkt::Any& typed_config RETURN_IF_NOT_OK(MessageUtil::unpackTo(typed_config, out_proto)); } else { #ifdef ENVOY_ENABLE_YAML - ProtobufWkt::Struct struct_config; + Protobuf::Struct struct_config; RETURN_IF_NOT_OK(MessageUtil::unpackTo(typed_config, struct_config)); MessageUtil::jsonConvert(struct_config, validation_visitor, out_proto); #else diff --git a/source/common/config/utility.h b/source/common/config/utility.h index 05f1f66f82641..469ffd47e2264 100644 --- a/source/common/config/utility.h +++ b/source/common/config/utility.h @@ -298,7 +298,7 @@ class Utility { * Get type URL from a typed config. * @param typed_config for the extension config. */ - static std::string getFactoryType(const ProtobufWkt::Any& typed_config) { + static std::string getFactoryType(const Protobuf::Any& typed_config) { static const std::string typed_struct_type( xds::type::v3::TypedStruct::default_instance().GetTypeName()); static const std::string legacy_typed_struct_type( @@ -324,7 +324,7 @@ class Utility { * Get a Factory from the registry by type URL. * @param typed_config for the extension config. */ - template static Factory* getFactoryByType(const ProtobufWkt::Any& typed_config) { + template static Factory* getFactoryByType(const Protobuf::Any& typed_config) { if (typed_config.type_url().empty()) { return nullptr; } @@ -367,7 +367,7 @@ class Utility { */ template static ProtobufTypes::MessagePtr - translateAnyToFactoryConfig(const ProtobufWkt::Any& typed_config, + translateAnyToFactoryConfig(const Protobuf::Any& typed_config, ProtobufMessage::ValidationVisitor& validation_visitor, Factory& factory) { ProtobufTypes::MessagePtr config = factory.createEmptyConfigProto(); @@ -387,6 +387,22 @@ class Utility { */ static std::string truncateGrpcStatusMessage(absl::string_view error_message); + /** + * Obtain Grpc service config from the api config source. + * @param api_config_source envoy::config::core::v3::ApiConfigSource. Must have config type GRPC. + * @param grpc_service_idx index of the grpc service in the api_config_source. If there's no entry + * in the given index, a nullptr factory will be returned. + * @param xdstp_config_source whether the config source will be used for xdstp config source. + * These sources must be of type AGGREGATED_GRPC or + * AGGREGATED_DELTA_GRPC. + * @return OptRef to either const envoy::config::core::v3::GrpcService or nullptr if there's no + * grpc_service in the given index. + */ + static absl::StatusOr> + getGrpcConfigFromApiConfigSource( + const envoy::config::core::v3::ApiConfigSource& api_config_source, int grpc_service_idx, + bool xdstp_config_source); + /** * Obtain gRPC async client factory from a envoy::config::core::v3::ApiConfigSource. * @param async_client_manager gRPC async client manager. @@ -413,7 +429,7 @@ class Utility { * @param out_proto the proto message instantiated by extensions * @return a status indicating if translation was a success */ - static absl::Status translateOpaqueConfig(const ProtobufWkt::Any& typed_config, + static absl::Status translateOpaqueConfig(const Protobuf::Any& typed_config, ProtobufMessage::ValidationVisitor& validation_visitor, Protobuf::Message& out_proto); diff --git a/source/common/config/well_known_names.cc b/source/common/config/well_known_names.cc index 5481186c989d4..62cc028565916 100644 --- a/source/common/config/well_known_names.cc +++ b/source/common/config/well_known_names.cc @@ -242,7 +242,7 @@ TagNameValues::TagNameValues() { // listener.[
.]ssl.certificate.(). or // cluster.[.]ssl.certificate.(). addRe2(TLS_CERTIFICATE, - R"(^\.ssl\.certificate(\.()\..*)$)", + R"(^\.ssl\.certificate\.(()\.).*$)", ".ssl.certificate"); } diff --git a/source/common/config/xds_context_params.cc b/source/common/config/xds_context_params.cc index fe3cf6137776a..bb09c143eb852 100644 --- a/source/common/config/xds_context_params.cc +++ b/source/common/config/xds_context_params.cc @@ -40,7 +40,7 @@ const NodeContextRenderers& nodeParamCbs() { } void mergeMetadataJson(Protobuf::Map& params, - const ProtobufWkt::Struct& metadata, const std::string& prefix) { + const Protobuf::Struct& metadata, const std::string& prefix) { #ifdef ENVOY_ENABLE_YAML for (const auto& it : metadata.fields()) { absl::StatusOr json_or_error = MessageUtil::getJsonStringFromMessage(it.second); diff --git a/source/common/config/xds_manager_impl.cc b/source/common/config/xds_manager_impl.cc index a1eda653d1ad1..e011c1288aac8 100644 --- a/source/common/config/xds_manager_impl.cc +++ b/source/common/config/xds_manager_impl.cc @@ -1,24 +1,73 @@ #include "source/common/config/xds_manager_impl.h" +#include +#include +#include +#include +#include +#include + +#include "envoy/common/exception.h" +#include "envoy/common/optref.h" +#include "envoy/config/core/v3/config_source.pb.h" #include "envoy/config/core/v3/config_source.pb.validate.h" - +#include "envoy/config/custom_config_validators.h" +#include "envoy/config/grpc_mux.h" +#include "envoy/config/subscription.h" +#include "envoy/config/subscription_factory.h" +#include "envoy/config/xds_config_tracker.h" +#include "envoy/config/xds_resources_delegate.h" +#include "envoy/grpc/async_client.h" +#include "envoy/grpc/async_client_manager.h" +#include "envoy/stats/scope.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/common/assert.h" +#include "source/common/common/backoff_strategy.h" +#include "source/common/common/cleanup.h" +#include "source/common/common/logger.h" #include "source/common/common/thread.h" #include "source/common/config/custom_config_validators_impl.h" #include "source/common/config/null_grpc_mux_impl.h" +#include "source/common/config/subscription_factory_impl.h" #include "source/common/config/utility.h" +#include "source/common/config/xds_resource.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" +#include "source/common/runtime/runtime_features.h" + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" namespace Envoy { namespace Config { namespace { -absl::Status createClients(Grpc::AsyncClientFactoryPtr& primary_factory, - Grpc::AsyncClientFactoryPtr& failover_factory, - Grpc::RawAsyncClientPtr& primary_client, - Grpc::RawAsyncClientPtr& failover_client) { - absl::StatusOr success = primary_factory->createUncachedRawAsyncClient(); +absl::Status createGrpcClients(Grpc::AsyncClientManager& async_client_manager, + const envoy::config::core::v3::ApiConfigSource& config_source, + Stats::Scope& stats_scope, bool skip_cluster_check, + bool xdstp_config_source, + Grpc::RawAsyncClientSharedPtr& primary_client, + Grpc::RawAsyncClientSharedPtr& failover_client) { + auto factory_primary_or_error = Config::Utility::factoryForGrpcApiConfigSource( + async_client_manager, config_source, stats_scope, skip_cluster_check, 0 /*grpc_service_idx*/, + xdstp_config_source); + RETURN_IF_NOT_OK_REF(factory_primary_or_error.status()); + Grpc::AsyncClientFactoryPtr factory_failover = nullptr; + if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { + auto factory_failover_or_error = Config::Utility::factoryForGrpcApiConfigSource( + async_client_manager, config_source, stats_scope, skip_cluster_check, + 1 /*grpc_service_idx*/, xdstp_config_source); + RETURN_IF_NOT_OK_REF(factory_failover_or_error.status()); + factory_failover = std::move(factory_failover_or_error.value()); + } + absl::StatusOr success = + factory_primary_or_error.value()->createUncachedRawAsyncClient(); RETURN_IF_NOT_OK_REF(success.status()); primary_client = std::move(*success); - if (failover_factory) { - success = failover_factory->createUncachedRawAsyncClient(); + if (factory_failover) { + success = factory_failover->createUncachedRawAsyncClient(); RETURN_IF_NOT_OK_REF(success.status()); failover_client = std::move(*success); } @@ -119,22 +168,13 @@ XdsManagerImpl::initializeAdsConnections(const envoy::config::bootstrap::v3::Boo if (!factory) { return absl::InvalidArgumentError(fmt::format("{} not found", name)); } - auto factory_primary_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), dyn_resources.ads_config(), *stats_.rootScope(), false, 0, - false); - RETURN_IF_NOT_OK_REF(factory_primary_or_error.status()); - Grpc::AsyncClientFactoryPtr factory_failover = nullptr; - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { - auto factory_failover_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), dyn_resources.ads_config(), *stats_.rootScope(), false, - 1, false); - RETURN_IF_NOT_OK_REF(factory_failover_or_error.status()); - factory_failover = std::move(factory_failover_or_error.value()); - } - Grpc::RawAsyncClientPtr primary_client; - Grpc::RawAsyncClientPtr failover_client; - RETURN_IF_NOT_OK(createClients(factory_primary_or_error.value(), factory_failover, - primary_client, failover_client)); + Grpc::RawAsyncClientSharedPtr primary_client; + Grpc::RawAsyncClientSharedPtr failover_client; + RETURN_IF_NOT_OK(createGrpcClients(cm_->grpcAsyncClientManager(), dyn_resources.ads_config(), + *stats_.rootScope(), /*skip_cluster_check*/ false, + /*xdstp_config_source*/ false, primary_client, + failover_client)); + ads_mux_ = factory->create(std::move(primary_client), std::move(failover_client), main_thread_dispatcher_, random_, *stats_.rootScope(), dyn_resources.ads_config(), local_info_, @@ -154,24 +194,14 @@ XdsManagerImpl::initializeAdsConnections(const envoy::config::bootstrap::v3::Boo if (!factory) { return absl::InvalidArgumentError(fmt::format("{} not found", name)); } - auto factory_primary_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), dyn_resources.ads_config(), *stats_.rootScope(), false, 0, - false); - RETURN_IF_NOT_OK_REF(factory_primary_or_error.status()); - Grpc::AsyncClientFactoryPtr factory_failover = nullptr; - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { - auto factory_failover_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), dyn_resources.ads_config(), *stats_.rootScope(), false, - 1, false); - RETURN_IF_NOT_OK_REF(factory_failover_or_error.status()); - factory_failover = std::move(factory_failover_or_error.value()); - } + Grpc::RawAsyncClientSharedPtr primary_client; + Grpc::RawAsyncClientSharedPtr failover_client; + RETURN_IF_NOT_OK(createGrpcClients(cm_->grpcAsyncClientManager(), dyn_resources.ads_config(), + *stats_.rootScope(), /*skip_cluster_check*/ false, + /*xdstp_config_source*/ false, primary_client, + failover_client)); OptRef xds_resources_delegate = makeOptRefFromPtr(xds_resources_delegate_.get()); - Grpc::RawAsyncClientPtr primary_client; - Grpc::RawAsyncClientPtr failover_client; - RETURN_IF_NOT_OK(createClients(factory_primary_or_error.value(), factory_failover, - primary_client, failover_client)); ads_mux_ = factory->create(std::move(primary_client), std::move(failover_client), main_thread_dispatcher_, random_, *stats_.rootScope(), dyn_resources.ads_config(), local_info_, @@ -266,6 +296,27 @@ absl::StatusOr XdsManagerImpl::subscribeToSingletonResource( fmt::format("No valid authority was found for the given xDS-TP resource {}.", resource_name)); } +ScopedResume XdsManagerImpl::pause(const std::vector& type_urls) { + // Apply the pause on all "ADS" based sources (old-ADS-mux, and + // xdstp-config-based sources) by collecting the per-xDS-mux scopes under a + // single scope. Using a shared_ptr here so we can pass it to the Cleanup + // object that is created at the return statement. + auto scoped_resume_collection = std::make_shared>(); + if (ads_mux_ != nullptr) { + scoped_resume_collection->emplace_back(ads_mux_->pause(type_urls)); + } + for (auto& authority : authorities_) { + scoped_resume_collection->emplace_back(authority.grpc_mux_->pause(type_urls)); + } + if (default_authority_ != nullptr) { + scoped_resume_collection->emplace_back(default_authority_->grpc_mux_->pause(type_urls)); + } + return std::make_unique([scoped_resume_collection]() { + // Do nothing. After this function is called the scoped_resume_collection + // will be destroyed, and all the internal cleanups will be invoked. + }); +} + absl::Status XdsManagerImpl::setAdsConfigSource(const envoy::config::core::v3::ApiConfigSource& config_source) { ASSERT_IS_MAIN_OR_TEST_THREAD(); @@ -342,20 +393,12 @@ XdsManagerImpl::createAuthority(const envoy::config::core::v3::ConfigSource& con if (!factory) { return absl::InvalidArgumentError(fmt::format("{} not found", name)); } - auto factory_primary_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), api_config_source, *stats_.rootScope(), false, 0, true); - RETURN_IF_NOT_OK_REF(factory_primary_or_error.status()); - Grpc::AsyncClientFactoryPtr factory_failover = nullptr; - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { - auto factory_failover_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), api_config_source, *stats_.rootScope(), false, 1, true); - RETURN_IF_NOT_OK_REF(factory_failover_or_error.status()); - factory_failover = std::move(factory_failover_or_error.value()); - } - Grpc::RawAsyncClientPtr primary_client; - Grpc::RawAsyncClientPtr failover_client; - RETURN_IF_NOT_OK(createClients(factory_primary_or_error.value(), factory_failover, - primary_client, failover_client)); + Grpc::RawAsyncClientSharedPtr primary_client; + Grpc::RawAsyncClientSharedPtr failover_client; + RETURN_IF_NOT_OK(createGrpcClients(cm_->grpcAsyncClientManager(), api_config_source, + *stats_.rootScope(), /*skip_cluster_check*/ false, + /*xdstp_config_source*/ true, primary_client, + failover_client)); authority_mux = factory->create( std::move(primary_client), std::move(failover_client), main_thread_dispatcher_, random_, *stats_.rootScope(), api_config_source, local_info_, std::move(custom_config_validators), @@ -376,22 +419,14 @@ XdsManagerImpl::createAuthority(const envoy::config::core::v3::ConfigSource& con if (!factory) { return absl::InvalidArgumentError(fmt::format("{} not found", name)); } - auto factory_primary_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), api_config_source, *stats_.rootScope(), false, 0, true); - RETURN_IF_NOT_OK_REF(factory_primary_or_error.status()); - Grpc::AsyncClientFactoryPtr factory_failover = nullptr; - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { - auto factory_failover_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), api_config_source, *stats_.rootScope(), false, 1, true); - RETURN_IF_NOT_OK_REF(factory_failover_or_error.status()); - factory_failover = std::move(factory_failover_or_error.value()); - } + Grpc::RawAsyncClientSharedPtr primary_client; + Grpc::RawAsyncClientSharedPtr failover_client; + RETURN_IF_NOT_OK(createGrpcClients(cm_->grpcAsyncClientManager(), api_config_source, + *stats_.rootScope(), /*skip_cluster_check*/ false, + /*xdstp_config_source*/ true, primary_client, + failover_client)); OptRef xds_resources_delegate = makeOptRefFromPtr(xds_resources_delegate_.get()); - Grpc::RawAsyncClientPtr primary_client; - Grpc::RawAsyncClientPtr failover_client; - RETURN_IF_NOT_OK(createClients(factory_primary_or_error.value(), factory_failover, - primary_client, failover_client)); authority_mux = factory->create( std::move(primary_client), std::move(failover_client), main_thread_dispatcher_, random_, *stats_.rootScope(), api_config_source, local_info_, std::move(custom_config_validators), @@ -464,20 +499,11 @@ XdsManagerImpl::replaceAdsMux(const envoy::config::core::v3::ApiConfigSource& ad absl::Status status = Config::Utility::checkTransportVersion(ads_config); RETURN_IF_NOT_OK(status); - auto factory_primary_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), ads_config, *stats_.rootScope(), false, 0, false); - RETURN_IF_NOT_OK_REF(factory_primary_or_error.status()); - Grpc::AsyncClientFactoryPtr factory_failover = nullptr; - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { - auto factory_failover_or_error = Config::Utility::factoryForGrpcApiConfigSource( - cm_->grpcAsyncClientManager(), ads_config, *stats_.rootScope(), false, 1, false); - RETURN_IF_NOT_OK_REF(factory_failover_or_error.status()); - factory_failover = std::move(factory_failover_or_error.value()); - } - Grpc::RawAsyncClientPtr primary_client; - Grpc::RawAsyncClientPtr failover_client; - RETURN_IF_NOT_OK(createClients(factory_primary_or_error.value(), factory_failover, primary_client, - failover_client)); + Grpc::RawAsyncClientSharedPtr primary_client; + Grpc::RawAsyncClientSharedPtr failover_client; + RETURN_IF_NOT_OK(createGrpcClients( + cm_->grpcAsyncClientManager(), ads_config, *stats_.rootScope(), /*skip_cluster_check*/ false, + /*xdstp_config_source*/ false, primary_client, failover_client)); // Primary client must not be null, as the primary xDS source must be a valid one. // The failover_client may be null (no failover defined). diff --git a/source/common/config/xds_manager_impl.h b/source/common/config/xds_manager_impl.h index 8400a25385b22..ecb35e6f8187c 100644 --- a/source/common/config/xds_manager_impl.h +++ b/source/common/config/xds_manager_impl.h @@ -28,6 +28,10 @@ class XdsManagerImpl : public XdsManager { absl::string_view resource_name, OptRef config, absl::string_view type_url, Stats::Scope& scope, SubscriptionCallbacks& callbacks, OpaqueResourceDecoderSharedPtr resource_decoder, const SubscriptionOptions& options) override; + ScopedResume pause(const std::string& type_url) override { + return pause(std::vector{type_url}); + } + ScopedResume pause(const std::vector& type_urls) override; void shutdown() override { ads_mux_.reset(); } absl::Status setAdsConfigSource(const envoy::config::core::v3::ApiConfigSource& config_source) override; diff --git a/source/common/event/dispatcher_impl.cc b/source/common/event/dispatcher_impl.cc index 5295f7eb67f8d..6cc1e52312528 100644 --- a/source/common/event/dispatcher_impl.cc +++ b/source/common/event/dispatcher_impl.cc @@ -26,6 +26,7 @@ #include "source/common/filesystem/watcher_impl.h" #include "source/common/network/address_impl.h" #include "source/common/network/connection_impl.h" +#include "source/common/network/utility.h" #include "source/common/runtime/runtime_features.h" #include "event2/event.h" @@ -79,13 +80,8 @@ DispatcherImpl::DispatcherImpl(const std::string& name, Thread::ThreadFactory& t ASSERT(!name_.empty()); FatalErrorHandler::registerFatalErrorHandler(*this); updateApproximateMonotonicTimeInternal(); - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.fix_dispatcher_approximate_now")) { - base_scheduler_.registerOnCheckCallback( - std::bind(&DispatcherImpl::updateApproximateMonotonicTime, this)); - } else { - base_scheduler_.registerOnPrepareCallback( - std::bind(&DispatcherImpl::updateApproximateMonotonicTime, this)); - } + base_scheduler_.registerOnCheckCallback( + std::bind(&DispatcherImpl::updateApproximateMonotonicTime, this)); } DispatcherImpl::~DispatcherImpl() { @@ -167,11 +163,31 @@ Network::ClientConnectionPtr DispatcherImpl::createClientConnection( auto* factory = Config::Utility::getFactoryByName( std::string(address->addressType())); + // The target address is usually offered by EDS and the EDS api should reject the unsupported // address. // TODO(lambdai): Return a closed connection if the factory is not found. Note that the caller // expects a non-null connection as of today so we cannot gracefully handle unsupported address // type. +#if defined(__linux__) + // For Linux, the source address' network namespace is relevant for client connections, since that + // is where the netns would be specified. + if (source_address && source_address->networkNamespace().has_value()) { + auto f = [&]() -> Network::ClientConnectionPtr { + return factory->createClientConnection( + *this, address, source_address, std::move(transport_socket), options, transport_options); + }; + auto result = Network::Utility::execInNetworkNamespace( + std::move(f), source_address->networkNamespace()->c_str()); + if (!result.ok()) { + ENVOY_LOG(error, "failed to create connection in network namespace {}: {}", + source_address->networkNamespace().value(), result.status().ToString()); + return nullptr; + } + return *std::move(result); + } +#endif + return factory->createClientConnection(*this, address, source_address, std::move(transport_socket), options, transport_options); } diff --git a/source/common/filter/config_discovery_impl.h b/source/common/filter/config_discovery_impl.h index 33121542ad4fb..d95300ae4d863 100644 --- a/source/common/filter/config_discovery_impl.h +++ b/source/common/filter/config_discovery_impl.h @@ -651,7 +651,7 @@ class FilterConfigProviderManagerImpl : public FilterConfigProviderManagerImplBa } absl::StatusOr - getDefaultConfig(const ProtobufWkt::Any& proto_config, const std::string& filter_config_name, + getDefaultConfig(const Protobuf::Any& proto_config, const std::string& filter_config_name, Server::Configuration::ServerFactoryContext& server_context, bool last_filter_in_filter_chain, const std::string& filter_chain_type, const absl::flat_hash_set& require_type_urls) const { diff --git a/source/common/formatter/http_specific_formatter.cc b/source/common/formatter/http_specific_formatter.cc index 6997e1953cf4c..d924af5626e7a 100644 --- a/source/common/formatter/http_specific_formatter.cc +++ b/source/common/formatter/http_specific_formatter.cc @@ -24,7 +24,7 @@ LocalReplyBodyFormatter::formatWithContext(const HttpFormatterContext& context, return std::string(context.localReplyBody()); } -ProtobufWkt::Value +Protobuf::Value LocalReplyBodyFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { return ValueUtil::stringValue(std::string(context.localReplyBody())); @@ -36,7 +36,7 @@ AccessLogTypeFormatter::formatWithContext(const HttpFormatterContext& context, return AccessLogType_Name(context.accessLogType()); } -ProtobufWkt::Value +Protobuf::Value AccessLogTypeFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { return ValueUtil::stringValue(AccessLogType_Name(context.accessLogType())); @@ -70,7 +70,7 @@ absl::optional HeaderFormatter::format(const Http::HeaderMap& heade return std::string(val); } -ProtobufWkt::Value HeaderFormatter::formatValue(const Http::HeaderMap& headers) const { +Protobuf::Value HeaderFormatter::formatValue(const Http::HeaderMap& headers) const { const Http::HeaderEntry* header = findHeader(headers); if (!header) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -92,7 +92,7 @@ ResponseHeaderFormatter::formatWithContext(const HttpFormatterContext& context, return HeaderFormatter::format(context.responseHeaders()); } -ProtobufWkt::Value +Protobuf::Value ResponseHeaderFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { return HeaderFormatter::formatValue(context.responseHeaders()); @@ -109,7 +109,7 @@ RequestHeaderFormatter::formatWithContext(const HttpFormatterContext& context, return HeaderFormatter::format(context.requestHeaders()); } -ProtobufWkt::Value +Protobuf::Value RequestHeaderFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { return HeaderFormatter::formatValue(context.requestHeaders()); @@ -126,7 +126,7 @@ ResponseTrailerFormatter::formatWithContext(const HttpFormatterContext& context, return HeaderFormatter::format(context.responseTrailers()); } -ProtobufWkt::Value +Protobuf::Value ResponseTrailerFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { return HeaderFormatter::formatValue(context.responseTrailers()); @@ -156,15 +156,15 @@ HeadersByteSizeFormatter::formatWithContext(const HttpFormatterContext& context, context.responseTrailers())); } -ProtobufWkt::Value +Protobuf::Value HeadersByteSizeFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { return ValueUtil::numberValue(extractHeadersByteSize( context.requestHeaders(), context.responseHeaders(), context.responseTrailers())); } -ProtobufWkt::Value TraceIDFormatter::formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo&) const { +Protobuf::Value TraceIDFormatter::formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo&) const { auto trace_id = context.activeSpan().getTraceId(); if (trace_id.empty()) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -236,7 +236,7 @@ GrpcStatusFormatter::formatWithContext(const HttpFormatterContext& context, PANIC_DUE_TO_CORRUPT_ENUM; } -ProtobufWkt::Value +Protobuf::Value GrpcStatusFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& info) const { if (!Grpc::Common::isGrpcRequestHeaders(context.requestHeaders())) { @@ -288,7 +288,7 @@ QueryParameterFormatter::formatWithContext(const HttpFormatterContext& context, return value; } -ProtobufWkt::Value +Protobuf::Value QueryParameterFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { return ValueUtil::optionalStringValue(formatWithContext(context, stream_info)); @@ -334,7 +334,7 @@ absl::optional PathFormatter::formatWithContext(const HttpFormatter return std::string(path_view); } -ProtobufWkt::Value +Protobuf::Value PathFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { return ValueUtil::optionalStringValue(formatWithContext(context, stream_info)); diff --git a/source/common/formatter/http_specific_formatter.h b/source/common/formatter/http_specific_formatter.h index f0badbff5e409..f0ba656f3ed64 100644 --- a/source/common/formatter/http_specific_formatter.h +++ b/source/common/formatter/http_specific_formatter.h @@ -30,9 +30,8 @@ class LocalReplyBodyFormatter : public FormatterProvider { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; /** @@ -46,9 +45,8 @@ class AccessLogTypeFormatter : public FormatterProvider { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; class HeaderFormatter { @@ -58,7 +56,7 @@ class HeaderFormatter { protected: absl::optional format(const Http::HeaderMap& headers) const; - ProtobufWkt::Value formatValue(const Http::HeaderMap& headers) const; + Protobuf::Value formatValue(const Http::HeaderMap& headers) const; private: const Http::HeaderEntry* findHeader(const Http::HeaderMap& headers) const; @@ -81,9 +79,8 @@ class HeadersByteSizeFormatter : public FormatterProvider { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; private: uint64_t extractHeadersByteSize(const Http::RequestHeaderMap& request_headers, @@ -104,9 +101,8 @@ class RequestHeaderFormatter : public FormatterProvider, HeaderFormatter { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; /** @@ -121,9 +117,8 @@ class ResponseHeaderFormatter : public FormatterProvider, HeaderFormatter { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; /** @@ -138,9 +133,8 @@ class ResponseTrailerFormatter : public FormatterProvider, HeaderFormatter { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; /** @@ -151,9 +145,8 @@ class TraceIDFormatter : public FormatterProvider { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; class GrpcStatusFormatter : public FormatterProvider, HeaderFormatter { @@ -171,9 +164,8 @@ class GrpcStatusFormatter : public FormatterProvider, HeaderFormatter { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; static Format parseFormat(absl::string_view format); @@ -189,9 +181,8 @@ class QueryParameterFormatter : public FormatterProvider { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; private: const std::string parameter_key_; @@ -213,9 +204,8 @@ class PathFormatter : public FormatterProvider { absl::optional formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; PathFormatter(bool with_query, PathFormatterOption option, absl::optional max_length) : with_query_(with_query), option_(option), max_length_(max_length) {} diff --git a/source/common/formatter/stream_info_formatter.cc b/source/common/formatter/stream_info_formatter.cc index ec1026e3cbfa2..31d07f793dad7 100644 --- a/source/common/formatter/stream_info_formatter.cc +++ b/source/common/formatter/stream_info_formatter.cc @@ -47,14 +47,14 @@ MetadataFormatter::MetadataFormatter(absl::string_view filter_namespace, absl::optional MetadataFormatter::formatMetadata(const envoy::config::core::v3::Metadata& metadata) const { - ProtobufWkt::Value value = formatMetadataValue(metadata); - if (value.kind_case() == ProtobufWkt::Value::kNullValue) { + Protobuf::Value value = formatMetadataValue(metadata); + if (value.kind_case() == Protobuf::Value::kNullValue) { return absl::nullopt; } std::string str; str.reserve(256); - if (value.kind_case() == ProtobufWkt::Value::kStringValue) { + if (value.kind_case() == Protobuf::Value::kStringValue) { str = value.string_value(); } else { Json::Utility::appendValueToString(value, str); @@ -63,21 +63,20 @@ MetadataFormatter::formatMetadata(const envoy::config::core::v3::Metadata& metad return str; } -ProtobufWkt::Value +Protobuf::Value MetadataFormatter::formatMetadataValue(const envoy::config::core::v3::Metadata& metadata) const { if (path_.empty()) { const auto filter_it = metadata.filter_metadata().find(filter_namespace_); if (filter_it == metadata.filter_metadata().end()) { return SubstitutionFormatUtils::unspecifiedValue(); } - ProtobufWkt::Value output; + Protobuf::Value output; output.mutable_struct_value()->CopyFrom(filter_it->second); return output; } - const ProtobufWkt::Value& val = - Config::Metadata::metadataValue(&metadata, filter_namespace_, path_); - if (val.kind_case() == ProtobufWkt::Value::KindCase::KIND_NOT_SET) { + const Protobuf::Value& val = Config::Metadata::metadataValue(&metadata, filter_namespace_, path_); + if (val.kind_case() == Protobuf::Value::KindCase::KIND_NOT_SET) { return SubstitutionFormatUtils::unspecifiedValue(); } @@ -90,7 +89,7 @@ MetadataFormatter::format(const StreamInfo::StreamInfo& stream_info) const { return (metadata != nullptr) ? formatMetadata(*metadata) : absl::nullopt; } -ProtobufWkt::Value MetadataFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { +Protobuf::Value MetadataFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { auto metadata = get_func_(stream_info); return formatMetadataValue((metadata != nullptr) ? *metadata : envoy::config::core::v3::Metadata()); @@ -259,8 +258,7 @@ FilterStateFormatter::format(const StreamInfo::StreamInfo& stream_info) const { } } -ProtobufWkt::Value -FilterStateFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { +Protobuf::Value FilterStateFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { const Envoy::StreamInfo::FilterState::Object* state = filterState(stream_info); if (!state) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -282,7 +280,7 @@ FilterStateFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) con } #ifdef ENVOY_ENABLE_YAML - ProtobufWkt::Value val; + Protobuf::Value val; if (MessageUtil::jsonConvertValue(*proto, val)) { return val; } @@ -476,7 +474,7 @@ CommonDurationFormatter::format(const StreamInfo::StreamInfo& info) const { } return fmt::format_int(duration.value()).str(); } -ProtobufWkt::Value CommonDurationFormatter::formatValue(const StreamInfo::StreamInfo& info) const { +Protobuf::Value CommonDurationFormatter::formatValue(const StreamInfo::StreamInfo& info) const { auto duration = getDurationCount(info); if (!duration.has_value()) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -561,8 +559,7 @@ SystemTimeFormatter::format(const StreamInfo::StreamInfo& stream_info) const { return date_formatter_.fromTime(time_field.value()); } -ProtobufWkt::Value -SystemTimeFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { +Protobuf::Value SystemTimeFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { return ValueUtil::optionalStringValue(format(stream_info)); } @@ -584,7 +581,7 @@ EnvironmentFormatter::EnvironmentFormatter(absl::string_view key, absl::optional EnvironmentFormatter::format(const StreamInfo::StreamInfo&) const { return str_.string_value(); } -ProtobufWkt::Value EnvironmentFormatter::formatValue(const StreamInfo::StreamInfo&) const { +Protobuf::Value EnvironmentFormatter::formatValue(const StreamInfo::StreamInfo&) const { return str_; } @@ -599,7 +596,7 @@ class StreamInfoStringFormatterProvider : public StreamInfoFormatterProvider { absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { return field_extractor_(stream_info); } - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { return ValueUtil::optionalStringValue(field_extractor_(stream_info)); } @@ -624,7 +621,7 @@ class StreamInfoDurationFormatterProvider : public StreamInfoFormatterProvider { return fmt::format_int(millis.value()).str(); } - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { const auto millis = extractMillis(stream_info); if (!millis) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -656,7 +653,7 @@ class StreamInfoUInt64FormatterProvider : public StreamInfoFormatterProvider { absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { return fmt::format_int(field_extractor_(stream_info)).str(); } - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { return ValueUtil::numberValue(field_extractor_(stream_info)); } @@ -698,7 +695,7 @@ class StreamInfoAddressFormatterProvider : public StreamInfoFormatterProvider { return toString(*address); } - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { Network::Address::InstanceConstSharedPtr address = field_extractor_(stream_info); if (!address) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -753,7 +750,7 @@ class StreamInfoSslConnectionInfoFormatterProvider : public StreamInfoFormatterP return value; } - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { if (stream_info.downstreamAddressProvider().sslConnection() == nullptr) { return SubstitutionFormatUtils::unspecifiedValue(); } @@ -791,7 +788,7 @@ class StreamInfoUpstreamSslConnectionInfoFormatterProvider : public StreamInfoFo return value; } - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { if (!stream_info.upstreamInfo() || stream_info.upstreamInfo()->upstreamSslConnection() == nullptr) { return SubstitutionFormatUtils::unspecifiedValue(); @@ -921,6 +918,15 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return bytes_meter ? bytes_meter->headerBytesReceived() : 0; }); }}}, + {"UPSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, absl::optional) { + return std::make_unique( + [](const StreamInfo::StreamInfo& stream_info) { + const auto& bytes_meter = stream_info.getUpstreamBytesMeter(); + return bytes_meter ? bytes_meter->decompressedHeaderBytesReceived() : 0; + }); + }}}, {"DOWNSTREAM_WIRE_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, [](absl::string_view, absl::optional) { @@ -939,6 +945,15 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return bytes_meter ? bytes_meter->headerBytesReceived() : 0; }); }}}, + {"DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, absl::optional) { + return std::make_unique( + [](const StreamInfo::StreamInfo& stream_info) { + const auto& bytes_meter = stream_info.getDownstreamBytesMeter(); + return bytes_meter ? bytes_meter->decompressedHeaderBytesReceived() : 0; + }); + }}}, {"PROTOCOL", {CommandSyntaxChecker::COMMAND_ONLY, [](absl::string_view, absl::optional) { @@ -1013,6 +1028,15 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return bytes_meter ? bytes_meter->headerBytesSent() : 0; }); }}}, + {"UPSTREAM_DECOMPRESSED_HEADER_BYTES_SENT", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, absl::optional) { + return std::make_unique( + [](const StreamInfo::StreamInfo& stream_info) { + const auto& bytes_meter = stream_info.getUpstreamBytesMeter(); + return bytes_meter ? bytes_meter->decompressedHeaderBytesSent() : 0; + }); + }}}, {"DOWNSTREAM_WIRE_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, [](absl::string_view, absl::optional) { @@ -1031,6 +1055,15 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return bytes_meter ? bytes_meter->headerBytesSent() : 0; }); }}}, + {"DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_SENT", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, absl::optional) { + return std::make_unique( + [](const StreamInfo::StreamInfo& stream_info) { + const auto& bytes_meter = stream_info.getDownstreamBytesMeter(); + return bytes_meter ? bytes_meter->decompressedHeaderBytesSent() : 0; + }); + }}}, {"DURATION", {CommandSyntaxChecker::COMMAND_ONLY, [](absl::string_view, absl::optional) { diff --git a/source/common/formatter/stream_info_formatter.h b/source/common/formatter/stream_info_formatter.h index 4989d1131c31d..805e5e922db61 100644 --- a/source/common/formatter/stream_info_formatter.h +++ b/source/common/formatter/stream_info_formatter.h @@ -26,8 +26,8 @@ class StreamInfoFormatterProvider : public FormatterProvider { formatWithContext(const Context&, const StreamInfo::StreamInfo& stream_info) const override { return format(stream_info); } - ProtobufWkt::Value - formatValueWithContext(const Context&, const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValueWithContext(const Context&, + const StreamInfo::StreamInfo& stream_info) const override { return formatValue(stream_info); } @@ -42,9 +42,9 @@ class StreamInfoFormatterProvider : public FormatterProvider { /** * Format the value with the given stream info. * @param stream_info supplies the stream info. - * @return ProtobufWkt::Value containing a single value extracted from the given stream info. + * @return Protobuf::Value containing a single value extracted from the given stream info. */ - virtual ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const PURE; + virtual Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const PURE; }; using StreamInfoFormatterProviderPtr = std::unique_ptr; @@ -68,12 +68,12 @@ class MetadataFormatter : public StreamInfoFormatterProvider { // StreamInfoFormatterProvider absl::optional format(const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override; protected: absl::optional formatMetadata(const envoy::config::core::v3::Metadata& metadata) const; - ProtobufWkt::Value formatMetadataValue(const envoy::config::core::v3::Metadata& metadata) const; + Protobuf::Value formatMetadataValue(const envoy::config::core::v3::Metadata& metadata) const; private: std::string filter_namespace_; @@ -128,7 +128,7 @@ class FilterStateFormatter : public StreamInfoFormatterProvider { // StreamInfoFormatterProvider absl::optional format(const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; private: const Envoy::StreamInfo::FilterState::Object* @@ -156,7 +156,7 @@ class CommonDurationFormatter : public StreamInfoFormatterProvider { // StreamInfoFormatterProvider absl::optional format(const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; static const absl::flat_hash_map KnownTimePointGetters; @@ -210,7 +210,7 @@ class SystemTimeFormatter : public StreamInfoFormatterProvider { // StreamInfoFormatterProvider absl::optional format(const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; private: const Envoy::DateFormatter date_formatter_; @@ -272,10 +272,10 @@ class EnvironmentFormatter : public StreamInfoFormatterProvider { // StreamInfoFormatterProvider absl::optional format(const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValue(const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; private: - ProtobufWkt::Value str_; + Protobuf::Value str_; }; class DefaultBuiltInStreamInfoCommandParserFactory : public BuiltInCommandParserFactory { diff --git a/source/common/formatter/substitution_format_string.cc b/source/common/formatter/substitution_format_string.cc index c6c305de1bcca..60cabc98f997d 100644 --- a/source/common/formatter/substitution_format_string.cc +++ b/source/common/formatter/substitution_format_string.cc @@ -52,7 +52,7 @@ absl::StatusOr SubstitutionFormatStringUtils::fromProtoConfig( } FormatterPtr -SubstitutionFormatStringUtils::createJsonFormatter(const ProtobufWkt::Struct& struct_format, +SubstitutionFormatStringUtils::createJsonFormatter(const Protobuf::Struct& struct_format, bool omit_empty_values, const std::vector& commands) { return std::make_unique(struct_format, omit_empty_values, commands); diff --git a/source/common/formatter/substitution_format_string.h b/source/common/formatter/substitution_format_string.h index cbf31a130ad37..686f54542ad39 100644 --- a/source/common/formatter/substitution_format_string.h +++ b/source/common/formatter/substitution_format_string.h @@ -24,7 +24,7 @@ namespace Formatter { class SubstitutionFormatStringUtils { public: using FormattersConfig = - ProtobufWkt::RepeatedPtrField; + Protobuf::RepeatedPtrField; /** * Parse list of formatter configurations to commands. @@ -44,7 +44,7 @@ class SubstitutionFormatStringUtils { /** * Generate a Json formatter object from proto::Struct config */ - static FormatterPtr createJsonFormatter(const ProtobufWkt::Struct& struct_format, + static FormatterPtr createJsonFormatter(const Protobuf::Struct& struct_format, bool omit_empty_values, const std::vector& commands = {}); }; diff --git a/source/common/formatter/substitution_format_utility.cc b/source/common/formatter/substitution_format_utility.cc index a2190c0f0bc45..cd3f6e0bb241c 100644 --- a/source/common/formatter/substitution_format_utility.cc +++ b/source/common/formatter/substitution_format_utility.cc @@ -69,7 +69,7 @@ const absl::optional SubstitutionFormatUtils::getHostname() { return hostname; } -const ProtobufWkt::Value& SubstitutionFormatUtils::unspecifiedValue() { +const Protobuf::Value& SubstitutionFormatUtils::unspecifiedValue() { return ValueUtil::nullValue(); } diff --git a/source/common/formatter/substitution_format_utility.h b/source/common/formatter/substitution_format_utility.h index 907dd04d62adb..e5e5bee0ff16a 100644 --- a/source/common/formatter/substitution_format_utility.h +++ b/source/common/formatter/substitution_format_utility.h @@ -44,7 +44,7 @@ class SubstitutionFormatUtils { /** * Unspecified value for protobuf. */ - static const ProtobufWkt::Value& unspecifiedValue(); + static const Protobuf::Value& unspecifiedValue(); /** * Truncate a string to a maximum length. Do nothing if max_length is not set or diff --git a/source/common/formatter/substitution_formatter.cc b/source/common/formatter/substitution_formatter.cc index 72c0b3c55abfc..e52c49070105d 100644 --- a/source/common/formatter/substitution_formatter.cc +++ b/source/common/formatter/substitution_formatter.cc @@ -167,14 +167,14 @@ class JsonFormatBuilder { * * @param struct_format the proto struct format configuration. */ - FormatElements fromStruct(const ProtobufWkt::Struct& struct_format); + FormatElements fromStruct(const Protobuf::Struct& struct_format); private: - using ProtoDict = Protobuf::Map; - using ProtoList = Protobuf::RepeatedPtrField; + using ProtoDict = Protobuf::Map; + using ProtoList = Protobuf::RepeatedPtrField; void formatValueToFormatElements(const ProtoDict& dict_value); - void formatValueToFormatElements(const ProtobufWkt::Value& value); + void formatValueToFormatElements(const Protobuf::Value& value); void formatValueToFormatElements(const ProtoList& list_value); std::string buffer_; // JSON writer buffer. @@ -183,7 +183,7 @@ class JsonFormatBuilder { }; JsonFormatBuilder::FormatElements -JsonFormatBuilder::fromStruct(const ProtobufWkt::Struct& struct_format) { +JsonFormatBuilder::fromStruct(const Protobuf::Struct& struct_format) { elements_.clear(); // This call will iterate through the map tree and serialize the key/values as JSON. @@ -197,16 +197,16 @@ JsonFormatBuilder::fromStruct(const ProtobufWkt::Struct& struct_format) { return std::move(elements_); }; -void JsonFormatBuilder::formatValueToFormatElements(const ProtobufWkt::Value& value) { +void JsonFormatBuilder::formatValueToFormatElements(const Protobuf::Value& value) { switch (value.kind_case()) { - case ProtobufWkt::Value::KIND_NOT_SET: - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::KIND_NOT_SET: + case Protobuf::Value::kNullValue: serializer_.addNull(); break; - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: serializer_.addNumber(value.number_value()); break; - case ProtobufWkt::Value::kStringValue: { + case Protobuf::Value::kStringValue: { absl::string_view string_format = value.string_value(); if (!absl::StrContains(string_format, '%')) { serializer_.addString(string_format); @@ -223,13 +223,13 @@ void JsonFormatBuilder::formatValueToFormatElements(const ProtobufWkt::Value& va elements_.push_back(FormatElement{std::string(string_format), true}); break; } - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: serializer_.addBool(value.bool_value()); break; - case ProtobufWkt::Value::kStructValue: { + case Protobuf::Value::kStructValue: { formatValueToFormatElements(value.struct_value().fields()); break; - case ProtobufWkt::Value::kListValue: + case Protobuf::Value::kListValue: formatValueToFormatElements(value.list_value().values()); break; } @@ -400,8 +400,8 @@ void stringValueToLogLine(const JsonFormatterImpl::Formatters& formatters, const log_line.push_back('"'); // End the JSON string. } -JsonFormatterImpl::JsonFormatterImpl(const ProtobufWkt::Struct& struct_format, - bool omit_empty_values, const CommandParsers& commands) +JsonFormatterImpl::JsonFormatterImpl(const Protobuf::Struct& struct_format, bool omit_empty_values, + const CommandParsers& commands) : omit_empty_values_(omit_empty_values) { for (JsonFormatBuilder::FormatElement& element : JsonFormatBuilder().fromStruct(struct_format)) { if (element.is_template_) { diff --git a/source/common/formatter/substitution_formatter.h b/source/common/formatter/substitution_formatter.h index cc9fc64346c6c..e88dcb9834946 100644 --- a/source/common/formatter/substitution_formatter.h +++ b/source/common/formatter/substitution_formatter.h @@ -37,13 +37,13 @@ class PlainStringFormatter : public FormatterProvider { const StreamInfo::StreamInfo&) const override { return str_.string_value(); } - ProtobufWkt::Value formatValueWithContext(const Context&, - const StreamInfo::StreamInfo&) const override { + Protobuf::Value formatValueWithContext(const Context&, + const StreamInfo::StreamInfo&) const override { return str_; } private: - ProtobufWkt::Value str_; + Protobuf::Value str_; }; /** @@ -59,13 +59,13 @@ class PlainNumberFormatter : public FormatterProvider { std::string str = absl::StrFormat("%g", num_.number_value()); return str; } - ProtobufWkt::Value formatValueWithContext(const Context&, - const StreamInfo::StreamInfo&) const override { + Protobuf::Value formatValueWithContext(const Context&, + const StreamInfo::StreamInfo&) const override { return num_; } private: - ProtobufWkt::Value num_; + Protobuf::Value num_; }; /** @@ -114,7 +114,7 @@ class JsonFormatterImpl : public Formatter { using Formatter = FormatterProviderPtr; using Formatters = std::vector; - JsonFormatterImpl(const ProtobufWkt::Struct& struct_format, bool omit_empty_values, + JsonFormatterImpl(const Protobuf::Struct& struct_format, bool omit_empty_values, const CommandParsers& commands = {}); // Formatter diff --git a/source/common/grpc/async_client_impl.cc b/source/common/grpc/async_client_impl.cc index 1f0a18ecf5f58..55fe75428abce 100644 --- a/source/common/grpc/async_client_impl.cc +++ b/source/common/grpc/async_client_impl.cc @@ -25,7 +25,7 @@ std::string enhancedGrpcMessage(const std::string& original_message, : absl::StrCat(original_message, "{", http_response_code_details, "}"); } -void Base64EscapeBinHeaders(Http::RequestHeaderMap& headers) { +void base64EscapeBinHeaders(Http::RequestHeaderMap& headers) { absl::flat_hash_map bin_metadata; headers.iterate([&bin_metadata](const Http::HeaderEntry& header) { if (absl::EndsWith(header.key().getStringView(), "-bin")) { @@ -209,7 +209,7 @@ void AsyncStreamImpl::initialize(bool buffer_body_for_retry) { current_span_->injectContext(trace_context, upstream_context); callbacks_.onCreateInitialMetadata(headers_message_->headers()); // base64 encode on "-bin" metadata. - Base64EscapeBinHeaders(headers_message_->headers()); + base64EscapeBinHeaders(headers_message_->headers()); stream_->sendHeaders(headers_message_->headers(), false); } @@ -427,6 +427,21 @@ const StreamInfo::StreamInfo& AsyncRequestImpl::streamInfo() const { return AsyncStreamImpl::streamInfo(); } +void AsyncRequestImpl::detach() { + // TODO(wbpcode): In most tracers the span will hold a reference to the tracer self + // and it's possible that become a dangling reference for long time async request. + // This require further PR to resolve. + + if (options_.sidestream_watermark_callbacks != nullptr) { + stream_->removeWatermarkCallbacks(); + options_.sidestream_watermark_callbacks = nullptr; + } + options_.parent_span_ = nullptr; + options_.parent_context.stream_info = nullptr; + + streamInfo().clearParentStreamInfo(); +} + void AsyncRequestImpl::onCreateInitialMetadata(Http::RequestHeaderMap& metadata) { Tracing::HttpTraceContext trace_context(metadata); Tracing::UpstreamContext upstream_context(nullptr, // host_ diff --git a/source/common/grpc/async_client_impl.h b/source/common/grpc/async_client_impl.h index c255657459209..8953cbde033c9 100644 --- a/source/common/grpc/async_client_impl.h +++ b/source/common/grpc/async_client_impl.h @@ -121,6 +121,7 @@ class AsyncStreamImpl : public RawAsyncStream, // Deliver notification and update span when the connection closes. void notifyRemoteClose(Grpc::Status::GrpcStatus status, const std::string& message); +protected: Event::Dispatcher* dispatcher_{}; Http::RequestMessagePtr headers_message_; AsyncClientImpl& parent_; @@ -153,6 +154,7 @@ class AsyncRequestImpl : public AsyncRequest, public AsyncStreamImpl, RawAsyncSt // Grpc::AsyncRequest void cancel() override; const StreamInfo::StreamInfo& streamInfo() const override; + void detach() override; private: using AsyncStreamImpl::streamInfo; diff --git a/source/common/grpc/google_async_client_impl.cc b/source/common/grpc/google_async_client_impl.cc index 5f54652295d34..b8ec5c12d8ae4 100644 --- a/source/common/grpc/google_async_client_impl.cc +++ b/source/common/grpc/google_async_client_impl.cc @@ -518,6 +518,18 @@ void GoogleAsyncRequestImpl::cancel() { resetStream(); } +void GoogleAsyncRequestImpl::detach() { + // TODO(wbpcode): In most tracers the span will hold a reference to the tracer self + // and it's possible that become a dangling reference for long time async request. + // This require further PR to resolve. + + options_.sidestream_watermark_callbacks = nullptr; + options_.parent_span_ = nullptr; + options_.parent_context.stream_info = nullptr; + + streamInfo().clearParentStreamInfo(); +} + void GoogleAsyncRequestImpl::onCreateInitialMetadata(Http::RequestHeaderMap& metadata) { Tracing::HttpTraceContext trace_context(metadata); Tracing::UpstreamContext upstream_context(nullptr, // host_ diff --git a/source/common/grpc/google_async_client_impl.h b/source/common/grpc/google_async_client_impl.h index 65a6a7e8e2ebc..5c5d13ad29bff 100644 --- a/source/common/grpc/google_async_client_impl.h +++ b/source/common/grpc/google_async_client_impl.h @@ -274,10 +274,11 @@ class GoogleAsyncStreamImpl : public RawAsyncStream, // End-of-stream with no additional message. PendingMessage() = default; - const absl::optional buf_{}; + const absl::optional buf_; const bool end_stream_{true}; }; +protected: GoogleAsyncTag init_tag_{*this, GoogleAsyncTag::Operation::Init}; GoogleAsyncTag read_initial_metadata_tag_{*this, GoogleAsyncTag::Operation::ReadInitialMetadata}; GoogleAsyncTag read_tag_{*this, GoogleAsyncTag::Operation::Read}; @@ -298,7 +299,7 @@ class GoogleAsyncStreamImpl : public RawAsyncStream, std::string service_full_name_; std::string method_name_; RawAsyncStreamCallbacks& callbacks_; - const Http::AsyncClient::StreamOptions options_; + Http::AsyncClient::StreamOptions options_; grpc::ClientContext ctxt_; std::unique_ptr rw_; std::queue write_pending_queue_; @@ -352,6 +353,7 @@ class GoogleAsyncRequestImpl : public AsyncRequest, const StreamInfo::StreamInfo& streamInfo() const override { return GoogleAsyncStreamImpl::streamInfo(); } + void detach() override; private: using GoogleAsyncStreamImpl::streamInfo; diff --git a/source/common/grpc/typed_async_client.h b/source/common/grpc/typed_async_client.h index 30c2ecdc52630..e664106c04688 100644 --- a/source/common/grpc/typed_async_client.h +++ b/source/common/grpc/typed_async_client.h @@ -113,7 +113,8 @@ template class AsyncClient /* : public Raw public: AsyncClient() = default; AsyncClient(RawAsyncClientPtr&& client) : client_(std::move(client)) {} - AsyncClient(RawAsyncClientSharedPtr client) : client_(client) {} + AsyncClient(const RawAsyncClientSharedPtr& client) : client_(client) {} + AsyncClient(RawAsyncClientSharedPtr&& client) : client_(std::move(client)) {} virtual ~AsyncClient() = default; virtual AsyncRequest* send(const Protobuf::MethodDescriptor& service_method, diff --git a/source/common/http/BUILD b/source/common/http/BUILD index 288761f3f7fbb..7e1002a8080d4 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -467,7 +467,6 @@ envoy_cc_library( "//source/common/common:empty_string", "//source/common/common:non_copyable", "//source/common/common:utility_lib", - "//source/common/runtime:runtime_features_lib", "//source/common/singleton:const_singleton", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], @@ -628,6 +627,7 @@ envoy_cc_library( ":header_map_lib", ":utility_lib", "//envoy/http:header_evaluator", + "//source/common/common:matchers_lib", "//source/common/router:header_parser_lib", "@envoy_api//envoy/config/common/mutation_rules/v3:pkg_cc_proto", ], diff --git a/source/common/http/async_client_impl.cc b/source/common/http/async_client_impl.cc index 6fb949da25300..1413861c6982f 100644 --- a/source/common/http/async_client_impl.cc +++ b/source/common/http/async_client_impl.cc @@ -90,23 +90,19 @@ AsyncClient::Stream* AsyncClientImpl::start(AsyncClient::StreamCallbacks& callba return active_streams_.front().get(); } -std::unique_ptr -createRetryPolicy(AsyncClientImpl& parent, const AsyncClient::StreamOptions& options, +Router::RetryPolicyConstSharedPtr +createRetryPolicy(const AsyncClient::StreamOptions& options, Server::Configuration::CommonFactoryContext& context, absl::Status& creation_status) { if (options.retry_policy.has_value()) { - Upstream::RetryExtensionFactoryContextImpl factory_context( - parent.factory_context_.singletonManager()); auto policy_or_error = Router::RetryPolicyImpl::create( - options.retry_policy.value(), ProtobufMessage::getNullValidationVisitor(), factory_context, - context); + options.retry_policy.value(), ProtobufMessage::getNullValidationVisitor(), context); creation_status = policy_or_error.status(); - return policy_or_error.status().ok() ? std::move(policy_or_error.value()) : nullptr; + return policy_or_error.status().ok() ? std::move(policy_or_error.value()) + : Router::RetryPolicyImpl::DefaultRetryPolicy; } - if (options.parsed_retry_policy == nullptr) { - return std::make_unique(); - } - return nullptr; + return options.parsed_retry_policy != nullptr ? options.parsed_retry_policy + : Router::RetryPolicyImpl::DefaultRetryPolicy; } AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCallbacks& callbacks, @@ -122,9 +118,10 @@ AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCal : std::make_shared( StreamInfo::FilterState::LifeSpan::FilterChain)), tracing_config_(Tracing::EgressConfig::get()), local_reply_(*parent.local_reply_), - retry_policy_(createRetryPolicy(parent, options, parent_.factory_context_, creation_status)), account_(options.account_), buffer_limit_(options.buffer_limit_), send_xff_(options.send_xff), send_internal_(options.send_internal) { + auto retry_policy = createRetryPolicy(options, parent.factory_context_, creation_status); + // A field initialization may set the creation-status as unsuccessful. // In that case return immediately. if (!creation_status.ok()) { @@ -134,9 +131,11 @@ AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCal const Router::MetadataMatchCriteria* metadata_matching_criteria = nullptr; if (options.parent_context.stream_info != nullptr) { stream_info_.setParentStreamInfo(*options.parent_context.stream_info); - const auto route = options.parent_context.stream_info->route(); - if (route != nullptr) { - const auto* route_entry = route->routeEntry(); + // Keep the parent root to ensure the metadata_matching_criteria will not become + // dangling pointer once the parent downstream request is gone. + parent_route_ = options.parent_context.stream_info->route(); + if (parent_route_ != nullptr) { + const auto* route_entry = parent_route_->routeEntry(); if (route_entry != nullptr) { metadata_matching_criteria = route_entry->metadataMatchCriteria(); } @@ -144,10 +143,8 @@ AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCal } auto route_or_error = NullRouteImpl::create( - parent_.cluster_->name(), - retry_policy_ != nullptr ? *retry_policy_ : *options.parsed_retry_policy, - parent_.factory_context_.regexEngine(), options.timeout, options.hash_policy, - metadata_matching_criteria); + parent_.cluster_->name(), std::move(retry_policy), parent_.factory_context_.regexEngine(), + options.timeout, options.hash_policy, metadata_matching_criteria); SET_AND_RETURN_IF_NOT_OK(route_or_error.status(), creation_status); route_ = std::move(*route_or_error); stream_info_.dynamicMetadata().MergeFrom(options.metadata); diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h index 9e0bc1248dda6..f2b841647b2a5 100644 --- a/source/common/http/async_client_impl.h +++ b/source/common/http/async_client_impl.h @@ -230,10 +230,10 @@ class AsyncStreamImpl : public virtual AsyncClient::Stream, void removeDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks&) override {} void sendGoAwayAndClose() override {} - void setDecoderBufferLimit(uint32_t) override { + void setDecoderBufferLimit(uint64_t) override { IS_ENVOY_BUG("decoder buffer limits should not be overridden on async streams."); } - uint32_t decoderBufferLimit() override { return buffer_limit_.value_or(0); } + uint64_t decoderBufferLimit() override { return buffer_limit_.value_or(0); } bool recreateStream(const ResponseHeaderMap*) override { return false; } const ScopeTrackedObject& scope() override { return *this; } void restoreContextOnContinue(ScopeTrackedObjectStack& tracked_object_stack) override { @@ -278,14 +278,14 @@ class AsyncStreamImpl : public virtual AsyncClient::Stream, Tracing::NullSpan active_span_; const Tracing::Config& tracing_config_; const LocalReply::LocalReply& local_reply_; - const std::unique_ptr retry_policy_; + Router::RouteConstSharedPtr parent_route_; std::shared_ptr route_; uint32_t high_watermark_calls_{}; bool local_closed_{}; bool remote_closed_{}; Buffer::InstancePtr buffered_body_; Buffer::BufferMemoryAccountSharedPtr account_{nullptr}; - absl::optional buffer_limit_{absl::nullopt}; + absl::optional buffer_limit_{absl::nullopt}; RequestHeaderMap* request_headers_{}; RequestTrailerMap* request_trailers_{}; bool encoded_response_headers_{}; diff --git a/source/common/http/codec_helper.h b/source/common/http/codec_helper.h index a6ee26e1db765..d614f1353990f 100644 --- a/source/common/http/codec_helper.h +++ b/source/common/http/codec_helper.h @@ -1,6 +1,7 @@ #pragma once #include "envoy/event/dispatcher.h" +#include "envoy/event/scaled_timer.h" #include "envoy/event/timer.h" #include "envoy/http/codec.h" @@ -89,11 +90,11 @@ class StreamCallbackHelper { class MultiplexedStreamImplBase : public Stream, public StreamCallbackHelper { public: MultiplexedStreamImplBase(Event::Dispatcher& dispatcher) : dispatcher_(dispatcher) {} - ~MultiplexedStreamImplBase() override { ASSERT(stream_idle_timer_ == nullptr); } + ~MultiplexedStreamImplBase() override { ASSERT(stream_flush_timer_ == nullptr); } // TODO(mattklein123): Optimally this would be done in the destructor but there are currently // deferred delete lifetime issues that need sorting out if the destructor of the stream is // going to be able to refer to the parent connection. - virtual void destroy() { disarmStreamIdleTimer(); } + virtual void destroy() { disarmStreamFlushTimer(); } void onLocalEndStream() { ASSERT(local_end_stream_); @@ -102,11 +103,11 @@ class MultiplexedStreamImplBase : public Stream, public StreamCallbackHelper { } } - void disarmStreamIdleTimer() { - if (stream_idle_timer_ != nullptr) { + void disarmStreamFlushTimer() { + if (stream_flush_timer_ != nullptr) { // To ease testing and the destructor assertion. - stream_idle_timer_->disableTimer(); - stream_idle_timer_.reset(); + stream_flush_timer_->disableTimer(); + stream_flush_timer_.reset(); } } @@ -117,18 +118,19 @@ class MultiplexedStreamImplBase : public Stream, public StreamCallbackHelper { protected: void setFlushTimeout(std::chrono::milliseconds timeout) override { - stream_idle_timeout_ = timeout; + stream_flush_timeout_ = timeout; } void createPendingFlushTimer() { - ASSERT(stream_idle_timer_ == nullptr); - if (stream_idle_timeout_.count() > 0) { - stream_idle_timer_ = dispatcher_.createTimer([this] { onPendingFlushTimer(); }); - stream_idle_timer_->enableTimer(stream_idle_timeout_); + ASSERT(stream_flush_timer_ == nullptr); + if (stream_flush_timeout_.count() > 0) { + stream_flush_timer_ = dispatcher_.createScaledTimer( + Event::ScaledTimerType::HttpDownstreamStreamFlush, [this] { onPendingFlushTimer(); }); + stream_flush_timer_->enableTimer(stream_flush_timeout_); } } - virtual void onPendingFlushTimer() { stream_idle_timer_.reset(); } + virtual void onPendingFlushTimer() { stream_flush_timer_.reset(); } virtual bool hasPendingData() PURE; @@ -136,9 +138,9 @@ class MultiplexedStreamImplBase : public Stream, public StreamCallbackHelper { private: Event::Dispatcher& dispatcher_; - // See HttpConnectionManager.stream_idle_timeout. - std::chrono::milliseconds stream_idle_timeout_{}; - Event::TimerPtr stream_idle_timer_; + // See HttpConnectionManager.stream_flush_timeout. + std::chrono::milliseconds stream_flush_timeout_{}; + Event::TimerPtr stream_flush_timer_; }; } // namespace Http diff --git a/source/common/http/codes.cc b/source/common/http/codes.cc index b0561cedc51ce..949fe3d8d778b 100644 --- a/source/common/http/codes.cc +++ b/source/common/http/codes.cc @@ -292,6 +292,7 @@ const char* CodeUtility::toString(Code code) { case Code::LoopDetected: return "Loop Detected"; case Code::NotExtended: return "Not Extended"; case Code::NetworkAuthenticationRequired: return "Network Authentication Required"; + case Code::LastUnassignedServerErrorCode: return "Last Unassigned Server Error Code"; } // clang-format on diff --git a/source/common/http/conn_manager_config.h b/source/common/http/conn_manager_config.h index 406b06332a04b..071b6464af3fb 100644 --- a/source/common/http/conn_manager_config.h +++ b/source/common/http/conn_manager_config.h @@ -126,7 +126,7 @@ struct ConnectionManagerTracingStats { CONN_MAN_TRACING_STATS(GENERATE_COUNTER_STRUCT) }; -using TracingConnectionManagerConfig = Tracing::ConnectionManagerTracingConfigImpl; +using TracingConnectionManagerConfig = Tracing::ConnectionManagerTracingConfig; using TracingConnectionManagerConfigPtr = std::unique_ptr; /** @@ -190,13 +190,7 @@ class InternalAddressConfig { */ class DefaultInternalAddressConfig : public Http::InternalAddressConfig { public: - bool isInternalAddress(const Network::Address::Instance& address) const override { - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.explicit_internal_address_config")) { - return false; - } - return Network::Utility::isInternalAddress(address); - } + bool isInternalAddress(const Network::Address::Instance&) const override { return false; } }; /** @@ -317,6 +311,12 @@ class ConnectionManagerConfig { */ virtual std::chrono::milliseconds streamIdleTimeout() const PURE; + /** + * @return per-stream flush timeout for incoming connection manager connections. Zero indicates a + * disabled idle timeout. + */ + virtual absl::optional streamFlushTimeout() const PURE; + /** * @return request timeout for incoming connection manager connections. Zero indicates * a disabled request timeout. diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index d99900e23eff7..4e12fff583b7e 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -431,7 +431,12 @@ RequestDecoder& ConnectionManagerImpl::newStream(ResponseEncoder& response_encod new_stream->response_encoder_ = &response_encoder; new_stream->response_encoder_->getStream().addCallbacks(*new_stream); new_stream->response_encoder_->getStream().registerCodecEventCallbacks(new_stream.get()); - new_stream->response_encoder_->getStream().setFlushTimeout(new_stream->idle_timeout_ms_); + if (config_->streamFlushTimeout().has_value()) { + new_stream->response_encoder_->getStream().setFlushTimeout( + config_->streamFlushTimeout().value()); + } else { + new_stream->response_encoder_->getStream().setFlushTimeout(config_->streamIdleTimeout()); + } new_stream->streamInfo().setDownstreamBytesMeter(response_encoder.getStream().bytesMeter()); // If the network connection is backed up, the stream should be made aware of it on creation. // Both HTTP/1.x and HTTP/2 codecs handle this in StreamCallbackHelper::addCallbacksHelper. @@ -510,9 +515,6 @@ Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool handleCodecError(status.message()); return Network::FilterStatus::StopIteration; } else if (isEnvoyOverloadError(status)) { - // The other codecs aren't wired to send this status. - ASSERT(codec_->protocol() < Protocol::Http2, - "Expected only HTTP1.1 and below to send overload error."); stats_.named_.downstream_rq_overload_close_.inc(); handleCodecOverloadError(status.message()); return Network::FilterStatus::StopIteration; @@ -684,6 +686,23 @@ bool ConnectionManagerImpl::isPrematureRstStream(const ActiveStream& stream) con // Sends a GOAWAY if too many streams have been reset prematurely on this // connection. void ConnectionManagerImpl::maybeDrainDueToPrematureResets() { + // If the connection has been drained due to premature resets, do not check this again. + // Without this flag, recursion may occur, as shown in the following stack trace: + // + // maybeDrainDueToPrematureResets() + // doConnectionClose() + // resetAllStreams() + // onResetStream() + // doDeferredStreamDestroy() + // maybeDrainDueToPrematureResets() + // ... + // + // The recursion will continue until all streams are destroyed. If there are many streams + // that may result in a stack overflow. This flag is used to avoid above recursion. + if (drained_due_to_premature_resets_) { + return; + } + if (closed_non_internally_destroyed_requests_ == 0) { return; } @@ -706,6 +725,10 @@ void ConnectionManagerImpl::maybeDrainDueToPrematureResets() { if (read_callbacks_->connection().state() == Network::Connection::State::Open) { stats_.named_.downstream_rq_too_many_premature_resets_.inc(); + + // Mark the the connection has been drained due to too many premature resets. + drained_due_to_premature_resets_ = true; + doConnectionClose(Network::ConnectionCloseType::Abort, absl::nullopt, "too_many_premature_resets"); } @@ -828,8 +851,12 @@ ConnectionManagerImpl::ActiveStream::ActiveStream(ConnectionManagerImpl& connect connection_manager_.overload_manager_), request_response_timespan_(new Stats::HistogramCompletableTimespanImpl( connection_manager_.stats_.named_.downstream_rq_time_, connection_manager_.timeSource())), + has_explicit_global_flush_timeout_( + connection_manager.config_->streamFlushTimeout().has_value()), header_validator_( - connection_manager.config_->makeHeaderValidator(connection_manager.codec_->protocol())) { + connection_manager.config_->makeHeaderValidator(connection_manager.codec_->protocol())), + trace_refresh_after_route_refresh_(Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.trace_refresh_after_route_refresh")) { ASSERT(!connection_manager.config_->isRoutable() || ((connection_manager.config_->routeConfigProvider() == nullptr && connection_manager.config_->scopedRouteConfigProvider() != nullptr && @@ -1003,6 +1030,13 @@ void ConnectionManagerImpl::ActiveStream::onStreamMaxDurationReached() { } void ConnectionManagerImpl::ActiveStream::chargeStats(const ResponseHeaderMap& headers) { + if (trace_refresh_after_route_refresh_ && connection_manager_tracing_config_.has_value()) { + const Tracing::Decision tracing_decision = + Tracing::TracerUtility::shouldTraceRequest(filter_manager_.streamInfo()); + ConnectionManagerImpl::chargeTracingStats(tracing_decision.reason, + connection_manager_.config_->tracingStats()); + } + uint64_t response_code = Utility::getResponseStatus(headers); filter_manager_.streamInfo().setResponseCode(response_code); @@ -1061,6 +1095,78 @@ bool streamErrorOnlyErrors(absl::string_view error_details) { } } // namespace +void ConnectionManagerImpl::ActiveStream::setRequestDecorator(RequestHeaderMap& headers) { + ASSERT(active_span_ != nullptr); + + const Router::Decorator* decorater = + hasCachedRoute() ? cached_route_.value()->decorator() : nullptr; + + // If a decorator has been defined, apply it to the active span. + absl::string_view decorated_operation; + if (decorater != nullptr) { + decorated_operation = decorater->getOperation(); + + decorater->apply(*active_span_); + state_.decorated_propagate_ = decorater->propagate(); + } + + if (connection_manager_tracing_config_->operation_name_ == Tracing::OperationName::Egress) { + // For egress (outbound) requests, pass the decorator's operation name (if defined and + // propagation enabled) as a request header to enable the receiving service to use it in its + // server span. + if (!decorated_operation.empty() && state_.decorated_propagate_) { + headers.setEnvoyDecoratorOperation(decorated_operation); + } + } else { + absl::string_view req_operation_override = headers.getEnvoyDecoratorOperationValue(); + + // For ingress (inbound) requests, if a decorator operation name has been provided, it + // should be used to override the active span's operation. + if (!req_operation_override.empty()) { + active_span_->setOperation(req_operation_override); + + // Set the decorator operation as overridden to avoid propagating the route decorator + // operation to the client when the setResponseDecorator() is called. + state_.decorator_overriden_ = true; + } + // Remove header so not propagated to service + headers.removeEnvoyDecoratorOperation(); + } +} + +void ConnectionManagerImpl::ActiveStream::setResponseDecorator(ResponseHeaderMap& headers) { + ASSERT(active_span_ != nullptr); + + if (connection_manager_tracing_config_->operation_name_ == Tracing::OperationName::Ingress) { + // For ingress (inbound) responses, if the request headers do not include a + // decorator operation (override), and the decorated operation should be + // propagated, then pass the decorator's operation name (if defined) + // as a response header to enable the client service to use it in its client span. + if (state_.decorated_propagate_ && !state_.decorator_overriden_) { + const Router::Decorator* decorater = + hasCachedRoute() ? cached_route_.value()->decorator() : nullptr; + absl::string_view decorated_operation = + decorater != nullptr ? decorater->getOperation() : absl::string_view(); + + if (!decorated_operation.empty()) { + // If the decorator operation is defined, set it as the response header. + headers.setEnvoyDecoratorOperation(decorated_operation); + } + } + } else if (connection_manager_tracing_config_->operation_name_ == + Tracing::OperationName::Egress) { + const absl::string_view resp_operation_override = headers.getEnvoyDecoratorOperationValue(); + + // For Egress (outbound) response, if a decorator operation name has been provided, it + // should be used to override the active span's operation. + if (!resp_operation_override.empty()) { + active_span_->setOperation(resp_operation_override); + } + // Remove header so not propagated to service. + headers.removeEnvoyDecoratorOperation(); + } +} + bool ConnectionManagerImpl::ActiveStream::validateHeaders() { if (header_validator_) { auto validation_result = header_validator_->validateRequestHeaders(*request_headers_); @@ -1438,8 +1544,11 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt void ConnectionManagerImpl::ActiveStream::traceRequest() { const Tracing::Decision tracing_decision = Tracing::TracerUtility::shouldTraceRequest(filter_manager_.streamInfo()); - ConnectionManagerImpl::chargeTracingStats(tracing_decision.reason, - connection_manager_.config_->tracingStats()); + + if (!trace_refresh_after_route_refresh_) { + ConnectionManagerImpl::chargeTracingStats(tracing_decision.reason, + connection_manager_.config_->tracingStats()); + } Tracing::HttpTraceContext trace_context(*request_headers_); active_span_ = connection_manager_.tracer().startSpan( @@ -1449,47 +1558,7 @@ void ConnectionManagerImpl::ActiveStream::traceRequest() { return; } - // TODO: Need to investigate the following code based on the cached route, as may - // be broken in the case a filter changes the route. - - // If a decorator has been defined, apply it to the active span. - if (hasCachedRoute() && cached_route_.value()->decorator()) { - const Router::Decorator* decorator = cached_route_.value()->decorator(); - - decorator->apply(*active_span_); - - state_.decorated_propagate_ = decorator->propagate(); - - // Cache decorated operation. - if (!decorator->getOperation().empty()) { - decorated_operation_ = &decorator->getOperation(); - } - } - - if (connection_manager_tracing_config_->operation_name_ == Tracing::OperationName::Egress) { - // For egress (outbound) requests, pass the decorator's operation name (if defined and - // propagation enabled) as a request header to enable the receiving service to use it in its - // server span. - if (decorated_operation_ && state_.decorated_propagate_) { - request_headers_->setEnvoyDecoratorOperation(*decorated_operation_); - } - } else { - const HeaderEntry* req_operation_override = request_headers_->EnvoyDecoratorOperation(); - - // For ingress (inbound) requests, if a decorator operation name has been provided, it - // should be used to override the active span's operation. - if (req_operation_override) { - if (!req_operation_override->value().empty()) { - active_span_->setOperation(req_operation_override->value().getStringView()); - - // Clear the decorated operation so won't be used in the response header, as - // it has been overridden by the inbound decorator operation request header. - decorated_operation_ = nullptr; - } - // Remove header so not propagated to service - request_headers_->removeEnvoyDecoratorOperation(); - } - } + setRequestDecorator(*request_headers_); } void ConnectionManagerImpl::ActiveStream::decodeData(Buffer::Instance& data, bool end_stream) { @@ -1647,7 +1716,7 @@ void ConnectionManagerImpl::ActiveStream::refreshDurationTimeout() { // See how long this stream has been alive, and adjust the timeout // accordingly. - std::chrono::duration time_used = std::chrono::duration_cast( + std::chrono::milliseconds time_used = std::chrono::duration_cast( connection_manager_.timeSource().monotonicTime() - filter_manager_.streamInfo().startTimeMonotonic()); if (timeout > time_used) { @@ -1688,27 +1757,32 @@ void ConnectionManagerImpl::ActiveStream::refreshCachedRoute(const Router::Route setVirtualHostRoute(std::move(route_result)); } -void ConnectionManagerImpl::ActiveStream::refreshCachedTracingCustomTags() { - if (!connection_manager_tracing_config_.has_value()) { +void ConnectionManagerImpl::ActiveStream::refreshTracing() { + if (!trace_refresh_after_route_refresh_) { return; } - const Tracing::CustomTagMap& conn_manager_tags = connection_manager_tracing_config_->custom_tags_; - const Tracing::CustomTagMap* route_tags = nullptr; - if (hasCachedRoute() && cached_route_.value()->tracingConfig()) { - route_tags = &cached_route_.value()->tracingConfig()->getCustomTags(); - } - const bool configured_in_conn = !conn_manager_tags.empty(); - const bool configured_in_route = route_tags && !route_tags->empty(); - if (!configured_in_conn && !configured_in_route) { + + if (!connection_manager_tracing_config_.has_value() || active_span_ == nullptr || + request_headers_ == nullptr) { return; } - Tracing::CustomTagMap& custom_tag_map = getOrMakeTracingCustomTagMap(); - if (configured_in_route) { - custom_tag_map.insert(route_tags->begin(), route_tags->end()); - } - if (configured_in_conn) { - custom_tag_map.insert(conn_manager_tags.begin(), conn_manager_tags.end()); + + ASSERT(cached_route_.has_value()); + + // NOTE: if the trace reason have been encoded into the request id then the trace reason may + // not be updated. That means we may cannot to force a traced request to be untraced by the + // refreshing. + const auto trace_reason = ConnectionManagerUtility::mutateTracingRequestHeader( + *request_headers_, connection_manager_.runtime_, *connection_manager_.config_, + cached_route_.value().get()); + filter_manager_.streamInfo().setTraceReason(trace_reason); + const Tracing::Decision tracing_decision = + Tracing::TracerUtility::shouldTraceRequest(filter_manager_.streamInfo()); + if (active_span_->useLocalDecision()) { + active_span_->setSampled(tracing_decision.traced); } + + setRequestDecorator(*request_headers_); } // TODO(chaoqin-li1123): Make on demand vhds and on demand srds works at the same time. @@ -1860,29 +1934,8 @@ void ConnectionManagerImpl::ActiveStream::encodeHeaders(ResponseHeaderMap& heade } } - if (connection_manager_tracing_config_.has_value()) { - if (connection_manager_tracing_config_->operation_name_ == Tracing::OperationName::Ingress) { - // For ingress (inbound) responses, if the request headers do not include a - // decorator operation (override), and the decorated operation should be - // propagated, then pass the decorator's operation name (if defined) - // as a response header to enable the client service to use it in its client span. - if (decorated_operation_ && state_.decorated_propagate_) { - headers.setEnvoyDecoratorOperation(*decorated_operation_); - } - } else if (connection_manager_tracing_config_->operation_name_ == - Tracing::OperationName::Egress) { - const HeaderEntry* resp_operation_override = headers.EnvoyDecoratorOperation(); - - // For Egress (outbound) response, if a decorator operation name has been provided, it - // should be used to override the active span's operation. - if (resp_operation_override) { - if (!resp_operation_override->value().empty() && active_span_) { - active_span_->setOperation(resp_operation_override->value().getStringView()); - } - // Remove header so not propagated to service. - headers.removeEnvoyDecoratorOperation(); - } - } + if (connection_manager_tracing_config_.has_value() && active_span_ != nullptr) { + setResponseDecorator(headers); } chargeStats(headers); @@ -2039,8 +2092,29 @@ Tracing::OperationName ConnectionManagerImpl::ActiveStream::operationName() cons return connection_manager_tracing_config_->operation_name_; } -const Tracing::CustomTagMap* ConnectionManagerImpl::ActiveStream::customTags() const { - return tracing_custom_tags_.get(); +void ConnectionManagerImpl::ActiveStream::modifySpan(Tracing::Span& span) const { + ASSERT(connection_manager_tracing_config_.has_value()); + + const Tracing::HttpTraceContext trace_context(*request_headers_); + const Tracing::CustomTagContext ctx{trace_context, filter_manager_.streamInfo()}; + + // Cache the optional custom tags from the route first. + OptRef route_custom_tags; + + if (hasCachedRoute() && cached_route_.value()->tracingConfig() != nullptr) { + route_custom_tags.emplace(cached_route_.value()->tracingConfig()->getCustomTags()); + for (const auto& tag : *route_custom_tags) { + tag.second->applySpan(span, ctx); + } + } + + for (const auto& tag : connection_manager_tracing_config_->custom_tags_) { + if (!route_custom_tags.has_value() || !route_custom_tags->contains(tag.first)) { + // If the tag is defined in both the connection manager and the route, + // use the route's tag. + tag.second->applySpan(span, ctx); + } + } } bool ConnectionManagerImpl::ActiveStream::verbose() const { @@ -2147,32 +2221,45 @@ void ConnectionManagerImpl::ActiveStream::setVirtualHostRoute( filter_manager_.streamInfo().vhost_ = std::move(vhost_route.vhost); filter_manager_.streamInfo().setUpstreamClusterInfo(cached_cluster_info_.value()); - refreshCachedTracingCustomTags(); + refreshTracing(); refreshDurationTimeout(); - refreshIdleTimeout(); + refreshIdleAndFlushTimeouts(); } -void ConnectionManagerImpl::ActiveStream::refreshIdleTimeout() { - if (hasCachedRoute()) { - const Router::RouteEntry* route_entry = cached_route_.value()->routeEntry(); - if (route_entry != nullptr && route_entry->idleTimeout()) { - idle_timeout_ms_ = route_entry->idleTimeout().value(); - response_encoder_->getStream().setFlushTimeout(idle_timeout_ms_); - if (idle_timeout_ms_.count()) { - // If we have a route-level idle timeout but no global stream idle timeout, create a timer. - if (stream_idle_timer_ == nullptr) { - stream_idle_timer_ = connection_manager_.dispatcher_->createScaledTimer( - Event::ScaledTimerType::HttpDownstreamIdleStreamTimeout, - [this]() -> void { onIdleTimeout(); }); - } - } else if (stream_idle_timer_ != nullptr) { - // If we had a global stream idle timeout but the route-level idle timeout is set to zero - // (to override), we disable the idle timer. - stream_idle_timer_->disableTimer(); - stream_idle_timer_ = nullptr; +void ConnectionManagerImpl::ActiveStream::refreshIdleAndFlushTimeouts() { + if (!hasCachedRoute()) { + return; + } + const Router::RouteEntry* route_entry = cached_route_.value()->routeEntry(); + if (route_entry == nullptr) { + return; + } + + if (route_entry->idleTimeout().has_value()) { + idle_timeout_ms_ = route_entry->idleTimeout().value(); + if (idle_timeout_ms_.count()) { + // If we have a route-level idle timeout but no global stream idle timeout, create a timer. + if (stream_idle_timer_ == nullptr) { + stream_idle_timer_ = connection_manager_.dispatcher_->createScaledTimer( + Event::ScaledTimerType::HttpDownstreamIdleStreamTimeout, + [this]() -> void { onIdleTimeout(); }); } + } else if (stream_idle_timer_ != nullptr) { + // If we had a global stream idle timeout but the route-level idle timeout is set to zero + // (to override), we disable the idle timer. + stream_idle_timer_->disableTimer(); + stream_idle_timer_ = nullptr; } } + + if (route_entry->flushTimeout().has_value()) { + response_encoder_->getStream().setFlushTimeout(route_entry->flushTimeout().value()); + } else if (!has_explicit_global_flush_timeout_ && route_entry->idleTimeout().has_value()) { + // If there is no route-level flush timeout, and the global flush timeout was also inherited + // from the idle timeout, also inherit the route-level idle timeout. This is for backwards + // compatibility. + response_encoder_->getStream().setFlushTimeout(idle_timeout_ms_); + } } void ConnectionManagerImpl::ActiveStream::refreshAccessLogFlushTimer() { @@ -2190,11 +2277,7 @@ void ConnectionManagerImpl::ActiveStream::clearRouteCache() { } setCachedRoute({}); - cached_cluster_info_ = absl::optional(); - if (tracing_custom_tags_) { - tracing_custom_tags_->clear(); - } } void ConnectionManagerImpl::ActiveStream::refreshRouteCluster() { diff --git a/source/common/http/conn_manager_impl.h b/source/common/http/conn_manager_impl.h index 812530b06c8c0..4a6743e0e27fa 100644 --- a/source/common/http/conn_manager_impl.h +++ b/source/common/http/conn_manager_impl.h @@ -332,10 +332,13 @@ class ConnectionManagerImpl : Logger::Loggable, void refreshCachedRoute(const Router::RouteCallback& cb); - void refreshCachedTracingCustomTags(); void refreshDurationTimeout(); - void refreshIdleTimeout(); + void refreshIdleAndFlushTimeouts(); void refreshAccessLogFlushTimer(); + void refreshTracing(); + + void setRequestDecorator(RequestHeaderMap& headers); + void setResponseDecorator(ResponseHeaderMap& headers); // All state for the stream. Put here for readability. struct State { @@ -370,6 +373,9 @@ class ConnectionManagerImpl : Logger::Loggable, bool decorated_propagate_ : 1; + // True if the decorator operation is overridden by the request header. + bool decorator_overriden_ : 1 = false; + // Indicates that sending headers to the filter manager is deferred to the // next I/O cycle. If data or trailers are received when this flag is set // they are deferred too. @@ -407,13 +413,6 @@ class ConnectionManagerImpl : Logger::Loggable, return os; } - Tracing::CustomTagMap& getOrMakeTracingCustomTagMap() { - if (tracing_custom_tags_ == nullptr) { - tracing_custom_tags_ = std::make_unique(); - } - return *tracing_custom_tags_; - } - // Note: this method is a noop unless ENVOY_ENABLE_UHV is defined // Call header validator extension to validate request header map after it was deserialized. // If header map failed validation, it sends an error response and returns false. @@ -474,6 +473,11 @@ class ConnectionManagerImpl : Logger::Loggable, Event::TimerPtr access_log_flush_timer_; std::chrono::milliseconds idle_timeout_ms_{}; + // If an explicit global flush timeout is set, never override it with the route entry idle + // timeout. If there is no explicit global flush timeout, then override with the route entry + // idle timeout if it exists. This is to prevent breaking existing user expectations that the + // flush timeout is the same as the idle timeout. + const bool has_explicit_global_flush_timeout_{false}; State state_; // Snapshot of the route configuration at the time of request is started. This is used to ensure @@ -505,9 +509,7 @@ class ConnectionManagerImpl : Logger::Loggable, absl::InlinedVector cleared_cached_routes_; absl::optional cached_cluster_info_; - const std::string* decorated_operation_{nullptr}; absl::optional> route_config_update_requester_; - std::unique_ptr tracing_custom_tags_{nullptr}; Http::ServerHeaderValidatorPtr header_validator_; friend FilterManager; @@ -517,7 +519,7 @@ class ConnectionManagerImpl : Logger::Loggable, // returned by the public tracingConfig() method. // Tracing::TracingConfig Tracing::OperationName operationName() const override; - const Tracing::CustomTagMap* customTags() const override; + void modifySpan(Tracing::Span& span) const override; bool verbose() const override; uint32_t maxPathTagLength() const override; bool spawnUpstreamSpan() const override; @@ -526,6 +528,7 @@ class ConnectionManagerImpl : Logger::Loggable, std::unique_ptr deferred_data_; std::queue deferred_metadata_; RequestTrailerMapPtr deferred_request_trailers_; + const bool trace_refresh_after_route_refresh_{true}; }; using ActiveStreamPtr = std::unique_ptr; @@ -674,6 +677,9 @@ class ConnectionManagerImpl : Logger::Loggable, // request was incomplete at response completion, the stream is reset. const bool allow_upstream_half_close_{}; + + // Whether the connection manager is drained due to premature resets. + bool drained_due_to_premature_resets_{false}; }; } // namespace Http diff --git a/source/common/http/conn_pool_base.cc b/source/common/http/conn_pool_base.cc index 896c0596cff01..e3733f97e97dc 100644 --- a/source/common/http/conn_pool_base.cc +++ b/source/common/http/conn_pool_base.cc @@ -87,6 +87,10 @@ void HttpConnPoolImplBase::onPoolReady(Envoy::ConnectionPool::ActiveClient& clie auto& http_context = typedContext(context); Http::ResponseDecoder& response_decoder = *http_context.decoder_; Http::ConnectionPool::Callbacks& callbacks = *http_context.callbacks_; + + // Track this request on the connection + http_client->request_count_++; + Http::RequestEncoder& new_encoder = http_client->newStreamEncoder(response_decoder); callbacks.onPoolReady(new_encoder, client.real_host_description_, http_client->codec_client_->streamInfo(), diff --git a/source/common/http/conn_pool_base.h b/source/common/http/conn_pool_base.h index 5927d3f8a2644..4b0b0fd3674ea 100644 --- a/source/common/http/conn_pool_base.h +++ b/source/common/http/conn_pool_base.h @@ -140,6 +140,12 @@ class ActiveClient : public Envoy::ConnectionPool::ActiveClient { void close() override { codec_client_->close(); } virtual Http::RequestEncoder& newStreamEncoder(Http::ResponseDecoder& response_decoder) PURE; void onEvent(Network::ConnectionEvent event) override { + // Record request metrics only for successfully connected connections that handled requests + if ((event == Network::ConnectionEvent::LocalClose || + event == Network::ConnectionEvent::RemoteClose) && + hasHandshakeCompleted()) { + parent_.host()->cluster().trafficStats()->upstream_rq_per_cx_.recordValue(request_count_); + } parent_.onConnectionEvent(*this, codec_client_->connectionFailureReason(), event); } uint32_t numActiveStreams() const override { return codec_client_->numActiveRequests(); } @@ -147,6 +153,8 @@ class ActiveClient : public Envoy::ConnectionPool::ActiveClient { HttpConnPoolImplBase& parent() { return *static_cast(&parent_); } Http::CodecClientPtr codec_client_; + // Request tracking for HTTP protocols + uint32_t request_count_{0}; }; /* An implementation of Envoy::ConnectionPool::ConnPoolImplBase for HTTP/1 and HTTP/2 diff --git a/source/common/http/conn_pool_grid.cc b/source/common/http/conn_pool_grid.cc index 6b2dfe93fd4d9..efd45066c9068 100644 --- a/source/common/http/conn_pool_grid.cc +++ b/source/common/http/conn_pool_grid.cc @@ -33,8 +33,7 @@ std::string getTargetHostname(const Network::TransportSocketOptionsConstSharedPt } std::string default_sni = std::string(host->transportSocketFactory().defaultServerNameIndication()); - if (!default_sni.empty() || - !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.allow_alt_svc_for_ips")) { + if (!default_sni.empty()) { return default_sni; } // If there's no configured SNI the hostname is probably an IP address. Return it here. diff --git a/source/common/http/filter_manager.cc b/source/common/http/filter_manager.cc index 2db8bec5d04de..9e124fe23e8ca 100644 --- a/source/common/http/filter_manager.cc +++ b/source/common/http/filter_manager.cc @@ -12,6 +12,7 @@ #include "source/common/http/header_map_impl.h" #include "source/common/http/header_utility.h" #include "source/common/http/utility.h" +#include "source/common/runtime/runtime_features.h" #include "matching/data_impl.h" @@ -45,6 +46,16 @@ void recordLatestDataFilter(typename Filters::Iterator current_filter, } } +void finalizeHeaders(FilterManagerCallbacks& callbacks, StreamInfo::StreamInfo& stream_info, + ResponseHeaderMap& headers) { + const Router::RouteConstSharedPtr& route = stream_info.route(); + if (route != nullptr && route->routeEntry() != nullptr) { + const Formatter::HttpFormatterContext formatter_context{ + callbacks.requestHeaders().ptr(), &headers, {}, {}, {}, &callbacks.activeSpan()}; + route->routeEntry()->finalizeResponseHeaders(headers, formatter_context, stream_info); + } +} + } // namespace void ActiveStreamFilterBase::commonContinue() { @@ -373,10 +384,7 @@ bool ActiveStreamEncoderFilter::canContinue() { // As with ActiveStreamDecoderFilter::canContinue() make sure we do not // continue if a local reply has been sent or ActiveStreamDecoderFilter::recreateStream() is // called, etc. - return !parent_.state_.encoder_filter_chain_complete_ && - (!Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.filter_chain_aborted_can_not_continue") || - !parent_.stopEncoderFilterChain()); + return !parent_.state_.encoder_filter_chain_complete_ && !parent_.stopEncoderFilterChain(); } Buffer::InstancePtr ActiveStreamDecoderFilter::createBuffer() { @@ -1014,9 +1022,7 @@ void DownstreamFilterManager::sendLocalReply( if (!filter_manager_callbacks_.responseHeaders().has_value() && (!filter_manager_callbacks_.informationalHeaders().has_value() || - (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.local_reply_traverses_filter_chain_after_1xx") && - !(state_.filter_call_state_ & FilterCallState::IsEncodingMask)))) { + !(state_.filter_call_state_ & FilterCallState::IsEncodingMask))) { // If the response has not started at all, or if the only response so far is an informational // 1xx that has already been fully processed, send the response through the filter chain. @@ -1026,10 +1032,16 @@ void DownstreamFilterManager::sendLocalReply( // route refreshment in the response filter chain. cb->route(nullptr); } - - // We only prepare a local reply to execute later if we're actively - // invoking filters to avoid re-entrant in filters. - if (state_.filter_call_state_ & FilterCallState::IsDecodingMask) { + // We only prepare a local reply to execute later if we're actively invoking filters to avoid + // re-entrant in filters. + // + // For reverse connections (where upstream initiates the connection to downstream), we need to + // send local replies immediately rather than queuing them. This ensures proper handling of the + // reversed connection flow and prevents potential issues with connection state and filter chain + // processing. + if (!Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.reverse_conn_force_local_reply") && + (state_.filter_call_state_ & FilterCallState::IsDecodingMask)) { prepareLocalReplyViaFilterChain(is_grpc_request, code, body, modify_headers, is_head_request, grpc_status, details); } else { @@ -1075,9 +1087,7 @@ void DownstreamFilterManager::prepareLocalReplyViaFilterChain( prepared_local_reply_ = Utility::prepareLocalReply( Utility::EncodeFunctions{ [this, modify_headers](ResponseHeaderMap& headers) -> void { - if (streamInfo().route() && streamInfo().route()->routeEntry()) { - streamInfo().route()->routeEntry()->finalizeResponseHeaders(headers, streamInfo()); - } + finalizeHeaders(filter_manager_callbacks_, streamInfo(), headers); if (modify_headers) { modify_headers(headers); } @@ -1126,9 +1136,7 @@ void DownstreamFilterManager::sendLocalReplyViaFilterChain( state_.destroyed_, Utility::EncodeFunctions{ [this, modify_headers](ResponseHeaderMap& headers) -> void { - if (streamInfo().route() && streamInfo().route()->routeEntry()) { - streamInfo().route()->routeEntry()->finalizeResponseHeaders(headers, streamInfo()); - } + finalizeHeaders(filter_manager_callbacks_, streamInfo(), headers); if (modify_headers) { modify_headers(headers); } @@ -1159,9 +1167,7 @@ void DownstreamFilterManager::sendDirectLocalReply( state_.destroyed_, Utility::EncodeFunctions{ [this, modify_headers](ResponseHeaderMap& headers) -> void { - if (streamInfo().route() && streamInfo().route()->routeEntry()) { - streamInfo().route()->routeEntry()->finalizeResponseHeaders(headers, streamInfo()); - } + finalizeHeaders(filter_manager_callbacks_, streamInfo(), headers); if (modify_headers) { modify_headers(headers); } @@ -1638,7 +1644,7 @@ void FilterManager::callLowWatermarkCallbacks() { } } -void FilterManager::setBufferLimit(uint32_t new_limit) { +void FilterManager::setBufferLimit(uint64_t new_limit) { ENVOY_STREAM_LOG(debug, "setting buffer limit to {}", *this, new_limit); buffer_limit_ = new_limit; if (buffered_request_data_) { @@ -1753,11 +1759,11 @@ void ActiveStreamDecoderFilter::removeDownstreamWatermarkCallbacks( parent_.watermark_callbacks_.remove(&watermark_callbacks); } -void ActiveStreamDecoderFilter::setDecoderBufferLimit(uint32_t limit) { +void ActiveStreamDecoderFilter::setDecoderBufferLimit(uint64_t limit) { parent_.setBufferLimit(limit); } -uint32_t ActiveStreamDecoderFilter::decoderBufferLimit() { return parent_.buffer_limit_; } +uint64_t ActiveStreamDecoderFilter::decoderBufferLimit() { return parent_.buffer_limit_; } bool ActiveStreamDecoderFilter::recreateStream(const ResponseHeaderMap* headers) { // Because the filter's and the HCM view of if the stream has a body and if diff --git a/source/common/http/filter_manager.h b/source/common/http/filter_manager.h index 3819705f77e32..5f1b179e91fc8 100644 --- a/source/common/http/filter_manager.h +++ b/source/common/http/filter_manager.h @@ -48,7 +48,7 @@ class LocalReplyOwnerObject : public StreamInfo::FilterState::Object { : filter_config_name_(filter_config_name) {} ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(filter_config_name_); return message; } @@ -289,8 +289,8 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, void addDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks& watermark_callbacks) override; void removeDownstreamWatermarkCallbacks(DownstreamWatermarkCallbacks& watermark_callbacks) override; - void setDecoderBufferLimit(uint32_t limit) override; - uint32_t decoderBufferLimit() override; + void setDecoderBufferLimit(uint64_t limit) override; + uint64_t decoderBufferLimit() override; bool recreateStream(const Http::ResponseHeaderMap* original_response_headers) override; void addUpstreamSocketOptions(const Network::Socket::OptionsSharedPtr& options) override; @@ -682,7 +682,7 @@ class FilterManager : public ScopeTrackedObject, FilterManager(FilterManagerCallbacks& filter_manager_callbacks, Event::Dispatcher& dispatcher, OptRef connection, uint64_t stream_id, Buffer::BufferMemoryAccountSharedPtr account, bool proxy_100_continue, - uint32_t buffer_limit) + uint64_t buffer_limit) : filter_manager_callbacks_(filter_manager_callbacks), dispatcher_(dispatcher), connection_(connection), stream_id_(stream_id), account_(std::move(account)), proxy_100_continue_(proxy_100_continue), buffer_limit_(buffer_limit) {} @@ -800,7 +800,7 @@ class FilterManager : public ScopeTrackedObject, virtual void executeLocalReplyIfPrepared() PURE; // Possibly increases buffer_limit_ to the value of limit. - void setBufferLimit(uint32_t limit); + void setBufferLimit(uint64_t limit); /** * @return bool whether any above high watermark triggers are currently active @@ -1114,7 +1114,7 @@ class FilterManager : public ScopeTrackedObject, std::unique_ptr request_metadata_map_vector_; Buffer::InstancePtr buffered_response_data_; Buffer::InstancePtr buffered_request_data_; - uint32_t buffer_limit_{0}; + uint64_t buffer_limit_{0}; uint32_t high_watermark_count_{0}; std::list watermark_callbacks_; Network::Socket::OptionsSharedPtr upstream_options_ = @@ -1181,9 +1181,7 @@ class DownstreamFilterManager : public FilterManager { std::move(parent_filter_state)), local_reply_(local_reply), filter_chain_factory_(filter_chain_factory), downstream_filter_load_shed_point_(overload_manager.getLoadShedPoint( - Server::LoadShedPointName::get().HttpDownstreamFilterCheck)), - use_filter_manager_state_for_downstream_end_stream_(Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.use_filter_manager_state_for_downstream_end_stream")) { + Server::LoadShedPointName::get().HttpDownstreamFilterCheck)) { ENVOY_LOG_ONCE_IF( trace, downstream_filter_load_shed_point_ == nullptr, "LoadShedPoint envoy.load_shed_points.http_downstream_filter_check is not found. " @@ -1219,15 +1217,7 @@ class DownstreamFilterManager : public FilterManager { /** * Whether downstream has observed end_stream. */ - bool decoderObservedEndStream() const override { - // Set by the envoy.reloadable_features.use_filter_manager_state_for_downstream_end_stream - // runtime flag. - if (use_filter_manager_state_for_downstream_end_stream_) { - return state_.observed_decode_end_stream_; - } - - return hasLastDownstreamByteReceived(); - } + bool decoderObservedEndStream() const override { return state_.observed_decode_end_stream_; } /** * Return true if the timestamp of the downstream end_stream was recorded. @@ -1284,9 +1274,6 @@ class DownstreamFilterManager : public FilterManager { const FilterChainFactory& filter_chain_factory_; Utility::PreparedLocalReplyPtr prepared_local_reply_{nullptr}; Server::LoadShedPoint* downstream_filter_load_shed_point_{nullptr}; - // Set by the envoy.reloadable_features.use_filter_manager_state_for_downstream_end_stream runtime - // flag. - const bool use_filter_manager_state_for_downstream_end_stream_{}; }; } // namespace Http diff --git a/source/common/http/header_map_impl.cc b/source/common/http/header_map_impl.cc index 4a6e8ac2c8248..1a5b4e6886a1a 100644 --- a/source/common/http/header_map_impl.cc +++ b/source/common/http/header_map_impl.cc @@ -10,7 +10,6 @@ #include "source/common/common/assert.h" #include "source/common/common/dump_state_utils.h" #include "source/common/common/empty_string.h" -#include "source/common/runtime/runtime_features.h" #include "source/common/singleton/const_singleton.h" #include "absl/strings/match.h" diff --git a/source/common/http/header_map_impl.h b/source/common/http/header_map_impl.h index 24ebc79bd414b..f053e42cacbd5 100644 --- a/source/common/http/header_map_impl.h +++ b/source/common/http/header_map_impl.h @@ -15,7 +15,6 @@ #include "source/common/common/non_copyable.h" #include "source/common/common/utility.h" #include "source/common/http/headers.h" -#include "source/common/runtime/runtime_features.h" namespace Envoy { namespace Http { diff --git a/source/common/http/header_mutation.cc b/source/common/http/header_mutation.cc index 3b438c52e47d5..5eccf09fea2ae 100644 --- a/source/common/http/header_mutation.cc +++ b/source/common/http/header_mutation.cc @@ -1,5 +1,6 @@ #include "source/common/http/header_mutation.h" +#include "source/common/common/matchers.h" #include "source/common/router/header_parser.h" namespace Envoy { @@ -65,18 +66,38 @@ class RemoveMutation : public HeaderEvaluator { private: const Envoy::Http::LowerCaseString header_name_; }; + +class RemoveOnMatchMutation : public HeaderEvaluator { +public: + RemoveOnMatchMutation(const envoy::type::matcher::v3::StringMatcher& key_matcher, + Server::Configuration::CommonFactoryContext& context) + : key_matcher_(key_matcher, context) {} + + void evaluateHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext&, + const StreamInfo::StreamInfo&) const override { + headers.removeIf([this](const Http::HeaderEntry& header) { + return key_matcher_.match(header.key().getStringView()); + }); + } + +private: + const Matchers::StringMatcherImpl key_matcher_; +}; + } // namespace absl::StatusOr> -HeaderMutations::create(const ProtoHeaderMutatons& header_mutations) { +HeaderMutations::create(const ProtoHeaderMutatons& header_mutations, + Server::Configuration::CommonFactoryContext& context) { absl::Status creation_status = absl::OkStatus(); - auto ret = - std::unique_ptr(new HeaderMutations(header_mutations, creation_status)); + auto ret = std::unique_ptr( + new HeaderMutations(header_mutations, context, creation_status)); RETURN_IF_NOT_OK(creation_status); return ret; } HeaderMutations::HeaderMutations(const ProtoHeaderMutatons& header_mutations, + Server::Configuration::CommonFactoryContext& context, absl::Status& creation_status) { for (const auto& mutation : header_mutations) { switch (mutation.action_case()) { @@ -90,6 +111,10 @@ HeaderMutations::HeaderMutations(const ProtoHeaderMutatons& header_mutations, case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kRemove: header_mutations_.emplace_back(std::make_unique(mutation.remove())); break; + case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kRemoveOnMatch: + header_mutations_.emplace_back(std::make_unique( + mutation.remove_on_match().key_matcher(), context)); + break; default: PANIC_DUE_TO_PROTO_UNSET; } diff --git a/source/common/http/header_mutation.h b/source/common/http/header_mutation.h index fb32a15530e19..0d38ce771b1fa 100644 --- a/source/common/http/header_mutation.h +++ b/source/common/http/header_mutation.h @@ -15,14 +15,17 @@ using ProtoHeaderValueOption = envoy::config::core::v3::HeaderValueOption; class HeaderMutations : public HeaderEvaluator { public: static absl::StatusOr> - create(const ProtoHeaderMutatons& header_mutations); + create(const ProtoHeaderMutatons& header_mutations, + Server::Configuration::CommonFactoryContext& context); // Http::HeaderEvaluator void evaluateHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; private: - HeaderMutations(const ProtoHeaderMutatons& header_mutations, absl::Status& creation_status); + HeaderMutations(const ProtoHeaderMutatons& header_mutations, + Server::Configuration::CommonFactoryContext& context, + absl::Status& creation_status); std::vector> header_mutations_; }; diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index aa28156d5ff1f..f18bcbe5f07bc 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -236,12 +236,9 @@ bool HeaderUtility::authorityIsValid(const absl::string_view header_value) { } bool HeaderUtility::isSpecial1xx(const ResponseHeaderMap& response_headers) { - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.proxy_104") && - response_headers.Status()->value() == "104") { - return true; - } return response_headers.Status()->value() == "100" || - response_headers.Status()->value() == "102" || response_headers.Status()->value() == "103"; + response_headers.Status()->value() == "102" || + response_headers.Status()->value() == "103" || response_headers.Status()->value() == "104"; } bool HeaderUtility::isConnect(const RequestHeaderMap& headers) { diff --git a/source/common/http/headers.h b/source/common/http/headers.h index 8d66a6b139de5..03695a125ba12 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -240,6 +240,8 @@ class HeaderValues { const LowerCaseString WWWAuthenticate{"www-authenticate"}; const LowerCaseString XContentTypeOptions{"x-content-type-options"}; const LowerCaseString EarlyData{"early-data"}; + const LowerCaseString EnvoyDstNodeUUID{"x-remote-node-id"}; + const LowerCaseString EnvoyDstClusterUUID{"x-dst-cluster-uuid"}; struct { const std::string Close{"close"}; diff --git a/source/common/http/http1/balsa_parser.cc b/source/common/http/http1/balsa_parser.cc index 2642e3692bea7..23dd28a4e6468 100644 --- a/source/common/http/http1/balsa_parser.cc +++ b/source/common/http/http1/balsa_parser.cc @@ -366,7 +366,7 @@ void BalsaParser::MessageDone() { if (status_ == ParserStatus::Error || // In the case of early 1xx, MessageDone() can be called twice in a row. // The !first_byte_processed_ check is to make this function idempotent. - (wait_for_first_byte_before_msg_done_ && !first_byte_processed_)) { + !first_byte_processed_) { return; } status_ = convertResult(connection_->onMessageComplete()); diff --git a/source/common/http/http1/balsa_parser.h b/source/common/http/http1/balsa_parser.h index e4475a3983932..609bfe5ac88eb 100644 --- a/source/common/http/http1/balsa_parser.h +++ b/source/common/http/http1/balsa_parser.h @@ -85,9 +85,6 @@ class BalsaParser : public Parser, public quiche::BalsaVisitorInterface { // Latched value of `envoy.reloadable_features.http1_balsa_delay_reset`. const bool delay_reset_ = Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http1_balsa_delay_reset"); - // Latched value of `envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done`. - const bool wait_for_first_byte_before_msg_done_ = Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done"); // Latched value of `envoy.reloadable_features.http1_balsa_allow_cr_or_lf_at_request_start`. const bool allow_newlines_between_requests_ = Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.http1_balsa_allow_cr_or_lf_at_request_start"); diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index 343c1e8b28448..fef23db545003 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -120,7 +120,10 @@ void StreamEncoderImpl::encodeHeader(absl::string_view key, absl::string_view va const uint64_t header_size = connection_.buffer().addFragments({key, COLON_SPACE, value, CRLF}); + // There is no header field compression in HTTP/1.1, so the wire representation is the same as the + // decompressed representation. bytes_meter_->addHeaderBytesSent(header_size); + bytes_meter_->addDecompressedHeaderBytesSent(header_size); } void StreamEncoderImpl::encodeFormattedHeader(absl::string_view key, absl::string_view value, @@ -838,6 +841,10 @@ StatusOr ConnectionImpl::onHeadersCompleteImpl() { ENVOY_CONN_LOG(trace, "onHeadersCompleteImpl", connection_); RETURN_IF_ERROR(completeCurrentHeader()); + // There is no header field compression in HTTP/1.1, so the wire representation is the same as the + // decompressed representation. + getBytesMeter().addDecompressedHeaderBytesReceived(getBytesMeter().headerBytesReceived()); + if (!parser_->isHttp11()) { // This is not necessarily true, but it's good enough since higher layers only care if this is // HTTP/1.1 or not. diff --git a/source/common/http/http1/settings.cc b/source/common/http/http1/settings.cc index 35eceba9874e4..e530690e5b740 100644 --- a/source/common/http/http1/settings.cc +++ b/source/common/http/http1/settings.cc @@ -52,7 +52,7 @@ Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOpt Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, Server::Configuration::CommonFactoryContext& context, ProtobufMessage::ValidationVisitor& validation_visitor, - const ProtobufWkt::BoolValue& hcm_stream_error, + const Protobuf::BoolValue& hcm_stream_error, bool validate_scheme) { Http1Settings ret = parseHttp1Settings(config, context, validation_visitor); ret.validate_scheme_ = validate_scheme; diff --git a/source/common/http/http1/settings.h b/source/common/http/http1/settings.h index 51ed573b426e1..0e808d61c115e 100644 --- a/source/common/http/http1/settings.h +++ b/source/common/http/http1/settings.h @@ -20,8 +20,7 @@ Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOpt Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, Server::Configuration::CommonFactoryContext& context, ProtobufMessage::ValidationVisitor& validation_visitor, - const ProtobufWkt::BoolValue& hcm_stream_error, - bool validate_scheme); + const Protobuf::BoolValue& hcm_stream_error, bool validate_scheme); } // namespace Http1 } // namespace Http diff --git a/source/common/http/http2/BUILD b/source/common/http/http2/BUILD index 34b9dadd3868f..b01b0f9d7235c 100644 --- a/source/common/http/http2/BUILD +++ b/source/common/http/http2/BUILD @@ -39,6 +39,7 @@ envoy_cc_library( "//envoy/http:codes_interface", "//envoy/http:header_map_interface", "//envoy/network:connection_interface", + "//envoy/server/overload:overload_manager_interface", "//envoy/stats:stats_interface", "//source/common/buffer:buffer_lib", "//source/common/buffer:watermark_buffer_lib", @@ -50,7 +51,6 @@ envoy_cc_library( "//source/common/common:utility_lib", "//source/common/http:codec_helper_lib", "//source/common/http:codes_lib", - "//source/common/http:exception_lib", "//source/common/http:header_map_lib", "//source/common/http:header_utility_lib", "//source/common/http:headers_lib", diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index c03b77f372d8e..b3dd8917337d6 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -15,12 +15,9 @@ #include "source/common/common/cleanup.h" #include "source/common/common/dump_state_utils.h" #include "source/common/common/enum_to_int.h" -#include "source/common/common/fmt.h" -#include "source/common/common/safe_memcpy.h" #include "source/common/common/scope_tracker.h" #include "source/common/common/utility.h" #include "source/common/http/codes.h" -#include "source/common/http/exception.h" #include "source/common/http/header_utility.h" #include "source/common/http/headers.h" #include "source/common/http/http2/codec_stats.h" @@ -28,7 +25,6 @@ #include "source/common/runtime/runtime_features.h" #include "absl/cleanup/cleanup.h" -#include "absl/container/fixed_array.h" #include "quiche/common/quiche_endian.h" #include "quiche/http2/adapter/nghttp2_adapter.h" #include "quiche/http2/adapter/oghttp2_adapter.h" @@ -262,6 +258,9 @@ void ConnectionImpl::ServerStreamImpl::encode1xxHeaders(const ResponseHeaderMap& void ConnectionImpl::StreamImpl::encodeHeadersBase(const HeaderMap& headers, bool end_stream) { local_end_stream_ = end_stream; + + bytes_meter_->addDecompressedHeaderBytesSent(headers.byteSize()); + submitHeaders(headers, end_stream); if (parent_.sendPendingFramesAndHandleError()) { // Intended to check through coverage that this error case is tested @@ -332,6 +331,9 @@ void ConnectionImpl::StreamImpl::encodeTrailersBase(const HeaderMap& trailers) { parent_.updateActiveStreamsOnEncode(*this); ASSERT(!local_end_stream_); local_end_stream_ = true; + + bytes_meter_->addDecompressedHeaderBytesSent(trailers.byteSize()); + if (pending_send_data_->length() > 0) { // In this case we want trailers to come after we release all pending body data that is // waiting on window updates. We need to save the trailers so that we can emit them later. @@ -1232,7 +1234,7 @@ int ConnectionImpl::onFrameSend(int32_t stream_id, size_t length, uint8_t type, // teardown. As part of the work to remove exceptions we should aim to clean up all of this // error handling logic and only handle this type of case at the end of dispatch. for (auto& stream : active_streams_) { - stream->disarmStreamIdleTimer(); + stream->disarmStreamFlushTimer(); } return ERR_CALLBACK_FAILURE; } @@ -1521,6 +1523,8 @@ int ConnectionImpl::saveHeader(int32_t stream_id, HeaderString&& name, HeaderStr return 0; } + stream->bytes_meter_->addDecompressedHeaderBytesReceived(name.size() + value.size()); + // TODO(10646): Switch to use HeaderUtility::checkHeaderNameForUnderscores(). auto should_return = checkHeaderNameForUnderscores(name.getStringView()); if (should_return) { @@ -1999,7 +2003,7 @@ ConnectionImpl::ClientHttp2Options::ClientHttp2Options( : Http2Options(http2_options, max_headers_kb) { og_options_.perspective = http2::adapter::Perspective::kClient; og_options_.remote_max_concurrent_streams = - ::Envoy::Http2::Utility::OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS; + ::Envoy::Http2::Utility::OptionsLimits::MAX_MAX_CONCURRENT_STREAMS; #ifdef ENVOY_NGHTTP2 // Temporarily disable initial max streams limit/protection, since we might want to create @@ -2007,7 +2011,7 @@ ConnectionImpl::ClientHttp2Options::ClientHttp2Options( // // TODO(PiotrSikora): remove this once multiple upstream connections or queuing are implemented. nghttp2_option_set_peer_max_concurrent_streams( - options_, ::Envoy::Http2::Utility::OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS); + options_, ::Envoy::Http2::Utility::OptionsLimits::MAX_MAX_CONCURRENT_STREAMS); // nghttp2 REQUIRES setting max number of CONTINUATION frames. // 1024 is chosen to accommodate Envoy's 8Mb max limit of max_request_headers_kb @@ -2214,10 +2218,16 @@ ServerConnectionImpl::ServerConnectionImpl( max_request_headers_count), callbacks_(callbacks), headers_with_underscores_action_(headers_with_underscores_action), should_send_go_away_on_dispatch_(overload_manager.getLoadShedPoint( - Server::LoadShedPointName::get().H2ServerGoAwayOnDispatch)) { + Server::LoadShedPointName::get().H2ServerGoAwayOnDispatch)), + should_send_go_away_and_close_on_dispatch_(overload_manager.getLoadShedPoint( + Server::LoadShedPointName::get().H2ServerGoAwayAndCloseOnDispatch)) { ENVOY_LOG_ONCE_IF(trace, should_send_go_away_on_dispatch_ == nullptr, "LoadShedPoint envoy.load_shed_points.http2_server_go_away_on_dispatch is not " "found. Is it configured?"); + ENVOY_LOG_ONCE_IF( + trace, should_send_go_away_and_close_on_dispatch_ == nullptr, + "LoadShedPoint envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch is not " + "found. Is it configured?"); Http2Options h2_options(http2_options, max_request_headers_kb); auto direct_visitor = std::make_unique(this); @@ -2283,6 +2293,13 @@ int ServerConnectionImpl::onHeader(int32_t stream_id, HeaderString&& name, Heade Http::Status ServerConnectionImpl::dispatch(Buffer::Instance& data) { // Make sure downstream outbound queue was not flooded by the upstream frames. RETURN_IF_ERROR(protocol_constraints_.checkOutboundFrameLimits()); + if (should_send_go_away_and_close_on_dispatch_ != nullptr && + should_send_go_away_and_close_on_dispatch_->shouldShedLoad()) { + ConnectionImpl::goAway(); + sent_go_away_on_dispatch_ = true; + return envoyOverloadError( + "Load shed point http2_server_go_away_and_close_on_dispatch triggered"); + } if (should_send_go_away_on_dispatch_ != nullptr && !sent_go_away_on_dispatch_ && should_send_go_away_on_dispatch_->shouldShedLoad()) { ConnectionImpl::goAway(); diff --git a/source/common/http/http2/codec_impl.h b/source/common/http/http2/codec_impl.h index a3a118c38e6fe..ae0757576489d 100644 --- a/source/common/http/http2/codec_impl.h +++ b/source/common/http/http2/codec_impl.h @@ -15,14 +15,12 @@ #include "envoy/event/deferred_deletable.h" #include "envoy/http/codec.h" #include "envoy/network/connection.h" +#include "envoy/server/overload/overload_manager.h" #include "source/common/buffer/buffer_impl.h" -#include "source/common/buffer/watermark_buffer.h" #include "source/common/common/assert.h" #include "source/common/common/linked_object.h" #include "source/common/common/logger.h" -#include "source/common/common/statusor.h" -#include "source/common/common/thread.h" #include "source/common/http/codec_helper.h" #include "source/common/http/header_map_impl.h" #include "source/common/http/http2/codec_stats.h" @@ -30,7 +28,6 @@ #include "source/common/http/http2/metadata_encoder.h" #include "source/common/http/http2/protocol_constraints.h" #include "source/common/http/status.h" -#include "source/common/http/utility.h" #include "absl/types/optional.h" #include "absl/types/span.h" @@ -877,6 +874,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action_; Server::LoadShedPoint* should_send_go_away_on_dispatch_{nullptr}; + Server::LoadShedPoint* should_send_go_away_and_close_on_dispatch_{nullptr}; bool sent_go_away_on_dispatch_{false}; }; diff --git a/source/common/http/http3/conn_pool.h b/source/common/http/http3/conn_pool.h index 5ded8dbefea24..e27b723106eef 100644 --- a/source/common/http/http3/conn_pool.h +++ b/source/common/http/http3/conn_pool.h @@ -158,6 +158,18 @@ class Http3ConnPoolImpl : public FixedHttpConnPoolImpl { ConnectionPool::Callbacks& callbacks, const Instance::StreamOptions& options) override; + void drainConnections(Envoy::ConnectionPool::DrainBehavior drain_behavior) override { + if (drain_behavior == + Envoy::ConnectionPool::DrainBehavior::DrainExistingNonMigratableConnections && + quic_info_.migration_config_.migrate_session_on_network_change) { + // If connection migration is enabled, don't drain existing connections. + // Each connection will observe network change signals and decide whether + // to migrate or drain. + return; + } + FixedHttpConnPoolImpl::drainConnections(drain_behavior); + } + // For HTTP/3 the base connection pool does not track stream capacity, rather // the HTTP3 active client does. bool trackStreamCapacity() override { return false; } diff --git a/source/common/http/http_option_limits.cc b/source/common/http/http_option_limits.cc index 2a48caa1ba8c1..e5ad39c21c3b8 100644 --- a/source/common/http/http_option_limits.cc +++ b/source/common/http/http_option_limits.cc @@ -10,12 +10,15 @@ const uint32_t OptionsLimits::DEFAULT_HPACK_TABLE_SIZE; const uint32_t OptionsLimits::MAX_HPACK_TABLE_SIZE; const uint32_t OptionsLimits::MIN_MAX_CONCURRENT_STREAMS; const uint32_t OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS; +const uint32_t OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS_LEGACY; const uint32_t OptionsLimits::MAX_MAX_CONCURRENT_STREAMS; const uint32_t OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE; const uint32_t OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE; +const uint32_t OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE_LEGACY; const uint32_t OptionsLimits::MAX_INITIAL_STREAM_WINDOW_SIZE; const uint32_t OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE; const uint32_t OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE; +const uint32_t OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE_LEGACY; const uint32_t OptionsLimits::MAX_INITIAL_CONNECTION_WINDOW_SIZE; const uint32_t OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES; const uint32_t OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES; diff --git a/source/common/http/http_option_limits.h b/source/common/http/http_option_limits.h index 9a126c8f21283..44e41c451e871 100644 --- a/source/common/http/http_option_limits.h +++ b/source/common/http/http_option_limits.h @@ -19,7 +19,9 @@ struct OptionsLimits { // TODO(jwfang): make this 0, the HTTP/2 spec minimum static const uint32_t MIN_MAX_CONCURRENT_STREAMS = 1; // defaults to maximum, same as nghttp2 - static const uint32_t DEFAULT_MAX_CONCURRENT_STREAMS = (1U << 31) - 1; + static const uint32_t DEFAULT_MAX_CONCURRENT_STREAMS_LEGACY = (1U << 31) - 1; + // Defaults to 1024 for safety and enough for most use cases. + static const uint32_t DEFAULT_MAX_CONCURRENT_STREAMS = 1024; // no maximum from HTTP/2 spec, total streams is unsigned 32-bit maximum, // one-side (client/server) is half that, and we need to exclude stream 0. // same as NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS from nghttp2 @@ -29,17 +31,17 @@ struct OptionsLimits { // NOTE: we only support increasing window size now, so this is also the minimum // TODO(jwfang): make this 0 to support decrease window size static const uint32_t MIN_INITIAL_STREAM_WINDOW_SIZE = (1 << 16) - 1; - // initial value from HTTP/2 spec is 65535, but we want more (256MiB) - static const uint32_t DEFAULT_INITIAL_STREAM_WINDOW_SIZE = 256 * 1024 * 1024; + // Initial value from HTTP/2 spec is 65535 (64KiB - 1) and we want more (16MiB). + static const uint32_t DEFAULT_INITIAL_STREAM_WINDOW_SIZE = 16 * 1024 * 1024; + static const uint32_t DEFAULT_INITIAL_STREAM_WINDOW_SIZE_LEGACY = 256 * 1024 * 1024; // maximum from HTTP/2 spec, same as NGHTTP2_MAX_WINDOW_SIZE from nghttp2 static const uint32_t MAX_INITIAL_STREAM_WINDOW_SIZE = (1U << 31) - 1; // CONNECTION_WINDOW_SIZE is similar to STREAM_WINDOW_SIZE, but for connection-level window // TODO(jwfang): make this 0 to support decrease window size static const uint32_t MIN_INITIAL_CONNECTION_WINDOW_SIZE = (1 << 16) - 1; - // nghttp2's default connection-level window equals to its stream-level, - // our default connection-level window also equals to our stream-level - static const uint32_t DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE = 256 * 1024 * 1024; + static const uint32_t DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE = 24 * 1024 * 1024; + static const uint32_t DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE_LEGACY = 256 * 1024 * 1024; static const uint32_t MAX_INITIAL_CONNECTION_WINDOW_SIZE = (1U << 31) - 1; // Default limit on the number of outbound frames of all types. diff --git a/source/common/http/null_route_impl.h b/source/common/http/null_route_impl.h index e976911162570..29a9a21fc878a 100644 --- a/source/common/http/null_route_impl.h +++ b/source/common/http/null_route_impl.h @@ -63,7 +63,7 @@ struct NullVirtualHost : public Router::VirtualHost { bool includeAttemptCountInRequest() const override { return false; } bool includeAttemptCountInResponse() const override { return false; } bool includeIsTimeoutRetryHeader() const override { return false; } - uint32_t retryShadowBufferLimit() const override { return std::numeric_limits::max(); } + uint64_t requestBodyBufferLimit() const override { return std::numeric_limits::max(); } const Router::RouteSpecificFilterConfig* mostSpecificPerFilterConfig(absl::string_view) const override { return nullptr; @@ -95,12 +95,12 @@ struct RouteEntryImpl : public Router::RouteEntry { create(const std::string& cluster_name, const absl::optional& timeout, const Protobuf::RepeatedPtrField& hash_policy, - const Router::RetryPolicy& retry_policy, Regex::Engine& regex_engine, + Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, const Router::MetadataMatchCriteria* metadata_match) { absl::Status creation_status = absl::OkStatus(); auto ret = std::unique_ptr( - new RouteEntryImpl(cluster_name, timeout, hash_policy, retry_policy, regex_engine, - creation_status, metadata_match)); + new RouteEntryImpl(cluster_name, timeout, hash_policy, std::move(retry_policy), + regex_engine, creation_status, metadata_match)); RETURN_IF_NOT_OK(creation_status); return ret; } @@ -110,10 +110,10 @@ struct RouteEntryImpl : public Router::RouteEntry { const std::string& cluster_name, const absl::optional& timeout, const Protobuf::RepeatedPtrField& hash_policy, - const Router::RetryPolicy& retry_policy, Regex::Engine& regex_engine, + Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, absl::Status& creation_status, const Router::MetadataMatchCriteria* metadata_match) - : metadata_match_(metadata_match), retry_policy_(retry_policy), cluster_name_(cluster_name), - timeout_(timeout) { + : metadata_match_(metadata_match), retry_policy_(std::move(retry_policy)), + cluster_name_(cluster_name), timeout_(timeout) { if (!hash_policy.empty()) { auto policy_or_error = HashPolicyImpl::create(hash_policy, regex_engine); SET_AND_RETURN_IF_NOT_OK(policy_or_error.status(), creation_status); @@ -134,13 +134,13 @@ struct RouteEntryImpl : public Router::RouteEntry { currentUrlPathAfterRewrite(const Http::RequestHeaderMap&) const override { return {}; } - void finalizeRequestHeaders(Http::RequestHeaderMap&, const StreamInfo::StreamInfo&, - bool) const override {} + void finalizeRequestHeaders(Http::RequestHeaderMap&, const Formatter::HttpFormatterContext&, + const StreamInfo::StreamInfo&, bool) const override {} Http::HeaderTransforms requestHeaderTransforms(const StreamInfo::StreamInfo&, bool) const override { return {}; } - void finalizeResponseHeaders(Http::ResponseHeaderMap&, + void finalizeResponseHeaders(Http::ResponseHeaderMap&, const Formatter::HttpFormatterContext&, const StreamInfo::StreamInfo&) const override {} Http::HeaderTransforms responseHeaderTransforms(const StreamInfo::StreamInfo&, bool) const override { @@ -155,13 +155,13 @@ struct RouteEntryImpl : public Router::RouteEntry { return Upstream::ResourcePriority::Default; } const Router::RateLimitPolicy& rateLimitPolicy() const override { return rate_limit_policy_; } - const Router::RetryPolicy& retryPolicy() const override { return retry_policy_; } + const Router::RetryPolicyConstSharedPtr& retryPolicy() const override { return retry_policy_; } const Router::InternalRedirectPolicy& internalRedirectPolicy() const override { return internal_redirect_policy_; } const Router::PathMatcherSharedPtr& pathMatcher() const override { return path_matcher_; } const Router::PathRewriterSharedPtr& pathRewriter() const override { return path_rewriter_; } - uint32_t retryShadowBufferLimit() const override { return std::numeric_limits::max(); } + uint64_t requestBodyBufferLimit() const override { return std::numeric_limits::max(); } const std::vector& shadowPolicies() const override { return shadow_policies_; } @@ -174,6 +174,7 @@ struct RouteEntryImpl : public Router::RouteEntry { } bool usingNewTimeouts() const override { return false; } absl::optional idleTimeout() const override { return absl::nullopt; } + absl::optional flushTimeout() const override { return absl::nullopt; } absl::optional maxStreamDuration() const override { return absl::nullopt; } @@ -213,7 +214,7 @@ struct RouteEntryImpl : public Router::RouteEntry { const Router::MetadataMatchCriteria* metadata_match_; std::unique_ptr hash_policy_; - const Router::RetryPolicy& retry_policy_; + const Router::RetryPolicyConstSharedPtr retry_policy_; static const NullHedgePolicy hedge_policy_; static const NullRateLimitPolicy rate_limit_policy_; @@ -235,15 +236,15 @@ struct RouteEntryImpl : public Router::RouteEntry { struct NullRouteImpl : public Router::Route { static absl::StatusOr> - create(const std::string cluster_name, const Router::RetryPolicy& retry_policy, + create(const std::string cluster_name, Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, const absl::optional& timeout = {}, const Protobuf::RepeatedPtrField& hash_policy = {}, const Router::MetadataMatchCriteria* metadata_match = nullptr) { absl::Status creation_status; - auto ret = std::unique_ptr(new NullRouteImpl(cluster_name, retry_policy, - regex_engine, timeout, hash_policy, - creation_status, metadata_match)); + auto ret = std::unique_ptr( + new NullRouteImpl(cluster_name, std::move(retry_policy), regex_engine, timeout, hash_policy, + creation_status, metadata_match)); RETURN_IF_NOT_OK(creation_status); return ret; } @@ -274,15 +275,15 @@ struct NullRouteImpl : public Router::Route { static const Router::VirtualHostConstSharedPtr virtual_host_; protected: - NullRouteImpl(const std::string cluster_name, const Router::RetryPolicy& retry_policy, + NullRouteImpl(const std::string cluster_name, Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, const absl::optional& timeout, const Protobuf::RepeatedPtrField& hash_policy, absl::Status& creation_status, const Router::MetadataMatchCriteria* metadata_match) { - auto entry_or_error = RouteEntryImpl::create(cluster_name, timeout, hash_policy, retry_policy, - regex_engine, metadata_match); + auto entry_or_error = RouteEntryImpl::create( + cluster_name, timeout, hash_policy, std::move(retry_policy), regex_engine, metadata_match); SET_AND_RETURN_IF_NOT_OK(entry_or_error.status(), creation_status); route_entry_ = std::move(*entry_or_error); } diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 126a0814331f1..7a207abd0911a 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -174,7 +174,7 @@ validateCustomSettingsParameters(const envoy::config::core::v3::Http2ProtocolOpt absl::StatusOr initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions& options, bool hcm_stream_error_set, - const ProtobufWkt::BoolValue& hcm_stream_error) { + const Protobuf::BoolValue& hcm_stream_error) { auto ret = initializeAndValidateOptions(options); if (ret.status().ok() && !options.has_override_stream_error_on_invalid_http_message() && hcm_stream_error_set) { @@ -198,24 +198,42 @@ initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions options_clone.mutable_hpack_table_size()->set_value(OptionsLimits::DEFAULT_HPACK_TABLE_SIZE); } ASSERT(options_clone.hpack_table_size().value() <= OptionsLimits::MAX_HPACK_TABLE_SIZE); + const bool safe_http2_options = + Runtime::runtimeFeatureEnabled("envoy.reloadable_features.safe_http2_options"); + if (!options_clone.has_max_concurrent_streams()) { - options_clone.mutable_max_concurrent_streams()->set_value( - OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS); + if (safe_http2_options) { + options_clone.mutable_max_concurrent_streams()->set_value( + OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS); + } else { + options_clone.mutable_max_concurrent_streams()->set_value( + OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS_LEGACY); + } } ASSERT( options_clone.max_concurrent_streams().value() >= OptionsLimits::MIN_MAX_CONCURRENT_STREAMS && options_clone.max_concurrent_streams().value() <= OptionsLimits::MAX_MAX_CONCURRENT_STREAMS); if (!options_clone.has_initial_stream_window_size()) { - options_clone.mutable_initial_stream_window_size()->set_value( - OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE); + if (safe_http2_options) { + options_clone.mutable_initial_stream_window_size()->set_value( + OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE); + } else { + options_clone.mutable_initial_stream_window_size()->set_value( + OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE_LEGACY); + } } ASSERT(options_clone.initial_stream_window_size().value() >= OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE && options_clone.initial_stream_window_size().value() <= OptionsLimits::MAX_INITIAL_STREAM_WINDOW_SIZE); if (!options_clone.has_initial_connection_window_size()) { - options_clone.mutable_initial_connection_window_size()->set_value( - OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE); + if (safe_http2_options) { + options_clone.mutable_initial_connection_window_size()->set_value( + OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE); + } else { + options_clone.mutable_initial_connection_window_size()->set_value( + OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE_LEGACY); + } } ASSERT(options_clone.initial_connection_window_size().value() >= OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE && @@ -254,7 +272,7 @@ namespace Utility { envoy::config::core::v3::Http3ProtocolOptions initializeAndValidateOptions(const envoy::config::core::v3::Http3ProtocolOptions& options, bool hcm_stream_error_set, - const ProtobufWkt::BoolValue& hcm_stream_error) { + const Protobuf::BoolValue& hcm_stream_error) { if (options.has_override_stream_error_on_invalid_http_message()) { return options; } diff --git a/source/common/http/utility.h b/source/common/http/utility.h index 0c7b9a78c137d..73716add98be2 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -59,7 +59,7 @@ initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions absl::StatusOr initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions& options, bool hcm_stream_error_set, - const ProtobufWkt::BoolValue& hcm_stream_error); + const Protobuf::BoolValue& hcm_stream_error); } // namespace Utility } // namespace Http2 namespace Http3 { @@ -68,7 +68,7 @@ namespace Utility { envoy::config::core::v3::Http3ProtocolOptions initializeAndValidateOptions(const envoy::config::core::v3::Http3ProtocolOptions& options, bool hcm_stream_error_set, - const ProtobufWkt::BoolValue& hcm_stream_error); + const Protobuf::BoolValue& hcm_stream_error); } // namespace Utility } // namespace Http3 diff --git a/source/common/json/json_internal.cc b/source/common/json/json_internal.cc index 9b67d36d47fce..5925dcc60e3d1 100644 --- a/source/common/json/json_internal.cc +++ b/source/common/json/json_internal.cc @@ -743,20 +743,20 @@ absl::StatusOr Factory::loadFromString(const std::string& json) } absl::StatusOr -loadFromProtobufStructInternal(const ProtobufWkt::Struct& protobuf_struct); +loadFromProtobufStructInternal(const Protobuf::Struct& protobuf_struct); absl::StatusOr -loadFromProtobufValueInternal(const ProtobufWkt::Value& protobuf_value) { +loadFromProtobufValueInternal(const Protobuf::Value& protobuf_value) { switch (protobuf_value.kind_case()) { - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: return Field::createValue(protobuf_value.string_value()); - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: return Field::createValue(protobuf_value.number_value()); - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: return Field::createValue(protobuf_value.bool_value()); - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::kNullValue: return Field::createNull(); - case ProtobufWkt::Value::kListValue: { + case Protobuf::Value::kListValue: { FieldSharedPtr array = Field::createArray(); for (const auto& list_value : protobuf_value.list_value().values()) { absl::StatusOr proto_or_error = loadFromProtobufValueInternal(list_value); @@ -765,16 +765,16 @@ loadFromProtobufValueInternal(const ProtobufWkt::Value& protobuf_value) { } return array; } - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: return loadFromProtobufStructInternal(protobuf_value.struct_value()); - case ProtobufWkt::Value::KIND_NOT_SET: + case Protobuf::Value::KIND_NOT_SET: break; } return absl::InvalidArgumentError("Protobuf value case not implemented"); } absl::StatusOr -loadFromProtobufStructInternal(const ProtobufWkt::Struct& protobuf_struct) { +loadFromProtobufStructInternal(const Protobuf::Struct& protobuf_struct) { auto root = Field::createObject(); for (const auto& field : protobuf_struct.fields()) { absl::StatusOr proto_or_error = loadFromProtobufValueInternal(field.second); @@ -785,7 +785,7 @@ loadFromProtobufStructInternal(const ProtobufWkt::Struct& protobuf_struct) { return root; } -ObjectSharedPtr Factory::loadFromProtobufStruct(const ProtobufWkt::Struct& protobuf_struct) { +ObjectSharedPtr Factory::loadFromProtobufStruct(const Protobuf::Struct& protobuf_struct) { return THROW_OR_RETURN_VALUE(loadFromProtobufStructInternal(protobuf_struct), ObjectSharedPtr); } diff --git a/source/common/json/json_internal.h b/source/common/json/json_internal.h index 6e43a0da73e34..530f195ec802a 100644 --- a/source/common/json/json_internal.h +++ b/source/common/json/json_internal.h @@ -23,7 +23,7 @@ class Factory { /** * Constructs a Json Object from a Protobuf struct. */ - static ObjectSharedPtr loadFromProtobufStruct(const ProtobufWkt::Struct& protobuf_struct); + static ObjectSharedPtr loadFromProtobufStruct(const Protobuf::Struct& protobuf_struct); /** * Serializes a string in JSON format, throwing an exception if not valid UTF-8. diff --git a/source/common/json/json_loader.cc b/source/common/json/json_loader.cc index 130c9dbeac645..80f7ab45acef7 100644 --- a/source/common/json/json_loader.cc +++ b/source/common/json/json_loader.cc @@ -10,7 +10,7 @@ absl::StatusOr Factory::loadFromString(const std::string& json) return Nlohmann::Factory::loadFromString(json); } -ObjectSharedPtr Factory::loadFromProtobufStruct(const ProtobufWkt::Struct& protobuf_struct) { +ObjectSharedPtr Factory::loadFromProtobufStruct(const Protobuf::Struct& protobuf_struct) { return Nlohmann::Factory::loadFromProtobufStruct(protobuf_struct); } diff --git a/source/common/json/json_loader.h b/source/common/json/json_loader.h index f659e7ea25f7f..af7eac7a04c09 100644 --- a/source/common/json/json_loader.h +++ b/source/common/json/json_loader.h @@ -23,7 +23,7 @@ class Factory { /** * Constructs a Json Object from a Protobuf struct. */ - static ObjectSharedPtr loadFromProtobufStruct(const ProtobufWkt::Struct& protobuf_struct); + static ObjectSharedPtr loadFromProtobufStruct(const Protobuf::Struct& protobuf_struct); /* * Serializes a JSON string to a byte vector using the MessagePack serialization format. diff --git a/source/common/json/json_utility.cc b/source/common/json/json_utility.cc index d0110e506c315..27760e5e33169 100644 --- a/source/common/json/json_utility.cc +++ b/source/common/json/json_utility.cc @@ -5,36 +5,36 @@ namespace Json { namespace { -void structValueToJson(const ProtobufWkt::Struct& struct_value, StringStreamer::Map& level); -void listValueToJson(const ProtobufWkt::ListValue& list_value, StringStreamer::Array& level); +void structValueToJson(const Protobuf::Struct& struct_value, StringStreamer::Map& level); +void listValueToJson(const Protobuf::ListValue& list_value, StringStreamer::Array& level); -void valueToJson(const ProtobufWkt::Value& value, StringStreamer::Level& level) { +void valueToJson(const Protobuf::Value& value, StringStreamer::Level& level) { switch (value.kind_case()) { - case ProtobufWkt::Value::KIND_NOT_SET: - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::KIND_NOT_SET: + case Protobuf::Value::kNullValue: level.addNull(); break; - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: level.addNumber(value.number_value()); break; - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: level.addString(value.string_value()); break; - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: level.addBool(value.bool_value()); break; - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: structValueToJson(value.struct_value(), *level.addMap()); break; - case ProtobufWkt::Value::kListValue: + case Protobuf::Value::kListValue: listValueToJson(value.list_value(), *level.addArray()); break; } } -void structValueToJson(const ProtobufWkt::Struct& struct_value, StringStreamer::Map& map) { +void structValueToJson(const Protobuf::Struct& struct_value, StringStreamer::Map& map) { using PairRefWrapper = - std::reference_wrapper::value_type>; + std::reference_wrapper::value_type>; absl::InlinedVector sorted_fields; sorted_fields.reserve(struct_value.fields_size()); @@ -51,34 +51,34 @@ void structValueToJson(const ProtobufWkt::Struct& struct_value, StringStreamer:: } } -void listValueToJson(const ProtobufWkt::ListValue& list_value, StringStreamer::Array& arr) { - for (const ProtobufWkt::Value& value : list_value.values()) { +void listValueToJson(const Protobuf::ListValue& list_value, StringStreamer::Array& arr) { + for (const Protobuf::Value& value : list_value.values()) { valueToJson(value, arr); } } } // namespace -void Utility::appendValueToString(const ProtobufWkt::Value& value, std::string& dest) { +void Utility::appendValueToString(const Protobuf::Value& value, std::string& dest) { StringStreamer streamer(dest); switch (value.kind_case()) { - case ProtobufWkt::Value::KIND_NOT_SET: - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::KIND_NOT_SET: + case Protobuf::Value::kNullValue: streamer.addNull(); break; - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: streamer.addNumber(value.number_value()); break; - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: streamer.addString(value.string_value()); break; - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: streamer.addBool(value.bool_value()); break; - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: structValueToJson(value.struct_value(), *streamer.makeRootMap()); break; - case ProtobufWkt::Value::kListValue: + case Protobuf::Value::kListValue: listValueToJson(value.list_value(), *streamer.makeRootArray()); break; } diff --git a/source/common/json/json_utility.h b/source/common/json/json_utility.h index 3797a3befdaaf..43fe22dd4dc23 100644 --- a/source/common/json/json_utility.h +++ b/source/common/json/json_utility.h @@ -11,11 +11,11 @@ namespace Json { class Utility { public: /** - * Convert a ProtobufWkt::Value to a JSON string. + * Convert a Protobuf::Value to a JSON string. * @param value message of type type.googleapis.com/google.protobuf.Value * @param dest JSON string. */ - static void appendValueToString(const ProtobufWkt::Value& value, std::string& dest); + static void appendValueToString(const Protobuf::Value& value, std::string& dest); }; } // namespace Json diff --git a/source/common/listener_manager/active_tcp_socket.cc b/source/common/listener_manager/active_tcp_socket.cc index 0db63c47199e3..9e12149136e93 100644 --- a/source/common/listener_manager/active_tcp_socket.cc +++ b/source/common/listener_manager/active_tcp_socket.cc @@ -74,6 +74,7 @@ void ActiveTcpSocket::createListenerFilterBuffer() { listener_filter_buffer_ = std::make_unique( socket_->ioHandle(), listener_.dispatcher(), [this](bool error) { + (*iter_)->onClose(); socket_->ioHandle().close(); if (error) { listener_.stats_.downstream_listener_filter_error_.inc(); @@ -172,13 +173,11 @@ void ActiveTcpSocket::continueFilterChain(bool success) { } } -void ActiveTcpSocket::setDynamicMetadata(const std::string& name, - const ProtobufWkt::Struct& value) { +void ActiveTcpSocket::setDynamicMetadata(const std::string& name, const Protobuf::Struct& value) { stream_info_->setDynamicMetadata(name, value); } -void ActiveTcpSocket::setDynamicTypedMetadata(const std::string& name, - const ProtobufWkt::Any& value) { +void ActiveTcpSocket::setDynamicTypedMetadata(const std::string& name, const Protobuf::Any& value) { stream_info_->setDynamicTypedMetadata(name, value); } diff --git a/source/common/listener_manager/active_tcp_socket.h b/source/common/listener_manager/active_tcp_socket.h index 6423f3ba54bdc..6170e4389edb4 100644 --- a/source/common/listener_manager/active_tcp_socket.h +++ b/source/common/listener_manager/active_tcp_socket.h @@ -53,6 +53,8 @@ class ActiveTcpSocket : public Network::ListenerFilterManager, } size_t maxReadBytes() const override { return listener_filter_->maxReadBytes(); } + + void onClose() override { return listener_filter_->onClose(); } }; using ListenerFilterWrapperPtr = std::unique_ptr; @@ -73,8 +75,8 @@ class ActiveTcpSocket : public Network::ListenerFilterManager, void startFilterChain() { continueFilterChain(true); } - void setDynamicMetadata(const std::string& name, const ProtobufWkt::Struct& value) override; - void setDynamicTypedMetadata(const std::string& name, const ProtobufWkt::Any& value) override; + void setDynamicMetadata(const std::string& name, const Protobuf::Struct& value) override; + void setDynamicTypedMetadata(const std::string& name, const Protobuf::Any& value) override; envoy::config::core::v3::Metadata& dynamicMetadata() override { return stream_info_->dynamicMetadata(); }; diff --git a/source/common/listener_manager/filter_chain_manager_impl.cc b/source/common/listener_manager/filter_chain_manager_impl.cc index 7261bea02234a..24fae4608bf7c 100644 --- a/source/common/listener_manager/filter_chain_manager_impl.cc +++ b/source/common/listener_manager/filter_chain_manager_impl.cc @@ -32,7 +32,7 @@ Network::Address::InstanceConstSharedPtr fakeAddress() { } struct FilterChainNameAction - : public Matcher::ActionBase { + : public Matcher::ActionBase { explicit FilterChainNameAction(const std::string& name) : name_(name) {} const Network::FilterChain* get(const FilterChainsByName& filter_chains_by_name, const StreamInfo::StreamInfo&) const override { @@ -49,14 +49,14 @@ class FilterChainNameActionFactory : public Matcher::ActionFactory { public: std::string name() const override { return "filter-chain-name"; } - Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message& config, - FilterChainActionFactoryContext&, - ProtobufMessage::ValidationVisitor&) override { - const auto& name = dynamic_cast(config); - return [value = name.value()]() { return std::make_unique(value); }; + Matcher::ActionConstSharedPtr createAction(const Protobuf::Message& config, + FilterChainActionFactoryContext&, + ProtobufMessage::ValidationVisitor&) override { + return std::make_shared( + dynamic_cast(config).value()); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; @@ -150,6 +150,15 @@ absl::Status FilterChainManagerImpl::addFilterChains( filter_chain_factory_builder, context_creator)); maybeConstructMatcher(filter_chain_matcher, filter_chains_by_name, parent_context_); + const auto* origin = getOriginFilterChainManager(); + if (origin != nullptr) { + for (const auto& message_and_filter_chain : origin->fc_contexts_) { + if (fc_contexts_.find(message_and_filter_chain.first) == fc_contexts_.end()) { + origin->draining_filter_chains_.push_back(message_and_filter_chain.second); + } + } + } + ENVOY_LOG(debug, "new fc_contexts has {} filter chains, including {} newly built", fc_contexts_.size(), new_filter_chain_size); return absl::OkStatus(); @@ -573,9 +582,8 @@ FilterChainManagerImpl::findFilterChainUsingMatcher(const Network::ConnectionSoc Matcher::evaluateMatch(*matcher_, data); ASSERT(match_result.isComplete(), "Matching must complete for network streams."); if (match_result.isMatch()) { - const Matcher::ActionPtr action = match_result.action(); - return action->getTyped().get(filter_chains_by_name_, - info); + return match_result.action()->getTyped().get( + filter_chains_by_name_, info); } return default_filter_chain_.get(); } diff --git a/source/common/listener_manager/filter_chain_manager_impl.h b/source/common/listener_manager/filter_chain_manager_impl.h index 2161513de10a3..012047d6315f0 100644 --- a/source/common/listener_manager/filter_chain_manager_impl.h +++ b/source/common/listener_manager/filter_chain_manager_impl.h @@ -165,6 +165,10 @@ class FilterChainManagerImpl : public Network::FilterChainManager, static bool isWildcardServerName(const std::string& name); + const std::vector& drainingFilterChains() const { + return draining_filter_chains_; + } + // Return the current view of filter chains, keyed by filter chain message. Used by the owning // listener to calculate the intersection of filter chains with another listener. const FcContextMap& filterChainsByMessage() const { return fc_contexts_; } @@ -350,6 +354,9 @@ class FilterChainManagerImpl : public Network::FilterChainManager, // Index filter chains by name, used by the matcher actions. FilterChainsByName filter_chains_by_name_; + + // Used to hint listener which filter chains it should drain. + mutable std::vector draining_filter_chains_; }; namespace FilterChain { diff --git a/source/common/listener_manager/lds_api.cc b/source/common/listener_manager/lds_api.cc index 39fecc0de0930..b5059231e8d21 100644 --- a/source/common/listener_manager/lds_api.cc +++ b/source/common/listener_manager/lds_api.cc @@ -23,13 +23,13 @@ namespace Server { LdsApiImpl::LdsApiImpl(const envoy::config::core::v3::ConfigSource& lds_config, const xds::core::v3::ResourceLocator* lds_resources_locator, - Upstream::ClusterManager& cm, Init::Manager& init_manager, - Stats::Scope& scope, ListenerManager& lm, + Config::XdsManager& xds_manager, Upstream::ClusterManager& cm, + Init::Manager& init_manager, Stats::Scope& scope, ListenerManager& lm, ProtobufMessage::ValidationVisitor& validation_visitor) : Envoy::Config::SubscriptionBase(validation_visitor, "name"), - listener_manager_(lm), scope_(scope.createScope("listener_manager.lds.")), cm_(cm), - init_target_("LDS", [this]() { subscription_->start({}); }) { + listener_manager_(lm), scope_(scope.createScope("listener_manager.lds.")), + xds_manager_(xds_manager), init_target_("LDS", [this]() { subscription_->start({}); }) { const auto resource_name = getResourceName(); if (lds_resources_locator == nullptr) { subscription_ = THROW_OR_RETURN_VALUE(cm.subscriptionFactory().subscriptionFromConfigSource( @@ -49,14 +49,11 @@ absl::Status LdsApiImpl::onConfigUpdate(const std::vector& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& system_version_info) { - Config::ScopedResume maybe_resume_rds_sds; - if (cm_.adsMux()) { - const std::vector paused_xds_types{ - Config::getTypeUrl(), - Config::getTypeUrl(), - Config::getTypeUrl()}; - maybe_resume_rds_sds = cm_.adsMux()->pause(paused_xds_types); - } + const std::vector paused_xds_types{ + Config::getTypeUrl(), + Config::getTypeUrl(), + Config::getTypeUrl()}; + Config::ScopedResume resume_rds_sds = xds_manager_.pause(paused_xds_types); bool any_applied = false; listener_manager_.beginListenerUpdate(); diff --git a/source/common/listener_manager/lds_api.h b/source/common/listener_manager/lds_api.h index 473fd8c85345d..1500441182c80 100644 --- a/source/common/listener_manager/lds_api.h +++ b/source/common/listener_manager/lds_api.h @@ -28,8 +28,9 @@ class LdsApiImpl : public LdsApi, public: LdsApiImpl(const envoy::config::core::v3::ConfigSource& lds_config, const xds::core::v3::ResourceLocator* lds_resources_locator, - Upstream::ClusterManager& cm, Init::Manager& init_manager, Stats::Scope& scope, - ListenerManager& lm, ProtobufMessage::ValidationVisitor& validation_visitor); + Config::XdsManager& xds_manager, Upstream::ClusterManager& cm, + Init::Manager& init_manager, Stats::Scope& scope, ListenerManager& lm, + ProtobufMessage::ValidationVisitor& validation_visitor); // Server::LdsApi std::string versionInfo() const override { return system_version_info_; } @@ -48,7 +49,7 @@ class LdsApiImpl : public LdsApi, std::string system_version_info_; ListenerManager& listener_manager_; Stats::ScopeSharedPtr scope_; - Upstream::ClusterManager& cm_; + Config::XdsManager& xds_manager_; Init::TargetImpl init_target_; }; diff --git a/source/common/listener_manager/listener_impl.cc b/source/common/listener_manager/listener_impl.cc index f3bec6b49beeb..fc93131aa9e8b 100644 --- a/source/common/listener_manager/listener_impl.cc +++ b/source/common/listener_manager/listener_impl.cc @@ -877,7 +877,7 @@ void ListenerImpl::buildOriginalDstListenerFilter( "envoy.filters.listener.original_dst"); Network::ListenerFilterFactoryCb callback = factory.createListenerFilterFactoryFromProto( - Envoy::ProtobufWkt::Empty(), nullptr, *listener_factory_context_); + Envoy::Protobuf::Empty(), nullptr, *listener_factory_context_); auto* cfg_provider_manager = parent_.factory_->getTcpListenerConfigProviderManager(); auto filter_config_provider = cfg_provider_manager->createStaticFilterConfigProvider( callback, "envoy.filters.listener.original_dst"); @@ -974,7 +974,7 @@ bool ListenerImpl::createQuicListenerFilterChain(Network::QuicListenerFilterMana return false; } -void ListenerImpl::dumpListenerConfig(ProtobufWkt::Any& dump) const { +void ListenerImpl::dumpListenerConfig(Protobuf::Any& dump) const { dump.PackFrom(config_maybe_partial_filter_chains_); } @@ -1072,14 +1072,10 @@ ListenerImpl::newListenerWithFilterChain(const envoy::config::listener::v3::List void ListenerImpl::diffFilterChain(const ListenerImpl& another_listener, std::function callback) { - for (const auto& message_and_filter_chain : filter_chain_manager_->filterChainsByMessage()) { - if (another_listener.filter_chain_manager_->filterChainsByMessage().find( - message_and_filter_chain.first) == - another_listener.filter_chain_manager_->filterChainsByMessage().end()) { - // The filter chain exists in `this` listener but not in the listener passed in. - callback(*message_and_filter_chain.second); - } + for (const auto& draining_filter_chain : filter_chain_manager_->drainingFilterChains()) { + callback(*draining_filter_chain); } + // Filter chain manager maintains an optional default filter chain besides the filter chains // indexed by message. if (auto eq = MessageUtil(); @@ -1139,7 +1135,7 @@ bool ListenerImpl::hasCompatibleAddress(const ListenerImpl& other) const { return false; } - // Second, check if the listener has the same addresses. + // Second, check if the listener has the same addresses (including network namespaces if Linux). // The listener support listening on the zero port address for test. Multiple zero // port addresses are also supported. For comparing two listeners with multiple // zero port addresses, only need to ensure there are the same number of zero diff --git a/source/common/listener_manager/listener_impl.h b/source/common/listener_manager/listener_impl.h index 9cbc6dc897054..9dcd00035f133 100644 --- a/source/common/listener_manager/listener_impl.h +++ b/source/common/listener_manager/listener_impl.h @@ -259,7 +259,7 @@ class ListenerImpl final : public Network::ListenerConfig, return socket_factories_; } void debugLog(const std::string& message); - void dumpListenerConfig(ProtobufWkt::Any& dump) const; + void dumpListenerConfig(Protobuf::Any& dump) const; void initialize(); DrainManager& localDrainManager() const { return listener_factory_context_->listener_factory_context_base_->drainManager(); diff --git a/source/common/listener_manager/listener_manager_impl.cc b/source/common/listener_manager/listener_manager_impl.cc index 70d80ec81ed80..c6399a7b9fe5b 100644 --- a/source/common/listener_manager/listener_manager_impl.cc +++ b/source/common/listener_manager/listener_manager_impl.cc @@ -21,10 +21,13 @@ #include "source/common/network/filter_matcher.h" #include "source/common/network/io_socket_handle_impl.h" #include "source/common/network/listen_socket_impl.h" +#include "source/common/network/socket_interface.h" #include "source/common/network/socket_option_factory.h" #include "source/common/network/utility.h" #include "source/common/protobuf/utility.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "absl/synchronization/blocking_counter.h" #if defined(ENVOY_ENABLE_QUIC) @@ -292,16 +295,22 @@ absl::StatusOr ProdListenerComponentFactory::createLis worker_index); }; - auto result = Network::Utility::execInNetworkNamespace(fn, netns.value().c_str()); - - // We have a nested absl::StatusOr type, so if there were no issues with changing the namespace, - // we want to return the inner absl::StatusOr. - if (result->ok()) { - return result->value(); + // Here we're running `fn` in a different network namespace. It will return a `absl::StatusOr` + // that wraps the result of the function we pass in, which is another `absl::StatusOr`. + auto outer_result = Network::Utility::execInNetworkNamespace(fn, netns.value().c_str()); + + // We have a nested absl::StatusOr type. The "outer" result is the result of our attempt to jump + // between network namespaces. The "inner" result is that of the `createListenSocketInternal` + // function we passed in to run in the other netns. + if (outer_result.ok()) { + // We successfully jumped network namespaces and ran `createListenSocketInternal` in that + // namespace before jumping back. Here we return the result of that + // `createListenSocketInternal` function. + return outer_result.value(); } - // The result was not ok, so we want to return the outer status. - return result.status(); + // The "outer" result was not ok, which means we failed to jump network namespaces. + return outer_result.status(); } #endif @@ -316,6 +325,22 @@ absl::StatusOr ProdListenerComponentFactory::createLis ASSERT(socket_type == Network::Socket::Type::Stream || socket_type == Network::Socket::Type::Datagram); + // Use the address's socket interface for socket creation. + const Network::SocketInterface& socket_interface = address->socketInterface(); + const Network::SocketInterface& default_interface = Network::SocketInterfaceSingleton::get(); + + // Check if this address specifies a custom socket interface. + if (&socket_interface != &default_interface) { + ENVOY_LOG(debug, "creating socket using custom interface for address: {}", + address->logicalName()); + auto io_handle = socket_interface.socket(socket_type, address, creation_options); + if (!io_handle) { + return absl::InvalidArgumentError("failed to create socket using custom interface"); + } + return std::make_shared(std::move(io_handle), address, options); + } + + // Continue with standard socket creation for addresses using the default interface. // First we try to get the socket from our parent if applicable in each case below. if (address->type() == Network::Address::Type::Pipe) { if (socket_type != Network::Socket::Type::Stream) { @@ -401,6 +426,7 @@ ListenerManagerImpl::ListenerManagerImpl(Instance& server, for (uint32_t i = 0; i < server.options().concurrency(); i++) { workers_.emplace_back(worker_factory.createWorker( i, server.overloadManager(), server.nullOverloadManager(), absl::StrCat("worker_", i))); + ENVOY_LOG(debug, "starting worker: {}", i); } } diff --git a/source/common/listener_manager/listener_manager_impl.h b/source/common/listener_manager/listener_manager_impl.h index 2754b98b766b2..f590cada7574e 100644 --- a/source/common/listener_manager/listener_manager_impl.h +++ b/source/common/listener_manager/listener_manager_impl.h @@ -86,8 +86,8 @@ class ProdListenerComponentFactory : public ListenerComponentFactory, LdsApiPtr createLdsApi(const envoy::config::core::v3::ConfigSource& lds_config, const xds::core::v3::ResourceLocator* lds_resources_locator) override { return std::make_unique( - lds_config, lds_resources_locator, server_.clusterManager(), server_.initManager(), - *server_.stats().rootScope(), server_.listenerManager(), + lds_config, lds_resources_locator, server_.xdsManager(), server_.clusterManager(), + server_.initManager(), *server_.stats().rootScope(), server_.listenerManager(), server_.messageValidationContext().dynamicValidationVisitor()); } absl::StatusOr createNetworkFilterFactoryList( diff --git a/source/common/matcher/BUILD b/source/common/matcher/BUILD index 82cd7e7f137d4..22234d5a0fb85 100644 --- a/source/common/matcher/BUILD +++ b/source/common/matcher/BUILD @@ -29,7 +29,7 @@ envoy_cc_library( hdrs = ["prefix_map_matcher.h"], deps = [ ":map_matcher_lib", - "//source/common/common:trie_lookup_table_lib", + "//source/common/common:radix_tree_lib", "//source/common/runtime:runtime_features_lib", ], ) diff --git a/source/common/matcher/matcher.h b/source/common/matcher/matcher.h index a7aef43c642de..5cc9b0059b42c 100644 --- a/source/common/matcher/matcher.h +++ b/source/common/matcher/matcher.h @@ -324,10 +324,10 @@ class MatchTreeFactory : public OnMatchFactory { on_match.action().typed_config(), server_factory_context_.messageValidationVisitor(), factory); - auto action_factory = factory.createActionFactoryCb( - *message, action_factory_context_, server_factory_context_.messageValidationVisitor()); - return [action_factory, keep_matching = on_match.keep_matching()] { - return OnMatch{action_factory, {}, keep_matching}; + auto action = factory.createAction(*message, action_factory_context_, + server_factory_context_.messageValidationVisitor()); + return [action, keep_matching = on_match.keep_matching()] { + return OnMatch{action, {}, keep_matching}; }; } diff --git a/source/common/matcher/prefix_map_matcher.h b/source/common/matcher/prefix_map_matcher.h index 4a597d8322813..a70a7e28593e7 100644 --- a/source/common/matcher/prefix_map_matcher.h +++ b/source/common/matcher/prefix_map_matcher.h @@ -1,6 +1,6 @@ #pragma once -#include "source/common/common/trie_lookup_table.h" +#include "source/common/common/radix_tree.h" #include "source/common/matcher/map_matcher.h" #include "source/common/runtime/runtime_features.h" @@ -52,7 +52,7 @@ template class PrefixMapMatcher : public MapMatcher { } private: - TrieLookupTable>> children_; + RadixTree>> children_; }; } // namespace Matcher diff --git a/source/common/network/BUILD b/source/common/network/BUILD index 62772ee0a6edc..789b768c72e5b 100644 --- a/source/common/network/BUILD +++ b/source/common/network/BUILD @@ -500,6 +500,7 @@ envoy_cc_library( deps = [ ":address_lib", ":default_socket_interface_lib", + ":ip_address_parsing_lib", ":socket_lib", ":socket_option_lib", "//envoy/api:os_sys_calls_interface", @@ -517,6 +518,16 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "ip_address_parsing_lib", + srcs = ["ip_address_parsing.cc"], + hdrs = ["ip_address_parsing.h"], + deps = [ + "//source/common/api:os_sys_calls_lib", + "//source/common/common:statusor_lib", + ], +) + envoy_cc_library( name = "transport_socket_options_lib", srcs = ["transport_socket_options_impl.cc"], diff --git a/source/common/network/address_impl.cc b/source/common/network/address_impl.cc index ae17e8e1466c3..ea49f89045dea 100644 --- a/source/common/network/address_impl.cc +++ b/source/common/network/address_impl.cc @@ -164,7 +164,8 @@ Ipv4Instance::Ipv4Instance(absl::Status& status, const sockaddr_in* address, bool Ipv4Instance::operator==(const Instance& rhs) const { const Ipv4Instance* rhs_casted = dynamic_cast(&rhs); return (rhs_casted && (ip_.ipv4_.address() == rhs_casted->ip_.ipv4_.address()) && - (ip_.port() == rhs_casted->ip_.port())); + (ip_.port() == rhs_casted->ip_.port()) && + (InstanceBase::networkNamespace() == rhs.networkNamespace())); } std::string Ipv4Instance::sockaddrToString(const sockaddr_in& addr) { @@ -313,7 +314,8 @@ bool Ipv6Instance::operator==(const Instance& rhs) const { const auto* rhs_casted = dynamic_cast(&rhs); return (rhs_casted && (ip_.ipv6_.address() == rhs_casted->ip_.ipv6_.address()) && (ip_.port() == rhs_casted->ip_.port()) && - (ip_.ipv6_.scopeId() == rhs_casted->ip_.ipv6_.scopeId())); + (ip_.ipv6_.scopeId() == rhs_casted->ip_.ipv6_.scopeId()) && + (InstanceBase::networkNamespace() == rhs.networkNamespace())); } Ipv6Instance::Ipv6Instance(absl::Status& status, const sockaddr_in6& address, bool v6only, diff --git a/source/common/network/address_impl.h b/source/common/network/address_impl.h index b61a1dd8b0598..97dbf2e817c04 100644 --- a/source/common/network/address_impl.h +++ b/source/common/network/address_impl.h @@ -179,6 +179,22 @@ class Ipv4Instance : public InstanceBase { // inlined IN_MULTICAST() to avoid byte swapping !((ipv4_.address_.sin_addr.s_addr & htonl(0xf0000000)) == htonl(0xe0000000)); } + bool isLinkLocalAddress() const override { + // Check if the address is in the link-local range: 169.254.0.0/16. + return (ipv4_.address_.sin_addr.s_addr & htonl(0xffff0000)) == htonl(0xa9fe0000); + } + bool isUniqueLocalAddress() const override { + // Unique Local Addresses (ULA) are not applicable to IPv4. + return false; + } + bool isSiteLocalAddress() const override { + // Site-Local Addresses are not applicable to IPv4. + return false; + } + bool isTeredoAddress() const override { + // Teredo addresses are not applicable to IPv4. + return false; + } const Ipv4* ipv4() const override { return &ipv4_; } const Ipv6* ipv6() const override { return nullptr; } uint32_t port() const override { return ntohs(ipv4_.address_.sin_port); } @@ -291,6 +307,29 @@ class Ipv6Instance : public InstanceBase { bool isUnicastAddress() const override { return !isAnyAddress() && !IN6_IS_ADDR_MULTICAST(&ipv6_.address_.sin6_addr); } + bool isLinkLocalAddress() const override { + // Check if the address is in the link-local range: fe80::/10 or in the v4 mapped link-local + // range: [::ffff:169.254.0.0]. + return IN6_IS_ADDR_LINKLOCAL(&ipv6_.address_.sin6_addr) || + (IN6_IS_ADDR_V4MAPPED(&ipv6_.address_.sin6_addr) && + (ipv6_.address_.sin6_addr.s6_addr[12] == 0xa9 && + ipv6_.address_.sin6_addr.s6_addr[13] == 0xfe)); + } + bool isUniqueLocalAddress() const override { + // Unique Local Addresses (ULA) are in the range fc00::/7. + return (ipv6_.address_.sin6_addr.s6_addr[0] & 0xfe) == 0xfc; + } + bool isSiteLocalAddress() const override { + // Site-Local Addresses are in the range fec0::/10. + return IN6_IS_ADDR_SITELOCAL(&ipv6_.address_.sin6_addr); + } + bool isTeredoAddress() const override { + // Teredo addresses have the prefix 2001:0000::/32. + return ipv6_.address_.sin6_addr.s6_addr[0] == 0x20 && + ipv6_.address_.sin6_addr.s6_addr[1] == 0x01 && + ipv6_.address_.sin6_addr.s6_addr[2] == 0x00 && + ipv6_.address_.sin6_addr.s6_addr[3] == 0x00; + } const Ipv4* ipv4() const override { return nullptr; } const Ipv6* ipv6() const override { return &ipv6_; } uint32_t port() const override { return ipv6_.port(); } diff --git a/source/common/network/connection_balancer_impl.cc b/source/common/network/connection_balancer_impl.cc index fc0471c675a51..2107d44f1fea3 100644 --- a/source/common/network/connection_balancer_impl.cc +++ b/source/common/network/connection_balancer_impl.cc @@ -4,12 +4,12 @@ namespace Envoy { namespace Network { void ExactConnectionBalancerImpl::registerHandler(BalancedConnectionHandler& handler) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); handlers_.push_back(&handler); } void ExactConnectionBalancerImpl::unregisterHandler(BalancedConnectionHandler& handler) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); // This could be made more efficient in various ways, but the number of listeners is generally // small and this is a rare operation so we can start with this and optimize later if this // becomes a perf bottleneck. @@ -20,7 +20,7 @@ BalancedConnectionHandler& ExactConnectionBalancerImpl::pickTargetHandler(BalancedConnectionHandler&) { BalancedConnectionHandler* min_connection_handler = nullptr; { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); for (BalancedConnectionHandler* handler : handlers_) { if (min_connection_handler == nullptr || handler->numConnections() < min_connection_handler->numConnections()) { diff --git a/source/common/network/connection_impl.cc b/source/common/network/connection_impl.cc index 5ee1c51d7a9c5..5a1df2875a77f 100644 --- a/source/common/network/connection_impl.cc +++ b/source/common/network/connection_impl.cc @@ -87,10 +87,11 @@ ConnectionImpl::ConnectionImpl(Event::Dispatcher& dispatcher, ConnectionSocketPt write_buffer_above_high_watermark_(false), detect_early_close_(true), enable_half_close_(false), read_end_stream_raised_(false), read_end_stream_(false), write_end_stream_(false), current_write_end_stream_(false), dispatch_buffered_data_(false), - transport_wants_read_(false), + transport_wants_read_(false), reuse_socket_(false), enable_close_through_filter_manager_(Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.connection_close_through_filter_manager")) { + ENVOY_CONN_LOG(debug, "ConnectionImpl constructor called", *this); if (!socket_->isOpen()) { IS_ENVOY_BUG("Client socket failure"); return; @@ -103,9 +104,15 @@ ConnectionImpl::ConnectionImpl(Event::Dispatcher& dispatcher, ConnectionSocketPt // We never ask for both early close and read at the same time. If we are reading, we want to // consume all available data. + ENVOY_CONN_LOG( + debug, "Initializing file event with callback that captures this={}, connection_id={}, fd={}", + *this, static_cast(this), id(), socket_->ioHandle().fdDoNotUse()); + socket_->ioHandle().initializeFileEvent( dispatcher_, [this](uint32_t events) { + ENVOY_CONN_LOG(debug, "File event callback ENTRY - this={}, connection_id={}, events={}", + *this, static_cast(this), id(), events); onFileEvent(events); return absl::OkStatus(); }, @@ -121,7 +128,7 @@ ConnectionImpl::ConnectionImpl(Event::Dispatcher& dispatcher, ConnectionSocketPt ConnectionImpl::~ConnectionImpl() { ASSERT(!socket_->isOpen() && delayed_close_timer_ == nullptr, - "ConnectionImpl was unexpectedly torn down without being closed."); + "ConnectionImpl destroyed with open socket and/or active timer"); // In general we assume that owning code has called close() previously to the destructor being // run. This generally must be done so that callbacks run in the correct context (vs. deferred @@ -147,7 +154,15 @@ void ConnectionImpl::removeReadFilter(ReadFilterSharedPtr filter) { bool ConnectionImpl::initializeReadFilters() { return filter_manager_.initializeReadFilters(); } void ConnectionImpl::close(ConnectionCloseType type) { + ENVOY_CONN_LOG( + debug, + "ConnectionImpl::close() ENTRY - this={}, type={}, connection_id={}, fd={}, socket_isOpen={}", + *this, static_cast(this), static_cast(type), id(), + socket_ ? socket_->ioHandle().fdDoNotUse() : -1, socket_ ? socket_->isOpen() : false); + if (!socket_->isOpen()) { + ENVOY_CONN_LOG_EVENT(debug, "connection_closing", "Not closing conn, socket is not open", + *this); return; } @@ -188,7 +203,12 @@ void ConnectionImpl::closeInternal(ConnectionCloseType type) { if (data_to_write > 0 && type != ConnectionCloseType::Abort) { // We aren't going to wait to flush, but try to write as much as we can if there is pending // data. - transport_socket_->doWrite(*write_buffer_, true); + if (reuse_socket_) { + // Don't close connection socket in case of reversed connection. + transport_socket_->doWrite(*write_buffer_, false); + } else { + transport_socket_->doWrite(*write_buffer_, true); + } } if (type != ConnectionCloseType::FlushWriteAndDelay || !delayed_close_timeout_set) { @@ -286,10 +306,12 @@ void ConnectionImpl::setDetectedCloseType(DetectedCloseType close_type) { } void ConnectionImpl::closeThroughFilterManager(ConnectionCloseAction close_action) { + ENVOY_LOG_MISC(debug, "ConnectionImpl: closeThroughFilterManager() called."); if (!socket_->isOpen()) { + ENVOY_LOG_MISC(debug, "socket is not open"); return; } - + ENVOY_LOG_MISC(debug, "socket is open"); if (!enable_close_through_filter_manager_) { ENVOY_CONN_LOG(trace, "connection is closing not through the filter manager", *this); closeConnection(close_action); @@ -301,7 +323,11 @@ void ConnectionImpl::closeThroughFilterManager(ConnectionCloseAction close_actio } void ConnectionImpl::closeSocket(ConnectionEvent close_type) { + ENVOY_CONN_LOG(trace, "closeSocket called, socket_={}, socket_isOpen={}", *this, + socket_ ? "not_null" : "null", socket_ ? socket_->isOpen() : false); + if (!socket_->isOpen()) { + ENVOY_CONN_LOG(trace, "closeSocket: socket is not open, returning", *this); return; } @@ -312,7 +338,12 @@ void ConnectionImpl::closeSocket(ConnectionEvent close_type) { } ENVOY_CONN_LOG(debug, "closing socket: {}", *this, static_cast(close_type)); - transport_socket_->closeSocket(close_type); + + // Don't close the socket transport if this is a reused connection + if (!reuse_socket_) { + ENVOY_CONN_LOG(debug, "closing socket transport_socket_", *this); + transport_socket_->closeSocket(close_type); + } // Drain input and output buffers. updateReadBufferStats(0, 0); @@ -421,7 +452,8 @@ void ConnectionImpl::onRead(uint64_t read_buffer_size) { } read_end_stream_raised_ = true; } - + ENVOY_CONN_LOG(debug, "calling filter_manager_.onRead() - connection_id={}, fd={}", *this, id(), + socket_->ioHandle().fdDoNotUse()); filter_manager_.onRead(); } @@ -652,7 +684,7 @@ void ConnectionImpl::setFailureReason(absl::string_view failure_reason) { void ConnectionImpl::onFileEvent(uint32_t events) { ScopeTrackerScopeState scope(this, this->dispatcher_); - ENVOY_CONN_LOG(trace, "socket event: {}", *this, events); + ENVOY_CONN_LOG(debug, "onFileEvent() ENTRY", *this); if (immediate_error_event_ == ConnectionEvent::LocalClose || immediate_error_event_ == ConnectionEvent::RemoteClose) { @@ -691,12 +723,27 @@ void ConnectionImpl::onFileEvent(uint32_t events) { // It's possible for a write event callback to close the socket (which will cause fd_ to be -1). // In this case ignore read event processing. + ENVOY_CONN_LOG(debug, "onFileEvent() read check - socket_isOpen={}, readEvent={}, fd={}", *this, + socket_ ? socket_->isOpen() : false, + (events & Event::FileReadyType::Read) ? "true" : "false", + socket_ ? socket_->ioHandle().fdDoNotUse() : -1); + if (socket_->isOpen() && (events & Event::FileReadyType::Read)) { + ENVOY_CONN_LOG(debug, "onFileEvent() calling onReadReady() - connection_id={}, fd={}", *this, + id(), socket_->ioHandle().fdDoNotUse()); onReadReady(); + } else { + ENVOY_CONN_LOG(debug, + "onFileEvent() NOT calling onReadReady() - socket_={}, isOpen={}, readEvent={}", + *this, socket_ ? "not_null" : "null", socket_ ? socket_->isOpen() : false, + (events & Event::FileReadyType::Read) ? "true" : "false"); } } void ConnectionImpl::onReadReady() { + ENVOY_CONN_LOG(debug, "onReadReady() ENTRY - socket_={}, state={}, id={}", *this, + socket_ ? "not_null" : "null", static_cast(state()), id()); + ENVOY_CONN_LOG(trace, "read ready. dispatch_buffered_data={}", *this, static_cast(dispatch_buffered_data_)); const bool latched_dispatch_buffered_data = dispatch_buffered_data_; @@ -725,7 +772,9 @@ void ConnectionImpl::onReadReady() { } return; } - + ENVOY_CONN_LOG(debug, + "onReadReady() calling transport_socket_->doRead() - connection_id={}, fd={}", + *this, id(), socket_->ioHandle().fdDoNotUse()); // Clear transport_wants_read_ just before the call to doRead. This is the only way to ensure that // the transport socket read resumption happens as requested; onReadReady() returns early without // reading from the transport if the read buffer is above high watermark at the start of the @@ -734,7 +783,9 @@ void ConnectionImpl::onReadReady() { IoResult result = transport_socket_->doRead(*read_buffer_); uint64_t new_buffer_size = read_buffer_->length(); updateReadBufferStats(result.bytes_processed_, new_buffer_size); - + ENVOY_CONN_LOG(debug, + "onReadReady() transport_socket_->doRead() returned - connection_id={}, fd={}", + *this, id(), socket_->ioHandle().fdDoNotUse()); // The socket is closed immediately when receiving RST. if (result.err_code_.has_value() && result.err_code_ == Api::IoError::IoErrorCode::ConnectionReset) { @@ -761,6 +812,8 @@ void ConnectionImpl::onReadReady() { } read_end_stream_ |= result.end_stream_read_; + ENVOY_CONN_LOG(debug, "calling onRead() - connection_id={}, fd={}", *this, id(), + socket_->ioHandle().fdDoNotUse()); if (result.bytes_processed_ != 0 || result.end_stream_read_ || (latched_dispatch_buffered_data && read_buffer_->length() > 0)) { // Skip onRead if no bytes were processed unless we explicitly want to force onRead for @@ -768,7 +821,8 @@ void ConnectionImpl::onReadReady() { // more data. onRead(new_buffer_size); } - + ENVOY_CONN_LOG(debug, "onRead() returned - connection_id={}, fd={}", *this, id(), + socket_->ioHandle().fdDoNotUse()); // The read callback may have already closed the connection. if (result.action_ == PostIoAction::Close || bothSidesHalfClosed()) { ENVOY_CONN_LOG(debug, "remote close", *this); @@ -925,8 +979,6 @@ bool ConnectionImpl::setSocketOption(Network::SocketOptionName name, absl::Span< Api::SysCallIntResult result = SocketOptionImpl::setSocketOption(*socket_, name, value.data(), value.size()); if (result.return_value_ != 0) { - ENVOY_LOG(warn, "Setting option on socket failed, errno: {}, message: {}", result.errno_, - errorDetails(result.errno_)); return false; } diff --git a/source/common/network/connection_impl.h b/source/common/network/connection_impl.h index 8bfa88878c5cd..b5be8243778af 100644 --- a/source/common/network/connection_impl.h +++ b/source/common/network/connection_impl.h @@ -62,6 +62,8 @@ class ConnectionImpl : public ConnectionImplBase, public TransportSocketCallback void removeReadFilter(ReadFilterSharedPtr filter) override; bool initializeReadFilters() override; + const ConnectionSocketPtr& getSocket() const override { return socket_; } + // Network::Connection void addBytesSentCallback(BytesSentCb cb) override; void enableHalfClose(bool enabled) override; @@ -91,7 +93,7 @@ class ConnectionImpl : public ConnectionImplBase, public TransportSocketCallback absl::optional unixSocketPeerCredentials() const override; Ssl::ConnectionInfoConstSharedPtr ssl() const override { // SSL info may be overwritten by a filter in the provider. - return socket_->connectionInfoProvider().sslConnection(); + return (socket_ != nullptr) ? socket_->connectionInfoProvider().sslConnection() : nullptr; } State state() const override; bool connecting() const override { @@ -173,6 +175,10 @@ class ConnectionImpl : public ConnectionImplBase, public TransportSocketCallback // then the filter chain has called readDisable, and does not want additional data. bool filterChainWantsData(); + // Cleans up the connection resources without closing the socket. + // Used when transferring socket ownership for reverse connections. + // void cleanUpConnectionImpl(); + // Network::ConnectionImplBase void closeConnectionImmediately() final; void closeThroughFilterManager(ConnectionCloseAction close_action); @@ -261,6 +267,11 @@ class ConnectionImpl : public ConnectionImplBase, public TransportSocketCallback // read_disable_count_ == 0 to ensure that read resumption happens when remaining bytes are held // in transport socket internal buffers. bool transport_wants_read_ : 1; + + // Used on the responder envoy to mark an active connection accepted by a listener which will + // be used as a reverse connection. The socket for such a connection is closed upon draining + // of the owning listener. + bool reuse_socket_ : 1; bool enable_close_through_filter_manager_ : 1; }; @@ -302,9 +313,13 @@ class ClientConnectionImpl : public ConnectionImpl, virtual public ClientConnect Network::TransportSocketPtr&& transport_socket, const Network::ConnectionSocket::OptionsSharedPtr& options, const Network::TransportSocketOptionsConstSharedPtr& transport_options); + // Method to create client connection from downstream connection + ClientConnectionImpl(Event::Dispatcher& dispatcher, + Network::TransportSocketPtr&& transport_socket, + Network::ConnectionSocketPtr&& downstream_socket); // Network::ClientConnection - void connect() override; + virtual void connect() override; private: void onConnected() override; diff --git a/source/common/network/connection_impl_base.cc b/source/common/network/connection_impl_base.cc index e047afd0382ef..487e4d1a70923 100644 --- a/source/common/network/connection_impl_base.cc +++ b/source/common/network/connection_impl_base.cc @@ -63,6 +63,7 @@ void ConnectionImplBase::raiseConnectionEvent(ConnectionEvent event) { } if (callback != nullptr) { + ENVOY_LOG_MISC(debug, "ConnectionImplBase: calling onEvent()"); callback->onEvent(event); } } diff --git a/source/common/network/io_socket_handle_impl.cc b/source/common/network/io_socket_handle_impl.cc index 2509e3fbd391f..a2505ca9c06d3 100644 --- a/source/common/network/io_socket_handle_impl.cc +++ b/source/common/network/io_socket_handle_impl.cc @@ -60,13 +60,20 @@ IoSocketHandleImpl::~IoSocketHandleImpl() { } Api::IoCallUint64Result IoSocketHandleImpl::close() { + ENVOY_LOG_MISC(error, "IoSocketHandleImpl::close() called, fd_={}, SOCKET_VALID={}", fd_, + SOCKET_VALID(fd_)); + if (file_event_) { + ENVOY_LOG_MISC(error, "IoSocketHandleImpl::close() resetting file_event_"); file_event_.reset(); } ASSERT(SOCKET_VALID(fd_)); + ENVOY_LOG_MISC(error, "IoSocketHandleImpl::close() calling system close(fd_={})", fd_); const int rc = Api::OsSysCallsSingleton::get().close(fd_).return_value_; + ENVOY_LOG_MISC(error, "IoSocketHandleImpl::close() system close returned rc={}", rc); SET_SOCKET_INVALID(fd_); + ENVOY_LOG_MISC(error, "IoSocketHandleImpl::close() completed, fd_ set to invalid"); return {static_cast(rc), Api::IoError::none()}; } @@ -223,8 +230,8 @@ Api::IoCallUint64Result IoSocketHandleImpl::sendmsg(const Buffer::RawSlice* slic } const Api::SysCallSizeResult result = os_syscalls.sendmsg(fd_, &message, flags); if (result.return_value_ < 0 && result.errno_ == SOCKET_ERROR_INVAL) { - ENVOY_LOG(error, fmt::format("EINVAL error. Socket is open: {}, IPv{}.", isOpen(), - self_ip->version() == Address::IpVersion::v6 ? 6 : 4)); + ENVOY_LOG_MISC(error, fmt::format("EINVAL error. Socket is open: {}, IPv{}.", isOpen(), + self_ip->version() == Address::IpVersion::v6 ? 6 : 4)); } return sysCallResultToIoCallResult(result); } @@ -600,7 +607,28 @@ void IoSocketHandleImpl::initializeFileEvent(Event::Dispatcher& dispatcher, Even Event::FileTriggerType trigger, uint32_t events) { ASSERT(file_event_ == nullptr, "Attempting to initialize two `file_event_` for the same " "file descriptor. This is not allowed."); + + // Add trace logging to identify thread + ENVOY_LOG_MISC( + trace, + "IoSocketHandleImpl::initializeFileEvent() called for fd={} on thread: {} (isThreadSafe={})", + fd_, dispatcher.name(), dispatcher.isThreadSafe()); + + // Log additional thread info + if (dispatcher.isThreadSafe()) { + ENVOY_LOG_MISC(trace, "initializeFileEvent: Called on MAIN thread for fd={}", fd_); + } else { + ENVOY_LOG_MISC(trace, "initializeFileEvent: Called on WORKER thread '{}' for fd={}", + dispatcher.name(), fd_); + } + + ENVOY_LOG_MISC(trace, + "initializeFileEvent: Creating file event with trigger={}, events={} for fd={}", + static_cast(trigger), events, fd_); + file_event_ = dispatcher.createFileEvent(fd_, cb, trigger, events); + + ENVOY_LOG_MISC(trace, "initializeFileEvent: File event created successfully for fd={}", fd_); } void IoSocketHandleImpl::activateFileEvents(uint32_t events) { diff --git a/source/common/network/io_socket_handle_impl.h b/source/common/network/io_socket_handle_impl.h index 28430cc9eac4e..80fa3f451974b 100644 --- a/source/common/network/io_socket_handle_impl.h +++ b/source/common/network/io_socket_handle_impl.h @@ -75,7 +75,12 @@ class IoSocketHandleImpl : public IoSocketHandleBaseImpl { void activateFileEvents(uint32_t events) override; void enableFileEvents(uint32_t events) override; - void resetFileEvents() override { file_event_.reset(); } + void resetFileEvents() override { + ENVOY_LOG_MISC(debug, "IoSocketHandleImpl::resetFileEvents() called for fd={}, file_event_={}", + fd_, file_event_ ? "not_null" : "null"); + file_event_.reset(); + ENVOY_LOG_MISC(debug, "IoSocketHandleImpl::resetFileEvents() completed for fd={}", fd_); + } Api::SysCallIntResult shutdown(int how) override; diff --git a/source/common/network/ip_address_parsing.cc b/source/common/network/ip_address_parsing.cc new file mode 100644 index 0000000000000..9ea583662b555 --- /dev/null +++ b/source/common/network/ip_address_parsing.cc @@ -0,0 +1,67 @@ +#include "source/common/network/ip_address_parsing.h" + +#include + +#include "source/common/api/os_sys_calls_impl.h" + +namespace Envoy { +namespace Network { +namespace IpAddressParsing { + +StatusOr parseIPv4(const std::string& ip_address, uint16_t port) { + // Use inet_pton() for IPv4 as it's simpler, faster, and already enforces + // strict dotted-quad format while rejecting non-standard notations. + sockaddr_in sa4; + memset(&sa4, 0, sizeof(sa4)); + if (inet_pton(AF_INET, ip_address.c_str(), &sa4.sin_addr) != 1) { + return absl::FailedPreconditionError("failed parsing ipv4"); + } + sa4.sin_family = AF_INET; + sa4.sin_port = htons(port); + return sa4; +} + +StatusOr parseIPv6(const std::string& ip_address, uint16_t port) { + // Parse IPv6 with optional scope using getaddrinfo(). + // While inet_pton() would be faster and simpler, it does not support IPv6 + // addresses that specify a scope, e.g. `::%eth0` to listen on only one interface. + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + struct addrinfo* res = nullptr; + // Suppresses any potentially lengthy network host address lookups and inhibit the + // invocation of a name resolution service. + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + hints.ai_family = AF_INET6; + // Given that we do not specify a service but we use getaddrinfo() to only parse the node + // address, specifying the socket type allows to hint the getaddrinfo() to return only an + // element with the below socket type. The behavior though remains platform dependent and + // anyway we consume only the first element if the call succeeds. + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + // We want to use the interface of OsSysCalls for this for the platform-independence, but + // we do not want to use the common OsSysCallsSingleton. + // + // The problem with using OsSysCallsSingleton is that we likely want to override getaddrinfo() + // for DNS lookups in tests, but typically that override would resolve a name to e.g. the + // address from resolveUrl("tcp://[::1]:80"). But resolveUrl calls ``parseIPv6``, which calls + // getaddrinfo(), so if we use the mock here then mocking DNS causes infinite recursion. + // + // We do not ever need to mock this getaddrinfo() call, because it is only used to parse + // numeric IP addresses, per ``ai_flags``, so it should be deterministic resolution. There + // is no need to mock it to test failure cases. + static Api::OsSysCallsImpl os_sys_calls; + const Api::SysCallIntResult rc = + os_sys_calls.getaddrinfo(ip_address.c_str(), /*service=*/nullptr, &hints, &res); + if (rc.return_value_ != 0) { + return absl::FailedPreconditionError(absl::StrCat("getaddrinfo error: ", rc.return_value_)); + } + sockaddr_in6 sa6 = *reinterpret_cast(res->ai_addr); + os_sys_calls.freeaddrinfo(res); + sa6.sin6_port = htons(port); + return sa6; +} + +} // namespace IpAddressParsing +} // namespace Network +} // namespace Envoy diff --git a/source/common/network/ip_address_parsing.h b/source/common/network/ip_address_parsing.h new file mode 100644 index 0000000000000..dbf5090d5ba0d --- /dev/null +++ b/source/common/network/ip_address_parsing.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "envoy/common/platform.h" + +#include "source/common/common/statusor.h" + +namespace Envoy { +namespace Network { + +// Utilities for parsing numeric IP addresses into sockaddr structures. +// These helper methods avoid higher-level dependencies and are suitable for +// use by multiple components that need low-level parsing without constructing +// `Address::Instance` objects. +namespace IpAddressParsing { + +// Parse an IPv4 address string into a sockaddr_in with the provided port. +// Returns a failure status if the address is not a valid numeric IPv4 string. +StatusOr parseIPv4(const std::string& ip_address, uint16_t port); + +// Parse an IPv6 address string (optionally with a scope id, e.g. ``fe80::1%2`` +// or ``fe80::1%eth0``) into a sockaddr_in6 with the provided port. +// +// Uses getaddrinfo() with ``AI_NUMERICHOST|AI_NUMERICSERV`` to avoid DNS lookups +// and to support scoped addresses consistently across all platforms. +// +// Returns a failure status if the address is not a valid numeric IPv6 string. +StatusOr parseIPv6(const std::string& ip_address, uint16_t port); + +} // namespace IpAddressParsing +} // namespace Network +} // namespace Envoy diff --git a/source/common/network/multi_connection_base_impl.h b/source/common/network/multi_connection_base_impl.h index 13e0c0a636a17..c725ad75dca62 100644 --- a/source/common/network/multi_connection_base_impl.h +++ b/source/common/network/multi_connection_base_impl.h @@ -134,6 +134,8 @@ class MultiConnectionBaseImpl : public ClientConnection, void hashKey(std::vector& hash_key) const override; void dumpState(std::ostream& os, int indent_level) const override; + const Network::ConnectionSocketPtr& getSocket() const override { PANIC("not implemented"); } + private: // ConnectionCallbacks which will be set on an ClientConnection which // sends connection events back to the MultiConnectionBaseImpl. diff --git a/source/common/network/socket_impl.h b/source/common/network/socket_impl.h index 0892f81984cad..726037014f3f0 100644 --- a/source/common/network/socket_impl.h +++ b/source/common/network/socket_impl.h @@ -133,8 +133,14 @@ class SocketImpl : public virtual Socket { IoHandle& ioHandle() override { return *io_handle_; } const IoHandle& ioHandle() const override { return *io_handle_; } void close() override { + ENVOY_LOG_MISC(trace, "SocketImpl::close() called, io_handle_={}, io_handle_isOpen={}", + io_handle_ ? "not_null" : "null", io_handle_ ? io_handle_->isOpen() : false); if (io_handle_ && io_handle_->isOpen()) { + ENVOY_LOG_MISC(trace, "SocketImpl::close() calling io_handle_->close()"); io_handle_->close(); + ENVOY_LOG_MISC(trace, "SocketImpl::close() io_handle_->close() completed"); + } else { + ENVOY_LOG_MISC(trace, "SocketImpl::close() skipping close - io_handle is null or not open"); } } bool isOpen() const override { return io_handle_ && io_handle_->isOpen(); } diff --git a/source/common/network/tcp_listener_impl.cc b/source/common/network/tcp_listener_impl.cc index a17be51b16c8b..22e97b36681bf 100644 --- a/source/common/network/tcp_listener_impl.cc +++ b/source/common/network/tcp_listener_impl.cc @@ -119,8 +119,8 @@ absl::Status TcpListenerImpl::onSocketEvent(short flags) { track_global_cx_limit_in_overload_manager_)); } - ENVOY_LOG_MISC(trace, "TcpListener accepted {} new connections.", - connections_accepted_from_kernel_count); + // ENVOY_LOG_MISC(trace, "TcpListener accepted {} new connections.", + // connections_accepted_from_kernel_count); cb_.recordConnectionsAcceptedOnSocketEvent(connections_accepted_from_kernel_count); return absl::OkStatus(); } diff --git a/source/common/network/udp_listener_impl.cc b/source/common/network/udp_listener_impl.cc index 838db8b4c9c75..082fe4e92c640 100644 --- a/source/common/network/udp_listener_impl.cc +++ b/source/common/network/udp_listener_impl.cc @@ -174,7 +174,7 @@ UdpListenerWorkerRouterImpl::UdpListenerWorkerRouterImpl(uint32_t concurrency) : workers_(concurrency) {} void UdpListenerWorkerRouterImpl::registerWorkerForListener(UdpListenerCallbacks& listener) { - absl::WriterMutexLock lock(&mutex_); + absl::WriterMutexLock lock(mutex_); ASSERT(listener.workerIndex() < workers_.size()); ASSERT(workers_.at(listener.workerIndex()) == nullptr); @@ -182,14 +182,14 @@ void UdpListenerWorkerRouterImpl::registerWorkerForListener(UdpListenerCallbacks } void UdpListenerWorkerRouterImpl::unregisterWorkerForListener(UdpListenerCallbacks& listener) { - absl::WriterMutexLock lock(&mutex_); + absl::WriterMutexLock lock(mutex_); ASSERT(workers_.at(listener.workerIndex()) == &listener); workers_.at(listener.workerIndex()) = nullptr; } void UdpListenerWorkerRouterImpl::deliver(uint32_t dest_worker_index, UdpRecvData&& data) { - absl::ReaderMutexLock lock(&mutex_); + absl::ReaderMutexLock lock(mutex_); ASSERT(dest_worker_index < workers_.size(), "UdpListenerCallbacks::destination returned out-of-range value"); diff --git a/source/common/network/utility.cc b/source/common/network/utility.cc index 9a0ba0d082f3a..c3271f301d717 100644 --- a/source/common/network/utility.cc +++ b/source/common/network/utility.cc @@ -22,6 +22,7 @@ #include "source/common/common/utility.h" #include "source/common/network/address_impl.h" #include "source/common/network/io_socket_error_impl.h" +#include "source/common/network/ip_address_parsing.h" #include "source/common/network/socket_option_impl.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -106,75 +107,18 @@ Api::IoCallUint64Result receiveMessage(uint64_t max_rx_datagram_size, Buffer::In return result; } -StatusOr parseV4Address(const std::string& ip_address, uint16_t port) { - sockaddr_in sa4; - memset(&sa4, 0, sizeof(sa4)); - if (inet_pton(AF_INET, ip_address.c_str(), &sa4.sin_addr) != 1) { - return absl::FailedPreconditionError("failed parsing ipv4"); - } - sa4.sin_family = AF_INET; - sa4.sin_port = htons(port); - return sa4; -} - -StatusOr parseV6Address(const std::string& ip_address, uint16_t port) { - // Parse IPv6 with optional scope using getaddrinfo(). - // While inet_pton() would be faster and simpler, it does not support IPv6 - // addresses that specify a scope, e.g. `::%eth0` to listen on only one interface. - struct addrinfo hints; - memset(&hints, 0, sizeof(hints)); - struct addrinfo* res = nullptr; - // Suppresses any potentially lengthy network host address lookups and inhibit the invocation of - // a name resolution service. - hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; - hints.ai_family = AF_INET6; - // Given that we don't specify a service but we use getaddrinfo() to only parse the node - // address, specifying the socket type allows to hint the getaddrinfo() to return only an - // element with the below socket type. The behavior though remains platform dependent and anyway - // we consume only the first element (if the call succeeds). - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - - // We want to use the interface of OsSysCalls for this for the - // platform-independence, but we don't want to use the common - // OsSysCallsSingleton. - // - // The problem with using OsSysCallsSingleton is that we likely - // want to override getaddrinfo() for DNS lookups in tests, but - // typically that override would resolve a name to e.g. - // the address from resolveUrl("tcp://[::1]:80") - but resolveUrl - // calls parseV6Address, which calls getaddrinfo(), so if we use - // the mock *here* then mocking DNS causes infinite recursion. - // - // We don't ever need to mock *this* getaddrinfo() call, because - // it's only used to parse numeric IP addresses, per `ai_flags`, - // so it should be deterministic resolution; there's no need to - // mock it to test failure cases. - static Api::OsSysCallsImpl os_sys_calls; - - const Api::SysCallIntResult rc = - os_sys_calls.getaddrinfo(ip_address.c_str(), /*service=*/nullptr, &hints, &res); - if (rc.return_value_ != 0) { - return absl::FailedPreconditionError(fmt::format("getaddrinfo error: {}", rc.return_value_)); - } - sockaddr_in6 sa6 = *reinterpret_cast(res->ai_addr); - os_sys_calls.freeaddrinfo(res); - sa6.sin6_port = htons(port); - return sa6; -} - } // namespace Address::InstanceConstSharedPtr Utility::parseInternetAddressNoThrow(const std::string& ip_address, uint16_t port, bool v6only, absl::optional network_namespace) { - StatusOr sa4 = parseV4Address(ip_address, port); + StatusOr sa4 = IpAddressParsing::parseIPv4(ip_address, port); if (sa4.ok()) { return instanceOrNull(Address::InstanceFactory::createInstancePtr( &sa4.value(), nullptr, network_namespace)); } - StatusOr sa6 = parseV6Address(ip_address, port); + StatusOr sa6 = IpAddressParsing::parseIPv6(ip_address, port); if (sa6.ok()) { return instanceOrNull(Address::InstanceFactory::createInstancePtr( *sa6, v6only, nullptr, network_namespace)); @@ -200,7 +144,7 @@ Utility::parseInternetAddressAndPortNoThrow(const std::string& ip_address, bool if (port_str.empty() || !absl::SimpleAtoi(port_str, &port64) || port64 > 65535) { return nullptr; } - StatusOr sa6 = parseV6Address(ip_str, port64); + StatusOr sa6 = IpAddressParsing::parseIPv6(ip_str, static_cast(port64)); if (sa6.ok()) { return instanceOrNull(Address::InstanceFactory::createInstancePtr( *sa6, v6only, nullptr, network_namespace)); @@ -218,10 +162,10 @@ Utility::parseInternetAddressAndPortNoThrow(const std::string& ip_address, bool if (port_str.empty() || !absl::SimpleAtoi(port_str, &port64) || port64 > 65535) { return nullptr; } - StatusOr sa4 = parseV4Address(ip_str, port64); + StatusOr sa4 = IpAddressParsing::parseIPv4(ip_str, static_cast(port64)); if (sa4.ok()) { - return instanceOrNull( - Address::InstanceFactory::createInstancePtr(&sa4.value())); + return instanceOrNull(Address::InstanceFactory::createInstancePtr( + &sa4.value(), nullptr, network_namespace)); } return nullptr; } @@ -780,35 +724,16 @@ Api::IoErrorPtr Utility::readPacketsFromSocket(IoHandle& handle, // this goes over MAX_NUM_PACKETS_PER_EVENT_LOOP. size_t num_packets_to_read = std::min( MAX_NUM_PACKETS_PER_EVENT_LOOP, udp_packet_processor.numPacketsExpectedPerEventLoop()); - const bool apply_read_limit_differently = Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.udp_socket_apply_aggregated_read_limit"); - size_t num_reads; - if (apply_read_limit_differently) { - // Call socket read at least once and at most num_packets_read to avoid infinite loop. - num_reads = std::max(1, num_packets_to_read); - } else { - switch (recv_msg_method) { - case UdpRecvMsgMethod::RecvMsgWithGro: - num_reads = (num_packets_to_read / NUM_DATAGRAMS_PER_RECEIVE); - break; - case UdpRecvMsgMethod::RecvMmsg: - num_reads = (num_packets_to_read / NUM_DATAGRAMS_PER_RECEIVE); - break; - case UdpRecvMsgMethod::RecvMsg: - num_reads = num_packets_to_read; - break; - } - // Make sure to read at least once. - num_reads = std::max(1, num_reads); - } + // Call socket read at least once and at most num_packets_read to avoid infinite loop. + size_t num_reads = std::max(1, num_packets_to_read); do { const uint32_t old_packets_dropped = packets_dropped; uint32_t num_packets_processed = 0; const MonotonicTime receive_time = time_source.monotonicTime(); - Api::IoCallUint64Result result = Utility::readFromSocket( - handle, local_address, udp_packet_processor, receive_time, recv_msg_method, - &packets_dropped, apply_read_limit_differently ? &num_packets_processed : nullptr); + Api::IoCallUint64Result result = + Utility::readFromSocket(handle, local_address, udp_packet_processor, receive_time, + recv_msg_method, &packets_dropped, &num_packets_processed); if (!result.ok()) { // No more to read or encountered a system error. @@ -831,12 +756,10 @@ Api::IoErrorPtr Utility::readPacketsFromSocket(IoHandle& handle, delta); udp_packet_processor.onDatagramsDropped(delta); } - if (apply_read_limit_differently) { - if (num_packets_to_read <= num_packets_processed) { - return std::move(result.err_); - } - num_packets_to_read -= num_packets_processed; + if (num_packets_to_read <= num_packets_processed) { + return std::move(result.err_); } + num_packets_to_read -= num_packets_processed; --num_reads; if (num_reads == 0) { return std::move(result.err_); diff --git a/source/common/network/utility.h b/source/common/network/utility.h index 31ca56e4f99a2..df0d22307f2c5 100644 --- a/source/common/network/utility.h +++ b/source/common/network/utility.h @@ -410,6 +410,10 @@ class Utility { // Restore the original network namespace before returning the function result. setns_result = Api::LinuxOsSysCallsSingleton().get().setns(og_netns_fd, CLONE_NEWNET); + + // If we cannot jump back into the original network namespace, this is an unrecoverable error. + // It would leave the current thread in another network namespace indefinitely, so we cannot + // continue running in that state. RELEASE_ASSERT( setns_result.return_value_ == 0, fmt::format("failed to restore original netns (fd={}): {}", netns_fd, errorDetails(errno))); diff --git a/source/common/orca/BUILD b/source/common/orca/BUILD index 56d750828fb64..0787c80e2bbf6 100644 --- a/source/common/orca/BUILD +++ b/source/common/orca/BUILD @@ -19,7 +19,7 @@ envoy_cc_library( "//source/common/http:header_utility_lib", "//source/common/protobuf:utility_lib_header", "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", - "@com_github_fmtlib_fmt//:fmtlib", + "@com_github_fmtlib_fmt//:fmt", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", ], @@ -35,7 +35,7 @@ envoy_cc_library( "//source/common/http:header_utility_lib", "//source/common/protobuf:utility_lib_header", "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", - "@com_github_fmtlib_fmt//:fmtlib", + "@com_github_fmtlib_fmt//:fmt", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", diff --git a/source/common/protobuf/BUILD b/source/common/protobuf/BUILD index b3e31d715c8e0..e1dafd16e44d6 100644 --- a/source/common/protobuf/BUILD +++ b/source/common/protobuf/BUILD @@ -1,5 +1,5 @@ load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load( "//bazel:envoy_build_system.bzl", "envoy_cc_library", @@ -64,6 +64,7 @@ envoy_cc_library( "//envoy/protobuf:message_validator_interface", "//source/common/common:statusor_lib", "//source/common/common:stl_helpers", + "@com_google_absl//absl/strings", "@envoy_api//envoy/type/v3:pkg_cc_proto", ], ) diff --git a/source/common/protobuf/deterministic_hash.cc b/source/common/protobuf/deterministic_hash.cc index 62fa7a5745153..67db37db978c8 100644 --- a/source/common/protobuf/deterministic_hash.cc +++ b/source/common/protobuf/deterministic_hash.cc @@ -176,7 +176,7 @@ absl::string_view typeUrlToDescriptorFullName(absl::string_view url) { return url; } -std::unique_ptr unpackAnyForReflection(const ProtobufWkt::Any& any) { +std::unique_ptr unpackAnyForReflection(const Protobuf::Any& any) { const Protobuf::Descriptor* descriptor = Protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName( typeUrlToDescriptorFullName(any.type_url())); @@ -189,7 +189,9 @@ std::unique_ptr unpackAnyForReflection(const ProtobufWkt::Any Protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor); ASSERT(prototype != nullptr, "should be impossible since the descriptor is known"); std::unique_ptr msg(prototype->New()); - any.UnpackTo(msg.get()); + if (!any.UnpackTo(msg.get())) { + return nullptr; + } return msg; } @@ -200,7 +202,7 @@ uint64_t reflectionHashMessage(const Protobuf::Message& message, uint64_t seed) const Protobuf::Descriptor* descriptor = message.GetDescriptor(); seed = HashUtil::xxHash64(descriptor->full_name(), seed); if (descriptor->well_known_type() == Protobuf::Descriptor::WELLKNOWNTYPE_ANY) { - const ProtobufWkt::Any* any = Protobuf::DynamicCastMessage(&message); + const Protobuf::Any* any = Protobuf::DynamicCastMessage(&message); ASSERT(any != nullptr, "casting to any should always work for WELLKNOWNTYPE_ANY"); std::unique_ptr submsg = unpackAnyForReflection(*any); if (submsg == nullptr) { diff --git a/source/common/protobuf/protobuf.h b/source/common/protobuf/protobuf.h index 0af00b515d4ad..cec93d68d034a 100644 --- a/source/common/protobuf/protobuf.h +++ b/source/common/protobuf/protobuf.h @@ -65,32 +65,42 @@ namespace Protobuf { using Closure = ::google::protobuf::Closure; +using ::google::protobuf::Any; // NOLINT(misc-unused-using-decls) using ::google::protobuf::Arena; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::BoolValue; // NOLINT(misc-unused-using-decls) using ::google::protobuf::BytesValue; // NOLINT(misc-unused-using-decls) using ::google::protobuf::Descriptor; // NOLINT(misc-unused-using-decls) using ::google::protobuf::DescriptorPool; // NOLINT(misc-unused-using-decls) using ::google::protobuf::DescriptorPoolDatabase; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::Duration; // NOLINT(misc-unused-using-decls) using ::google::protobuf::DynamicCastMessage; // NOLINT(misc-unused-using-decls) using ::google::protobuf::DynamicMessageFactory; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::Empty; // NOLINT(misc-unused-using-decls) using ::google::protobuf::EnumValueDescriptor; // NOLINT(misc-unused-using-decls) using ::google::protobuf::FieldDescriptor; // NOLINT(misc-unused-using-decls) using ::google::protobuf::FieldMask; // NOLINT(misc-unused-using-decls) using ::google::protobuf::FileDescriptor; // NOLINT(misc-unused-using-decls) using ::google::protobuf::FileDescriptorProto; // NOLINT(misc-unused-using-decls) using ::google::protobuf::FileDescriptorSet; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::ListValue; // NOLINT(misc-unused-using-decls) using ::google::protobuf::Map; // NOLINT(misc-unused-using-decls) using ::google::protobuf::MapPair; // NOLINT(misc-unused-using-decls) using ::google::protobuf::MessageFactory; // NOLINT(misc-unused-using-decls) using ::google::protobuf::MethodDescriptor; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::NULL_VALUE; // NOLINT(misc-unused-using-decls) using ::google::protobuf::OneofDescriptor; // NOLINT(misc-unused-using-decls) using ::google::protobuf::Reflection; // NOLINT(misc-unused-using-decls) using ::google::protobuf::RepeatedField; // NOLINT(misc-unused-using-decls) using ::google::protobuf::RepeatedFieldBackInserter; // NOLINT(misc-unused-using-decls) using ::google::protobuf::RepeatedPtrField; // NOLINT(misc-unused-using-decls) using ::google::protobuf::RepeatedPtrFieldBackInserter; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::StringValue; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::Struct; // NOLINT(misc-unused-using-decls) using ::google::protobuf::TextFormat; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::Timestamp; // NOLINT(misc-unused-using-decls) using ::google::protobuf::Type; // NOLINT(misc-unused-using-decls) using ::google::protobuf::UInt32Value; // NOLINT(misc-unused-using-decls) +using ::google::protobuf::Value; // NOLINT(misc-unused-using-decls) using Message = ::google::protobuf::MessageLite; @@ -147,10 +157,6 @@ namespace Envoy { // Allows mapping from google::protobuf::util to other util libraries. namespace ProtobufUtil = ::google::protobuf::util; -// Protobuf well-known types (WKT) should be referenced via the ProtobufWkt -// namespace. -namespace ProtobufWkt = ::google::protobuf; - // Alternative protobuf implementations might not have the same basic types. // Below we provide wrappers to facilitate remapping of the type during import. namespace ProtobufTypes { diff --git a/source/common/protobuf/utility.cc b/source/common/protobuf/utility.cc index 9f1b7b7a87695..19b249972369e 100644 --- a/source/common/protobuf/utility.cc +++ b/source/common/protobuf/utility.cc @@ -29,7 +29,7 @@ namespace { // Validates that the max value of nanoseconds and seconds doesn't cause an // overflow in the protobuf time-util computations. -absl::Status validateDurationNoThrow(const ProtobufWkt::Duration& duration) { +absl::Status validateDurationNoThrow(const Protobuf::Duration& duration) { // Apply a strict max boundary to the `seconds` value to avoid overflow when // both seconds and nanoseconds are at their highest values. // Note that protobuf internally converts to the input's seconds and @@ -54,7 +54,7 @@ absl::Status validateDurationNoThrow(const ProtobufWkt::Duration& duration) { return absl::OkStatus(); } -void validateDuration(const ProtobufWkt::Duration& duration) { +void validateDuration(const Protobuf::Duration& duration) { const absl::Status result = validateDurationNoThrow(duration); if (!result.ok()) { throwEnvoyExceptionOrPanic(std::string(result.message())); @@ -341,7 +341,7 @@ class DurationFieldProtoVisitor : public ProtobufMessage::ConstProtoVisitor { absl::Span, bool) override { const Protobuf::ReflectableMessage reflectable_message = createReflectableMessage(message); if (reflectable_message->GetDescriptor()->full_name() == "google.protobuf.Duration") { - ProtobufWkt::Duration duration_message; + Protobuf::Duration duration_message; #if defined(ENVOY_ENABLE_FULL_PROTOS) duration_message.CheckTypeAndMergeFrom(message); #else @@ -393,7 +393,7 @@ void MessageUtil::recursivePgvCheck(const Protobuf::Message& message) { THROW_IF_NOT_OK(ProtobufMessage::traverseMessage(visitor, message, true)); } -void MessageUtil::packFrom(ProtobufWkt::Any& any_message, const Protobuf::Message& message) { +void MessageUtil::packFrom(Protobuf::Any& any_message, const Protobuf::Message& message) { #if defined(ENVOY_ENABLE_FULL_PROTOS) any_message.PackFrom(message); #else @@ -402,8 +402,7 @@ void MessageUtil::packFrom(ProtobufWkt::Any& any_message, const Protobuf::Messag #endif } -absl::Status MessageUtil::unpackTo(const ProtobufWkt::Any& any_message, - Protobuf::Message& message) { +absl::Status MessageUtil::unpackTo(const Protobuf::Any& any_message, Protobuf::Message& message) { #if defined(ENVOY_ENABLE_FULL_PROTOS) if (!any_message.UnpackTo(&message)) { return absl::InternalError(absl::StrCat("Unable to unpack as ", @@ -430,17 +429,17 @@ std::string MessageUtil::convertToStringForLogs(const Protobuf::Message& message #endif } -ProtobufWkt::Struct MessageUtil::keyValueStruct(const std::string& key, const std::string& value) { - ProtobufWkt::Struct struct_obj; - ProtobufWkt::Value val; +Protobuf::Struct MessageUtil::keyValueStruct(const std::string& key, const std::string& value) { + Protobuf::Struct struct_obj; + Protobuf::Value val; val.set_string_value(value); (*struct_obj.mutable_fields())[key] = val; return struct_obj; } -ProtobufWkt::Struct MessageUtil::keyValueStruct(const std::map& fields) { - ProtobufWkt::Struct struct_obj; - ProtobufWkt::Value val; +Protobuf::Struct MessageUtil::keyValueStruct(const std::map& fields) { + Protobuf::Struct struct_obj; + Protobuf::Value val; for (const auto& pair : fields) { val.set_string_value(pair.second); (*struct_obj.mutable_fields())[pair.first] = val; @@ -669,31 +668,31 @@ std::string MessageUtil::toTextProto(const Protobuf::Message& message) { #endif } -bool ValueUtil::equal(const ProtobufWkt::Value& v1, const ProtobufWkt::Value& v2) { - ProtobufWkt::Value::KindCase kind = v1.kind_case(); +bool ValueUtil::equal(const Protobuf::Value& v1, const Protobuf::Value& v2) { + Protobuf::Value::KindCase kind = v1.kind_case(); if (kind != v2.kind_case()) { return false; } switch (kind) { - case ProtobufWkt::Value::KIND_NOT_SET: - return v2.kind_case() == ProtobufWkt::Value::KIND_NOT_SET; + case Protobuf::Value::KIND_NOT_SET: + return v2.kind_case() == Protobuf::Value::KIND_NOT_SET; - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::kNullValue: return true; - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: return v1.number_value() == v2.number_value(); - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: return v1.string_value() == v2.string_value(); - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: return v1.bool_value() == v2.bool_value(); - case ProtobufWkt::Value::kStructValue: { - const ProtobufWkt::Struct& s1 = v1.struct_value(); - const ProtobufWkt::Struct& s2 = v2.struct_value(); + case Protobuf::Value::kStructValue: { + const Protobuf::Struct& s1 = v1.struct_value(); + const Protobuf::Struct& s2 = v2.struct_value(); if (s1.fields_size() != s2.fields_size()) { return false; } @@ -710,9 +709,9 @@ bool ValueUtil::equal(const ProtobufWkt::Value& v1, const ProtobufWkt::Value& v2 return true; } - case ProtobufWkt::Value::kListValue: { - const ProtobufWkt::ListValue& l1 = v1.list_value(); - const ProtobufWkt::ListValue& l2 = v2.list_value(); + case Protobuf::Value::kListValue: { + const Protobuf::ListValue& l1 = v1.list_value(); + const Protobuf::ListValue& l2 = v2.list_value(); if (l1.values_size() != l2.values_size()) { return false; } @@ -727,57 +726,57 @@ bool ValueUtil::equal(const ProtobufWkt::Value& v1, const ProtobufWkt::Value& v2 return false; } -const ProtobufWkt::Value& ValueUtil::nullValue() { - static const auto* v = []() -> ProtobufWkt::Value* { - auto* vv = new ProtobufWkt::Value(); - vv->set_null_value(ProtobufWkt::NULL_VALUE); +const Protobuf::Value& ValueUtil::nullValue() { + static const auto* v = []() -> Protobuf::Value* { + auto* vv = new Protobuf::Value(); + vv->set_null_value(Protobuf::NULL_VALUE); return vv; }(); return *v; } -ProtobufWkt::Value ValueUtil::stringValue(absl::string_view str) { - ProtobufWkt::Value val; +Protobuf::Value ValueUtil::stringValue(absl::string_view str) { + Protobuf::Value val; val.set_string_value(str); return val; } -ProtobufWkt::Value ValueUtil::optionalStringValue(const absl::optional& str) { +Protobuf::Value ValueUtil::optionalStringValue(const absl::optional& str) { if (str.has_value()) { return ValueUtil::stringValue(str.value()); } return ValueUtil::nullValue(); } -ProtobufWkt::Value ValueUtil::boolValue(bool b) { - ProtobufWkt::Value val; +Protobuf::Value ValueUtil::boolValue(bool b) { + Protobuf::Value val; val.set_bool_value(b); return val; } -ProtobufWkt::Value ValueUtil::structValue(const ProtobufWkt::Struct& obj) { - ProtobufWkt::Value val; +Protobuf::Value ValueUtil::structValue(const Protobuf::Struct& obj) { + Protobuf::Value val; (*val.mutable_struct_value()) = obj; return val; } -ProtobufWkt::Value ValueUtil::listValue(const std::vector& values) { - auto list = std::make_unique(); +Protobuf::Value ValueUtil::listValue(const std::vector& values) { + auto list = std::make_unique(); for (const auto& value : values) { *list->add_values() = value; } - ProtobufWkt::Value val; + Protobuf::Value val; val.set_allocated_list_value(list.release()); return val; } -uint64_t DurationUtil::durationToMilliseconds(const ProtobufWkt::Duration& duration) { +uint64_t DurationUtil::durationToMilliseconds(const Protobuf::Duration& duration) { validateDuration(duration); return Protobuf::util::TimeUtil::DurationToMilliseconds(duration); } absl::StatusOr -DurationUtil::durationToMillisecondsNoThrow(const ProtobufWkt::Duration& duration) { +DurationUtil::durationToMillisecondsNoThrow(const Protobuf::Duration& duration) { const absl::Status result = validateDurationNoThrow(duration); if (!result.ok()) { return result; @@ -785,13 +784,13 @@ DurationUtil::durationToMillisecondsNoThrow(const ProtobufWkt::Duration& duratio return Protobuf::util::TimeUtil::DurationToMilliseconds(duration); } -uint64_t DurationUtil::durationToSeconds(const ProtobufWkt::Duration& duration) { +uint64_t DurationUtil::durationToSeconds(const Protobuf::Duration& duration) { validateDuration(duration); return Protobuf::util::TimeUtil::DurationToSeconds(duration); } void TimestampUtil::systemClockToTimestamp(const SystemTime system_clock_time, - ProtobufWkt::Timestamp& timestamp) { + Protobuf::Timestamp& timestamp) { // Converts to millisecond-precision Timestamp by explicitly casting to millisecond-precision // time_point. timestamp.MergeFrom(Protobuf::util::TimeUtil::MillisecondsToTimestamp( @@ -812,7 +811,7 @@ std::string TypeUtil::descriptorFullNameToTypeUrl(absl::string_view type) { return "type.googleapis.com/" + std::string(type); } -void StructUtil::update(ProtobufWkt::Struct& obj, const ProtobufWkt::Struct& with) { +void StructUtil::update(Protobuf::Struct& obj, const Protobuf::Struct& with) { auto& obj_fields = *obj.mutable_fields(); for (const auto& [key, val] : with.fields()) { @@ -828,24 +827,24 @@ void StructUtil::update(ProtobufWkt::Struct& obj, const ProtobufWkt::Struct& wit // Otherwise, the strategy depends on the value kind. switch (val.kind_case()) { // For scalars, the last one wins. - case ProtobufWkt::Value::kNullValue: - case ProtobufWkt::Value::kNumberValue: - case ProtobufWkt::Value::kStringValue: - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kNullValue: + case Protobuf::Value::kNumberValue: + case Protobuf::Value::kStringValue: + case Protobuf::Value::kBoolValue: obj_key = val; break; // If we got a structure, recursively update. - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: update(*obj_key.mutable_struct_value(), val.struct_value()); break; // For lists, append the new values. - case ProtobufWkt::Value::kListValue: { + case Protobuf::Value::kListValue: { auto& obj_key_vec = *obj_key.mutable_list_value()->mutable_values(); const auto& vals = val.list_value().values(); obj_key_vec.MergeFrom(vals); break; } - case ProtobufWkt::Value::KIND_NOT_SET: + case Protobuf::Value::KIND_NOT_SET: break; } } diff --git a/source/common/protobuf/utility.h b/source/common/protobuf/utility.h index fa3297b4932b9..1c5ae2b989d5e 100644 --- a/source/common/protobuf/utility.h +++ b/source/common/protobuf/utility.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/api/api.h" #include "envoy/common/exception.h" @@ -17,6 +18,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" // Obtain the value of a wrapped field (e.g. google.protobuf.UInt32Value) if set. Otherwise, return // the default value. @@ -266,7 +268,7 @@ class MessageUtil { */ static absl::Status loadFromJsonNoThrow(absl::string_view json, Protobuf::Message& message, bool& has_unknown_fileld); - static void loadFromJson(absl::string_view json, ProtobufWkt::Struct& message); + static void loadFromJson(absl::string_view json, Protobuf::Struct& message); static void loadFromYaml(const std::string& yaml, Protobuf::Message& message, ProtobufMessage::ValidationVisitor& validation_visitor); #endif @@ -379,6 +381,12 @@ class MessageUtil { return typed_config; } + /** + * Utility method to swap between protobuf bytes type using absl::Cord instead of std::string. + * Noop for now. + */ + static const std::string& bytesToString(const std::string& bytes) { return bytes; } + /** * Convert from a typed message into a google.protobuf.Any. This should be used * instead of the inbuilt PackTo, as PackTo is not available with lite protos. @@ -388,7 +396,7 @@ class MessageUtil { * * @throw EnvoyException if the message does not unpack. */ - static void packFrom(ProtobufWkt::Any& any_message, const Protobuf::Message& message); + static void packFrom(Protobuf::Any& any_message, const Protobuf::Message& message); /** * Convert from google.protobuf.Any to a typed message. This should be used @@ -399,7 +407,7 @@ class MessageUtil { * * @return absl::Status */ - static absl::Status unpackTo(const ProtobufWkt::Any& any_message, Protobuf::Message& message); + static absl::Status unpackTo(const Protobuf::Any& any_message, Protobuf::Message& message); /** * Convert from google.protobuf.Any to bytes as std::string @@ -407,18 +415,18 @@ class MessageUtil { * * @return std::string consists of bytes in the input message or error status. */ - static absl::StatusOr anyToBytes(const ProtobufWkt::Any& any) { - if (any.Is()) { - ProtobufWkt::StringValue s; + static absl::StatusOr anyToBytes(const Protobuf::Any& any) { + if (any.Is()) { + Protobuf::StringValue s; RETURN_IF_NOT_OK(MessageUtil::unpackTo(any, s)); return s.value(); } - if (any.Is()) { + if (any.Is()) { Protobuf::BytesValue b; RETURN_IF_NOT_OK(MessageUtil::unpackTo(any, b)); - return b.value(); + return bytesToString(b.value()); } - return any.value(); + return bytesToString(any.value()); }; /** @@ -428,12 +436,11 @@ class MessageUtil { * @return MessageType the typed message inside the Any. */ template - static inline void anyConvert(const ProtobufWkt::Any& message, MessageType& typed_message) { + static inline void anyConvert(const Protobuf::Any& message, MessageType& typed_message) { THROW_IF_NOT_OK(unpackTo(message, typed_message)); }; - template - static inline MessageType anyConvert(const ProtobufWkt::Any& message) { + template static inline MessageType anyConvert(const Protobuf::Any& message) { MessageType typed_message; anyConvert(message, typed_message); return typed_message; @@ -447,8 +454,7 @@ class MessageUtil { * @throw EnvoyException if the message does not satisfy its type constraints. */ template - static inline void anyConvertAndValidate(const ProtobufWkt::Any& message, - MessageType& typed_message, + static inline void anyConvertAndValidate(const Protobuf::Any& message, MessageType& typed_message, ProtobufMessage::ValidationVisitor& validation_visitor) { anyConvert(message, typed_message); validate(typed_message, validation_visitor); @@ -456,7 +462,7 @@ class MessageUtil { template static inline MessageType - anyConvertAndValidate(const ProtobufWkt::Any& message, + anyConvertAndValidate(const Protobuf::Any& message, ProtobufMessage::ValidationVisitor& validation_visitor) { MessageType typed_message; anyConvertAndValidate(message, typed_message, validation_visitor); @@ -490,12 +496,12 @@ class MessageUtil { * @param dest message. */ static void jsonConvert(const Protobuf::Message& source, Protobuf::Message& dest); - static void jsonConvert(const Protobuf::Message& source, ProtobufWkt::Struct& dest); - static void jsonConvert(const ProtobufWkt::Struct& source, + static void jsonConvert(const Protobuf::Message& source, Protobuf::Struct& dest); + static void jsonConvert(const Protobuf::Struct& source, ProtobufMessage::ValidationVisitor& validation_visitor, Protobuf::Message& dest); - // Convert a message to a ProtobufWkt::Value, return false upon failure. - static bool jsonConvertValue(const Protobuf::Message& source, ProtobufWkt::Value& dest); + // Convert a message to a Protobuf::Value, return false upon failure. + static bool jsonConvertValue(const Protobuf::Message& source, Protobuf::Value& dest); /** * Extract YAML as string from a google.protobuf.Message. @@ -548,14 +554,14 @@ class MessageUtil { * @param key the key to use to set the value * @param value the string value to associate with the key */ - static ProtobufWkt::Struct keyValueStruct(const std::string& key, const std::string& value); + static Protobuf::Struct keyValueStruct(const std::string& key, const std::string& value); /** * Utility method to create a Struct containing the passed in key/value map. * * @param fields the key/value pairs to initialize the Struct proto */ - static ProtobufWkt::Struct keyValueStruct(const std::map& fields); + static Protobuf::Struct keyValueStruct(const std::map& fields); /** * Utility method to print a human readable string of the code passed in. @@ -573,10 +579,10 @@ class MessageUtil { * traversed recursively to redact their contents. * * LIMITATION: This works properly for strongly-typed messages, as well as for messages packed in - * a `ProtobufWkt::Any` with a `type_url` corresponding to a proto that was compiled into the - * Envoy binary. However it does not work for messages encoded as `ProtobufWkt::Struct`, since + * a `Protobuf::Any` with a `type_url` corresponding to a proto that was compiled into the + * Envoy binary. However it does not work for messages encoded as `Protobuf::Struct`, since * structs are missing the "sensitive" annotations that this function expects. Similarly, it fails - * for messages encoded as `ProtobufWkt::Any` with a `type_url` that isn't registered with the + * for messages encoded as `Protobuf::Any` with a `type_url` that isn't registered with the * binary. If you're working with struct-typed messages, including those that might be hiding * within strongly-typed messages, please reify them to strongly-typed messages using * `MessageUtil::jsonConvert()` before calling `MessageUtil::redact()`. @@ -601,86 +607,86 @@ class MessageUtil { class ValueUtil { public: - static std::size_t hash(const ProtobufWkt::Value& value) { return MessageUtil::hash(value); } + static std::size_t hash(const Protobuf::Value& value) { return MessageUtil::hash(value); } #ifdef ENVOY_ENABLE_YAML /** - * Load YAML string into ProtobufWkt::Value. + * Load YAML string into Protobuf::Value. */ - static ProtobufWkt::Value loadFromYaml(const std::string& yaml); + static Protobuf::Value loadFromYaml(const std::string& yaml); #endif /** - * Compare two ProtobufWkt::Values for equality. + * Compare two Protobuf::Values for equality. * @param v1 message of type type.googleapis.com/google.protobuf.Value * @param v2 message of type type.googleapis.com/google.protobuf.Value * @return true if v1 and v2 are identical */ - static bool equal(const ProtobufWkt::Value& v1, const ProtobufWkt::Value& v2); + static bool equal(const Protobuf::Value& v1, const Protobuf::Value& v2); /** - * @return wrapped ProtobufWkt::NULL_VALUE. + * @return wrapped Protobuf::NULL_VALUE. */ - static const ProtobufWkt::Value& nullValue(); + static const Protobuf::Value& nullValue(); /** - * Wrap absl::string_view into ProtobufWkt::Value string value. + * Wrap absl::string_view into Protobuf::Value string value. * @param str string to be wrapped. * @return wrapped string. */ - static ProtobufWkt::Value stringValue(absl::string_view str); + static Protobuf::Value stringValue(absl::string_view str); /** - * Wrap optional std::string into ProtobufWkt::Value string value. - * If the argument contains a null optional, return ProtobufWkt::NULL_VALUE. + * Wrap optional std::string into Protobuf::Value string value. + * If the argument contains a null optional, return Protobuf::NULL_VALUE. * @param str string to be wrapped. * @return wrapped string. */ - static ProtobufWkt::Value optionalStringValue(const absl::optional& str); + static Protobuf::Value optionalStringValue(const absl::optional& str); /** - * Wrap boolean into ProtobufWkt::Value boolean value. + * Wrap boolean into Protobuf::Value boolean value. * @param str boolean to be wrapped. * @return wrapped boolean. */ - static ProtobufWkt::Value boolValue(bool b); + static Protobuf::Value boolValue(bool b); /** - * Wrap ProtobufWkt::Struct into ProtobufWkt::Value struct value. + * Wrap Protobuf::Struct into Protobuf::Value struct value. * @param obj struct to be wrapped. * @return wrapped struct. */ - static ProtobufWkt::Value structValue(const ProtobufWkt::Struct& obj); + static Protobuf::Value structValue(const Protobuf::Struct& obj); /** - * Wrap number into ProtobufWkt::Value double value. + * Wrap number into Protobuf::Value double value. * @param num number to be wrapped. * @return wrapped number. */ - template static ProtobufWkt::Value numberValue(const T num) { - ProtobufWkt::Value val; + template static Protobuf::Value numberValue(const T num) { + Protobuf::Value val; val.set_number_value(static_cast(num)); return val; } /** - * Wrap a collection of ProtobufWkt::Values into ProtobufWkt::Value list value. - * @param values collection of ProtobufWkt::Values to be wrapped. + * Wrap a collection of Protobuf::Values into Protobuf::Value list value. + * @param values collection of Protobuf::Values to be wrapped. * @return wrapped list value. */ - static ProtobufWkt::Value listValue(const std::vector& values); + static Protobuf::Value listValue(const std::vector& values); }; /** - * HashedValue is a wrapper around ProtobufWkt::Value that computes + * HashedValue is a wrapper around Protobuf::Value that computes * and stores a hash code for the Value at construction. */ class HashedValue { public: - HashedValue(const ProtobufWkt::Value& value) : value_(value), hash_(ValueUtil::hash(value)) {}; + HashedValue(const Protobuf::Value& value) : value_(value), hash_(ValueUtil::hash(value)) {}; HashedValue(const HashedValue& v) = default; - const ProtobufWkt::Value& value() const { return value_; } + const Protobuf::Value& value() const { return value_; } std::size_t hash() const { return hash_; } bool operator==(const HashedValue& rhs) const { @@ -690,7 +696,7 @@ class HashedValue { bool operator!=(const HashedValue& rhs) const { return !(*this == rhs); } private: - const ProtobufWkt::Value value_; + const Protobuf::Value value_; const std::size_t hash_; }; @@ -704,15 +710,14 @@ class DurationUtil { * @return duration in milliseconds. * @throw EnvoyException when duration is out-of-range. */ - static uint64_t durationToMilliseconds(const ProtobufWkt::Duration& duration); + static uint64_t durationToMilliseconds(const Protobuf::Duration& duration); /** * Same as DurationUtil::durationToMilliseconds but does not throw an exception. * @param duration protobuf. * @return duration in milliseconds or an error status. */ - static absl::StatusOr - durationToMillisecondsNoThrow(const ProtobufWkt::Duration& duration); + static absl::StatusOr durationToMillisecondsNoThrow(const Protobuf::Duration& duration); /** * Same as Protobuf::util::TimeUtil::DurationToSeconds but with extra validation logic. @@ -721,7 +726,7 @@ class DurationUtil { * @return duration in seconds. * @throw EnvoyException when duration is out-of-range. */ - static uint64_t durationToSeconds(const ProtobufWkt::Duration& duration); + static uint64_t durationToSeconds(const Protobuf::Duration& duration); }; class TimestampUtil { @@ -732,7 +737,7 @@ class TimestampUtil { * @param timestamp a pointer to the mutable protobuf member to be written into. */ static void systemClockToTimestamp(const SystemTime system_clock_time, - ProtobufWkt::Timestamp& timestamp); + Protobuf::Timestamp& timestamp); }; class StructUtil { @@ -751,7 +756,7 @@ class StructUtil { * @param obj the object to update in-place * @param with the object to update \p obj with */ - static void update(ProtobufWkt::Struct& obj, const ProtobufWkt::Struct& with); + static void update(Protobuf::Struct& obj, const Protobuf::Struct& with); }; } // namespace Envoy diff --git a/source/common/protobuf/visitor.cc b/source/common/protobuf/visitor.cc index 70890648959c9..d94c354161578 100644 --- a/source/common/protobuf/visitor.cc +++ b/source/common/protobuf/visitor.cc @@ -23,7 +23,7 @@ absl::Status traverseMessageWorker(ConstProtoVisitor& visitor, const Protobuf::M absl::string_view target_type_url; if (message.GetTypeName() == "google.protobuf.Any") { - auto* any_message = Protobuf::DynamicCastMessage(&message); + auto* any_message = Protobuf::DynamicCastMessage(&message); inner_message = Helper::typeUrlToMessage(any_message->type_url()); target_type_url = any_message->type_url(); // inner_message must be valid as parsing would have already failed to load if there was an diff --git a/source/common/protobuf/yaml_utility.cc b/source/common/protobuf/yaml_utility.cc index 741817bc04315..2ee35579d4834 100644 --- a/source/common/protobuf/yaml_utility.cc +++ b/source/common/protobuf/yaml_utility.cc @@ -42,11 +42,11 @@ void blockFormat(YAML::Node node) { } } -ProtobufWkt::Value parseYamlNode(const YAML::Node& node) { - ProtobufWkt::Value value; +Protobuf::Value parseYamlNode(const YAML::Node& node) { + Protobuf::Value value; switch (node.Type()) { case YAML::NodeType::Null: - value.set_null_value(ProtobufWkt::NULL_VALUE); + value.set_null_value(Protobuf::NULL_VALUE); break; case YAML::NodeType::Scalar: { if (node.Tag() == "!") { @@ -63,7 +63,7 @@ ProtobufWkt::Value parseYamlNode(const YAML::Node& node) { if (std::numeric_limits::min() <= int_value && std::numeric_limits::max() >= int_value) { // We could convert all integer values to string but it will break some stuff relying on - // ProtobufWkt::Struct itself, only convert small numbers into number_value here. + // Protobuf::Struct itself, only convert small numbers into number_value here. value.set_number_value(int_value); } else { // Proto3 JSON mapping allows use string for integer, this still has to be converted from @@ -164,7 +164,7 @@ absl::Status MessageUtil::loadFromJsonNoThrow(absl::string_view json, Protobuf:: return relaxed_status; } -void MessageUtil::loadFromJson(absl::string_view json, ProtobufWkt::Struct& message) { +void MessageUtil::loadFromJson(absl::string_view json, Protobuf::Struct& message) { // No need to validate if converting to a Struct, since there are no unknown // fields possible. loadFromJson(json, message, ProtobufMessage::getNullValidationVisitor()); @@ -172,9 +172,9 @@ void MessageUtil::loadFromJson(absl::string_view json, ProtobufWkt::Struct& mess void MessageUtil::loadFromYaml(const std::string& yaml, Protobuf::Message& message, ProtobufMessage::ValidationVisitor& validation_visitor) { - ProtobufWkt::Value value = ValueUtil::loadFromYaml(yaml); - if (value.kind_case() == ProtobufWkt::Value::kStructValue || - value.kind_case() == ProtobufWkt::Value::kListValue) { + Protobuf::Value value = ValueUtil::loadFromYaml(yaml); + if (value.kind_case() == Protobuf::Value::kStructValue || + value.kind_case() == Protobuf::Value::kListValue) { jsonConvertInternal(value, validation_visitor, message); return; } @@ -248,20 +248,20 @@ void MessageUtil::jsonConvert(const Protobuf::Message& source, Protobuf::Message jsonConvertInternal(source, ProtobufMessage::getNullValidationVisitor(), dest); } -void MessageUtil::jsonConvert(const Protobuf::Message& source, ProtobufWkt::Struct& dest) { +void MessageUtil::jsonConvert(const Protobuf::Message& source, Protobuf::Struct& dest) { // Any proto3 message can be transformed to Struct, so there is no need to check for unknown // fields. There is one catch; Duration/Timestamp etc. which have non-object canonical JSON // representations don't work. jsonConvertInternal(source, ProtobufMessage::getNullValidationVisitor(), dest); } -void MessageUtil::jsonConvert(const ProtobufWkt::Struct& source, +void MessageUtil::jsonConvert(const Protobuf::Struct& source, ProtobufMessage::ValidationVisitor& validation_visitor, Protobuf::Message& dest) { jsonConvertInternal(source, validation_visitor, dest); } -bool MessageUtil::jsonConvertValue(const Protobuf::Message& source, ProtobufWkt::Value& dest) { +bool MessageUtil::jsonConvertValue(const Protobuf::Message& source, Protobuf::Value& dest) { Protobuf::util::JsonPrintOptions json_options; json_options.preserve_proto_field_names = true; std::string json; @@ -277,7 +277,7 @@ bool MessageUtil::jsonConvertValue(const Protobuf::Message& source, ProtobufWkt: return false; } -ProtobufWkt::Value ValueUtil::loadFromYaml(const std::string& yaml) { +Protobuf::Value ValueUtil::loadFromYaml(const std::string& yaml) { TRY_ASSERT_MAIN_THREAD { return parseYamlNode(YAML::Load(yaml)); } END_TRY catch (YAML::ParserException& e) { diff --git a/source/common/quic/BUILD b/source/common/quic/BUILD index c44d4a489aa80..3e2ba82213d23 100644 --- a/source/common/quic/BUILD +++ b/source/common/quic/BUILD @@ -463,6 +463,7 @@ envoy_cc_library( "//source/common/network:socket_option_factory_lib", "//source/common/protobuf:utility_lib", "//source/common/quic:quic_io_handle_wrapper_lib", + "//source/common/runtime:runtime_lib", "@com_github_google_quiche//:quic_core_config_lib", "@com_github_google_quiche//:quic_core_http_header_list_lib", "@com_github_google_quiche//:quic_platform", diff --git a/source/common/quic/client_connection_factory_impl.cc b/source/common/quic/client_connection_factory_impl.cc index f0f1a91b022d3..c0ac52c733d70 100644 --- a/source/common/quic/client_connection_factory_impl.cc +++ b/source/common/quic/client_connection_factory_impl.cc @@ -10,6 +10,7 @@ PersistentQuicInfoImpl::PersistentQuicInfoImpl(Event::Dispatcher& dispatcher, ui : conn_helper_(dispatcher), alarm_factory_(dispatcher, *conn_helper_.GetClock()), buffer_limit_(buffer_limit), max_packet_length_(max_packet_length) { quiche::FlagRegistry::getInstance(); + migration_config_.migrate_session_on_network_change = false; } std::unique_ptr @@ -53,8 +54,7 @@ std::unique_ptr createQuicNetworkConnection( ASSERT(!quic_versions.empty()); auto connection = std::make_unique( quic::QuicUtils::CreateRandomConnectionId(), server_addr, info_impl->conn_helper_, - info_impl->alarm_factory_, quic_versions, local_addr, dispatcher, options, generator, - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.prefer_quic_client_udp_gro")); + info_impl->alarm_factory_, quic_versions, local_addr, dispatcher, options, generator); // Override the max packet length of the QUIC connection if the option value is not 0. if (info_impl->max_packet_length_ > 0) { connection->SetMaxPacketLength(info_impl->max_packet_length_); diff --git a/source/common/quic/client_connection_factory_impl.h b/source/common/quic/client_connection_factory_impl.h index 7d955602c43fa..f8edd9888a38c 100644 --- a/source/common/quic/client_connection_factory_impl.h +++ b/source/common/quic/client_connection_factory_impl.h @@ -12,6 +12,7 @@ #include "source/common/tls/client_ssl_socket.h" #include "source/extensions/quic/crypto_stream/envoy_quic_crypto_client_stream.h" +#include "quiche/quic/core/http/quic_connection_migration_manager.h" #include "quiche/quic/core/quic_utils.h" namespace Envoy { @@ -34,6 +35,8 @@ struct PersistentQuicInfoImpl : public Http::PersistentQuicInfo { // Override the maximum packet length of connections for tunneling. Use the default length in // QUICHE if this is set to 0. quic::QuicByteCount max_packet_length_; + // TODO(danzh): Add a config knob to configure connection migration. + quic::QuicConnectionMigrationConfig migration_config_; }; std::unique_ptr diff --git a/source/common/quic/envoy_quic_client_connection.cc b/source/common/quic/envoy_quic_client_connection.cc index 1e761823b8fb9..45bd6c5fc647c 100644 --- a/source/common/quic/envoy_quic_client_connection.cc +++ b/source/common/quic/envoy_quic_client_connection.cc @@ -31,38 +31,37 @@ EnvoyQuicClientConnection::EnvoyQuicClientConnection( const quic::ParsedQuicVersionVector& supported_versions, Network::Address::InstanceConstSharedPtr local_addr, Event::Dispatcher& dispatcher, const Network::ConnectionSocket::OptionsSharedPtr& options, - quic::ConnectionIdGeneratorInterface& generator, const bool prefer_gro) + quic::ConnectionIdGeneratorInterface& generator) : EnvoyQuicClientConnection( server_connection_id, helper, alarm_factory, supported_versions, dispatcher, - createConnectionSocket(initial_peer_address, local_addr, options, prefer_gro), generator, - prefer_gro) {} + createConnectionSocket(initial_peer_address, local_addr, options), generator) {} EnvoyQuicClientConnection::EnvoyQuicClientConnection( const quic::QuicConnectionId& server_connection_id, quic::QuicConnectionHelperInterface& helper, quic::QuicAlarmFactory& alarm_factory, const quic::ParsedQuicVersionVector& supported_versions, Event::Dispatcher& dispatcher, Network::ConnectionSocketPtr&& connection_socket, - quic::ConnectionIdGeneratorInterface& generator, const bool prefer_gro) + quic::ConnectionIdGeneratorInterface& generator) : EnvoyQuicClientConnection( server_connection_id, helper, alarm_factory, new EnvoyQuicPacketWriter( std::make_unique(connection_socket->ioHandle())), /*owns_writer=*/true, supported_versions, dispatcher, std::move(connection_socket), - generator, prefer_gro) {} + generator) {} EnvoyQuicClientConnection::EnvoyQuicClientConnection( const quic::QuicConnectionId& server_connection_id, quic::QuicConnectionHelperInterface& helper, quic::QuicAlarmFactory& alarm_factory, quic::QuicPacketWriter* writer, bool owns_writer, const quic::ParsedQuicVersionVector& supported_versions, Event::Dispatcher& dispatcher, Network::ConnectionSocketPtr&& connection_socket, - quic::ConnectionIdGeneratorInterface& generator, const bool prefer_gro) + quic::ConnectionIdGeneratorInterface& generator) : quic::QuicConnection(server_connection_id, quic::QuicSocketAddress(), envoyIpAddressToQuicSocketAddress( connection_socket->connectionInfoProvider().remoteAddress()->ip()), &helper, &alarm_factory, writer, owns_writer, quic::Perspective::IS_CLIENT, supported_versions, generator), QuicNetworkConnection(std::move(connection_socket)), dispatcher_(dispatcher), - prefer_gro_(prefer_gro), disallow_mmsg_(Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.disallow_quic_client_udp_mmsg")) {} + disallow_mmsg_(Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.disallow_quic_client_udp_mmsg")) {} void EnvoyQuicClientConnection::processPacket( Network::Address::InstanceConstSharedPtr local_address, @@ -185,7 +184,7 @@ void EnvoyQuicClientConnection::probeWithNewPort(const quic::QuicSocketAddress& auto probing_socket = createConnectionSocket( peer_addr == peer_address() ? connectionSocket()->connectionInfoProvider().remoteAddress() : quicAddressToEnvoyAddressInstance(peer_addr), - new_local_address, connectionSocket()->options(), prefer_gro_); + new_local_address, connectionSocket()->options()); setUpConnectionSocket(*probing_socket, delegate_); auto writer = std::make_unique( std::make_unique(probing_socket->ioHandle())); @@ -259,7 +258,7 @@ void EnvoyQuicClientConnection::onFileEvent(uint32_t events, if (connected() && (events & Event::FileReadyType::Read)) { Api::IoErrorPtr err = Network::Utility::readPacketsFromSocket( connection_socket.ioHandle(), *connection_socket.connectionInfoProvider().localAddress(), - *this, dispatcher_.timeSource(), prefer_gro_, !disallow_mmsg_, packets_dropped_); + *this, dispatcher_.timeSource(), /*allow_gro=*/true, !disallow_mmsg_, packets_dropped_); if (err == nullptr) { // If this READ event is on the probing socket and any packet read failed the path validation // (i.e. via STATELESS_RESET), the probing socket should have been closed and the default diff --git a/source/common/quic/envoy_quic_client_connection.h b/source/common/quic/envoy_quic_client_connection.h index c66af480535a1..c257219503c9f 100644 --- a/source/common/quic/envoy_quic_client_connection.h +++ b/source/common/quic/envoy_quic_client_connection.h @@ -6,6 +6,7 @@ #include "source/common/quic/envoy_quic_packet_writer.h" #include "source/common/quic/envoy_quic_utils.h" #include "source/common/quic/quic_network_connection.h" +#include "source/common/runtime/runtime_features.h" #include "quiche/quic/core/quic_connection.h" @@ -60,7 +61,7 @@ class EnvoyQuicClientConnection : public quic::QuicConnection, Network::Address::InstanceConstSharedPtr local_addr, Event::Dispatcher& dispatcher, const Network::ConnectionSocket::OptionsSharedPtr& options, - quic::ConnectionIdGeneratorInterface& generator, bool prefer_gro); + quic::ConnectionIdGeneratorInterface& generator); EnvoyQuicClientConnection(const quic::QuicConnectionId& server_connection_id, quic::QuicConnectionHelperInterface& helper, @@ -69,7 +70,7 @@ class EnvoyQuicClientConnection : public quic::QuicConnection, const quic::ParsedQuicVersionVector& supported_versions, Event::Dispatcher& dispatcher, Network::ConnectionSocketPtr&& connection_socket, - quic::ConnectionIdGeneratorInterface& generator, bool prefer_gro); + quic::ConnectionIdGeneratorInterface& generator); // Network::UdpPacketProcessor void processPacket(Network::Address::InstanceConstSharedPtr local_address, @@ -142,7 +143,7 @@ class EnvoyQuicClientConnection : public quic::QuicConnection, const quic::ParsedQuicVersionVector& supported_versions, Event::Dispatcher& dispatcher, Network::ConnectionSocketPtr&& connection_socket, - quic::ConnectionIdGeneratorInterface& generator, bool prefer_gro); + quic::ConnectionIdGeneratorInterface& generator); void onFileEvent(uint32_t events, Network::ConnectionSocket& connection_socket); @@ -157,7 +158,6 @@ class EnvoyQuicClientConnection : public quic::QuicConnection, bool migrate_port_on_path_degrading_{false}; uint8_t num_socket_switches_{0}; size_t num_packets_with_unknown_dst_address_{0}; - const bool prefer_gro_; const bool disallow_mmsg_; }; diff --git a/source/common/quic/envoy_quic_client_stream.cc b/source/common/quic/envoy_quic_client_stream.cc index cdfb83d2553f5..53773d3e33107 100644 --- a/source/common/quic/envoy_quic_client_stream.cc +++ b/source/common/quic/envoy_quic_client_stream.cc @@ -14,6 +14,7 @@ #include "quiche/common/http/http_header_block.h" #include "quiche/quic/core/http/quic_header_list.h" #include "quiche/quic/core/quic_session.h" +#include "quiche/quic/core/quic_types.h" namespace Envoy { namespace Quic { @@ -101,6 +102,7 @@ Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& } } #endif + addDecompressedHeaderBytesSent(spdy_headers); { IncrementalBytesSentTracker tracker(*this, *mutableBytesMeter(), true); size_t bytes_sent = WriteHeaders(std::move(spdy_headers), end_stream, nullptr); @@ -118,7 +120,9 @@ Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& void EnvoyQuicClientStream::encodeTrailers(const Http::RequestTrailerMap& trailers) { ENVOY_STREAM_LOG(debug, "encodeTrailers: {}.", *this, trailers); - encodeTrailersImpl(envoyHeadersToHttp2HeaderBlock(trailers)); + quiche::HttpHeaderBlock trailer_block = envoyHeadersToHttp2HeaderBlock(trailers); + addDecompressedHeaderBytesSent(trailer_block); + encodeTrailersImpl(std::move(trailer_block)); } void EnvoyQuicClientStream::resetStream(Http::StreamResetReason reason) { @@ -147,6 +151,7 @@ void EnvoyQuicClientStream::switchStreamBlockState() { void EnvoyQuicClientStream::OnInitialHeadersComplete(bool fin, size_t frame_len, const quic::QuicHeaderList& header_list) { mutableBytesMeter()->addHeaderBytesReceived(frame_len); + addDecompressedHeaderBytesReceived(header_list); if (read_side_closed()) { return; } @@ -257,10 +262,7 @@ bool EnvoyQuicClientStream::OnStopSending(quic::QuicResetStreamError error) { // Treat this as a remote reset, since the stream will be closed in both directions. runResetCallbacks( quicRstErrorToEnvoyRemoteResetReason(error.internal_code()), - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code") - ? absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), - "|FROM_PEER") - : absl::string_view()); + absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), "|FROM_PEER")); } return true; } @@ -316,6 +318,7 @@ void EnvoyQuicClientStream::OnBodyAvailable() { void EnvoyQuicClientStream::OnTrailingHeadersComplete(bool fin, size_t frame_len, const quic::QuicHeaderList& header_list) { mutableBytesMeter()->addHeaderBytesReceived(frame_len); + addDecompressedHeaderBytesReceived(header_list); if (read_side_closed()) { return; } @@ -360,9 +363,7 @@ void EnvoyQuicClientStream::OnStreamReset(const quic::QuicRstStreamFrame& frame) if (write_side_closed() && !end_stream_decoded_and_encoded) { runResetCallbacks( quicRstErrorToEnvoyRemoteResetReason(frame.error_code), - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code") - ? absl::StrCat(quic::QuicRstStreamErrorCodeToString(frame.error_code), "|FROM_PEER") - : absl::string_view()); + absl::StrCat(quic::QuicRstStreamErrorCodeToString(frame.error_code), "|FROM_PEER")); } } @@ -374,9 +375,7 @@ void EnvoyQuicClientStream::ResetWithError(quic::QuicResetStreamError error) { // Upper layers expect calling resetStream() to immediately raise reset callbacks. runResetCallbacks( quicRstErrorToEnvoyLocalResetReason(error.internal_code()), - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code") - ? absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), "|FROM_SELF") - : absl::string_view()); + absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), "|FROM_SELF")); if (session()->connection()->connected()) { quic::QuicSpdyClientStream::ResetWithError(error); } diff --git a/source/common/quic/envoy_quic_dispatcher.cc b/source/common/quic/envoy_quic_dispatcher.cc index 80b85f384d759..34c87f0dacee9 100644 --- a/source/common/quic/envoy_quic_dispatcher.cc +++ b/source/common/quic/envoy_quic_dispatcher.cc @@ -94,6 +94,7 @@ std::unique_ptr EnvoyQuicDispatcher::CreateQuicSession( // ALPN. Network::ConnectionSocketPtr connection_socket = createServerConnectionSocket( listen_socket_.ioHandle(), self_address, peer_address, std::string(parsed_chlo.sni), "h3"); + connection_socket->connectionInfoProvider().setListenerInfo(listener_config_->listenerInfo()); auto stream_info = std::make_unique( dispatcher_.timeSource(), connection_socket->connectionInfoProviderSharedPtr(), StreamInfo::FilterState::LifeSpan::Connection); @@ -124,8 +125,8 @@ std::unique_ptr EnvoyQuicDispatcher::CreateQuicSession( auto quic_connection = std::make_unique( server_connection_id, self_address, peer_address, *helper(), *alarm_factory(), writer(), - /*owns_writer=*/false, quic::ParsedQuicVersionVector{version}, std::move(connection_socket), - connection_id_generator, std::move(listener_filter_manager)); + quic::ParsedQuicVersionVector{version}, std::move(connection_socket), connection_id_generator, + std::move(listener_filter_manager)); auto quic_session = std::make_unique( quic_config, quic::ParsedQuicVersionVector{version}, std::move(quic_connection), this, session_helper(), crypto_config(), compressed_certs_cache(), dispatcher_, diff --git a/source/common/quic/envoy_quic_server_connection.cc b/source/common/quic/envoy_quic_server_connection.cc index 4829da12211dd..773be68edad3d 100644 --- a/source/common/quic/envoy_quic_server_connection.cc +++ b/source/common/quic/envoy_quic_server_connection.cc @@ -14,14 +14,10 @@ namespace Quic { namespace { std::unique_ptr -wrapWriter(quic::QuicPacketWriter* writer, bool owns_writer, +wrapWriter(quic::QuicPacketWriter* writer, quic::QuicPacketWriterWrapper::OnWriteDoneCallback on_write_done) { auto wrapper = std::make_unique(); - if (owns_writer) { - wrapper->set_writer(writer); - } else { - wrapper->set_non_owning_writer(writer); - } + wrapper->set_non_owning_writer(writer); wrapper->set_on_write_done(std::move(on_write_done)); return wrapper; } @@ -31,14 +27,13 @@ EnvoyQuicServerConnection::EnvoyQuicServerConnection( const quic::QuicConnectionId& server_connection_id, quic::QuicSocketAddress initial_self_address, quic::QuicSocketAddress initial_peer_address, quic::QuicConnectionHelperInterface& helper, quic::QuicAlarmFactory& alarm_factory, - quic::QuicPacketWriter* writer, bool owns_writer, - const quic::ParsedQuicVersionVector& supported_versions, + quic::QuicPacketWriter* writer, const quic::ParsedQuicVersionVector& supported_versions, Network::ConnectionSocketPtr connection_socket, quic::ConnectionIdGeneratorInterface& generator, std::unique_ptr listener_filter_manager) : quic::QuicConnection( server_connection_id, initial_self_address, initial_peer_address, &helper, &alarm_factory, // Wrap the packet writer to get notified when a packet is written. - wrapWriter(writer, owns_writer, + wrapWriter(writer, [this](size_t packet_size, const quic::WriteResult& result) { OnWritePacketDone(packet_size, result); }) diff --git a/source/common/quic/envoy_quic_server_connection.h b/source/common/quic/envoy_quic_server_connection.h index 28b655f4d70ae..f8fc79b39f38e 100644 --- a/source/common/quic/envoy_quic_server_connection.h +++ b/source/common/quic/envoy_quic_server_connection.h @@ -67,10 +67,10 @@ class QuicListenerFilterManagerImpl : public Network::QuicListenerFilterManager, Event::Dispatcher& dispatcher() override { return dispatcher_; } void continueFilterChain(bool /*success*/) override { IS_ENVOY_BUG("Should not be used."); } void useOriginalDst(bool /*use_original_dst*/) override { IS_ENVOY_BUG("Should not be used."); } - void setDynamicMetadata(const std::string& name, const ProtobufWkt::Struct& value) override { + void setDynamicMetadata(const std::string& name, const Protobuf::Struct& value) override { stream_info_.setDynamicMetadata(name, value); } - void setDynamicTypedMetadata(const std::string& name, const ProtobufWkt::Any& value) override { + void setDynamicTypedMetadata(const std::string& name, const Protobuf::Any& value) override { stream_info_.setDynamicTypedMetadata(name, value); } envoy::config::core::v3::Metadata& dynamicMetadata() override { @@ -132,12 +132,13 @@ class QuicListenerFilterManagerImpl : public Network::QuicListenerFilterManager, class EnvoyQuicServerConnection : public quic::QuicConnection, public QuicNetworkConnection { public: + // Creates a new `EnvoyQuicServerConnection`. `writer` is owned by the caller and must + // outlive the connection. EnvoyQuicServerConnection(const quic::QuicConnectionId& server_connection_id, quic::QuicSocketAddress initial_self_address, quic::QuicSocketAddress initial_peer_address, quic::QuicConnectionHelperInterface& helper, quic::QuicAlarmFactory& alarm_factory, quic::QuicPacketWriter* writer, - bool owns_writer, const quic::ParsedQuicVersionVector& supported_versions, Network::ConnectionSocketPtr connection_socket, quic::ConnectionIdGeneratorInterface& generator, diff --git a/source/common/quic/envoy_quic_server_session.cc b/source/common/quic/envoy_quic_server_session.cc index f3faca03eaf36..b56d38743f759 100644 --- a/source/common/quic/envoy_quic_server_session.cc +++ b/source/common/quic/envoy_quic_server_session.cc @@ -115,11 +115,6 @@ quic::QuicSpdyStream* EnvoyQuicServerSession::CreateOutgoingBidirectionalStream( return nullptr; } -quic::QuicSpdyStream* EnvoyQuicServerSession::CreateOutgoingUnidirectionalStream() { - IS_ENVOY_BUG("Unexpected function call"); - return nullptr; -} - void EnvoyQuicServerSession::setUpRequestDecoder(EnvoyQuicServerStream& stream) { ASSERT(http_connection_callbacks_ != nullptr); Http::RequestDecoder& decoder = http_connection_callbacks_->newStream(stream); diff --git a/source/common/quic/envoy_quic_server_session.h b/source/common/quic/envoy_quic_server_session.h index 27e9bf9cfd626..f0dbf5bba63ff 100644 --- a/source/common/quic/envoy_quic_server_session.h +++ b/source/common/quic/envoy_quic_server_session.h @@ -118,7 +118,6 @@ class EnvoyQuicServerSession : public quic::QuicServerSessionBase, quic::QuicSpdyStream* CreateIncomingStream(quic::QuicStreamId id) override; quic::QuicSpdyStream* CreateIncomingStream(quic::PendingStream* pending) override; quic::QuicSpdyStream* CreateOutgoingBidirectionalStream() override; - quic::QuicSpdyStream* CreateOutgoingUnidirectionalStream() override; quic::HttpDatagramSupport LocalHttpDatagramSupport() override { return http_datagram_support_; } diff --git a/source/common/quic/envoy_quic_server_stream.cc b/source/common/quic/envoy_quic_server_stream.cc index f2b4edf7ddd65..cff38f731a6e1 100644 --- a/source/common/quic/envoy_quic_server_stream.cc +++ b/source/common/quic/envoy_quic_server_stream.cc @@ -19,6 +19,7 @@ #include "quiche/common/http/http_header_block.h" #include "quiche/quic/core/http/quic_header_list.h" #include "quiche/quic/core/quic_session.h" +#include "quiche/quic/core/quic_types.h" #include "quiche_platform_impl/quiche_mem_slice_impl.h" namespace Envoy { @@ -59,7 +60,8 @@ void EnvoyQuicServerStream::encode1xxHeaders(const Http::ResponseHeaderMap& head void EnvoyQuicServerStream::encodeHeaders(const Http::ResponseHeaderMap& headers, bool end_stream) { ENVOY_STREAM_LOG(debug, "encodeHeaders (end_stream={}) {}.", *this, end_stream, headers); if (write_side_closed()) { - IS_ENVOY_BUG("encodeHeaders is called on write-closed stream."); + IS_ENVOY_BUG( + fmt::format("encodeHeaders is called on write-closed stream. {}", quicStreamState())); return; } @@ -82,8 +84,9 @@ void EnvoyQuicServerStream::encodeHeaders(const Http::ResponseHeaderMap& headers SendBufferMonitor::ScopedWatermarkBufferUpdater updater(this, this); { IncrementalBytesSentTracker tracker(*this, *mutableBytesMeter(), true); - size_t bytes_sent = - WriteHeaders(envoyHeadersToHttp2HeaderBlock(*header_map), end_stream, nullptr); + quiche::HttpHeaderBlock header_block = envoyHeadersToHttp2HeaderBlock(*header_map); + addDecompressedHeaderBytesSent(header_block); + size_t bytes_sent = WriteHeaders(std::move(header_block), end_stream, nullptr); stats_gatherer_->addBytesSent(bytes_sent, end_stream); ENVOY_BUG(bytes_sent != 0, "Failed to encode headers."); } @@ -97,17 +100,17 @@ void EnvoyQuicServerStream::encodeHeaders(const Http::ResponseHeaderMap& headers } void EnvoyQuicServerStream::encodeTrailers(const Http::ResponseTrailerMap& trailers) { - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_remove_empty_trailers")) { - if (trailers.empty()) { - ENVOY_STREAM_LOG(debug, "skipping submitting empty trailers", *this); - // Instead of submitting empty trailers, we send empty data with end_stream=true instead. - Buffer::OwnedImpl empty_buffer; - encodeData(empty_buffer, true); - return; - } + if (trailers.empty()) { + ENVOY_STREAM_LOG(debug, "skipping submitting empty trailers", *this); + // Instead of submitting empty trailers, we send empty data with end_stream=true instead. + Buffer::OwnedImpl empty_buffer; + encodeData(empty_buffer, true); + return; } ENVOY_STREAM_LOG(debug, "encodeTrailers: {}.", *this, trailers); - encodeTrailersImpl(envoyHeadersToHttp2HeaderBlock(trailers)); + quiche::HttpHeaderBlock trailer_block = envoyHeadersToHttp2HeaderBlock(trailers); + addDecompressedHeaderBytesSent(trailer_block); + encodeTrailersImpl(std::move(trailer_block)); } void EnvoyQuicServerStream::resetStream(Http::StreamResetReason reason) { @@ -152,6 +155,7 @@ void EnvoyQuicServerStream::switchStreamBlockState() { void EnvoyQuicServerStream::OnInitialHeadersComplete(bool fin, size_t frame_len, const quic::QuicHeaderList& header_list) { mutableBytesMeter()->addHeaderBytesReceived(frame_len); + addDecompressedHeaderBytesReceived(header_list); // TODO(danzh) Fix in QUICHE. If the stream has been reset in the call stack, // OnInitialHeadersComplete() shouldn't be called. if (read_side_closed()) { @@ -296,6 +300,7 @@ void EnvoyQuicServerStream::OnBodyAvailable() { void EnvoyQuicServerStream::OnTrailingHeadersComplete(bool fin, size_t frame_len, const quic::QuicHeaderList& header_list) { mutableBytesMeter()->addHeaderBytesReceived(frame_len); + addDecompressedHeaderBytesReceived(header_list); ENVOY_STREAM_LOG(debug, "Received trailers: {}.", *this, received_trailers().DebugString()); quic::QuicSpdyServerStreamBase::OnTrailingHeadersComplete(fin, frame_len, header_list); if (read_side_closed()) { @@ -359,10 +364,7 @@ bool EnvoyQuicServerStream::OnStopSending(quic::QuicResetStreamError error) { // Treat this as a remote reset, since the stream will be closed in both directions. runResetCallbacks( quicRstErrorToEnvoyRemoteResetReason(error.internal_code()), - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code") - ? absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), - "|FROM_PEER") - : absl::string_view()); + absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), "|FROM_PEER")); } return true; } @@ -380,9 +382,7 @@ void EnvoyQuicServerStream::OnStreamReset(const quic::QuicRstStreamFrame& frame) // stream callback. runResetCallbacks( quicRstErrorToEnvoyRemoteResetReason(frame.error_code), - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code") - ? absl::StrCat(quic::QuicRstStreamErrorCodeToString(frame.error_code), "|FROM_PEER") - : absl::string_view()); + absl::StrCat(quic::QuicRstStreamErrorCodeToString(frame.error_code), "|FROM_PEER")); } } @@ -395,10 +395,7 @@ void EnvoyQuicServerStream::ResetWithError(quic::QuicResetStreamError error) { // Upper layers expect calling resetStream() to immediately raise reset callbacks. runResetCallbacks( quicRstErrorToEnvoyLocalResetReason(error.internal_code()), - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code") - ? absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), - "|FROM_SELF") - : absl::string_view()); + absl::StrCat(quic::QuicRstStreamErrorCodeToString(error.internal_code()), "|FROM_SELF")); } quic::QuicSpdyServerStreamBase::ResetWithError(error); } diff --git a/source/common/quic/envoy_quic_stream.cc b/source/common/quic/envoy_quic_stream.cc index 021ccde92a899..ba8df49f225c3 100644 --- a/source/common/quic/envoy_quic_stream.cc +++ b/source/common/quic/envoy_quic_stream.cc @@ -17,7 +17,7 @@ void EnvoyQuicStream::encodeData(Buffer::Instance& data, bool end_stream) { return; } if (quic_stream_.write_side_closed()) { - IS_ENVOY_BUG("encodeData is called on write-closed stream."); + IS_ENVOY_BUG(fmt::format("encodeData is called on write-closed stream. {}", quicStreamState())); return; } ASSERT(!local_end_stream_); @@ -185,5 +185,18 @@ void EnvoyQuicStream::encodeMetadata(const Http::MetadataMapVector& metadata_map } } +std::string EnvoyQuicStream::quicStreamState() { + return fmt::format( + "QUIC stream state: local_end_stream_ {}, rst_received " + "{}, rst_sent {}, fin_received {}, fin_sent {}, fin_buffered {}, fin_outstanding {}, " + "stream_error {}, connection_error {}, connection connected: {}.", + local_end_stream_, quic_stream_.rst_received(), quic_stream_.rst_sent(), + quic_stream_.fin_received(), quic_stream_.fin_sent(), quic_stream_.fin_buffered(), + quic_stream_.fin_outstanding(), + quic::QuicRstStreamErrorCodeToString(quic_stream_.stream_error()), + quic::QuicErrorCodeToString(quic_stream_.connection_error()), + quic_session_.connection()->connected()); +} + } // namespace Quic } // namespace Envoy diff --git a/source/common/quic/envoy_quic_stream.h b/source/common/quic/envoy_quic_stream.h index de3ad82507083..a9e48b495bc88 100644 --- a/source/common/quic/envoy_quic_stream.h +++ b/source/common/quic/envoy_quic_stream.h @@ -218,6 +218,14 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, StreamInfo::BytesMeterSharedPtr& mutableBytesMeter() { return bytes_meter_; } + void addDecompressedHeaderBytesSent(const quiche::HttpHeaderBlock& headers) { + bytes_meter_->addDecompressedHeaderBytesSent(headers.TotalBytesUsed()); + } + + void addDecompressedHeaderBytesReceived(const quic::QuicHeaderList& header_list) { + bytes_meter_->addDecompressedHeaderBytesReceived(header_list.uncompressed_header_bytes()); + } + void encodeTrailersImpl(quiche::HttpHeaderBlock&& trailers); // Converts `header_list` into a new `Http::MetadataMap`. @@ -231,6 +239,8 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, return received_metadata_bytes_ > 1 << 20; } + std::string quicStreamState(); + http2::adapter::HeaderValidator& header_validator() { return header_validator_; } #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS diff --git a/source/common/quic/envoy_quic_utils.cc b/source/common/quic/envoy_quic_utils.cc index 06c89c55bfd1d..be01dd7d65032 100644 --- a/source/common/quic/envoy_quic_utils.cc +++ b/source/common/quic/envoy_quic_utils.cc @@ -1,17 +1,53 @@ #include "source/common/quic/envoy_quic_utils.h" +#include +#include +#include +#include #include +#include -#include "envoy/common/platform.h" -#include "envoy/config/core/v3/base.pb.h" +#include "envoy/api/os_sys_calls_common.h" +#include "envoy/http/header_map.h" +#include "envoy/http/stream_reset_handler.h" +#include "envoy/network/address.h" +#include "envoy/network/io_handle.h" +#include "envoy/network/listen_socket.h" +#include "envoy/network/socket.h" +#include "envoy/network/socket_interface.h" #include "source/common/api/os_sys_calls_impl.h" -#include "source/common/http/utility.h" +#include "source/common/common/assert.h" +#include "source/common/common/logger.h" +#include "source/common/common/utility.h" +#include "source/common/http/http_option_limits.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/connection_socket_impl.h" #include "source/common/network/socket_option_factory.h" -#include "source/common/network/utility.h" #include "source/common/protobuf/utility.h" +#include "source/common/quic/quic_io_handle_wrapper.h" +#include "source/common/runtime/runtime_features.h" +#include "absl/numeric/int128.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "openssl/crypto.h" +#include "openssl/ec.h" +#include "openssl/ec_key.h" +#include "openssl/evp.h" +#include "openssl/nid.h" +#include "openssl/rsa.h" +#include "openssl/ssl.h" +#include "openssl/x509.h" +#include "quiche/common/http/http_header_block.h" +#include "quiche/common/quiche_ip_address_family.h" +#include "quiche/quic/core/quic_config.h" +#include "quiche/quic/core/quic_constants.h" +#include "quiche/quic/core/quic_error_codes.h" +#include "quiche/quic/core/quic_tag.h" +#include "quiche/quic/core/quic_time.h" +#include "quiche/quic/core/quic_types.h" +#include "quiche/quic/platform/api/quic_socket_address.h" namespace Envoy { namespace Quic { @@ -184,8 +220,7 @@ Http::StreamResetReason quicErrorCodeToEnvoyRemoteResetReason(quic::QuicErrorCod Network::ConnectionSocketPtr createConnectionSocket(const Network::Address::InstanceConstSharedPtr& peer_addr, Network::Address::InstanceConstSharedPtr& local_addr, - const Network::ConnectionSocket::OptionsSharedPtr& options, - const bool prefer_gro) { + const Network::ConnectionSocket::OptionsSharedPtr& options) { ASSERT(peer_addr != nullptr); // NOTE: If changing the default cache size from 4 entries, make sure to profile it using // the benchmark test: //test/common/network:io_socket_handle_impl_benchmark @@ -212,7 +247,7 @@ createConnectionSocket(const Network::Address::InstanceConstSharedPtr& peer_addr connection_socket->addOptions(Network::SocketOptionFactory::buildIpPacketInfoOptions()); connection_socket->addOptions(Network::SocketOptionFactory::buildRxQueueOverFlowOptions()); connection_socket->addOptions(Network::SocketOptionFactory::buildIpRecvTosOptions()); - if (prefer_gro && Api::OsSysCallsSingleton::get().supportsUdpGro()) { + if (Api::OsSysCallsSingleton::get().supportsUdpGro()) { connection_socket->addOptions(Network::SocketOptionFactory::buildUdpGroOptions()); } if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.udp_set_do_not_fragment")) { diff --git a/source/common/quic/envoy_quic_utils.h b/source/common/quic/envoy_quic_utils.h index 9431d891436ff..f9d3b3eef674e 100644 --- a/source/common/quic/envoy_quic_utils.h +++ b/source/common/quic/envoy_quic_utils.h @@ -174,8 +174,7 @@ Http::StreamResetReason quicErrorCodeToEnvoyRemoteResetReason(quic::QuicErrorCod Network::ConnectionSocketPtr createConnectionSocket(const Network::Address::InstanceConstSharedPtr& peer_addr, Network::Address::InstanceConstSharedPtr& local_addr, - const Network::ConnectionSocket::OptionsSharedPtr& options, - bool prefer_gro = false); + const Network::ConnectionSocket::OptionsSharedPtr& options); // Convert a cert in string form to X509 object. // Return nullptr if the bytes passed cannot be passed. diff --git a/source/common/quic/http_datagram_handler.cc b/source/common/quic/http_datagram_handler.cc index 9de2e09d93469..19c4c0e82678b 100644 --- a/source/common/quic/http_datagram_handler.cc +++ b/source/common/quic/http_datagram_handler.cc @@ -8,6 +8,7 @@ #include "quiche/common/capsule.h" #include "quiche/common/quiche_buffer_allocator.h" #include "quiche/quic/core/http/quic_spdy_stream.h" +#include "quiche/quic/core/quic_types.h" namespace Envoy { namespace Quic { @@ -45,27 +46,27 @@ bool HttpDatagramHandler::OnCapsule(const quiche::Capsule& capsule) { stream_.WriteCapsule(capsule, fin_set_); return true; } - quic::MessageStatus status = + quic::DatagramStatus status = stream_.SendHttp3Datagram(capsule.datagram_capsule().http_datagram_payload); - if (status == quic::MessageStatus::MESSAGE_STATUS_SUCCESS) { + if (status == quic::DatagramStatus::DATAGRAM_STATUS_SUCCESS) { return true; } // When SendHttp3Datagram cannot send a datagram immediately, it puts it into the queue and - // returns MESSAGE_STATUS_BLOCKED. - if (status == quic::MessageStatus::MESSAGE_STATUS_BLOCKED) { + // returns DATAGRAM_STATUS_BLOCKED. + if (status == quic::DatagramStatus::DATAGRAM_STATUS_BLOCKED) { ENVOY_LOG(trace, fmt::format("SendHttpH3Datagram failed: status = {}, buffers the Datagram.", - quic::MessageStatusToString(status))); + quic::DatagramStatusToString(status))); return true; } - if (status == quic::MessageStatus::MESSAGE_STATUS_TOO_LARGE || - status == quic::MessageStatus::MESSAGE_STATUS_SETTINGS_NOT_RECEIVED) { + if (status == quic::DatagramStatus::DATAGRAM_STATUS_TOO_LARGE || + status == quic::DatagramStatus::DATAGRAM_STATUS_SETTINGS_NOT_RECEIVED) { ENVOY_LOG(warn, fmt::format("SendHttpH3Datagram failed: status = {}, drops the Datagram.", - quic::MessageStatusToString(status))); + quic::DatagramStatusToString(status))); return true; } // Otherwise, returns false and thus resets the corresponding stream. ENVOY_LOG(error, fmt::format("SendHttpH3Datagram failed: status = {}, resets the stream.", - quic::MessageStatusToString(status))); + quic::DatagramStatusToString(status))); return false; } diff --git a/source/common/quic/platform/quiche_flags_constants.h b/source/common/quic/platform/quiche_flags_constants.h index 97b2b2ef9889a..a70a4d50f78d8 100644 --- a/source/common/quic/platform/quiche_flags_constants.h +++ b/source/common/quic/platform/quiche_flags_constants.h @@ -35,8 +35,12 @@ /* TODO(#8826) Ideally we should use the negotiated value from upstream which is not accessible \ * for now. 512MB is way too large, but the actual bytes buffered should be bound by the \ * negotiated upstream flow control window. */ \ - KEY_VALUE_PAIR(quic_buffered_data_threshold, \ - 2 * ::Envoy::Http2::Utility::OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE) \ + /* TODO(wbpcode) 2 * Http2::Utility::OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE was \ + * used in previous implementation and the previous value of HTTP2 \ + * DEFAULT_INITIAL_STREAM_WINDOW_SIZE is 256MiB. But we updated HTTP2 \ + * DEFAULT_INITIAL_STREAM_WINDOW_SIZE to 1MiB for safety now. To ensure no behavior change here, \ + * we update it to 512 MiB manually*/ \ + KEY_VALUE_PAIR(quic_buffered_data_threshold, uint32_t(2 * 256 * 1024 * 1024)) \ /* Envoy should send server preferred address without a client option by default. */ \ KEY_VALUE_PAIR(quic_always_support_server_preferred_address, true) diff --git a/source/common/quic/quic_filter_manager_connection_impl.h b/source/common/quic/quic_filter_manager_connection_impl.h index 90a20b6e6ea70..d3cd1019d2482 100644 --- a/source/common/quic/quic_filter_manager_connection_impl.h +++ b/source/common/quic/quic_filter_manager_connection_impl.h @@ -146,6 +146,7 @@ class QuicFilterManagerConnectionImpl : public Network::ConnectionImplBase, void configureInitialCongestionWindow(uint64_t bandwidth_bits_per_sec, std::chrono::microseconds rtt) override; absl::optional congestionWindowInBytes() const override; + const Network::ConnectionSocketPtr& getSocket() const override { PANIC("not implemented"); } // Network::FilterManagerConnection void rawWrite(Buffer::Instance& data, bool end_stream) override; diff --git a/source/common/quic/quic_stats_gatherer.cc b/source/common/quic/quic_stats_gatherer.cc index c1a0da083082e..89241e5696a90 100644 --- a/source/common/quic/quic_stats_gatherer.cc +++ b/source/common/quic/quic_stats_gatherer.cc @@ -11,6 +11,9 @@ void QuicStatsGatherer::OnPacketAcked(int acked_bytes, quic::QuicTime::Delta /* delta_largest_observed */) { bytes_outstanding_ -= acked_bytes; if (bytes_outstanding_ == 0 && fin_sent_ && !logging_done_) { + if (time_source_ != nullptr) { + last_downstream_ack_timestamp_ = time_source_->monotonicTime(); + } maybeDoDeferredLog(); } } @@ -21,12 +24,20 @@ void QuicStatsGatherer::OnPacketRetransmitted(int retransmitted_bytes) { } void QuicStatsGatherer::maybeDoDeferredLog(bool record_ack_timing) { - logging_done_ = true; + if (!fix_defer_logging_miss_for_half_closed_stream_) { + logging_done_ = true; + } if (stream_info_ == nullptr) { return; } + if (fix_defer_logging_miss_for_half_closed_stream_) { + logging_done_ = true; + } if (time_source_ != nullptr && record_ack_timing) { stream_info_->downstreamTiming().onLastDownstreamAckReceived(*time_source_); + } else if (fix_defer_logging_miss_for_half_closed_stream_ && + last_downstream_ack_timestamp_.has_value()) { + stream_info_->downstreamTiming().last_downstream_ack_received_ = last_downstream_ack_timestamp_; } stream_info_->addBytesRetransmitted(retransmitted_bytes_); stream_info_->addPacketsRetransmitted(retransmitted_packets_); diff --git a/source/common/quic/quic_stats_gatherer.h b/source/common/quic/quic_stats_gatherer.h index 7d9af7f6fd40b..578a760570252 100644 --- a/source/common/quic/quic_stats_gatherer.h +++ b/source/common/quic/quic_stats_gatherer.h @@ -7,6 +7,8 @@ #include "envoy/http/header_map.h" #include "envoy/stream_info/stream_info.h" +#include "source/common/runtime/runtime_features.h" + #include "quiche/quic/core/quic_ack_listener_interface.h" #include "quiche/quic/platform/api/quic_flags.h" @@ -21,7 +23,8 @@ class QuicStatsGatherer : public quic::QuicAckListenerInterface { ~QuicStatsGatherer() override { if (!logging_done_) { if (notify_ack_listener_before_soon_to_be_destroyed_) { - ENVOY_LOG_MISC(error, "Stream destroyed without logging."); + ENVOY_BUG(stream_info_ == nullptr, + "Stream destroyed without logging metrics available in stream info."); } else { maybeDoDeferredLog(false); } @@ -73,10 +76,13 @@ class QuicStatsGatherer : public quic::QuicAckListenerInterface { bool logging_done_ = false; uint64_t retransmitted_packets_ = 0; uint64_t retransmitted_bytes_ = 0; + absl::optional last_downstream_ack_timestamp_; const bool notify_ack_listener_before_soon_to_be_destroyed_{ GetQuicReloadableFlag(quic_notify_ack_listener_earlier) && GetQuicReloadableFlag(quic_notify_stream_soon_to_destroy)}; + const bool fix_defer_logging_miss_for_half_closed_stream_{Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.quic_fix_defer_logging_miss_for_half_closed_stream")}; }; } // namespace Quic diff --git a/source/common/rds/rds_route_config_subscription.cc b/source/common/rds/rds_route_config_subscription.cc index 901ee16f97f99..c9b299ad333b0 100644 --- a/source/common/rds/rds_route_config_subscription.cc +++ b/source/common/rds/rds_route_config_subscription.cc @@ -49,9 +49,14 @@ RdsRouteConfigSubscription::RdsRouteConfigSubscription( resource_decoder_(std::move(resource_decoder)) { const auto resource_type = route_config_provider_manager_.protoTraits().resourceType(); auto subscription_or_error = - factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( - config_source, Envoy::Grpc::Common::typeUrl(resource_type), *scope_, *this, - resource_decoder_, {}); + Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions") + ? factory_context.xdsManager().subscribeToSingletonResource( + route_config_name_, config_source, Envoy::Grpc::Common::typeUrl(resource_type), + *scope_, *this, resource_decoder_, {}) + : factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( + config_source, Envoy::Grpc::Common::typeUrl(resource_type), *scope_, *this, + resource_decoder_, {}); SET_AND_RETURN_IF_NOT_OK(subscription_or_error.status(), creation_status); subscription_ = std::move(*subscription_or_error); local_init_manager_.add(local_init_target_); diff --git a/source/common/rds/route_config_provider_manager.h b/source/common/rds/route_config_provider_manager.h index f2c1b5c05c6d6..9ff2c245b3d49 100644 --- a/source/common/rds/route_config_provider_manager.h +++ b/source/common/rds/route_config_provider_manager.h @@ -43,22 +43,15 @@ class RouteConfigProviderManager { uint64_t manager_identifier)> create_dynamic_provider) { - uint64_t manager_identifier; - - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.normalize_rds_provider_config")) { - // Normalize the config_source part of the passed config. Some parts of the config_source - // do not affect selection of the RDS provider. They will be cleared (zeroed) and restored - // after calculating hash. - // Since rds is passed as const, the constness must be casted away before modifying rds. - auto* orig_initial_timeout = - const_cast(rds).mutable_config_source()->release_initial_fetch_timeout(); - manager_identifier = MessageUtil::hash(rds); - const_cast(rds).mutable_config_source()->set_allocated_initial_fetch_timeout( - orig_initial_timeout); - - } else { - manager_identifier = MessageUtil::hash(rds); - } + // Normalize the config_source part of the passed config. Some parts of the config_source + // do not affect selection of the RDS provider. They will be cleared (zeroed) and restored + // after calculating hash. + // Since rds is passed as const, the constness must be casted away before modifying rds. + auto* orig_initial_timeout = + const_cast(rds).mutable_config_source()->release_initial_fetch_timeout(); + const uint64_t manager_identifier = MessageUtil::hash(rds); + const_cast(rds).mutable_config_source()->set_allocated_initial_fetch_timeout( + orig_initial_timeout); auto existing_provider = reuseDynamicProvider(manager_identifier, init_manager, route_config_name); diff --git a/source/common/router/BUILD b/source/common/router/BUILD index d56ce3b5e208a..ba6ce85acb9b3 100644 --- a/source/common/router/BUILD +++ b/source/common/router/BUILD @@ -43,7 +43,7 @@ envoy_cc_library( ":matcher_visitor_lib", ":metadatamatchcriteria_lib", ":per_filter_config_lib", - ":reset_header_parser_lib", + ":retry_policy_lib", ":retry_state_lib", ":router_ratelimit_lib", ":tls_context_match_criteria_lib", @@ -358,42 +358,35 @@ envoy_cc_library( "upstream_request.h", ], deps = [ - ":config_lib", ":context_lib", ":debug_config_lib", ":header_parser_lib", + ":metadatamatchcriteria_lib", ":retry_state_lib", ":upstream_codec_filter_lib", "//envoy/event:dispatcher_interface", "//envoy/event:timer_interface", "//envoy/grpc:status", - "//envoy/http:codec_interface", "//envoy/http:codes_interface", "//envoy/http:conn_pool_interface", "//envoy/http:filter_factory_interface", "//envoy/http:filter_interface", - "//envoy/http:stateful_session_interface", "//envoy/local_info:local_info_interface", "//envoy/router:router_filter_interface", "//envoy/router:shadow_writer_interface", "//envoy/runtime:runtime_interface", "//envoy/server:factory_context_interface", - "//envoy/server:filter_config_interface", "//envoy/stats:stats_interface", "//envoy/stats:stats_macros", "//envoy/upstream:cluster_manager_interface", "//envoy/upstream:upstream_interface", "//source/common/access_log:access_log_lib", - "//source/common/buffer:watermark_buffer_lib", "//source/common/common:assert_lib", "//source/common/common:cleanup_lib", - "//source/common/common:empty_string", "//source/common/common:enum_to_int", "//source/common/common:hash_lib", "//source/common/common:hex_lib", - "//source/common/common:linked_object", "//source/common/common:minimal_logger_lib", - "//source/common/common:scope_tracker", "//source/common/common:utility_lib", "//source/common/config:utility_lib", "//source/common/grpc:common_lib", @@ -405,15 +398,11 @@ envoy_cc_library( "//source/common/http:message_lib", "//source/common/http:sidestream_watermark_lib", "//source/common/http:utility_lib", - "//source/common/network:application_protocol_lib", - "//source/common/network:socket_option_factory_lib", "//source/common/network:transport_socket_options_lib", "//source/common/network:upstream_socket_options_filter_state_lib", "//source/common/orca:orca_load_metrics_lib", "//source/common/orca:orca_parser", - "//source/common/stream_info:stream_info_lib", "//source/common/stream_info:uint32_accessor_lib", - "//source/common/tracing:http_tracer_lib", "//source/common/upstream:load_balancer_context_base_lib", "//source/common/upstream:upstream_factory_context_lib", "//source/extensions/common/proxy_protocol:proxy_protocol_header_lib", @@ -507,6 +496,7 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/json:json_loader_lib", "//source/common/protobuf:utility_lib", + "//source/common/runtime:runtime_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -552,3 +542,19 @@ envoy_cc_library( "//envoy/router:cluster_specifier_plugin_interface", ], ) + +envoy_cc_library( + name = "retry_policy_lib", + srcs = ["retry_policy_impl.cc"], + hdrs = ["retry_policy_impl.h"], + deps = [ + ":reset_header_parser_lib", + ":retry_state_lib", + "//envoy/router:router_interface", + "//envoy/server:factory_context_interface", + "//source/common/config:utility_lib", + "//source/common/upstream:retry_factory_lib", + "@com_google_absl//absl/types:optional", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + ], +) diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc index 5ac7fe53a144c..65de73ecf1b16 100644 --- a/source/common/router/config_impl.cc +++ b/source/common/router/config_impl.cc @@ -44,13 +44,10 @@ #include "source/common/router/context_impl.h" #include "source/common/router/header_cluster_specifier.h" #include "source/common/router/matcher_visitor.h" -#include "source/common/router/reset_header_parser.h" -#include "source/common/router/retry_state_impl.h" #include "source/common/router/weighted_cluster_specifier.h" #include "source/common/runtime/runtime_features.h" #include "source/common/tracing/custom_tag_impl.h" #include "source/common/tracing/http_tracer_impl.h" -#include "source/common/upstream/retry_factory.h" #include "source/extensions/early_data/default_early_data_policy.h" #include "source/extensions/matching/network/common/inputs.h" #include "source/extensions/path/match/uri_template/uri_template_match.h" @@ -217,128 +214,6 @@ HedgePolicyImpl::HedgePolicyImpl(const envoy::config::route::v3::HedgePolicy& he HedgePolicyImpl::HedgePolicyImpl() : initial_requests_(1), hedge_on_per_try_timeout_(false) {} -absl::StatusOr> -RetryPolicyImpl::create(const envoy::config::route::v3::RetryPolicy& retry_policy, - ProtobufMessage::ValidationVisitor& validation_visitor, - Upstream::RetryExtensionFactoryContext& factory_context, - Server::Configuration::CommonFactoryContext& common_context) { - absl::Status creation_status = absl::OkStatus(); - auto ret = std::unique_ptr(new RetryPolicyImpl( - retry_policy, validation_visitor, factory_context, common_context, creation_status)); - RETURN_IF_NOT_OK(creation_status); - return ret; -} -RetryPolicyImpl::RetryPolicyImpl(const envoy::config::route::v3::RetryPolicy& retry_policy, - ProtobufMessage::ValidationVisitor& validation_visitor, - Upstream::RetryExtensionFactoryContext& factory_context, - Server::Configuration::CommonFactoryContext& common_context, - absl::Status& creation_status) - : retriable_headers_(Http::HeaderUtility::buildHeaderMatcherVector( - retry_policy.retriable_headers(), common_context)), - retriable_request_headers_(Http::HeaderUtility::buildHeaderMatcherVector( - retry_policy.retriable_request_headers(), common_context)), - validation_visitor_(&validation_visitor) { - per_try_timeout_ = - std::chrono::milliseconds(PROTOBUF_GET_MS_OR_DEFAULT(retry_policy, per_try_timeout, 0)); - per_try_idle_timeout_ = - std::chrono::milliseconds(PROTOBUF_GET_MS_OR_DEFAULT(retry_policy, per_try_idle_timeout, 0)); - num_retries_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(retry_policy, num_retries, 1); - retry_on_ = RetryStateImpl::parseRetryOn(retry_policy.retry_on()).first; - retry_on_ |= RetryStateImpl::parseRetryGrpcOn(retry_policy.retry_on()).first; - - for (const auto& host_predicate : retry_policy.retry_host_predicate()) { - auto& factory = Envoy::Config::Utility::getAndCheckFactory( - host_predicate); - auto config = Envoy::Config::Utility::translateToFactoryConfig(host_predicate, - validation_visitor, factory); - retry_host_predicate_configs_.emplace_back(factory, std::move(config)); - } - - const auto& retry_priority = retry_policy.retry_priority(); - if (!retry_priority.name().empty()) { - auto& factory = - Envoy::Config::Utility::getAndCheckFactory(retry_priority); - retry_priority_config_ = - std::make_pair(&factory, Envoy::Config::Utility::translateToFactoryConfig( - retry_priority, validation_visitor, factory)); - } - - for (const auto& options_predicate : retry_policy.retry_options_predicates()) { - auto& factory = - Envoy::Config::Utility::getAndCheckFactory( - options_predicate); - retry_options_predicates_.emplace_back( - factory.createOptionsPredicate(*Envoy::Config::Utility::translateToFactoryConfig( - options_predicate, validation_visitor, factory), - factory_context)); - } - - auto host_selection_attempts = retry_policy.host_selection_retry_max_attempts(); - if (host_selection_attempts) { - host_selection_attempts_ = host_selection_attempts; - } - - for (auto code : retry_policy.retriable_status_codes()) { - retriable_status_codes_.emplace_back(code); - } - - if (retry_policy.has_retry_back_off()) { - base_interval_ = std::chrono::milliseconds( - PROTOBUF_GET_MS_REQUIRED(retry_policy.retry_back_off(), base_interval)); - if ((*base_interval_).count() < 1) { - base_interval_ = std::chrono::milliseconds(1); - } - - max_interval_ = PROTOBUF_GET_OPTIONAL_MS(retry_policy.retry_back_off(), max_interval); - if (max_interval_) { - // Apply the same rounding to max interval in case both are set to sub-millisecond values. - if ((*max_interval_).count() < 1) { - max_interval_ = std::chrono::milliseconds(1); - } - - if ((*max_interval_).count() < (*base_interval_).count()) { - creation_status = absl::InvalidArgumentError( - "retry_policy.max_interval must greater than or equal to the base_interval"); - return; - } - } - } - - if (retry_policy.has_rate_limited_retry_back_off()) { - reset_headers_ = ResetHeaderParserImpl::buildResetHeaderParserVector( - retry_policy.rate_limited_retry_back_off().reset_headers()); - - absl::optional reset_max_interval = - PROTOBUF_GET_OPTIONAL_MS(retry_policy.rate_limited_retry_back_off(), max_interval); - if (reset_max_interval.has_value()) { - std::chrono::milliseconds max_interval = reset_max_interval.value(); - if (max_interval.count() < 1) { - max_interval = std::chrono::milliseconds(1); - } - reset_max_interval_ = max_interval; - } - } -} - -std::vector RetryPolicyImpl::retryHostPredicates() const { - std::vector predicates; - predicates.reserve(retry_host_predicate_configs_.size()); - for (const auto& config : retry_host_predicate_configs_) { - predicates.emplace_back(config.first.createHostPredicate(*config.second, num_retries_)); - } - - return predicates; -} - -Upstream::RetryPrioritySharedPtr RetryPolicyImpl::retryPriority() const { - if (retry_priority_config_.first == nullptr) { - return nullptr; - } - - return retry_priority_config_.first->createRetryPriority(*retry_priority_config_.second, - *validation_visitor_, num_retries_); -} - absl::StatusOr> InternalRedirectPolicyImpl::create( const envoy::config::route::v3::InternalRedirectPolicy& policy_config, ProtobufMessage::ValidationVisitor& validator, absl::string_view current_route_name) { @@ -449,15 +324,8 @@ ShadowPolicyImpl::ShadowPolicyImpl(const RequestMirrorPolicy& config, absl::Stat // If trace sampling is not explicitly configured in shadow_policy, we pass null optional to // inherit the parent's sampling decision. This prevents oversampling when runtime sampling is // disabled. - if (config.has_trace_sampled()) { - trace_sampled_ = config.trace_sampled().value(); - } else { - // If the shadow policy does not specify trace_sampled, we will inherit the parent's sampling - // decision. - const bool user_parent_sampling_decision = Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.shadow_policy_inherit_trace_sampling"); - trace_sampled_ = user_parent_sampling_decision ? absl::nullopt : absl::make_optional(true); - } + trace_sampled_ = config.has_trace_sampled() ? absl::optional(config.trace_sampled().value()) + : absl::nullopt; } DecoratorImpl::DecoratorImpl(const envoy::config::route::v3::Decorator& decorator) @@ -571,8 +439,10 @@ RouteEntryImplBase::RouteEntryImplBase(const CommonVirtualHostSharedPtr& vhost, opaque_config_(parseOpaqueConfig(route)), decorator_(parseDecorator(route)), route_tracing_(parseRouteTracing(route)), route_name_(route.name()), time_source_(factory_context.mainThreadDispatcher().timeSource()), - retry_shadow_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( - route, per_request_buffer_limit_bytes, vhost->retryShadowBufferLimit())), + per_request_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( + route, per_request_buffer_limit_bytes, std::numeric_limits::max())), + request_body_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(route, request_body_buffer_limit, + vhost->requestBodyBufferLimit())), direct_response_code_(ConfigUtility::parseDirectResponseCode(route)), cluster_not_found_response_code_(ConfigUtility::parseClusterNotFoundResponseCode( route.route().cluster_not_found_response_code())), @@ -582,6 +452,7 @@ RouteEntryImplBase::RouteEntryImplBase(const CommonVirtualHostSharedPtr& vhost, using_new_timeouts_(route.route().has_max_stream_duration()), match_grpc_(route.match().has_grpc()), case_sensitive_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(route.match(), case_sensitive, true)) { + auto config_or_error = PerFilterConfigs::create(route.typed_per_filter_config(), factory_context, validator); SET_AND_RETURN_IF_NOT_OK(config_or_error.status(), creation_status); @@ -590,7 +461,8 @@ RouteEntryImplBase::RouteEntryImplBase(const CommonVirtualHostSharedPtr& vhost, auto policy_or_error = buildRetryPolicy(vhost->retryPolicy(), route.route(), validator, factory_context); SET_AND_RETURN_IF_NOT_OK(policy_or_error.status(), creation_status); - retry_policy_ = std::move(policy_or_error.value()); + retry_policy_ = policy_or_error.value() != nullptr ? std::move(policy_or_error.value()) + : RetryPolicyImpl::DefaultRetryPolicy; if (route.has_direct_response() && route.direct_response().has_body()) { auto provider_or_error = Envoy::Config::DataSource::DataSourceProvider::create( @@ -914,12 +786,13 @@ void RouteEntryImplBase::finalizeHostHeader(Http::RequestHeaderMap& headers, } void RouteEntryImplBase::finalizeRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info, bool keep_original_host_or_path) const { for (const HeaderParser* header_parser : getRequestHeaderParsers( /*specificity_ascend=*/vhost_->globalRouteConfig().mostSpecificHeaderMutationsWins())) { // Later evaluated header parser wins. - header_parser->evaluateHeaders(headers, stream_info); + header_parser->evaluateHeaders(headers, context, stream_info); } // Restore the port if this was a CONNECT request. @@ -944,12 +817,12 @@ void RouteEntryImplBase::finalizeRequestHeaders(Http::RequestHeaderMap& headers, } void RouteEntryImplBase::finalizeResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { for (const HeaderParser* header_parser : getResponseHeaderParsers( /*specificity_ascend=*/vhost_->globalRouteConfig().mostSpecificHeaderMutationsWins())) { // Later evaluated header parser wins. - header_parser->evaluateHeaders(headers, {stream_info.getRequestHeaders(), &headers}, - stream_info); + header_parser->evaluateHeaders(headers, context, stream_info); } } @@ -1116,7 +989,7 @@ RouteEntryImplBase::parseOpaqueConfig(const envoy::config::route::v3::Route& rou return ret; } for (const auto& it : filter_metadata->second.fields()) { - if (it.second.kind_case() == ProtobufWkt::Value::kStringValue) { + if (it.second.kind_case() == Protobuf::Value::kStringValue) { ret.emplace(it.first, it.second.string_value()); } } @@ -1141,27 +1014,20 @@ std::unique_ptr RouteEntryImplBase::buildHedgePolicy( return nullptr; } -absl::StatusOr> RouteEntryImplBase::buildRetryPolicy( - RetryPolicyConstOptRef vhost_retry_policy, +absl::StatusOr RouteEntryImplBase::buildRetryPolicy( + const RetryPolicyConstSharedPtr& vhost_retry_policy, const envoy::config::route::v3::RouteAction& route_config, ProtobufMessage::ValidationVisitor& validation_visitor, - Server::Configuration::ServerFactoryContext& factory_context) const { - Upstream::RetryExtensionFactoryContextImpl retry_factory_context( - factory_context.singletonManager()); + Server::Configuration::CommonFactoryContext& factory_context) const { // Route specific policy wins, if available. if (route_config.has_retry_policy()) { return RetryPolicyImpl::create(route_config.retry_policy(), validation_visitor, - retry_factory_context, factory_context); - } - - // If not, we fallback to the virtual host policy if there is one. - if (vhost_retry_policy.has_value()) { - return RetryPolicyImpl::create(*vhost_retry_policy, validation_visitor, retry_factory_context, factory_context); } - // Otherwise, an empty policy will do. - return nullptr; + // If not, we fallback to the virtual host policy if there is one. Note the + // virtual host policy may be nullptr. + return vhost_retry_policy; } absl::StatusOr> @@ -1192,6 +1058,7 @@ RouteEntryImplBase::OptionalTimeouts RouteEntryImplBase::buildOptionalTimeouts( // Calculate how many values are actually set, to initialize `OptionalTimeouts` packed_struct, // avoiding memory re-allocation on each set() call. int num_timeouts_set = route.has_idle_timeout() ? 1 : 0; + num_timeouts_set += route.has_flush_timeout() ? 1 : 0; num_timeouts_set += route.has_max_grpc_timeout() ? 1 : 0; num_timeouts_set += route.has_grpc_timeout_offset() ? 1 : 0; if (route.has_max_stream_duration()) { @@ -1204,6 +1071,10 @@ RouteEntryImplBase::OptionalTimeouts RouteEntryImplBase::buildOptionalTimeouts( timeouts.set( std::chrono::milliseconds(PROTOBUF_GET_MS_REQUIRED(route, idle_timeout))); } + if (route.has_flush_timeout()) { + timeouts.set( + std::chrono::milliseconds(PROTOBUF_GET_MS_REQUIRED(route, flush_timeout))); + } if (route.has_max_grpc_timeout()) { timeouts.set( std::chrono::milliseconds(PROTOBUF_GET_MS_REQUIRED(route, max_grpc_timeout))); @@ -1563,11 +1434,14 @@ CommonVirtualHostImpl::CommonVirtualHostImpl( THROW_OR_RETURN_VALUE(PerFilterConfigs::create(virtual_host.typed_per_filter_config(), factory_context, validator), std::unique_ptr)), - retry_shadow_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( + per_request_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( virtual_host, per_request_buffer_limit_bytes, std::numeric_limits::max())), + request_body_buffer_limit_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( + virtual_host, request_body_buffer_limit, std::numeric_limits::max())), include_attempt_count_in_request_(virtual_host.include_request_attempt_count()), include_attempt_count_in_response_(virtual_host.include_attempt_count_in_response()), include_is_timeout_retry_header_(virtual_host.include_is_timeout_retry_header()) { + if (!virtual_host.request_headers_to_add().empty() || !virtual_host.request_headers_to_remove().empty()) { request_headers_parser_ = @@ -1585,8 +1459,10 @@ CommonVirtualHostImpl::CommonVirtualHostImpl( // Retry and Hedge policies must be set before routes, since they may use them. if (virtual_host.has_retry_policy()) { - retry_policy_ = std::make_unique(); - retry_policy_->CopyFrom(virtual_host.retry_policy()); + auto policy_or_error = + RetryPolicyImpl::create(virtual_host.retry_policy(), validator, factory_context); + SET_AND_RETURN_IF_NOT_OK(policy_or_error.status(), creation_status); + retry_policy_ = std::move(policy_or_error.value()); } if (virtual_host.has_hedge_policy()) { hedge_policy_ = std::make_unique(); @@ -1825,14 +1701,13 @@ RouteConstSharedPtr VirtualHostImpl::getRouteFromEntries(const RouteCallback& cb Matcher::evaluateMatch(*matcher_, data); if (match_result.isMatch()) { - const Matcher::ActionPtr result = match_result.action(); + const auto result = match_result.actionByMove(); if (result->typeUrl() == RouteMatchAction::staticTypeUrl()) { - const RouteMatchAction& route_action = result->getTyped(); - - return getRouteFromRoutes(cb, headers, stream_info, random_value, {route_action.route()}); + return getRouteFromRoutes( + cb, headers, stream_info, random_value, + {std::dynamic_pointer_cast(std::move(result))}); } else if (result->typeUrl() == RouteListMatchAction::staticTypeUrl()) { const RouteListMatchAction& action = result->getTyped(); - return getRouteFromRoutes(cb, headers, stream_info, random_value, action.routes()); } PANIC("Action in router matcher should be Route or RouteList"); @@ -2140,9 +2015,9 @@ const Envoy::Config::TypedMetadata& NullConfigImpl::typedMetadata() const { return DefaultRouteMetadataPack::get().typed_metadata_; } -Matcher::ActionFactoryCb RouteMatchActionFactory::createActionFactoryCb( - const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) { +Matcher::ActionConstSharedPtr +RouteMatchActionFactory::createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& route_config = MessageUtil::downcastAndValidate(config, validation_visitor); @@ -2150,14 +2025,14 @@ Matcher::ActionFactoryCb RouteMatchActionFactory::createActionFactoryCb( RouteCreator::createAndValidateRoute(route_config, context.vhost, context.factory_context, validation_visitor, false), RouteEntryImplBaseConstSharedPtr); - - return [route]() { return std::make_unique(route); }; + return route; } REGISTER_FACTORY(RouteMatchActionFactory, Matcher::ActionFactory); -Matcher::ActionFactoryCb RouteListMatchActionFactory::createActionFactoryCb( - const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) { +Matcher::ActionConstSharedPtr +RouteListMatchActionFactory::createAction(const Protobuf::Message& config, + RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& route_config = MessageUtil::downcastAndValidate( config, validation_visitor); @@ -2169,7 +2044,7 @@ Matcher::ActionFactoryCb RouteListMatchActionFactory::createActionFactoryCb( validation_visitor, false), RouteEntryImplBaseConstSharedPtr)); } - return [routes]() { return std::make_unique(routes); }; + return std::make_shared(std::move(routes)); } REGISTER_FACTORY(RouteListMatchActionFactory, Matcher::ActionFactory); diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h index c4106e056b8bf..bb00a2a054d3f 100644 --- a/source/common/router/config_impl.h +++ b/source/common/router/config_impl.h @@ -32,6 +32,7 @@ #include "source/common/router/header_parser.h" #include "source/common/router/metadatamatchcriteria_impl.h" #include "source/common/router/per_filter_config.h" +#include "source/common/router/retry_policy_impl.h" #include "source/common/router/router_ratelimit.h" #include "source/common/router/tls_context_match_criteria_impl.h" #include "source/common/stats/symbol_table.h" @@ -90,7 +91,7 @@ using RouteEntryImplBaseConstSharedPtr = std::shared_ptr& shadowPolicies() const { return shadow_policies_; } - RetryPolicyConstOptRef retryPolicy() const { - if (retry_policy_ != nullptr) { - return *retry_policy_; - } - return absl::nullopt; - } + const RetryPolicyConstSharedPtr& retryPolicy() const { return retry_policy_; } HedgePolicyConstOptRef hedgePolicy() const { if (hedge_policy_ != nullptr) { return *hedge_policy_; } return absl::nullopt; } - uint32_t retryShadowBufferLimit() const override { return retry_shadow_buffer_limit_; } + uint64_t requestBodyBufferLimit() const override { + // Return the new field if set, otherwise return the legacy field. + if (request_body_buffer_limit_ != std::numeric_limits::max()) { + return request_body_buffer_limit_; + } + if (per_request_buffer_limit_ != std::numeric_limits::max()) { + return static_cast(per_request_buffer_limit_); + } + return std::numeric_limits::max(); + } RouteSpecificFilterConfigs perFilterConfigs(absl::string_view) const override; const envoy::config::core::v3::Metadata& metadata() const override; @@ -343,12 +348,13 @@ class CommonVirtualHostImpl : public VirtualHost, Logger::Loggable per_filter_configs_; - std::unique_ptr retry_policy_; + RetryPolicyConstSharedPtr retry_policy_; std::unique_ptr hedge_policy_; std::unique_ptr virtual_cluster_catch_all_; RouteMetadataPackPtr metadata_; // Keep small members (bools and enums) at the end of class, to reduce alignment overhead. - uint32_t retry_shadow_buffer_limit_{std::numeric_limits::max()}; + uint32_t per_request_buffer_limit_{std::numeric_limits::max()}; + uint64_t request_body_buffer_limit_{std::numeric_limits::max()}; const bool include_attempt_count_in_request_ : 1; const bool include_attempt_count_in_response_ : 1; const bool include_is_timeout_retry_header_ : 1; @@ -391,80 +397,6 @@ class VirtualHostImpl : Logger::Loggable { using VirtualHostImplSharedPtr = std::shared_ptr; -/** - * Implementation of RetryPolicy that reads from the proto route or virtual host config. - */ -class RetryPolicyImpl : public RetryPolicy { - -public: - static absl::StatusOr> - create(const envoy::config::route::v3::RetryPolicy& retry_policy, - ProtobufMessage::ValidationVisitor& validation_visitor, - Upstream::RetryExtensionFactoryContext& factory_context, - Server::Configuration::CommonFactoryContext& common_context); - RetryPolicyImpl() = default; - - // Router::RetryPolicy - std::chrono::milliseconds perTryTimeout() const override { return per_try_timeout_; } - std::chrono::milliseconds perTryIdleTimeout() const override { return per_try_idle_timeout_; } - uint32_t numRetries() const override { return num_retries_; } - uint32_t retryOn() const override { return retry_on_; } - std::vector retryHostPredicates() const override; - Upstream::RetryPrioritySharedPtr retryPriority() const override; - absl::Span - retryOptionsPredicates() const override { - return retry_options_predicates_; - } - uint32_t hostSelectionMaxAttempts() const override { return host_selection_attempts_; } - const std::vector& retriableStatusCodes() const override { - return retriable_status_codes_; - } - const std::vector& retriableHeaders() const override { - return retriable_headers_; - } - const std::vector& retriableRequestHeaders() const override { - return retriable_request_headers_; - } - absl::optional baseInterval() const override { return base_interval_; } - absl::optional maxInterval() const override { return max_interval_; } - const std::vector& resetHeaders() const override { - return reset_headers_; - } - std::chrono::milliseconds resetMaxInterval() const override { return reset_max_interval_; } - -private: - RetryPolicyImpl(const envoy::config::route::v3::RetryPolicy& retry_policy, - ProtobufMessage::ValidationVisitor& validation_visitor, - Upstream::RetryExtensionFactoryContext& factory_context, - Server::Configuration::CommonFactoryContext& common_context, - absl::Status& creation_status); - std::chrono::milliseconds per_try_timeout_{0}; - std::chrono::milliseconds per_try_idle_timeout_{0}; - // We set the number of retries to 1 by default (i.e. when no route or vhost level retry policy is - // set) so that when retries get enabled through the x-envoy-retry-on header we default to 1 - // retry. - uint32_t num_retries_{1}; - uint32_t retry_on_{}; - // Each pair contains the name and config proto to be used to create the RetryHostPredicates - // that should be used when with this policy. - std::vector> - retry_host_predicate_configs_; - // Name and config proto to use to create the RetryPriority to use with this policy. Default - // initialized when no RetryPriority should be used. - std::pair retry_priority_config_; - uint32_t host_selection_attempts_{1}; - std::vector retriable_status_codes_; - std::vector retriable_headers_; - std::vector retriable_request_headers_; - absl::optional base_interval_; - absl::optional max_interval_; - std::vector reset_headers_; - std::chrono::milliseconds reset_max_interval_{300000}; - ProtobufMessage::ValidationVisitor* validation_visitor_{}; - std::vector retry_options_predicates_; -}; -using DefaultRetryPolicy = ConstSingleton; - /** * Implementation of ShadowPolicy that reads from the proto route config. */ @@ -619,6 +551,7 @@ class RouteEntryImplBase : public RouteEntryAndRoute, public Matchable, public DirectResponseEntry, public PathMatchCriterion, + public Matcher::ActionBase, public std::enable_shared_from_this, Logger::Loggable { protected: @@ -666,11 +599,13 @@ class RouteEntryImplBase : public RouteEntryAndRoute, return HeaderParser::defaultParser(); } void finalizeRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info, bool keep_original_host_or_path) const override; Http::HeaderTransforms requestHeaderTransforms(const StreamInfo::StreamInfo& stream_info, bool do_formatting = true) const override; void finalizeResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; Http::HeaderTransforms responseHeaderTransforms(const StreamInfo::StreamInfo& stream_info, bool do_formatting = true) const override; @@ -696,11 +631,9 @@ class RouteEntryImplBase : public RouteEntryAndRoute, } return DefaultRateLimitPolicy::get(); } - const RetryPolicy& retryPolicy() const override { - if (retry_policy_ != nullptr) { - return *retry_policy_; - } - return DefaultRetryPolicy::get(); + const RetryPolicyConstSharedPtr& retryPolicy() const override { + ASSERT(retry_policy_ != nullptr); + return retry_policy_; } const InternalRedirectPolicy& internalRedirectPolicy() const override { if (internal_redirect_policy_ != nullptr) { @@ -712,7 +645,16 @@ class RouteEntryImplBase : public RouteEntryAndRoute, const PathMatcherSharedPtr& pathMatcher() const override { return path_matcher_; } const PathRewriterSharedPtr& pathRewriter() const override { return path_rewriter_; } - uint32_t retryShadowBufferLimit() const override { return retry_shadow_buffer_limit_; } + uint64_t requestBodyBufferLimit() const override { + // Return the new field if set, otherwise return the legacy field. + if (request_body_buffer_limit_ != std::numeric_limits::max()) { + return request_body_buffer_limit_; + } + if (per_request_buffer_limit_ != std::numeric_limits::max()) { + return static_cast(per_request_buffer_limit_); + } + return std::numeric_limits::max(); + } const std::vector& shadowPolicies() const override { return shadow_policies_; } std::chrono::milliseconds timeout() const override { return timeout_; } bool usingNewTimeouts() const override { return using_new_timeouts_; } @@ -727,13 +669,17 @@ class RouteEntryImplBase : public RouteEntryAndRoute, GrpcTimeoutHeaderMax, GrpcTimeoutHeaderOffset, MaxGrpcTimeout, - GrpcTimeoutOffset + GrpcTimeoutOffset, + FlushTimeout, }; - using OptionalTimeouts = PackedStruct; + using OptionalTimeouts = PackedStruct; absl::optional idleTimeout() const override { return getOptionalTimeout(); } + absl::optional flushTimeout() const override { + return getOptionalTimeout(); + } absl::optional maxStreamDuration() const override { return getOptionalTimeout(); } @@ -883,11 +829,11 @@ class RouteEntryImplBase : public RouteEntryAndRoute, buildHedgePolicy(HedgePolicyConstOptRef vhost_hedge_policy, const envoy::config::route::v3::RouteAction& route_config) const; - absl::StatusOr> - buildRetryPolicy(RetryPolicyConstOptRef vhost_retry_policy, + absl::StatusOr + buildRetryPolicy(const RetryPolicyConstSharedPtr& vhost_retry_policy, const envoy::config::route::v3::RouteAction& route_config, ProtobufMessage::ValidationVisitor& validation_visitor, - Server::Configuration::ServerFactoryContext& factory_context) const; + Server::Configuration::CommonFactoryContext& factory_context) const; absl::StatusOr> buildInternalRedirectPolicy(const envoy::config::route::v3::RouteAction& route_config, @@ -934,7 +880,7 @@ class RouteEntryImplBase : public RouteEntryAndRoute, std::unique_ptr runtime_; std::unique_ptr redirect_config_; std::unique_ptr hedge_policy_; - std::unique_ptr retry_policy_; + RetryPolicyConstSharedPtr retry_policy_; std::unique_ptr internal_redirect_policy_; std::unique_ptr rate_limit_policy_; std::vector shadow_policies_; @@ -963,7 +909,8 @@ class RouteEntryImplBase : public RouteEntryAndRoute, EarlyDataPolicyPtr early_data_policy_; // Keep small members (bools and enums) at the end of class, to reduce alignment overhead. - uint32_t retry_shadow_buffer_limit_{std::numeric_limits::max()}; + uint32_t per_request_buffer_limit_{std::numeric_limits::max()}; + uint64_t request_body_buffer_limit_{std::numeric_limits::max()}; const absl::optional direct_response_code_; const Http::Code cluster_not_found_response_code_; const Upstream::ResourcePriority priority_; @@ -1191,9 +1138,9 @@ class RouteMatchAction : public Matcher::ActionBase { public: - Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; std::string name() const override { return "route"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); @@ -1217,9 +1164,9 @@ class RouteListMatchAction : public Matcher::ActionBase { public: - Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; std::string name() const override { return "route_match_action"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); diff --git a/source/common/router/delegating_route_impl.cc b/source/common/router/delegating_route_impl.cc index 97d5b1e9b5faa..f2dd426a768a2 100644 --- a/source/common/router/delegating_route_impl.cc +++ b/source/common/router/delegating_route_impl.cc @@ -5,8 +5,9 @@ namespace Router { // Router:DelegatingRouteEntry void DelegatingRouteEntry::finalizeResponseHeaders( - Http::ResponseHeaderMap& headers, const StreamInfo::StreamInfo& stream_info) const { - return base_route_entry_->finalizeResponseHeaders(headers, stream_info); + Http::ResponseHeaderMap& headers, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const { + return base_route_entry_->finalizeResponseHeaders(headers, context, stream_info); } Http::HeaderTransforms @@ -33,9 +34,10 @@ DelegatingRouteEntry::currentUrlPathAfterRewrite(const Http::RequestHeaderMap& h } void DelegatingRouteEntry::finalizeRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info, bool insert_envoy_original_path) const { - return base_route_entry_->finalizeRequestHeaders(headers, stream_info, + return base_route_entry_->finalizeRequestHeaders(headers, context, stream_info, insert_envoy_original_path); } @@ -61,7 +63,7 @@ const RateLimitPolicy& DelegatingRouteEntry::rateLimitPolicy() const { return base_route_entry_->rateLimitPolicy(); } -const RetryPolicy& DelegatingRouteEntry::retryPolicy() const { +const RetryPolicyConstSharedPtr& DelegatingRouteEntry::retryPolicy() const { return base_route_entry_->retryPolicy(); } @@ -77,8 +79,8 @@ const InternalRedirectPolicy& DelegatingRouteEntry::internalRedirectPolicy() con return base_route_entry_->internalRedirectPolicy(); } -uint32_t DelegatingRouteEntry::retryShadowBufferLimit() const { - return base_route_entry_->retryShadowBufferLimit(); +uint64_t DelegatingRouteEntry::requestBodyBufferLimit() const { + return base_route_entry_->requestBodyBufferLimit(); } const std::vector& DelegatingRouteEntry::shadowPolicies() const { @@ -93,6 +95,10 @@ absl::optional DelegatingRouteEntry::idleTimeout() co return base_route_entry_->idleTimeout(); } +absl::optional DelegatingRouteEntry::flushTimeout() const { + return base_route_entry_->flushTimeout(); +} + bool DelegatingRouteEntry::usingNewTimeouts() const { return base_route_entry_->usingNewTimeouts(); } diff --git a/source/common/router/delegating_route_impl.h b/source/common/router/delegating_route_impl.h index 8ceaac54c4729..89f616cb0f541 100644 --- a/source/common/router/delegating_route_impl.h +++ b/source/common/router/delegating_route_impl.h @@ -80,6 +80,7 @@ class DelegatingRouteEntry : public DelegatingRouteBase { // Router::ResponseEntry void finalizeResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; Http::HeaderTransforms responseHeaderTransforms(const StreamInfo::StreamInfo& stream_info, bool do_formatting = true) const override; @@ -91,6 +92,7 @@ class DelegatingRouteEntry : public DelegatingRouteBase { absl::optional currentUrlPathAfterRewrite(const Http::RequestHeaderMap& headers) const override; void finalizeRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info, bool insert_envoy_original_path) const override; Http::HeaderTransforms requestHeaderTransforms(const StreamInfo::StreamInfo& stream_info, @@ -100,14 +102,15 @@ class DelegatingRouteEntry : public DelegatingRouteBase { const HedgePolicy& hedgePolicy() const override; Upstream::ResourcePriority priority() const override; const RateLimitPolicy& rateLimitPolicy() const override; - const RetryPolicy& retryPolicy() const override; + const RetryPolicyConstSharedPtr& retryPolicy() const override; const Router::PathMatcherSharedPtr& pathMatcher() const override; const Router::PathRewriterSharedPtr& pathRewriter() const override; const InternalRedirectPolicy& internalRedirectPolicy() const override; - uint32_t retryShadowBufferLimit() const override; + uint64_t requestBodyBufferLimit() const override; const std::vector& shadowPolicies() const override; std::chrono::milliseconds timeout() const override; absl::optional idleTimeout() const override; + absl::optional flushTimeout() const override; bool usingNewTimeouts() const override; absl::optional maxStreamDuration() const override; absl::optional grpcTimeoutHeaderMax() const override; diff --git a/source/common/router/header_parser.cc b/source/common/router/header_parser.cc index a3f75e621223b..d0e2bbc27ddd6 100644 --- a/source/common/router/header_parser.cc +++ b/source/common/router/header_parser.cc @@ -13,6 +13,7 @@ #include "source/common/http/headers.h" #include "source/common/json/json_loader.h" #include "source/common/protobuf/utility.h" +#include "source/common/runtime/runtime_features.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_replace.h" @@ -38,14 +39,18 @@ parseHttpHeaderFormatter(const envoy::config::core::v3::HeaderValue& header_valu return absl::InvalidArgumentError(":-prefixed or host headers may not be modified"); } - // UPSTREAM_METADATA and DYNAMIC_METADATA must be translated from JSON ["a", "b"] format to colon - // format (a:b) - std::string final_header_value = HeaderParser::translateMetadataFormat(header_value.value()); - // Change PER_REQUEST_STATE to FILTER_STATE. - final_header_value = HeaderParser::translatePerRequestState(final_header_value); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.remove_legacy_route_formatter")) { + // UPSTREAM_METADATA and DYNAMIC_METADATA must be translated from JSON ["a", "b"] format to + // colon format (a:b) + std::string final_header_value = HeaderParser::translateMetadataFormat(header_value.value()); + // Change PER_REQUEST_STATE to FILTER_STATE. + final_header_value = HeaderParser::translatePerRequestState(final_header_value); + // Let the substitution formatter parse the final_header_value. + return Envoy::Formatter::FormatterImpl::create(final_header_value, true); + } - // Let the substitution formatter parse the final_header_value. - return Envoy::Formatter::FormatterImpl::create(final_header_value, true); + // Let the substitution formatter parse the header_value. + return Envoy::Formatter::FormatterImpl::create(header_value.value(), true); } } // namespace diff --git a/source/common/router/header_parser_utils.cc b/source/common/router/header_parser_utils.cc index db65eaedbb822..f9210482c1457 100644 --- a/source/common/router/header_parser_utils.cc +++ b/source/common/router/header_parser_utils.cc @@ -1,6 +1,8 @@ #include #include +#include "envoy/server/factory_context.h" + #include "source/common/common/assert.h" #include "source/common/json/json_loader.h" #include "source/common/router/header_parser.h" @@ -64,6 +66,13 @@ std::string HeaderParser::translateMetadataFormat(const std::string& header_valu "Header formatter: JSON format of {}_METADATA parameters has been obsoleted. " "Use colon format: {}", matches[1], new_format.c_str()); + // The parsing should only happen on the main thread and the singleton context should be + // available. In case it is not set in tests or other non-standard Envoy usage, we skip + // counting the deprecated feature usage instead of crashing. + auto* context = Server::Configuration::ServerFactoryContextInstance::getExisting(); + if (context != nullptr) { + context->runtime().countDeprecatedFeatureUse(); + } int subs = absl::StrReplaceAll({{matches[0], new_format}}, &new_header_value); ASSERT(subs > 0); @@ -94,6 +103,15 @@ std::string HeaderParser::translatePerRequestState(const std::string& header_val ENVOY_LOG_MISC(warn, "PER_REQUEST_STATE header formatter has been obsoleted. Use {}", new_format.c_str()); + + // The parsing should only happen on the main thread and the singleton context should be + // available. In case it is not set in tests or other non-standard Envoy usage, we skip + // counting the deprecated feature usage instead of crashing. + auto* context = Server::Configuration::ServerFactoryContextInstance::getExisting(); + if (context != nullptr) { + context->runtime().countDeprecatedFeatureUse(); + } + int subs = absl::StrReplaceAll({{matches[0], new_format}}, &new_header_value); ASSERT(subs > 0); } diff --git a/source/common/router/metadatamatchcriteria_impl.cc b/source/common/router/metadatamatchcriteria_impl.cc index 36fb3dcce3f12..129f2b899fd75 100644 --- a/source/common/router/metadatamatchcriteria_impl.cc +++ b/source/common/router/metadatamatchcriteria_impl.cc @@ -4,7 +4,7 @@ namespace Envoy { namespace Router { std::vector MetadataMatchCriteriaImpl::extractMetadataMatchCriteria(const MetadataMatchCriteriaImpl* parent, - const ProtobufWkt::Struct& matches) { + const Protobuf::Struct& matches) { std::vector v; // Track locations of each name (from the parent) in v to make it diff --git a/source/common/router/metadatamatchcriteria_impl.h b/source/common/router/metadatamatchcriteria_impl.h index e83e52e507d75..cbb120c0107e5 100644 --- a/source/common/router/metadatamatchcriteria_impl.h +++ b/source/common/router/metadatamatchcriteria_impl.h @@ -10,7 +10,7 @@ using MetadataMatchCriteriaImplConstPtr = std::unique_ptr extractMetadataMatchCriteria(const MetadataMatchCriteriaImpl* parent, - const ProtobufWkt::Struct& metadata_matches); + const Protobuf::Struct& metadata_matches); const std::vector metadata_match_criteria_; }; diff --git a/source/common/router/per_filter_config.cc b/source/common/router/per_filter_config.cc index ccbd6e2461e84..3229cda56a073 100644 --- a/source/common/router/per_filter_config.cc +++ b/source/common/router/per_filter_config.cc @@ -8,7 +8,7 @@ namespace Envoy { namespace Router { absl::StatusOr> -PerFilterConfigs::create(const Protobuf::Map& typed_configs, +PerFilterConfigs::create(const Protobuf::Map& typed_configs, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator) { absl::Status creation_status = absl::OkStatus(); @@ -20,7 +20,7 @@ PerFilterConfigs::create(const Protobuf::Map& typ absl::StatusOr PerFilterConfigs::createRouteSpecificFilterConfig( - const std::string& name, const ProtobufWkt::Any& typed_config, bool is_optional, + const std::string& name, const Protobuf::Any& typed_config, bool is_optional, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator) { Server::Configuration::NamedHttpFilterConfigFactory* factory = @@ -61,10 +61,10 @@ PerFilterConfigs::createRouteSpecificFilterConfig( return object; } -PerFilterConfigs::PerFilterConfigs( - const Protobuf::Map& typed_configs, - Server::Configuration::ServerFactoryContext& factory_context, - ProtobufMessage::ValidationVisitor& validator, absl::Status& creation_status) { +PerFilterConfigs::PerFilterConfigs(const Protobuf::Map& typed_configs, + Server::Configuration::ServerFactoryContext& factory_context, + ProtobufMessage::ValidationVisitor& validator, + absl::Status& creation_status) { std::string filter_config_type( envoy::config::route::v3::FilterConfig::default_instance().GetTypeName()); diff --git a/source/common/router/per_filter_config.h b/source/common/router/per_filter_config.h index 22967dd28b7fd..8bf515dfa4479 100644 --- a/source/common/router/per_filter_config.h +++ b/source/common/router/per_filter_config.h @@ -10,7 +10,7 @@ namespace Router { class PerFilterConfigs : public Logger::Loggable { public: static absl::StatusOr> - create(const Protobuf::Map& typed_configs, + create(const Protobuf::Map& typed_configs, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator); @@ -29,12 +29,12 @@ class PerFilterConfigs : public Logger::Loggable { absl::optional disabled(absl::string_view name) const; private: - PerFilterConfigs(const Protobuf::Map& typed_configs, + PerFilterConfigs(const Protobuf::Map& typed_configs, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator, absl::Status& creation_status); absl::StatusOr - createRouteSpecificFilterConfig(const std::string& name, const ProtobufWkt::Any& typed_config, + createRouteSpecificFilterConfig(const std::string& name, const Protobuf::Any& typed_config, bool is_optional, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator); diff --git a/source/common/router/rds_impl.h b/source/common/router/rds_impl.h index 0ffca155a3a94..cdfbfc1bd657f 100644 --- a/source/common/router/rds_impl.h +++ b/source/common/router/rds_impl.h @@ -94,7 +94,7 @@ class RdsRouteConfigSubscription : public Rds::RdsRouteConfigSubscription { VhdsSubscriptionPtr vhds_subscription_; RouteConfigUpdatePtr config_update_info_; - Common::CallbackManager<> update_callback_manager_; + Common::CallbackManager update_callback_manager_; // Access to addUpdateCallback friend class ScopedRdsConfigSubscription; diff --git a/source/common/router/retry_policy_impl.cc b/source/common/router/retry_policy_impl.cc new file mode 100644 index 0000000000000..2fd80ecd56dda --- /dev/null +++ b/source/common/router/retry_policy_impl.cc @@ -0,0 +1,138 @@ +#include "source/common/router/retry_policy_impl.h" + +#include + +#include "source/common/config/utility.h" +#include "source/common/router/reset_header_parser.h" +#include "source/common/router/retry_state_impl.h" +#include "source/common/upstream/retry_factory.h" + +namespace Envoy { +namespace Router { + +absl::StatusOr> +RetryPolicyImpl::create(const envoy::config::route::v3::RetryPolicy& retry_policy, + ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::CommonFactoryContext& common_context) { + absl::Status creation_status = absl::OkStatus(); + auto ret = std::shared_ptr( + new RetryPolicyImpl(retry_policy, validation_visitor, common_context, creation_status)); + RETURN_IF_NOT_OK(creation_status); + return ret; +} + +RetryPolicyConstSharedPtr RetryPolicyImpl::DefaultRetryPolicy = std::make_shared(); + +RetryPolicyImpl::RetryPolicyImpl(const envoy::config::route::v3::RetryPolicy& retry_policy, + ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::CommonFactoryContext& common_context, + absl::Status& creation_status) + : retriable_headers_(Http::HeaderUtility::buildHeaderMatcherVector( + retry_policy.retriable_headers(), common_context)), + retriable_request_headers_(Http::HeaderUtility::buildHeaderMatcherVector( + retry_policy.retriable_request_headers(), common_context)), + validation_visitor_(&validation_visitor) { + per_try_timeout_ = + std::chrono::milliseconds(PROTOBUF_GET_MS_OR_DEFAULT(retry_policy, per_try_timeout, 0)); + per_try_idle_timeout_ = + std::chrono::milliseconds(PROTOBUF_GET_MS_OR_DEFAULT(retry_policy, per_try_idle_timeout, 0)); + num_retries_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(retry_policy, num_retries, 1); + retry_on_ = RetryStateImpl::parseRetryOn(retry_policy.retry_on()).first; + retry_on_ |= RetryStateImpl::parseRetryGrpcOn(retry_policy.retry_on()).first; + + for (const auto& host_predicate : retry_policy.retry_host_predicate()) { + auto& factory = Envoy::Config::Utility::getAndCheckFactory( + host_predicate); + auto config = Envoy::Config::Utility::translateToFactoryConfig(host_predicate, + validation_visitor, factory); + retry_host_predicate_configs_.emplace_back(factory, std::move(config)); + } + + const auto& retry_priority = retry_policy.retry_priority(); + if (!retry_priority.name().empty()) { + auto& factory = + Envoy::Config::Utility::getAndCheckFactory(retry_priority); + retry_priority_config_ = + std::make_pair(&factory, Envoy::Config::Utility::translateToFactoryConfig( + retry_priority, validation_visitor, factory)); + } + + Upstream::RetryExtensionFactoryContextImpl factory_context(common_context.singletonManager()); + for (const auto& options_predicate : retry_policy.retry_options_predicates()) { + auto& factory = + Envoy::Config::Utility::getAndCheckFactory( + options_predicate); + retry_options_predicates_.emplace_back( + factory.createOptionsPredicate(*Envoy::Config::Utility::translateToFactoryConfig( + options_predicate, validation_visitor, factory), + factory_context)); + } + + auto host_selection_attempts = retry_policy.host_selection_retry_max_attempts(); + if (host_selection_attempts) { + host_selection_attempts_ = host_selection_attempts; + } + + for (auto code : retry_policy.retriable_status_codes()) { + retriable_status_codes_.emplace_back(code); + } + + if (retry_policy.has_retry_back_off()) { + base_interval_ = std::chrono::milliseconds( + PROTOBUF_GET_MS_REQUIRED(retry_policy.retry_back_off(), base_interval)); + if ((*base_interval_).count() < 1) { + base_interval_ = std::chrono::milliseconds(1); + } + + max_interval_ = PROTOBUF_GET_OPTIONAL_MS(retry_policy.retry_back_off(), max_interval); + if (max_interval_) { + // Apply the same rounding to max interval in case both are set to sub-millisecond values. + if ((*max_interval_).count() < 1) { + max_interval_ = std::chrono::milliseconds(1); + } + + if ((*max_interval_).count() < (*base_interval_).count()) { + creation_status = absl::InvalidArgumentError( + "retry_policy.max_interval must greater than or equal to the base_interval"); + return; + } + } + } + + if (retry_policy.has_rate_limited_retry_back_off()) { + reset_headers_ = ResetHeaderParserImpl::buildResetHeaderParserVector( + retry_policy.rate_limited_retry_back_off().reset_headers()); + + absl::optional reset_max_interval = + PROTOBUF_GET_OPTIONAL_MS(retry_policy.rate_limited_retry_back_off(), max_interval); + if (reset_max_interval.has_value()) { + std::chrono::milliseconds max_interval = reset_max_interval.value(); + if (max_interval.count() < 1) { + max_interval = std::chrono::milliseconds(1); + } + reset_max_interval_ = max_interval; + } + } +} + +std::vector RetryPolicyImpl::retryHostPredicates() const { + std::vector predicates; + predicates.reserve(retry_host_predicate_configs_.size()); + for (const auto& config : retry_host_predicate_configs_) { + predicates.emplace_back(config.first.createHostPredicate(*config.second, num_retries_)); + } + + return predicates; +} + +Upstream::RetryPrioritySharedPtr RetryPolicyImpl::retryPriority() const { + if (retry_priority_config_.first == nullptr) { + return nullptr; + } + + return retry_priority_config_.first->createRetryPriority(*retry_priority_config_.second, + *validation_visitor_, num_retries_); +} + +} // namespace Router +} // namespace Envoy diff --git a/source/common/router/retry_policy_impl.h b/source/common/router/retry_policy_impl.h new file mode 100644 index 0000000000000..3acdfdda38b6a --- /dev/null +++ b/source/common/router/retry_policy_impl.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/config/route/v3/route_components.pb.validate.h" +#include "envoy/router/router.h" +#include "envoy/server/factory_context.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Router { + +/** + * Implementation of RetryPolicy that reads from the proto route or virtual host config. + */ +class RetryPolicyImpl : public RetryPolicy { + +public: + static RetryPolicyConstSharedPtr DefaultRetryPolicy; + + static absl::StatusOr> + create(const envoy::config::route::v3::RetryPolicy& retry_policy, + ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::CommonFactoryContext& common_context); + RetryPolicyImpl() = default; + + // Router::RetryPolicy + std::chrono::milliseconds perTryTimeout() const override { return per_try_timeout_; } + std::chrono::milliseconds perTryIdleTimeout() const override { return per_try_idle_timeout_; } + uint32_t numRetries() const override { return num_retries_; } + uint32_t retryOn() const override { return retry_on_; } + std::vector retryHostPredicates() const override; + Upstream::RetryPrioritySharedPtr retryPriority() const override; + absl::Span + retryOptionsPredicates() const override { + return retry_options_predicates_; + } + uint32_t hostSelectionMaxAttempts() const override { return host_selection_attempts_; } + const std::vector& retriableStatusCodes() const override { + return retriable_status_codes_; + } + const std::vector& retriableHeaders() const override { + return retriable_headers_; + } + const std::vector& retriableRequestHeaders() const override { + return retriable_request_headers_; + } + absl::optional baseInterval() const override { return base_interval_; } + absl::optional maxInterval() const override { return max_interval_; } + const std::vector& resetHeaders() const override { + return reset_headers_; + } + std::chrono::milliseconds resetMaxInterval() const override { return reset_max_interval_; } + +private: + RetryPolicyImpl(const envoy::config::route::v3::RetryPolicy& retry_policy, + ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::CommonFactoryContext& common_context, + absl::Status& creation_status); + std::chrono::milliseconds per_try_timeout_{0}; + std::chrono::milliseconds per_try_idle_timeout_{0}; + // We set the number of retries to 1 by default (i.e. when no route or vhost level retry policy is + // set) so that when retries get enabled through the x-envoy-retry-on header we default to 1 + // retry. + uint32_t num_retries_{1}; + uint32_t retry_on_{}; + // Each pair contains the name and config proto to be used to create the RetryHostPredicates + // that should be used when with this policy. + std::vector> + retry_host_predicate_configs_; + // Name and config proto to use to create the RetryPriority to use with this policy. Default + // initialized when no RetryPriority should be used. + std::pair retry_priority_config_; + uint32_t host_selection_attempts_{1}; + std::vector retriable_status_codes_; + std::vector retriable_headers_; + std::vector retriable_request_headers_; + absl::optional base_interval_; + absl::optional max_interval_; + std::vector reset_headers_; + std::chrono::milliseconds reset_max_interval_{300000}; + ProtobufMessage::ValidationVisitor* validation_visitor_{}; + std::vector retry_options_predicates_; +}; + +} // namespace Router +} // namespace Envoy diff --git a/source/common/router/router.cc b/source/common/router/router.cc index a01ff69ee1942..2cfb8484cd011 100644 --- a/source/common/router/router.cc +++ b/source/common/router/router.cc @@ -16,11 +16,10 @@ #include "envoy/upstream/health_check_host_monitor.h" #include "envoy/upstream/upstream.h" +#include "source/common/access_log/access_log_impl.h" #include "source/common/common/assert.h" #include "source/common/common/cleanup.h" -#include "source/common/common/empty_string.h" #include "source/common/common/enum_to_int.h" -#include "source/common/common/scope_tracker.h" #include "source/common/common/utility.h" #include "source/common/config/utility.h" #include "source/common/grpc/common.h" @@ -29,25 +28,21 @@ #include "source/common/http/headers.h" #include "source/common/http/message_impl.h" #include "source/common/http/utility.h" -#include "source/common/network/application_protocol.h" -#include "source/common/network/socket_option_factory.h" #include "source/common/network/transport_socket_options_impl.h" #include "source/common/network/upstream_server_name.h" #include "source/common/network/upstream_socket_options_filter_state.h" #include "source/common/network/upstream_subject_alt_names.h" #include "source/common/orca/orca_load_metrics.h" #include "source/common/orca/orca_parser.h" -#include "source/common/router/config_impl.h" #include "source/common/router/debug_config.h" #include "source/common/router/retry_state_impl.h" #include "source/common/runtime/runtime_features.h" #include "source/common/stream_info/uint32_accessor_impl.h" -#include "source/common/tracing/http_tracer_impl.h" namespace Envoy { namespace Router { namespace { -constexpr char NumInternalRedirectsFilterStateName[] = "num_internal_redirects"; +constexpr absl::string_view NumInternalRedirectsFilterStateName = "num_internal_redirects"; uint32_t getLength(const Buffer::Instance* instance) { return instance ? instance->length() : 0; } @@ -215,8 +210,8 @@ TimeoutData FilterUtility::finalTimeout(const RouteEntry& route, timeout.global_timeout_ = route.timeout(); } } - timeout.per_try_timeout_ = route.retryPolicy().perTryTimeout(); - timeout.per_try_idle_timeout_ = route.retryPolicy().perTryIdleTimeout(); + timeout.per_try_timeout_ = route.retryPolicy()->perTryTimeout(); + timeout.per_try_idle_timeout_ = route.retryPolicy()->perTryIdleTimeout(); uint64_t header_timeout; @@ -455,8 +450,6 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, modify_headers_from_upstream_lb_(headers); } - route_entry_->finalizeResponseHeaders(headers, callbacks_->streamInfo()); - if (attempt_count_ == 0 || !route_entry_->includeAttemptCountInResponse()) { return; } @@ -492,11 +485,11 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, direct_response->rewritePathHeader(headers, !config_->suppress_envoy_headers_); callbacks_->sendLocalReply( direct_response->responseCode(), direct_response->responseBody(), - [this, direct_response, - &request_headers = headers](Http::ResponseHeaderMap& response_headers) -> void { + [this, direct_response](Http::ResponseHeaderMap& response_headers) -> void { std::string new_uri; - if (request_headers.Path()) { - new_uri = direct_response->newUri(request_headers); + ASSERT(downstream_headers_ != nullptr); + if (downstream_headers_->Path()) { + new_uri = direct_response->newUri(*downstream_headers_); } // See https://tools.ietf.org/html/rfc7231#section-7.1.2. const auto add_location = @@ -505,7 +498,10 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, if (!new_uri.empty() && add_location) { response_headers.addReferenceKey(Http::Headers::get().Location, new_uri); } - direct_response->finalizeResponseHeaders(response_headers, callbacks_->streamInfo()); + const Formatter::HttpFormatterContext formatter_context( + downstream_headers_, &response_headers, {}, {}, {}, &callbacks_->activeSpan()); + direct_response->finalizeResponseHeaders(response_headers, formatter_context, + callbacks_->streamInfo()); }, absl::nullopt, StreamInfo::ResponseCodeDetails::get().DirectResponse); return Http::FilterHeadersStatus::StopIteration; @@ -513,10 +509,10 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, // A route entry matches for the request. route_entry_ = route_->routeEntry(); - // If there's a route specific limit and it's smaller than general downstream - // limits, apply the new cap. - retry_shadow_buffer_limit_ = - std::min(retry_shadow_buffer_limit_, route_entry_->retryShadowBufferLimit()); + // Store buffer limits from the route entry. + // The requestBodyBufferLimit() method handles both legacy per_request_buffer_limit_bytes + // and new request_body_buffer_limit configurations automatically. + request_body_buffer_limit_ = route_entry_->requestBodyBufferLimit(); Upstream::ThreadLocalCluster* cluster = config_->cm_.getThreadLocalCluster(route_entry_->clusterName()); if (!cluster) { @@ -590,12 +586,24 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, return Http::FilterHeadersStatus::StopIteration; } + // If large request buffering is enabled and its size is more than current buffer limit, update + // the buffer limit to a new larger value. + uint64_t effective_buffer_limit = calculateEffectiveBufferLimit(); + if (effective_buffer_limit != std::numeric_limits::max() && + effective_buffer_limit > callbacks_->decoderBufferLimit()) { + ENVOY_STREAM_LOG(debug, "Setting new filter manager buffer limit: {}", *callbacks_, + effective_buffer_limit); + callbacks_->setDecoderBufferLimit(effective_buffer_limit); + } + // Increment the attempt count from 0 to 1 at the first upstream request. attempt_count_++; callbacks_->streamInfo().setAttemptCount(attempt_count_); // Finalize the request headers before the host selection. - route_entry_->finalizeRequestHeaders(headers, callbacks_->streamInfo(), + const Formatter::HttpFormatterContext formatter_context(&headers, {}, {}, {}, {}, + &callbacks_->activeSpan()); + route_entry_->finalizeRequestHeaders(headers, formatter_context, callbacks_->streamInfo(), !config_->suppress_envoy_headers_); // Fetch a connection pool for the upstream cluster. @@ -782,7 +790,7 @@ bool Filter::continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster, ASSERT(headers.Scheme()); retry_state_ = createRetryState( - route_entry_->retryPolicy(), headers, *cluster_, request_vcluster_, route_stats_context_, + *route_entry_->retryPolicy(), headers, *cluster_, request_vcluster_, route_stats_context_, config_->factory_context_, callbacks_->dispatcher(), route_entry_->priority()); // Determine which shadow policies to use. It's possible that we don't do any shadowing due to @@ -815,47 +823,52 @@ bool Filter::continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster, allow_multiplexed_upstream_half_close_ /*enable_half_close*/); LinkedList::moveIntoList(std::move(upstream_request), upstream_requests_); upstream_requests_.front()->acceptHeadersFromRouter(end_stream); - if (streaming_shadows_) { - // start the shadow streams. - for (const auto& shadow_policy_wrapper : active_shadow_policies_) { - const auto& shadow_policy = shadow_policy_wrapper.get(); - const absl::optional shadow_cluster_name = - getShadowCluster(shadow_policy, *downstream_headers_); - if (!shadow_cluster_name.has_value()) { - continue; - } - auto shadow_headers = Http::createHeaderMap(*shadow_headers_); - const auto options = - Http::AsyncClient::RequestOptions() - .setTimeout(timeout_.global_timeout_) - .setParentSpan(callbacks_->activeSpan()) - .setChildSpanName("mirror") - .setSampled(shadow_policy.traceSampled()) - .setIsShadow(true) - .setIsShadowSuffixDisabled(shadow_policy.disableShadowHostSuffixAppend()) - .setBufferAccount(callbacks_->account()) - // A buffer limit of 1 is set in the case that retry_shadow_buffer_limit_ == 0, - // because a buffer limit of zero on async clients is interpreted as no buffer limit. - .setBufferLimit(1 > retry_shadow_buffer_limit_ ? 1 : retry_shadow_buffer_limit_) - .setDiscardResponseBody(true) - .setFilterConfig(config_) - .setParentContext(Http::AsyncClient::ParentContext{&callbacks_->streamInfo()}); - if (end_stream) { - // This is a header-only request, and can be dispatched immediately to the shadow - // without waiting. - Http::RequestMessagePtr request(new Http::RequestMessageImpl( - Http::createHeaderMap(*shadow_headers_))); - config_->shadowWriter().shadow(std::string(shadow_cluster_name.value()), std::move(request), - options); - } else { - Http::AsyncClient::OngoingRequest* shadow_stream = config_->shadowWriter().streamingShadow( - std::string(shadow_cluster_name.value()), std::move(shadow_headers), options); - if (shadow_stream != nullptr) { - shadow_streams_.insert(shadow_stream); - shadow_stream->setDestructorCallback( - [this, shadow_stream]() { shadow_streams_.erase(shadow_stream); }); - shadow_stream->setWatermarkCallbacks(watermark_callbacks_); - } + // Start the shadow streams. + for (const auto& shadow_policy_wrapper : active_shadow_policies_) { + const auto& shadow_policy = shadow_policy_wrapper.get(); + const absl::optional shadow_cluster_name = + getShadowCluster(shadow_policy, *downstream_headers_); + if (!shadow_cluster_name.has_value()) { + continue; + } + auto shadow_headers = Http::createHeaderMap(*shadow_headers_); + const auto options = + Http::AsyncClient::RequestOptions() + .setTimeout(timeout_.global_timeout_) + .setParentSpan(callbacks_->activeSpan()) + .setChildSpanName("mirror") + .setSampled(shadow_policy.traceSampled()) + .setIsShadow(true) + .setIsShadowSuffixDisabled(shadow_policy.disableShadowHostSuffixAppend()) + .setBufferAccount(callbacks_->account()) + // Calculate effective buffer limit for shadow streams using the same logic as main + // request. A buffer limit of 1 is set in the case that the effective limit == 0, + // because a buffer limit of zero on async clients is interpreted as no buffer limit. + .setBufferLimit([this]() -> uint32_t { + uint64_t effective_limit = calculateEffectiveBufferLimit(); + // Convert to uint32_t for AsyncClient, clamping to max uint32_t if needed + uint32_t shadow_limit = static_cast(std::min( + effective_limit, static_cast(std::numeric_limits::max()))); + return shadow_limit == 0 ? 1 : shadow_limit; + }()) + .setDiscardResponseBody(true) + .setFilterConfig(config_) + .setParentContext(Http::AsyncClient::ParentContext{&callbacks_->streamInfo()}); + if (end_stream) { + // This is a header-only request, and can be dispatched immediately to the shadow + // without waiting. + Http::RequestMessagePtr request(new Http::RequestMessageImpl( + Http::createHeaderMap(*shadow_headers_))); + config_->shadowWriter().shadow(std::string(shadow_cluster_name.value()), std::move(request), + options); + } else { + Http::AsyncClient::OngoingRequest* shadow_stream = config_->shadowWriter().streamingShadow( + std::string(shadow_cluster_name.value()), std::move(shadow_headers), options); + if (shadow_stream != nullptr) { + shadow_streams_.insert(shadow_stream); + shadow_stream->setDestructorCallback( + [this, shadow_stream]() { shadow_streams_.erase(shadow_stream); }); + shadow_stream->setWatermarkCallbacks(watermark_callbacks_); } } } @@ -922,42 +935,98 @@ void Filter::sendNoHealthyUpstreamResponse(absl::optional optional_ absl::nullopt, details); } +uint64_t Filter::calculateEffectiveBufferLimit() const { + // Use requestBodyBufferLimit() method which handles both legacy and new + // configurations. If no buffer limit is configured, fall back to connection limit. + uint64_t buffer_limit = request_body_buffer_limit_; + + if (buffer_limit != std::numeric_limits::max()) { + return buffer_limit; + } + + // If no route-level buffer limit is set, use the connection buffer limit. + uint32_t current_connection_limit = callbacks_->decoderBufferLimit(); + if (current_connection_limit != 0) { + return static_cast(current_connection_limit); + } + + // If no limits are set at all, return unlimited. + return std::numeric_limits::max(); +} + Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_stream) { // upstream_requests_.size() cannot be > 1 because that only happens when a per // try timeout occurs with hedge_on_per_try_timeout enabled but the per // try timeout timer is not started until onRequestComplete(). It could be zero // if the first request attempt has already failed and a retry is waiting for - // a backoff timer. + // a backoff timer.. ASSERT(upstream_requests_.size() <= 1); - bool buffering = (retry_state_ && retry_state_->enabled()) || - (!active_shadow_policies_.empty() && !streaming_shadows_) || - (route_entry_ && route_entry_->internalRedirectPolicy().enabled()); - if (buffering && - getLength(callbacks_->decodingBuffer()) + data.length() > retry_shadow_buffer_limit_) { + bool retry_enabled = retry_state_ && retry_state_->enabled(); + bool redirect_enabled = route_entry_ && route_entry_->internalRedirectPolicy().enabled(); + bool buffering = retry_enabled || redirect_enabled; + uint64_t effective_buffer_limit = calculateEffectiveBufferLimit(); + + // Check if we would exceed buffer limits, regardless of current buffering state + // This ensures error details are set even if retry state was cleared due to upstream reset. + bool would_exceed_buffer = + (getLength(callbacks_->decodingBuffer()) + data.length() > effective_buffer_limit); + + // Handle retry/shadow buffer overflow, excluding redirect-only scenarios. + // For redirect scenarios, buffer overflow should only affect redirect processing, not initial + // request. + bool had_retry_or_shadow = retry_enabled; + bool is_redirect_only = redirect_enabled && !retry_enabled; + + if (would_exceed_buffer && had_retry_or_shadow && !is_redirect_only && + !request_buffer_overflowed_) { ENVOY_LOG(debug, - "The request payload has at least {} bytes data which exceeds buffer limit {}. Give " - "up on the retry/shadow.", - getLength(callbacks_->decodingBuffer()) + data.length(), retry_shadow_buffer_limit_); + "The request payload has at least {} bytes data which exceeds buffer limit {}. " + "Giving up on buffering.", + getLength(callbacks_->decodingBuffer()) + data.length(), effective_buffer_limit); + cluster_->trafficStats()->retry_or_shadow_abandoned_.inc(); retry_state_.reset(); + ENVOY_LOG(debug, "retry or shadow overflow: retry_state_ reset, buffering set to false"); buffering = false; active_shadow_policies_.clear(); - request_buffer_overflowed_ = true; - // If we had to abandon buffering and there's no request in progress, abort the request and - // clean up. This happens if the initial upstream request failed, and we are currently waiting - // for a backoff timer before starting the next upstream attempt. + // Only send local reply and cleanup if we're in a retry waiting state (no active upstream + // requests). If there are active upstream requests, let the normal upstream failure handling + // take precedence. if (upstream_requests_.empty()) { + request_buffer_overflowed_ = true; + ENVOY_LOG(debug, + "retry or shadow overflow: No upstream requests, resetting and calling cleanup()"); + resetAll(); cleanup(); + callbacks_->streamInfo().setResponseCodeDetails( + StreamInfo::ResponseCodeDetails::get().RequestPayloadExceededRetryBufferLimit); callbacks_->sendLocalReply( Http::Code::InsufficientStorage, "exceeded request buffer limit while retrying upstream", modify_headers_, absl::nullopt, StreamInfo::ResponseCodeDetails::get().RequestPayloadExceededRetryBufferLimit); return Http::FilterDataStatus::StopIterationNoBuffer; + } else { + ENVOY_LOG(debug, "retry or shadow overflow: Upstream requests exist, deferring to normal " + "upstream failure handling"); } } + // Handle redirect-only buffer overflow when retry/shadow is not active. + // For redirect scenarios, buffer overflow should only affect redirect processing, not initial + // request. + if (would_exceed_buffer && is_redirect_only && !request_buffer_overflowed_) { + ENVOY_LOG(debug, + "The request payload has at least {} bytes data which exceeds buffer limit {}. " + "Marking request as buffer overflowed to cancel internal redirects.", + getLength(callbacks_->decodingBuffer()) + data.length(), effective_buffer_limit); + + // Set the flag to cancel internal redirect processing, but allow the request to proceed + // normally. + request_buffer_overflowed_ = true; + } + for (auto* shadow_stream : shadow_streams_) { if (end_stream) { shadow_stream->removeDestructorCallback(); @@ -1048,11 +1117,8 @@ void Filter::setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callb // it, it can latch the current buffer limit and does not need to update the // limit if another filter increases it. // - // The default is "do not limit". If there are configured (non-zero) buffer - // limits, apply them here. - if (callbacks_->decoderBufferLimit() != 0) { - retry_shadow_buffer_limit_ = callbacks_->decoderBufferLimit(); - } + // Store the connection buffer limit for use in the new buffer limit logic. + connection_buffer_limit_ = callbacks_->decoderBufferLimit(); watermark_callbacks_.setDecoderFilterCallbacks(callbacks_); } @@ -1062,6 +1128,7 @@ void Filter::cleanup() { // list as appropriate. ASSERT(upstream_requests_.empty()); + ENVOY_LOG(debug, "Executing cleanup(): resetting retry_state_ and disabling timers"); retry_state_.reset(); if (response_timeout_) { response_timeout_->disableTimer(); @@ -1085,38 +1152,22 @@ absl::optional Filter::getShadowCluster(const ShadowPolicy& p } } -void Filter::maybeDoShadowing() { - for (const auto& shadow_policy_wrapper : active_shadow_policies_) { - const auto& shadow_policy = shadow_policy_wrapper.get(); - - const absl::optional shadow_cluster_name = - getShadowCluster(shadow_policy, *downstream_headers_); - - // The cluster name got from headers is empty. - if (!shadow_cluster_name.has_value()) { - continue; - } +void Filter::setupRouteTimeoutForWebsocketUpgrade() { + // Set up the route timeout early for websocket upgrades, since the upstream request + // will be paused waiting for the upgrade response and we need the timeout active. + if (!response_timeout_ && timeout_.global_timeout_.count() > 0) { + Event::Dispatcher& dispatcher = callbacks_->dispatcher(); + response_timeout_ = dispatcher.createTimer([this]() -> void { onResponseTimeout(); }); + response_timeout_->enableTimer(timeout_.global_timeout_); + } +} - Http::RequestMessagePtr request(new Http::RequestMessageImpl( - Http::createHeaderMap(*shadow_headers_))); - if (callbacks_->decodingBuffer()) { - request->body().add(*callbacks_->decodingBuffer()); - } - if (shadow_trailers_) { - request->trailers(Http::createHeaderMap(*shadow_trailers_)); - } - const auto options = - Http::AsyncClient::RequestOptions() - .setTimeout(timeout_.global_timeout_) - .setParentSpan(callbacks_->activeSpan()) - .setChildSpanName("mirror") - .setSampled(shadow_policy.traceSampled()) - .setIsShadow(true) - .setIsShadowSuffixDisabled(shadow_policy.disableShadowHostSuffixAppend()) - .setFilterConfig(config_) - .setParentContext(Http::AsyncClient::ParentContext{&callbacks_->streamInfo()}); - config_->shadowWriter().shadow(std::string(shadow_cluster_name.value()), std::move(request), - options); +void Filter::disableRouteTimeoutForWebsocketUpgrade() { + // Disable the route timeout after websocket upgrade completes successfully + // to prevent timeout from firing after the upgrade is done. + if (response_timeout_) { + response_timeout_->disableTimer(); + response_timeout_.reset(); } } @@ -1131,11 +1182,7 @@ void Filter::onRequestComplete() { if (!upstream_requests_.empty()) { // Even if we got an immediate reset, we could still shadow, but that is a riskier change and // seems unnecessary right now. - if (!streaming_shadows_) { - maybeDoShadowing(); - } - - if (timeout_.global_timeout_.count() > 0) { + if (timeout_.global_timeout_.count() > 0 && !response_timeout_) { response_timeout_ = dispatcher.createTimer([this]() -> void { onResponseTimeout(); }); response_timeout_->enableTimer(timeout_.global_timeout_); } @@ -1366,6 +1413,14 @@ void Filter::onUpstreamAbort(Http::Code code, StreamInfo::CoreResponseFlag respo // If we have not yet sent anything downstream, send a response with an appropriate status code. // Otherwise just reset the ongoing response. callbacks_->streamInfo().setResponseFlag(response_flags); + + // Check if buffer overflow occurred and override error details accordingly + if (request_buffer_overflowed_) { + code = Http::Code::InsufficientStorage; + body = "exceeded request buffer limit while retrying upstream"; + details = StreamInfo::ResponseCodeDetails::get().RequestPayloadExceededRetryBufferLimit; + } + // This will destroy any created retry timers. cleanup(); // sendLocalReply may instead reset the stream if downstream_response_started_ is true. @@ -1758,6 +1813,9 @@ void Filter::onUpstreamHeaders(uint64_t response_code, Http::ResponseHeaderMapPt // Modify response headers after we have set the final upstream info because we may need to // modify the headers based on the upstream host. + const Formatter::HttpFormatterContext formatter_context(downstream_headers_, headers.get(), {}, + {}, {}, &callbacks_->activeSpan()); + route_entry_->finalizeResponseHeaders(*headers, formatter_context, callbacks_->streamInfo()); modify_headers_(*headers); if (end_stream) { @@ -2059,7 +2117,7 @@ bool Filter::convertRequestHeadersForInternalRedirect( } void Filter::runRetryOptionsPredicates(UpstreamRequest& retriable_request) { - for (const auto& options_predicate : route_entry_->retryPolicy().retryOptionsPredicates()) { + for (const auto& options_predicate : route_entry_->retryPolicy()->retryOptionsPredicates()) { const Upstream::RetryOptionsPredicate::UpdateOptionsParameters parameters{ retriable_request.streamInfo(), upstreamSocketOptions()}; auto ret = options_predicate->updateOptions(parameters); @@ -2301,9 +2359,9 @@ ProdFilter::createRetryState(const RetryPolicy& policy, Http::RequestHeaderMap& context, dispatcher, priority); if (retry_state != nullptr && retry_state->isAutomaticallyConfiguredForHttp3()) { // Since doing retry will make Envoy to buffer the request body, if upstream using HTTP/3 is the - // only reason for doing retry, set the retry shadow buffer limit to 0 so that we don't retry or + // only reason for doing retry, set the buffer limit to 0 so that we don't retry or // buffer safe requests with body which is not common. - setRetryShadowBufferLimit(0); + setRequestBodyBufferLimit(0); } return retry_state; } diff --git a/source/common/router/router.h b/source/common/router/router.h index 61213e4f8d64f..3d2df4063b0b7 100644 --- a/source/common/router/router.h +++ b/source/common/router/router.h @@ -4,45 +4,35 @@ #include #include #include -#include #include #include "envoy/common/random_generator.h" #include "envoy/extensions/filters/http/router/v3/router.pb.h" -#include "envoy/http/codec.h" #include "envoy/http/codes.h" #include "envoy/http/filter.h" #include "envoy/http/filter_factory.h" #include "envoy/http/hash_policy.h" -#include "envoy/http/stateful_session.h" #include "envoy/local_info/local_info.h" #include "envoy/router/router_filter_interface.h" #include "envoy/router/shadow_writer.h" #include "envoy/runtime/runtime.h" #include "envoy/server/factory_context.h" -#include "envoy/server/filter_config.h" #include "envoy/stats/scope.h" #include "envoy/stats/stats_macros.h" #include "envoy/stream_info/stream_info.h" #include "envoy/upstream/cluster_manager.h" -#include "source/common/access_log/access_log_impl.h" -#include "source/common/buffer/watermark_buffer.h" -#include "source/common/common/cleanup.h" #include "source/common/common/hash.h" #include "source/common/common/hex.h" -#include "source/common/common/linked_object.h" #include "source/common/common/logger.h" -#include "source/common/config/utility.h" #include "source/common/config/well_known_names.h" #include "source/common/http/filter_chain_helper.h" #include "source/common/http/sidestream_watermark.h" #include "source/common/http/utility.h" -#include "source/common/router/config_impl.h" #include "source/common/router/context_impl.h" +#include "source/common/router/metadatamatchcriteria_impl.h" #include "source/common/router/upstream_request.h" #include "source/common/stats/symbol_table.h" -#include "source/common/stream_info/stream_info_impl.h" #include "source/common/upstream/load_balancer_context_base.h" #include "source/common/upstream/upstream_factory_context_impl.h" @@ -311,8 +301,7 @@ class Filter : Logger::Loggable, Filter(const FilterConfigSharedPtr& config, FilterStats& stats) : config_(config), stats_(stats), grpc_request_(false), exclude_http_code_stats_(false), downstream_response_started_(false), downstream_end_stream_(false), is_retry_(false), - request_buffer_overflowed_(false), streaming_shadows_(Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.streaming_shadow")), + request_buffer_overflowed_(false), allow_multiplexed_upstream_half_close_(Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.allow_multiplexed_upstream_half_close")), upstream_request_started_(false), orca_load_report_received_(false) {} @@ -494,6 +483,8 @@ class Filter : Logger::Loggable, void onPerTryTimeout(UpstreamRequest& upstream_request) override; void onPerTryIdleTimeout(UpstreamRequest& upstream_request) override; void onStreamMaxDurationReached(UpstreamRequest& upstream_request) override; + void setupRouteTimeoutForWebsocketUpgrade() override; + void disableRouteTimeoutForWebsocketUpgrade() override; Http::StreamDecoderFilterCallbacks* callbacks() override { return callbacks_; } Upstream::ClusterInfoConstSharedPtr cluster() override { return cluster_; } FilterConfig& config() override { return *config_; } @@ -514,11 +505,12 @@ class Filter : Logger::Loggable, bool awaitingHost() { return host_selection_cancelable_ != nullptr; } protected: - void setRetryShadowBufferLimit(uint32_t retry_shadow_buffer_limit) { - ASSERT(retry_shadow_buffer_limit_ > retry_shadow_buffer_limit); - retry_shadow_buffer_limit_ = retry_shadow_buffer_limit; + void setRequestBodyBufferLimit(uint64_t buffer_limit) { + request_body_buffer_limit_ = buffer_limit; } + uint64_t calculateEffectiveBufferLimit() const; + private: friend class UpstreamRequest; @@ -547,8 +539,6 @@ class Filter : Logger::Loggable, UpstreamRequestPtr createUpstreamRequest(); absl::optional getShadowCluster(const ShadowPolicy& shadow_policy, const Http::HeaderMap& headers) const; - - void maybeDoShadowing(); bool maybeRetryReset(Http::StreamResetReason reset_reason, UpstreamRequest& upstream_request, TimeoutRetry is_timeout_retry); uint32_t numRequestsAwaitingHeaders(); @@ -635,7 +625,8 @@ class Filter : Logger::Loggable, absl::flat_hash_set shadow_streams_; // Keep small members (bools and enums) at the end of class, to reduce alignment overhead. - uint32_t retry_shadow_buffer_limit_{std::numeric_limits::max()}; + uint64_t request_body_buffer_limit_{std::numeric_limits::max()}; + uint32_t connection_buffer_limit_{0}; uint32_t attempt_count_{0}; uint32_t pending_retries_{0}; Http::Code timeout_response_code_ = Http::Code::GatewayTimeout; @@ -649,7 +640,6 @@ class Filter : Logger::Loggable, bool include_attempt_count_in_request_ : 1; bool include_timeout_retry_header_in_request_ : 1; bool request_buffer_overflowed_ : 1; - const bool streaming_shadows_ : 1; const bool allow_multiplexed_upstream_half_close_ : 1; bool upstream_request_started_ : 1; // Indicate that ORCA report is received to process it only once in either response headers or diff --git a/source/common/router/router_ratelimit.cc b/source/common/router/router_ratelimit.cc index 2afdd4122e5dd..29fff9b3ec2a7 100644 --- a/source/common/router/router_ratelimit.cc +++ b/source/common/router/router_ratelimit.cc @@ -58,9 +58,9 @@ bool MatchInputRateLimitDescriptor::populateDescriptor(RateLimit::DescriptorEntr bool DynamicMetadataRateLimitOverride::populateOverride( RateLimit::Descriptor& descriptor, const envoy::config::core::v3::Metadata* metadata) const { - const ProtobufWkt::Value& metadata_value = + const Protobuf::Value& metadata_value = Envoy::Config::Metadata::metadataValue(metadata, metadata_key_); - if (metadata_value.kind_case() != ProtobufWkt::Value::kStructValue) { + if (metadata_value.kind_case() != Protobuf::Value::kStructValue) { return false; } @@ -68,9 +68,9 @@ bool DynamicMetadataRateLimitOverride::populateOverride( const auto& limit_it = override_value.find("requests_per_unit"); const auto& unit_it = override_value.find("unit"); if (limit_it != override_value.end() && - limit_it->second.kind_case() == ProtobufWkt::Value::kNumberValue && + limit_it->second.kind_case() == Protobuf::Value::kNumberValue && unit_it != override_value.end() && - unit_it->second.kind_case() == ProtobufWkt::Value::kStringValue) { + unit_it->second.kind_case() == Protobuf::Value::kStringValue) { envoy::type::v3::RateLimitUnit unit; if (envoy::type::v3::RateLimitUnit_Parse(unit_it->second.string_value(), &unit)) { descriptor.limit_.emplace(RateLimit::RateLimitOverride{ diff --git a/source/common/router/scoped_rds.cc b/source/common/router/scoped_rds.cc index c773654308d66..1061fbc4f9191 100644 --- a/source/common/router/scoped_rds.cc +++ b/source/common/router/scoped_rds.cc @@ -394,8 +394,6 @@ absl::Status ScopedRdsConfigSubscription::onConfigUpdate( const std::vector& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& version_info) { - // NOTE: deletes are done before adds/updates. - absl::flat_hash_map to_be_removed_scopes; // Destruction of resume_rds will lift the floodgate for new RDS subscriptions. // Note in the case of partial acceptance, accepted RDS subscriptions should be started // despite of any error. @@ -413,9 +411,7 @@ absl::Status ScopedRdsConfigSubscription::onConfigUpdate( // Pause RDS to not send a burst of RDS requests until we start all the new subscriptions. // In the case that localInitManager is uninitialized, RDS is already paused // either by Server init or LDS init. - if (factory_context_.clusterManager().adsMux()) { - resume_rds = factory_context_.clusterManager().adsMux()->pause(type_url); - } + resume_rds = factory_context_.xdsManager().pause(type_url); // if local init manager is initialized, the parent init manager may have gone away. if (localInitManager().state() == Init::Manager::State::Initialized) { srds_init_mgr = diff --git a/source/common/router/string_accessor_impl.h b/source/common/router/string_accessor_impl.h index d4851e9b1a754..81f0cab4f8f3e 100644 --- a/source/common/router/string_accessor_impl.h +++ b/source/common/router/string_accessor_impl.h @@ -14,7 +14,7 @@ class StringAccessorImpl : public StringAccessor { // FilterState::Object ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(value_); return message; } diff --git a/source/common/router/upstream_codec_filter.cc b/source/common/router/upstream_codec_filter.cc index 6c4dd3c0ecc0c..2d9d2a92a15be 100644 --- a/source/common/router/upstream_codec_filter.cc +++ b/source/common/router/upstream_codec_filter.cc @@ -164,9 +164,20 @@ void UpstreamCodecFilter::CodecBridge::decodeHeaders(Http::ResponseHeaderMapPtr& (protocol.has_value() && protocol.value() != Envoy::Http::Protocol::Http11)) { // handshake is finished and continue the data processing. filter_.callbacks_->upstreamCallbacks()->setPausedForWebsocketUpgrade(false); + // Disable the route timeout since the websocket upgrade completed successfully + filter_.callbacks_->upstreamCallbacks()->disableRouteTimeoutForWebsocketUpgrade(); + // Disable per-try timeouts since the websocket upgrade completed successfully + filter_.callbacks_->upstreamCallbacks()->disablePerTryTimeoutForWebsocketUpgrade(); filter_.callbacks_->continueDecoding(); + } else if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.websocket_allow_4xx_5xx_through_filter_chain") && + status >= 400) { + maybeEndDecode(end_stream); + filter_.callbacks_->encodeHeaders(std::move(headers), end_stream, + StreamInfo::ResponseCodeDetails::get().ViaUpstream); + return; } else { - // Other status, e.g., 426 or 200, indicate a failed handshake, Envoy as a proxy will proxy + // Other status, e.g., 200, indicate a failed handshake, Envoy as a proxy will proxy // back the response header to downstream and then close the request, since WebSocket // just needs headers for handshake per RFC-6455. Note: HTTP/2 200 will be normalized to // 101 before this point in codec and this patch will skip this scenario from the above @@ -233,14 +244,7 @@ void UpstreamCodecFilter::CodecBridge::onResetStream(Http::StreamResetReason rea std::string failure_reason(transport_failure_reason); if (reason == Http::StreamResetReason::LocalReset) { - if (!Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.report_stream_reset_error_code")) { - ASSERT(transport_failure_reason.empty()); - // Use this to communicate to the upstream request to not force-terminate. - failure_reason = "codec_error"; - } else { - failure_reason = absl::StrCat(transport_failure_reason, "|codec_error"); - } + failure_reason = absl::StrCat(transport_failure_reason, "|codec_error"); } filter_.callbacks_->resetStream(reason, failure_reason); } diff --git a/source/common/router/upstream_request.cc b/source/common/router/upstream_request.cc index c81ef69b2a134..8ac147eadc213 100644 --- a/source/common/router/upstream_request.cc +++ b/source/common/router/upstream_request.cc @@ -393,6 +393,16 @@ void UpstreamRequest::acceptHeadersFromRouter(bool end_stream) { // method which is expecting 2xx response. } else if (Http::Utility::isWebSocketUpgradeRequest(*headers)) { paused_for_websocket_ = true; + + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.websocket_enable_timeout_on_upgrade_response")) { + // For websocket upgrades, we need to set up timeouts immediately + // because the upstream request will be paused waiting for the upgrade response. + if (!per_try_timeout_) { + setupPerTryTimeout(); + } + parent_.setupRouteTimeoutForWebsocketUpgrade(); + } } // Kick off creation of the upstream connection immediately upon receiving headers. @@ -646,7 +656,7 @@ void UpstreamRequest::onPoolReady(std::unique_ptr&& upstream, upstream_info.setUpstreamProtocol(protocol.value()); } - if (parent_.downstreamEndStream()) { + if (parent_.downstreamEndStream() && !per_try_timeout_) { setupPerTryTimeout(); } else { create_per_try_timeout_on_request_complete_ = true; @@ -797,12 +807,7 @@ void UpstreamRequestFilterManagerCallbacks::resetStream( // which should force reset the stream, and a codec driven reset, which should // tell the router the stream reset, and let the router make the decision to // send a local reply, or retry the stream. - bool is_codec_error; - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.report_stream_reset_error_code")) { - is_codec_error = absl::StrContains(transport_failure_reason, "codec_error"); - } else { - is_codec_error = transport_failure_reason == "codec_error"; - } + bool is_codec_error = absl::StrContains(transport_failure_reason, "codec_error"); if (reset_reason == Http::StreamResetReason::LocalReset && !is_codec_error) { upstream_request_.parent_.callbacks()->resetStream(); return; @@ -819,5 +824,27 @@ UpstreamRequestFilterManagerCallbacks::http1StreamEncoderOptions() { return upstream_request_.parent_.callbacks()->http1StreamEncoderOptions(); } +void UpstreamRequestFilterManagerCallbacks::disableRouteTimeoutForWebsocketUpgrade() { + upstream_request_.parent_.disableRouteTimeoutForWebsocketUpgrade(); +} + +void UpstreamRequestFilterManagerCallbacks::disablePerTryTimeoutForWebsocketUpgrade() { + // Disable the per-try timeout and idle timeout timers once websocket upgrade succeeds. + // This mirrors the behavior for route timeout disabling in upgrades. + upstream_request_.disablePerTryTimeoutForWebsocketUpgrade(); +} + +void UpstreamRequest::disablePerTryTimeoutForWebsocketUpgrade() { + // Disable and clear per-try timers so they do not fire after websocket upgrade. + if (per_try_timeout_ != nullptr) { + per_try_timeout_->disableTimer(); + per_try_timeout_.reset(); + } + if (per_try_idle_timeout_ != nullptr) { + per_try_idle_timeout_->disableTimer(); + per_try_idle_timeout_.reset(); + } +} + } // namespace Router } // namespace Envoy diff --git a/source/common/router/upstream_request.h b/source/common/router/upstream_request.h index 755e4e2fad0a9..0f2b1a063f94f 100644 --- a/source/common/router/upstream_request.h +++ b/source/common/router/upstream_request.h @@ -143,7 +143,6 @@ class UpstreamRequest : public Logger::Loggable, }; void readEnable(); - void encodeBodyAndTrailers(); // Getters and setters Upstream::HostDescriptionOptConstRef upstreamHost() { @@ -168,6 +167,8 @@ class UpstreamRequest : public Logger::Loggable, // Exposes streamInfo for the upstream stream. StreamInfo::StreamInfo& streamInfo() { return stream_info_; } bool hadUpstream() const { return had_upstream_; } + // Disable per-try timeouts for websocket upgrades after successful handshake + void disablePerTryTimeoutForWebsocketUpgrade(); private: friend class UpstreamFilterManager; @@ -370,6 +371,9 @@ class UpstreamRequestFilterManagerCallbacks : public Http::FilterManagerCallback upstream_request_.paused_for_websocket_ = value; } + void disableRouteTimeoutForWebsocketUpgrade() override; + void disablePerTryTimeoutForWebsocketUpgrade() override; + const Http::ConnectionPool::Instance::StreamOptions& upstreamStreamOptions() const override { return upstream_request_.upstreamStreamOptions(); } diff --git a/source/common/router/weighted_cluster_specifier.cc b/source/common/router/weighted_cluster_specifier.cc index f37ba65854a6a..adedaaf6e15e4 100644 --- a/source/common/router/weighted_cluster_specifier.cc +++ b/source/common/router/weighted_cluster_specifier.cc @@ -130,13 +130,15 @@ class WeightedClusterEntry : public DynamicRouteEntry { } void finalizeRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info, bool insert_envoy_original_path) const override { - requestHeaderParser().evaluateHeaders(headers, stream_info); + requestHeaderParser().evaluateHeaders(headers, context, stream_info); if (!config_->host_rewrite_.empty()) { headers.setHost(config_->host_rewrite_); } - DynamicRouteEntry::finalizeRequestHeaders(headers, stream_info, insert_envoy_original_path); + DynamicRouteEntry::finalizeRequestHeaders(headers, context, stream_info, + insert_envoy_original_path); } Http::HeaderTransforms requestHeaderTransforms(const StreamInfo::StreamInfo& stream_info, bool do_formatting) const override { @@ -146,13 +148,10 @@ class WeightedClusterEntry : public DynamicRouteEntry { return transforms; } void finalizeResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override { - const Http::RequestHeaderMap& request_headers = - stream_info.getRequestHeaders() == nullptr - ? *Http::StaticEmptyHeaders::get().request_headers - : *stream_info.getRequestHeaders(); - responseHeaderParser().evaluateHeaders(headers, {&request_headers, &headers}, stream_info); - DynamicRouteEntry::finalizeResponseHeaders(headers, stream_info); + responseHeaderParser().evaluateHeaders(headers, context, stream_info); + DynamicRouteEntry::finalizeResponseHeaders(headers, context, stream_info); } Http::HeaderTransforms responseHeaderTransforms(const StreamInfo::StreamInfo& stream_info, bool do_formatting) const override { diff --git a/source/common/runtime/BUILD b/source/common/runtime/BUILD index 4ef3f40fe7dfa..7cc617d4cce80 100644 --- a/source/common/runtime/BUILD +++ b/source/common/runtime/BUILD @@ -48,9 +48,8 @@ envoy_cc_library( # the harder it is to runtime-guard without dependency loops. "@com_google_absl//absl/flags:commandlineflag", "@com_google_absl//absl/flags:flag", - "//envoy/http:codec_interface", + "//envoy/http:codec_runtime_overrides", "//envoy/runtime:runtime_interface", - "//source/common/common:hash_lib", "//source/common/singleton:const_singleton", ], ) diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index c1af2c754ac8d..84eae43b26313 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -1,9 +1,12 @@ #include "source/common/runtime/runtime_features.h" -#include "envoy/http/codec.h" +#include "envoy/http/codec_runtime_overrides.h" + +#include "source/common/singleton/const_singleton.h" #include "absl/flags/commandlineflag.h" #include "absl/flags/flag.h" +#include "absl/flags/reflection.h" #include "absl/strings/match.h" #include "absl/strings/str_replace.h" @@ -31,22 +34,15 @@ // If issues are found that require a runtime feature to be disabled, it should be reported // ASAP by filing a bug on github. Overriding non-buggy code is strongly discouraged to avoid the // problem of the bugs being found after the old code path has been removed. -RUNTIME_GUARD(envoy_reloadable_features_allow_alt_svc_for_ips); RUNTIME_GUARD(envoy_reloadable_features_async_host_selection); -RUNTIME_GUARD(envoy_reloadable_features_avoid_dfp_cluster_removal_on_cds_update); +RUNTIME_GUARD(envoy_reloadable_features_decouple_explicit_drain_pools_and_dns_refresh); RUNTIME_GUARD(envoy_reloadable_features_dfp_cluster_resolves_hosts); -RUNTIME_GUARD(envoy_reloadable_features_dfp_fail_on_empty_host_header); RUNTIME_GUARD(envoy_reloadable_features_disallow_quic_client_udp_mmsg); RUNTIME_GUARD(envoy_reloadable_features_enable_cel_regex_precompilation); RUNTIME_GUARD(envoy_reloadable_features_enable_compression_bomb_protection); -RUNTIME_GUARD(envoy_reloadable_features_enable_include_histograms); RUNTIME_GUARD(envoy_reloadable_features_enable_new_query_param_present_match_behavior); -RUNTIME_GUARD(envoy_reloadable_features_enable_udp_proxy_outlier_detection); -RUNTIME_GUARD(envoy_reloadable_features_explicit_internal_address_config); RUNTIME_GUARD(envoy_reloadable_features_ext_proc_fail_close_spurious_resp); -RUNTIME_GUARD(envoy_reloadable_features_filter_chain_aborted_can_not_continue); -RUNTIME_GUARD(envoy_reloadable_features_gcp_authn_use_fixed_url); -RUNTIME_GUARD(envoy_reloadable_features_getaddrinfo_num_retries); +RUNTIME_GUARD(envoy_reloadable_features_generic_proxy_codec_buffer_limit); RUNTIME_GUARD(envoy_reloadable_features_grpc_side_stream_flow_control); RUNTIME_GUARD(envoy_reloadable_features_http1_balsa_allow_cr_or_lf_at_request_start); RUNTIME_GUARD(envoy_reloadable_features_http1_balsa_delay_reset); @@ -54,29 +50,19 @@ RUNTIME_GUARD(envoy_reloadable_features_http1_balsa_disallow_lone_cr_in_chunk_ex RUNTIME_GUARD(envoy_reloadable_features_http2_discard_host_header); RUNTIME_GUARD(envoy_reloadable_features_http2_propagate_reset_events); RUNTIME_GUARD(envoy_reloadable_features_http2_use_oghttp2); -RUNTIME_GUARD(envoy_reloadable_features_http3_happy_eyeballs); RUNTIME_GUARD(envoy_reloadable_features_http3_remove_empty_cookie); -RUNTIME_GUARD(envoy_reloadable_features_http3_remove_empty_trailers); // Delay deprecation and decommission until UHV is enabled. RUNTIME_GUARD(envoy_reloadable_features_http_reject_path_with_fragment); -RUNTIME_GUARD(envoy_reloadable_features_jwt_authn_remove_jwt_from_query_params); -RUNTIME_GUARD(envoy_reloadable_features_jwt_authn_validate_uri); RUNTIME_GUARD(envoy_reloadable_features_jwt_fetcher_use_scheme_from_uri); -RUNTIME_GUARD(envoy_reloadable_features_local_reply_traverses_filter_chain_after_1xx); -RUNTIME_GUARD(envoy_reloadable_features_mmdb_files_reload_enabled); RUNTIME_GUARD(envoy_reloadable_features_no_extension_lookup_by_name); -RUNTIME_GUARD(envoy_reloadable_features_normalize_rds_provider_config); RUNTIME_GUARD(envoy_reloadable_features_oauth2_cleanup_cookies); RUNTIME_GUARD(envoy_reloadable_features_oauth2_encrypt_tokens); -RUNTIME_GUARD(envoy_reloadable_features_oauth2_use_refresh_token); +RUNTIME_GUARD(envoy_reloadable_features_odcds_over_ads_fix); RUNTIME_GUARD(envoy_reloadable_features_original_dst_rely_on_idle_timeout); RUNTIME_GUARD(envoy_reloadable_features_original_src_fix_port_exhaustion); -RUNTIME_GUARD(envoy_reloadable_features_prefer_ipv6_dns_on_macos); -RUNTIME_GUARD(envoy_reloadable_features_prefer_quic_client_udp_gro); RUNTIME_GUARD(envoy_reloadable_features_prefix_map_matcher_resume_after_subtree_miss); -RUNTIME_GUARD(envoy_reloadable_features_proxy_104); -RUNTIME_GUARD(envoy_reloadable_features_proxy_ssl_port); -RUNTIME_GUARD(envoy_reloadable_features_proxy_status_mapping_more_core_response_flags); +RUNTIME_GUARD(envoy_reloadable_features_quic_defer_logging_to_ack_listener); +RUNTIME_GUARD(envoy_reloadable_features_quic_fix_defer_logging_miss_for_half_closed_stream); // Ignore the automated "remove this flag" issue: we should keep this for 1 year. Confirm with // @danzh2010 or @RyanTheOptimist before removing. RUNTIME_GUARD(envoy_reloadable_features_quic_send_server_preferred_address_to_all_clients); @@ -85,27 +71,24 @@ RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_reads_fixed_number_packets RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_socket_use_address_cache_for_read); RUNTIME_GUARD(envoy_reloadable_features_reject_empty_trusted_ca_file); RUNTIME_GUARD(envoy_reloadable_features_report_load_with_rq_issued); -RUNTIME_GUARD(envoy_reloadable_features_report_stream_reset_error_code); RUNTIME_GUARD(envoy_reloadable_features_router_filter_resetall_on_local_reply); -RUNTIME_GUARD(envoy_reloadable_features_shadow_policy_inherit_trace_sampling); +RUNTIME_GUARD(envoy_reloadable_features_safe_http2_options); RUNTIME_GUARD(envoy_reloadable_features_skip_dns_lookup_for_proxied_requests); RUNTIME_GUARD(envoy_reloadable_features_skip_ext_proc_on_local_reply); -RUNTIME_GUARD(envoy_reloadable_features_streaming_shadow); RUNTIME_GUARD(envoy_reloadable_features_tcp_proxy_retry_on_different_event_loop); +RUNTIME_GUARD(envoy_reloadable_features_tcp_proxy_set_idle_timer_immediately_on_new_connection); RUNTIME_GUARD(envoy_reloadable_features_test_feature_true); +RUNTIME_GUARD(envoy_reloadable_features_trace_refresh_after_route_refresh); RUNTIME_GUARD(envoy_reloadable_features_udp_set_do_not_fragment); -RUNTIME_GUARD(envoy_reloadable_features_udp_socket_apply_aggregated_read_limit); RUNTIME_GUARD(envoy_reloadable_features_uhv_allow_malformed_url_encoding); RUNTIME_GUARD(envoy_reloadable_features_uri_template_match_on_asterisk); -RUNTIME_GUARD(envoy_reloadable_features_use_config_in_happy_eyeballs); -RUNTIME_GUARD(envoy_reloadable_features_use_filter_manager_state_for_downstream_end_stream); -RUNTIME_GUARD(envoy_reloadable_features_use_typed_metadata_in_proxy_protocol_listener); RUNTIME_GUARD(envoy_reloadable_features_validate_connect); RUNTIME_GUARD(envoy_reloadable_features_validate_upstream_headers); -RUNTIME_GUARD(envoy_reloadable_features_wait_for_first_byte_before_balsa_msg_done); +RUNTIME_GUARD(envoy_reloadable_features_websocket_allow_4xx_5xx_through_filter_chain); +RUNTIME_GUARD(envoy_reloadable_features_websocket_enable_timeout_on_upgrade_response); RUNTIME_GUARD(envoy_reloadable_features_xds_failover_to_primary_enabled); -RUNTIME_GUARD(envoy_reloadable_features_xds_prevent_resource_copy); -RUNTIME_GUARD(envoy_restart_features_fix_dispatcher_approximate_now); + +RUNTIME_GUARD(envoy_restart_features_move_locality_schedulers_to_lb); RUNTIME_GUARD(envoy_restart_features_raise_file_limits); RUNTIME_GUARD(envoy_restart_features_skip_backing_cluster_check_for_sds); RUNTIME_GUARD(envoy_restart_features_use_eds_cache_for_ads); @@ -134,8 +117,6 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_reject_all); // For more information about Universal Header Validation, please see // https://github.com/envoyproxy/envoy/issues/10646 FALSE_RUNTIME_GUARD(envoy_reloadable_features_enable_universal_header_validator); -// TODO(pksohn): enable after canarying the feature internally without problems. -FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_defer_logging_to_ack_listener); // TODO(alyssar) evaluate and either make this a config knob or remove. FALSE_RUNTIME_GUARD(envoy_reloadable_features_reresolve_null_addresses); // TODO(alyssar) evaluate and either make this a config knob or remove. @@ -152,12 +133,21 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_drain_pools_on_network_change); FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_no_tcp_delay); // Adding runtime flag to use balsa_parser for http_inspector. FALSE_RUNTIME_GUARD(envoy_reloadable_features_http_inspector_use_balsa_parser); +// TODO(danzh) re-enable it when the issue of preferring TCP over v6 rather than QUIC over v4 is +// fixed. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_http3_happy_eyeballs); // TODO(renjietang): Evaluate and make this a config knob or remove. FALSE_RUNTIME_GUARD(envoy_reloadable_features_use_canonical_suffix_for_quic_brokenness); // TODO(abeyad): Evaluate and make this a config knob or remove. FALSE_RUNTIME_GUARD(envoy_reloadable_features_use_canonical_suffix_for_srtt); // TODO(fredyw): Remove after done with debugging. FALSE_RUNTIME_GUARD(envoy_reloadable_features_log_ip_families_on_network_error); +// TODO(abeyad): Flip to true after prod testing. Simple filtering applies to link-local addresses +// only. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_mobile_ipv6_probe_simple_filtering); +// TODO(abeyad): Flip to true after prod testing. Advanced filtering applies to all IP reserved +// range addresses. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_mobile_ipv6_probe_advanced_filtering); // TODO(botengyao): flip to true after canarying the feature internally without problems. FALSE_RUNTIME_GUARD(envoy_reloadable_features_connection_close_through_filter_manager); // TODO(adisuissa): flip to true after all xDS types use the new subscription @@ -177,6 +167,19 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_ext_proc_graceful_grpc_close); FALSE_RUNTIME_GUARD(envoy_reloadable_features_getaddrinfo_no_ai_flags); +// Flag to remove legacy route formatter support in header parser +// Flip to true after two release periods. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_remove_legacy_route_formatter); + +// TODO(grnmeira): +// Enables the new DNS implementation, a merged implementation of +// strict and logical DNS clusters. This new implementation will +// take over the split ones, and will be used as a base for the +// implementation of on-demand DNS. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_enable_new_dns_implementation); +// Force a local reply from upstream envoy for reverse connections. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_reverse_conn_force_local_reply); + // Block of non-boolean flags. Use of int flags is deprecated. Do not add more. ABSL_FLAG(uint64_t, re2_max_program_size_error_level, 100, ""); // NOLINT ABSL_FLAG(uint64_t, re2_max_program_size_warn_level, // NOLINT diff --git a/source/common/runtime/runtime_features.h b/source/common/runtime/runtime_features.h index bc36922f5fc7b..11200a31cb009 100644 --- a/source/common/runtime/runtime_features.h +++ b/source/common/runtime/runtime_features.h @@ -1,14 +1,10 @@ #pragma once -#include +#include #include "envoy/runtime/runtime.h" -#include "source/common/singleton/const_singleton.h" - -#include "absl/container/flat_hash_set.h" -#include "absl/flags/flag.h" -#include "absl/flags/reflection.h" +#include "absl/strings/string_view.h" namespace Envoy { namespace Runtime { diff --git a/source/common/runtime/runtime_impl.cc b/source/common/runtime/runtime_impl.cc index 1c3f09e6480ef..9ccf170365dd1 100644 --- a/source/common/runtime/runtime_impl.cc +++ b/source/common/runtime/runtime_impl.cc @@ -181,6 +181,7 @@ bool SnapshotImpl::featureEnabled(absl::string_view key, "WARNING runtime key '{}': numerator ({}) > denominator ({}), condition always " "evaluates to true", key, percent.numerator(), denominator_value); + return true; } return ProtobufPercentHelper::evaluateFractionalPercent(percent, random_value); @@ -233,7 +234,7 @@ SnapshotImpl::SnapshotImpl(Random::RandomGenerator& generator, RuntimeStats& sta stats.num_keys_.set(values_.size()); } -void parseFractionValue(SnapshotImpl::Entry& entry, const ProtobufWkt::Struct& value) { +void parseFractionValue(SnapshotImpl::Entry& entry, const Protobuf::Struct& value) { envoy::type::v3::FractionalPercent percent; static_assert(envoy::type::v3::FractionalPercent::MILLION == envoy::type::v3::FractionalPercent::DenominatorType_MAX); @@ -285,11 +286,11 @@ bool parseEntryDoubleValue(Envoy::Runtime::Snapshot::Entry& entry) { } void SnapshotImpl::addEntry(Snapshot::EntryMap& values, const std::string& key, - const ProtobufWkt::Value& value, absl::string_view raw_string) { + const Protobuf::Value& value, absl::string_view raw_string) { values.emplace(key, SnapshotImpl::createEntry(value, raw_string)); } -SnapshotImpl::Entry SnapshotImpl::createEntry(const ProtobufWkt::Value& value, +SnapshotImpl::Entry SnapshotImpl::createEntry(const Protobuf::Value& value, absl::string_view raw_string) { Entry entry; entry.raw_string_value_ = value.string_value(); @@ -297,26 +298,26 @@ SnapshotImpl::Entry SnapshotImpl::createEntry(const ProtobufWkt::Value& value, entry.raw_string_value_ = raw_string; } switch (value.kind_case()) { - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: setNumberValue(entry, value.number_value()); if (entry.raw_string_value_.empty()) { entry.raw_string_value_ = absl::StrCat(value.number_value()); } break; - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: entry.bool_value_ = value.bool_value(); if (entry.raw_string_value_.empty()) { // Convert boolean to "true"/"false" entry.raw_string_value_ = value.bool_value() ? "true" : "false"; } break; - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: if (entry.raw_string_value_.empty()) { entry.raw_string_value_ = value.struct_value().DebugString(); } parseFractionValue(entry, value.struct_value()); break; - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: parseEntryDoubleValue(entry); break; default: @@ -421,7 +422,7 @@ absl::Status DiskLayer::walkDirectory(const std::string& path, const std::string return absl::OkStatus(); } -ProtoLayer::ProtoLayer(absl::string_view name, const ProtobufWkt::Struct& proto, +ProtoLayer::ProtoLayer(absl::string_view name, const Protobuf::Struct& proto, absl::Status& creation_status) : OverrideLayerImpl{name} { creation_status = absl::OkStatus(); @@ -433,18 +434,18 @@ ProtoLayer::ProtoLayer(absl::string_view name, const ProtobufWkt::Struct& proto, } } -absl::Status ProtoLayer::walkProtoValue(const ProtobufWkt::Value& v, const std::string& prefix) { +absl::Status ProtoLayer::walkProtoValue(const Protobuf::Value& v, const std::string& prefix) { switch (v.kind_case()) { - case ProtobufWkt::Value::KIND_NOT_SET: - case ProtobufWkt::Value::kListValue: - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::KIND_NOT_SET: + case Protobuf::Value::kListValue: + case Protobuf::Value::kNullValue: return absl::InvalidArgumentError(absl::StrCat("Invalid runtime entry value for ", prefix)); break; - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: SnapshotImpl::addEntry(values_, prefix, v, ""); break; - case ProtobufWkt::Value::kNumberValue: - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kNumberValue: + case Protobuf::Value::kBoolValue: if (hasRuntimePrefix(prefix) && !isRuntimeFeature(prefix) && !isLegacyRuntimeFeature(prefix)) { IS_ENVOY_BUG(absl::StrCat( "Using a removed guard ", prefix, @@ -452,8 +453,8 @@ absl::Status ProtoLayer::walkProtoValue(const ProtobufWkt::Value& v, const std:: } SnapshotImpl::addEntry(values_, prefix, v, ""); break; - case ProtobufWkt::Value::kStructValue: { - const ProtobufWkt::Struct& s = v.struct_value(); + case Protobuf::Value::kStructValue: { + const Protobuf::Struct& s = v.struct_value(); if (s.fields().empty() || s.fields().find("numerator") != s.fields().end() || s.fields().find("denominator") != s.fields().end()) { SnapshotImpl::addEntry(values_, prefix, v, ""); diff --git a/source/common/runtime/runtime_impl.h b/source/common/runtime/runtime_impl.h index 986b6bd6bf3f1..37b43d962bcd3 100644 --- a/source/common/runtime/runtime_impl.h +++ b/source/common/runtime/runtime_impl.h @@ -88,9 +88,9 @@ class SnapshotImpl : public Snapshot, Logger::Loggable { const EntryMap& values() const; - static Entry createEntry(const ProtobufWkt::Value& value, absl::string_view raw_string); + static Entry createEntry(const Protobuf::Value& value, absl::string_view raw_string); static void addEntry(Snapshot::EntryMap& values, const std::string& key, - const ProtobufWkt::Value& value, absl::string_view raw_string = ""); + const Protobuf::Value& value, absl::string_view raw_string = ""); private: const std::vector layers_; @@ -164,11 +164,10 @@ class DiskLayer : public OverrideLayerImpl, Logger::Loggable { public: - ProtoLayer(absl::string_view name, const ProtobufWkt::Struct& proto, - absl::Status& creation_status); + ProtoLayer(absl::string_view name, const Protobuf::Struct& proto, absl::Status& creation_status); private: - absl::Status walkProtoValue(const ProtobufWkt::Value& v, const std::string& prefix); + absl::Status walkProtoValue(const Protobuf::Value& v, const std::string& prefix); }; class LoaderImpl; @@ -201,7 +200,7 @@ struct RtdsSubscription : Envoy::Config::SubscriptionBase; diff --git a/source/common/secret/sds_api.h b/source/common/secret/sds_api.h index a596a03321124..2cd521ab83890 100644 --- a/source/common/secret/sds_api.h +++ b/source/common/secret/sds_api.h @@ -71,7 +71,7 @@ class SdsApi : public Envoy::Config::SubscriptionBase< // Refresh secrets, e.g. re-resolve symlinks in secret paths. virtual void resolveSecret(const FileContentMap& /*files*/) {}; virtual void validateConfig(const envoy::extensions::transport_sockets::tls::v3::Secret&) PURE; - Common::CallbackManager<> update_callback_manager_; + Common::CallbackManager update_callback_manager_; // Config::SubscriptionCallbacks absl::Status onConfigUpdate(const std::vector& resources, @@ -229,6 +229,7 @@ class CertificateValidationContextSdsApi : public SdsApi, CertificateValidationContextPtr resolved_certificate_validation_context_secrets_; // Path based certificates are inlined for future read consistency. Common::CallbackManager< + absl::Status, const envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext&> validation_callback_manager_; }; @@ -282,7 +283,7 @@ class TlsSessionTicketKeysSdsApi : public SdsApi, public TlsSessionTicketKeysCon private: Secret::TlsSessionTicketKeysPtr tls_session_ticket_keys_; Common::CallbackManager< - const envoy::extensions::transport_sockets::tls::v3::TlsSessionTicketKeys&> + absl::Status, const envoy::extensions::transport_sockets::tls::v3::TlsSessionTicketKeys&> validation_callback_manager_; }; @@ -333,7 +334,8 @@ class GenericSecretSdsApi : public SdsApi, public GenericSecretConfigProvider { private: GenericSecretPtr generic_secret_; - Common::CallbackManager + Common::CallbackManager validation_callback_manager_; }; diff --git a/source/common/secret/secret_manager_impl.cc b/source/common/secret/secret_manager_impl.cc index fdd59bbf919d6..a995ab6403c63 100644 --- a/source/common/secret/secret_manager_impl.cc +++ b/source/common/secret/secret_manager_impl.cc @@ -233,7 +233,7 @@ SecretManagerImpl::dumpSecretConfigs(const Matchers::StringMatcher& name_matcher const bool secret_ready = tls_cert != nullptr; envoy::extensions::transport_sockets::tls::v3::Secret secret; secret.set_name(secret_data.resource_name_); - ProtobufWkt::Timestamp last_updated_ts; + Protobuf::Timestamp last_updated_ts; TimestampUtil::systemClockToTimestamp(secret_data.last_updated_, last_updated_ts); secret.set_name(secret_data.resource_name_); if (secret_ready) { @@ -271,7 +271,7 @@ SecretManagerImpl::dumpSecretConfigs(const Matchers::StringMatcher& name_matcher if (!name_matcher.match(secret.name())) { continue; } - ProtobufWkt::Timestamp last_updated_ts; + Protobuf::Timestamp last_updated_ts; envoy::admin::v3::SecretsConfigDump::DynamicSecret* dump_secret; TimestampUtil::systemClockToTimestamp(secret_data.last_updated_, last_updated_ts); if (secret_ready) { @@ -299,7 +299,7 @@ SecretManagerImpl::dumpSecretConfigs(const Matchers::StringMatcher& name_matcher if (!name_matcher.match(secret.name())) { continue; } - ProtobufWkt::Timestamp last_updated_ts; + Protobuf::Timestamp last_updated_ts; TimestampUtil::systemClockToTimestamp(secret_data.last_updated_, last_updated_ts); envoy::admin::v3::SecretsConfigDump::DynamicSecret* dump_secret; if (secret_ready) { @@ -328,7 +328,7 @@ SecretManagerImpl::dumpSecretConfigs(const Matchers::StringMatcher& name_matcher if (!name_matcher.match(secret.name())) { continue; } - ProtobufWkt::Timestamp last_updated_ts; + Protobuf::Timestamp last_updated_ts; TimestampUtil::systemClockToTimestamp(secret_data.last_updated_, last_updated_ts); envoy::admin::v3::SecretsConfigDump::DynamicSecret* dump_secret; if (secret_ready) { diff --git a/source/common/stats/BUILD b/source/common/stats/BUILD index 4e01a6e00729a..a05645dca61d9 100644 --- a/source/common/stats/BUILD +++ b/source/common/stats/BUILD @@ -60,6 +60,7 @@ envoy_cc_library( "//source/common/common:hash_lib", "//source/common/common:matchers_lib", "//source/common/common:utility_lib", + "//source/common/protobuf:utility_lib", "@com_github_openhistogram_libcircllhist//:libcircllhist", "@envoy_api//envoy/config/metrics/v3:pkg_cc_proto", ], diff --git a/source/common/stats/allocator_impl.cc b/source/common/stats/allocator_impl.cc index c8c6905f4f481..ad9c04b0e8099 100644 --- a/source/common/stats/allocator_impl.cc +++ b/source/common/stats/allocator_impl.cc @@ -85,6 +85,7 @@ template class StatsSharedImpl : public MetricImpl // Metric SymbolTable& symbolTable() final { return alloc_.symbolTable(); } bool used() const override { return flags_ & Metric::Flags::Used; } + void markUnused() override { flags_ &= ~Metric::Flags::Used; } bool hidden() const override { return flags_ & Metric::Flags::Hidden; } // RefcountInterface @@ -282,12 +283,12 @@ class TextReadoutImpl : public StatsSharedImpl { // Stats::TextReadout void set(absl::string_view value) override { std::string value_copy(value); - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); value_ = std::move(value_copy); flags_ |= Flags::Used; } std::string value() const override { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return value_; } diff --git a/source/common/stats/histogram_impl.cc b/source/common/stats/histogram_impl.cc index 4f9adc9a459f0..1ef1d02d1b783 100644 --- a/source/common/stats/histogram_impl.cc +++ b/source/common/stats/histogram_impl.cc @@ -4,6 +4,7 @@ #include #include "source/common/common/utility.h" +#include "source/common/protobuf/utility.h" #include "absl/strings/str_join.h" @@ -107,7 +108,10 @@ HistogramSettingsImpl::HistogramSettingsImpl(const envoy::config::metrics::v3::S std::vector buckets{matcher.buckets().begin(), matcher.buckets().end()}; std::sort(buckets.begin(), buckets.end()); configs.emplace_back(Matchers::StringMatcherImpl(matcher.match(), context), - std::move(buckets)); + buckets.empty() + ? absl::nullopt + : absl::make_optional(std::move(buckets)), + PROTOBUF_GET_OPTIONAL_WRAPPED(matcher, bins)); } return configs; @@ -115,13 +119,22 @@ HistogramSettingsImpl::HistogramSettingsImpl(const envoy::config::metrics::v3::S const ConstSupportedBuckets& HistogramSettingsImpl::buckets(absl::string_view stat_name) const { for (const auto& config : configs_) { - if (config.first.match(stat_name)) { - return config.second; + if (config.matcher_.match(stat_name) && config.buckets_.has_value()) { + return config.buckets_.value(); } } return defaultBuckets(); } +absl::optional HistogramSettingsImpl::bins(absl::string_view stat_name) const { + for (const auto& config : configs_) { + if (config.matcher_.match(stat_name) && config.bins_.has_value()) { + return config.bins_; + } + } + return {}; +} + const ConstSupportedBuckets& HistogramSettingsImpl::defaultBuckets() { CONSTRUCT_ON_FIRST_USE(ConstSupportedBuckets, {0.5, 1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, diff --git a/source/common/stats/histogram_impl.h b/source/common/stats/histogram_impl.h index 3b30f1b289df7..26441b09a073d 100644 --- a/source/common/stats/histogram_impl.h +++ b/source/common/stats/histogram_impl.h @@ -25,11 +25,16 @@ class HistogramSettingsImpl : public HistogramSettings { // HistogramSettings const ConstSupportedBuckets& buckets(absl::string_view stat_name) const override; + absl::optional bins(absl::string_view stat_name) const override; static ConstSupportedBuckets& defaultBuckets(); private: - using Config = std::pair; + struct Config { + Matchers::StringMatcherImpl matcher_; + absl::optional buckets_; + absl::optional bins_; + }; const std::vector configs_{}; }; @@ -111,6 +116,7 @@ class HistogramImpl : public HistogramImplHelper { void recordValue(uint64_t value) override { parent_.deliverHistogramToSinks(*this, value); } bool used() const override { return true; } + void markUnused() override {} bool hidden() const override { return false; } SymbolTable& symbolTable() final { return parent_.symbolTable(); } @@ -132,6 +138,7 @@ class NullHistogramImpl : public HistogramImplHelper { ~NullHistogramImpl() override { MetricImpl::clear(symbol_table_); } bool used() const override { return false; } + void markUnused() override {} bool hidden() const override { return false; } SymbolTable& symbolTable() override { return symbol_table_; } diff --git a/source/common/stats/isolated_store_impl.cc b/source/common/stats/isolated_store_impl.cc index 9b4097971eef5..d070a996a1f39 100644 --- a/source/common/stats/isolated_store_impl.cc +++ b/source/common/stats/isolated_store_impl.cc @@ -63,12 +63,12 @@ ConstScopeSharedPtr IsolatedStoreImpl::constRootScope() const { IsolatedStoreImpl::~IsolatedStoreImpl() = default; -ScopeSharedPtr IsolatedScopeImpl::createScope(const std::string& name) { +ScopeSharedPtr IsolatedScopeImpl::createScope(const std::string& name, bool) { StatNameManagedStorage stat_name_storage(Utility::sanitizeStatsName(name), symbolTable()); - return scopeFromStatName(stat_name_storage.statName()); + return scopeFromStatName(stat_name_storage.statName(), false); } -ScopeSharedPtr IsolatedScopeImpl::scopeFromStatName(StatName name) { +ScopeSharedPtr IsolatedScopeImpl::scopeFromStatName(StatName name, bool) { SymbolTable::StoragePtr prefix_name_storage = symbolTable().join({prefix(), name}); ScopeSharedPtr scope = store_.makeScope(StatName(prefix_name_storage.get())); addScopeToStore(scope); diff --git a/source/common/stats/isolated_store_impl.h b/source/common/stats/isolated_store_impl.h index 84ea935102b98..76c3d364bf77b 100644 --- a/source/common/stats/isolated_store_impl.h +++ b/source/common/stats/isolated_store_impl.h @@ -203,6 +203,10 @@ class IsolatedStoreImpl : public Store { } } + void evictUnused() override { + // Do nothing. Eviction is only supported on the thread local stores. + } + void forEachSinkedCounter(SizeFn f_size, StatFn f_stat) const override { forEachCounter(f_size, f_stat); } @@ -295,8 +299,8 @@ class IsolatedScopeImpl : public Scope { StatNameTagVectorOptConstRef tags) override { return store_.counters_.get(prefix(), name, tags, symbolTable()); } - ScopeSharedPtr createScope(const std::string& name) override; - ScopeSharedPtr scopeFromStatName(StatName name) override; + ScopeSharedPtr createScope(const std::string& name, bool evictable) override; + ScopeSharedPtr scopeFromStatName(StatName name, bool evictable) override; Gauge& gaugeFromStatNameWithTags(const StatName& name, StatNameTagVectorOptConstRef tags, Gauge::ImportMode import_mode) override { Gauge& gauge = store_.gauges_.get(prefix(), name, tags, symbolTable(), import_mode); diff --git a/source/common/stats/null_counter.h b/source/common/stats/null_counter.h index ca576310f0d79..f8b755c58c57d 100644 --- a/source/common/stats/null_counter.h +++ b/source/common/stats/null_counter.h @@ -31,6 +31,7 @@ class NullCounterImpl : public MetricImpl { // Metric bool used() const override { return false; } + void markUnused() override {} bool hidden() const override { return false; } SymbolTable& symbolTable() override { return symbol_table_; } diff --git a/source/common/stats/null_gauge.h b/source/common/stats/null_gauge.h index 5af09aa5999ee..642950de18d88 100644 --- a/source/common/stats/null_gauge.h +++ b/source/common/stats/null_gauge.h @@ -35,6 +35,7 @@ class NullGaugeImpl : public MetricImpl { // Metric bool used() const override { return false; } + void markUnused() override {} bool hidden() const override { return false; } SymbolTable& symbolTable() override { return symbol_table_; } diff --git a/source/common/stats/null_text_readout.h b/source/common/stats/null_text_readout.h index 3073fa8182b48..40a155b7ee66c 100644 --- a/source/common/stats/null_text_readout.h +++ b/source/common/stats/null_text_readout.h @@ -28,6 +28,7 @@ class NullTextReadoutImpl : public MetricImpl { // Metric bool used() const override { return false; } + void markUnused() override {} bool hidden() const override { return false; } SymbolTable& symbolTable() override { return symbol_table_; } diff --git a/source/common/stats/symbol_table.cc b/source/common/stats/symbol_table.cc index 354e65e790332..0f635b39bf1ba 100644 --- a/source/common/stats/symbol_table.cc +++ b/source/common/stats/symbol_table.cc @@ -757,7 +757,7 @@ StatNameSet::StatNameSet(SymbolTable& symbol_table, absl::string_view name) void StatNameSet::rememberBuiltin(absl::string_view str) { StatName stat_name; { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); stat_name = pool_.add(str); } builtin_stat_names_[str] = stat_name; diff --git a/source/common/stats/thread_local_store.cc b/source/common/stats/thread_local_store.cc index 175d7d473e3cb..d2a419999e7bf 100644 --- a/source/common/stats/thread_local_store.cc +++ b/source/common/stats/thread_local_store.cc @@ -38,7 +38,7 @@ ThreadLocalStoreImpl::ThreadLocalStoreImpl(Allocator& alloc) well_known_tags_->rememberBuiltin(desc.name_); } StatNameManagedStorage empty("", alloc.symbolTable()); - auto new_scope = std::make_shared(*this, StatName(empty.statName())); + auto new_scope = std::make_shared(*this, StatName(empty.statName()), false); addScope(new_scope); default_scope_ = new_scope; } @@ -154,14 +154,15 @@ std::vector ThreadLocalStoreImpl::counters() const { return ret; } -ScopeSharedPtr ThreadLocalStoreImpl::ScopeImpl::createScope(const std::string& name) { +ScopeSharedPtr ThreadLocalStoreImpl::ScopeImpl::createScope(const std::string& name, + bool evictable) { StatNameManagedStorage stat_name_storage(Utility::sanitizeStatsName(name), symbolTable()); - return scopeFromStatName(stat_name_storage.statName()); + return scopeFromStatName(stat_name_storage.statName(), evictable); } -ScopeSharedPtr ThreadLocalStoreImpl::ScopeImpl::scopeFromStatName(StatName name) { +ScopeSharedPtr ThreadLocalStoreImpl::ScopeImpl::scopeFromStatName(StatName name, bool evictable) { SymbolTable::StoragePtr joined = symbolTable().join({prefix_.statName(), name}); - auto new_scope = std::make_shared(parent_, StatName(joined.get())); + auto new_scope = std::make_shared(parent_, StatName(joined.get()), evictable); parent_.addScope(new_scope); return new_scope; } @@ -394,8 +395,9 @@ void ThreadLocalStoreImpl::clearHistogramsFromCaches() { } } -ThreadLocalStoreImpl::ScopeImpl::ScopeImpl(ThreadLocalStoreImpl& parent, StatName prefix) - : scope_id_(parent.next_scope_id_++), parent_(parent), +ThreadLocalStoreImpl::ScopeImpl::ScopeImpl(ThreadLocalStoreImpl& parent, StatName prefix, + bool evictable) + : scope_id_(parent.next_scope_id_++), parent_(parent), evictable_(evictable), prefix_(prefix, parent.alloc_.symbolTable()), central_cache_(new CentralCacheEntry(parent.alloc_.symbolTable())) {} @@ -673,7 +675,9 @@ Histogram& ThreadLocalStoreImpl::ScopeImpl::histogramFromStatNameWithTags( StatNameTagHelper tag_helper(parent_, joiner.tagExtractedName(), stat_name_tags); ConstSupportedBuckets* buckets = nullptr; - buckets = &parent_.histogram_settings_->buckets(symbolTable().toString(final_stat_name)); + const auto string_stat_name = symbolTable().toString(final_stat_name); + buckets = &parent_.histogram_settings_->buckets(string_stat_name); + const auto bins = parent_.histogram_settings_->bins(string_stat_name); RefcountPtr stat; { @@ -684,7 +688,7 @@ Histogram& ThreadLocalStoreImpl::ScopeImpl::histogramFromStatNameWithTags( } else { stat = new ParentHistogramImpl(final_stat_name, unit, parent_, tag_helper.tagExtractedName(), tag_helper.statNameTags(), - *buckets, parent_.next_histogram_id_++); + *buckets, bins, parent_.next_histogram_id_++); if (!parent_.shutting_down_) { parent_.histogram_set_.insert(stat.get()); if (parent_.sink_predicates_.has_value() && @@ -791,7 +795,7 @@ Histogram& ThreadLocalStoreImpl::tlsHistogram(ParentHistogramImpl& parent, uint6 TlsHistogramSharedPtr hist_tls_ptr( new ThreadLocalHistogramImpl(parent.statName(), parent.unit(), tag_helper.tagExtractedName(), - tag_helper.statNameTags(), symbolTable())); + tag_helper.statNameTags(), symbolTable(), parent.bins())); parent.addTlsHistogram(hist_tls_ptr); @@ -805,11 +809,12 @@ Histogram& ThreadLocalStoreImpl::tlsHistogram(ParentHistogramImpl& parent, uint6 ThreadLocalHistogramImpl::ThreadLocalHistogramImpl(StatName name, Histogram::Unit unit, StatName tag_extracted_name, const StatNameTagVector& stat_name_tags, - SymbolTable& symbol_table) + SymbolTable& symbol_table, + absl::optional bins) : HistogramImplHelper(name, tag_extracted_name, stat_name_tags, symbol_table), unit_(unit), used_(false), created_thread_id_(std::this_thread::get_id()), symbol_table_(symbol_table) { - histograms_[0] = hist_alloc(); - histograms_[1] = hist_alloc(); + histograms_[0] = bins ? hist_alloc_nbins(bins.value()) : hist_alloc(); + histograms_[1] = bins ? hist_alloc_nbins(bins.value()) : hist_alloc(); } ThreadLocalHistogramImpl::~ThreadLocalHistogramImpl() { @@ -834,10 +839,11 @@ ParentHistogramImpl::ParentHistogramImpl(StatName name, Histogram::Unit unit, ThreadLocalStoreImpl& thread_local_store, StatName tag_extracted_name, const StatNameTagVector& stat_name_tags, - ConstSupportedBuckets& supported_buckets, uint64_t id) + ConstSupportedBuckets& supported_buckets, + absl::optional bins, uint64_t id) : MetricImpl(name, tag_extracted_name, stat_name_tags, thread_local_store.symbolTable()), - unit_(unit), thread_local_store_(thread_local_store), interval_histogram_(hist_alloc()), - cumulative_histogram_(hist_alloc()), + unit_(unit), bins_(bins), thread_local_store_(thread_local_store), + interval_histogram_(hist_alloc()), cumulative_histogram_(hist_alloc()), interval_statistics_(interval_histogram_, unit, supported_buckets), cumulative_statistics_(cumulative_histogram_, unit, supported_buckets), id_(id) {} @@ -910,6 +916,14 @@ bool ParentHistogramImpl::used() const { return merged_; } +void ParentHistogramImpl::markUnused() { + merged_ = false; + Thread::LockGuard lock(merge_lock_); + for (const TlsHistogramSharedPtr& tls_histogram : tls_histograms_) { + tls_histogram->markUnused(); + } +} + bool ParentHistogramImpl::hidden() const { return false; } void ParentHistogramImpl::merge() { @@ -1030,6 +1044,109 @@ void ThreadLocalStoreImpl::forEachScope(std::function f_size, } } +namespace { +struct MetricBag { + explicit MetricBag(uint64_t scope_id) : scope_id_(scope_id) {} + const uint64_t scope_id_; + StatNameHashMap counters_; + StatNameHashMap gauges_; + StatNameHashMap histograms_; + StatNameHashMap text_readouts_; + bool empty() const { + return counters_.empty() && gauges_.empty() && histograms_.empty() && text_readouts_.empty(); + } +}; + +} // namespace + +void ThreadLocalStoreImpl::evictUnused() { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + + // If we are shutting down, we no longer perform eviction as workers may be shutting down + // and not able to complete their work. + if (shutting_down_ || !tls_cache_) { + return; + } + + auto evicted_metrics = std::make_shared>(); + { + Thread::LockGuard lock(lock_); + iterateScopesLockHeld([evicted_metrics](const ScopeImplSharedPtr& scope) -> bool { + if (scope->evictable_) { + MetricBag metrics(scope->scope_id_); + CentralCacheEntrySharedPtr& central_cache = scope->centralCacheMutableNoThreadAnalysis(); + auto filter_unused = [](StatNameHashMap& unused_metrics) { + return [&unused_metrics](std::pair kv) { + const auto& [name, metric] = kv; + if (metric->used()) { + metric->markUnused(); + return false; + } else { + unused_metrics.try_emplace(name, metric); + return true; + } + }; + }; + absl::erase_if(central_cache->counters_, filter_unused(metrics.counters_)); + absl::erase_if(central_cache->gauges_, filter_unused(metrics.gauges_)); + absl::erase_if(central_cache->text_readouts_, filter_unused(metrics.text_readouts_)); + absl::erase_if(central_cache->histograms_, filter_unused(metrics.histograms_)); + if (!metrics.empty()) { + evicted_metrics->push_back(std::move(metrics)); + } + } + return true; + }); + } + + // At this point, central caches no longer return the evicted stats, but we + // need to keep the storage for the evicted stats until after the thread + // local caches are cleared. + if (!evicted_metrics->empty()) { + tls_cache_->runOnAllThreads( + [evicted_metrics](OptRef tls_cache) { + for (const auto& metrics : *evicted_metrics) { + TlsCacheEntry& entry = tls_cache->insertScope(metrics.scope_id_); + absl::erase_if(entry.counters_, + [&](std::pair> kv) { + return metrics.counters_.contains(kv.first); + }); + absl::erase_if(entry.gauges_, + [&](std::pair> kv) { + return metrics.gauges_.contains(kv.first); + }); + absl::erase_if(entry.text_readouts_, + [&](std::pair> kv) { + return metrics.text_readouts_.contains(kv.first); + }); + absl::erase_if(entry.parent_histograms_, + [&](std::pair kv) { + return metrics.histograms_.contains(kv.first); + }); + } + }, + [evicted_metrics]() { + // We want to delete stale stats on the main thread since stat + // destructors lock the stats allocator. Note that we might have + // received fresh values on the stale cache-local stats after deleting them from the + // central cache.. Eventually, we might also want to defer the deletion further in the + // allocator until the values are flushed to the sinks. + size_t scopes = 0, counters = 0, gauges = 0, readouts = 0, histograms = 0; + for (const auto& metrics : *evicted_metrics) { + scopes += 1; + counters += metrics.counters_.size(); + gauges += metrics.gauges_.size(); + readouts += metrics.text_readouts_.size(); + histograms += metrics.histograms_.size(); + } + ENVOY_LOG(debug, + "deleted stale {} counters, {} gauges, {} text readouts, {} histograms from " + "{} scopes", + counters, gauges, readouts, histograms, scopes); + }); + } +} + bool ThreadLocalStoreImpl::iterateScopesLockHeld( const std::function fn) const ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { @@ -1064,8 +1181,7 @@ void ThreadLocalStoreImpl::forEachSinkedTextReadout(SizeFn f_size, void ThreadLocalStoreImpl::forEachSinkedHistogram(SizeFn f_size, StatFn f_stat) const { - if (sink_predicates_.has_value() && - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_include_histograms")) { + if (sink_predicates_.has_value()) { Thread::LockGuard lock(hist_mutex_); if (f_size != nullptr) { diff --git a/source/common/stats/thread_local_store.h b/source/common/stats/thread_local_store.h index b2d7f54990ea7..58316781d4a11 100644 --- a/source/common/stats/thread_local_store.h +++ b/source/common/stats/thread_local_store.h @@ -34,7 +34,8 @@ namespace Stats { class ThreadLocalHistogramImpl : public HistogramImplHelper { public: ThreadLocalHistogramImpl(StatName name, Histogram::Unit unit, StatName tag_extracted_name, - const StatNameTagVector& stat_name_tags, SymbolTable& symbol_table); + const StatNameTagVector& stat_name_tags, SymbolTable& symbol_table, + absl::optional bins); ~ThreadLocalHistogramImpl() override; void merge(histogram_t* target); @@ -60,15 +61,16 @@ class ThreadLocalHistogramImpl : public HistogramImplHelper { // Stats::Metric SymbolTable& symbolTable() final { return symbol_table_; } bool used() const override { return used_; } + void markUnused() override { used_ = false; } bool hidden() const override { return false; } private: - Histogram::Unit unit_; + const Histogram::Unit unit_; uint64_t otherHistogramIndex() const { return 1 - current_active_; } uint64_t current_active_{0}; histogram_t* histograms_[2]; std::atomic used_; - std::thread::id created_thread_id_; + const std::thread::id created_thread_id_; SymbolTable& symbol_table_; }; @@ -83,7 +85,8 @@ class ParentHistogramImpl : public MetricImpl { public: ParentHistogramImpl(StatName name, Histogram::Unit unit, ThreadLocalStoreImpl& parent, StatName tag_extracted_name, const StatNameTagVector& stat_name_tags, - ConstSupportedBuckets& supported_buckets, uint64_t id); + ConstSupportedBuckets& supported_buckets, absl::optional bins, + uint64_t id); ~ParentHistogramImpl() override; void addTlsHistogram(const TlsHistogramSharedPtr& hist_ptr); @@ -116,6 +119,7 @@ class ParentHistogramImpl : public MetricImpl { // Stats::Metric SymbolTable& symbolTable() override; bool used() const override; + void markUnused() override; bool hidden() const override; // RefcountInterface @@ -126,13 +130,15 @@ class ParentHistogramImpl : public MetricImpl { // Indicates that the ThreadLocalStore is shutting down, so no need to clear its histogram_set_. void setShuttingDown(bool shutting_down) { shutting_down_ = shutting_down; } bool shuttingDown() const { return shutting_down_; } + absl::optional bins() const { return bins_; } private: bool usedLockHeld() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(merge_lock_); static std::vector detailedlBucketsHelper(const histogram_t& histogram); - Histogram::Unit unit_; + const Histogram::Unit unit_; + const absl::optional bins_; ThreadLocalStoreImpl& thread_local_store_; histogram_t* interval_histogram_; histogram_t* cumulative_histogram_; @@ -184,6 +190,8 @@ class ThreadLocalStoreImpl : Logger::Loggable, public StoreRo void forEachHistogram(SizeFn f_size, StatFn f_stat) const override; void forEachScope(SizeFn f_size, StatFn f_stat) const override; + void evictUnused() override; + // Stats::StoreRoot void addSink(Sink& sink) override { timer_sinks_.push_back(sink); } void setTagProducer(TagProducerPtr&& tag_producer) override { @@ -277,7 +285,7 @@ class ThreadLocalStoreImpl : Logger::Loggable, public StoreRo using CentralCacheEntrySharedPtr = RefcountPtr; struct ScopeImpl : public Scope { - ScopeImpl(ThreadLocalStoreImpl& parent, StatName prefix); + ScopeImpl(ThreadLocalStoreImpl& parent, StatName prefix, bool evictable); ~ScopeImpl() override; // Stats::Scope @@ -290,8 +298,8 @@ class ThreadLocalStoreImpl : Logger::Loggable, public StoreRo Histogram::Unit unit) override; TextReadout& textReadoutFromStatNameWithTags(const StatName& name, StatNameTagVectorOptConstRef tags) override; - ScopeSharedPtr createScope(const std::string& name) override; - ScopeSharedPtr scopeFromStatName(StatName name) override; + ScopeSharedPtr createScope(const std::string& name, bool evictale) override; + ScopeSharedPtr scopeFromStatName(StatName name, bool evictable) override; const SymbolTable& constSymbolTable() const final { return parent_.constSymbolTable(); } SymbolTable& symbolTable() final { return parent_.symbolTable(); } @@ -429,6 +437,11 @@ class ThreadLocalStoreImpl : Logger::Loggable, public StoreRo return central_cache_; } + CentralCacheEntrySharedPtr& + centralCacheMutableNoThreadAnalysis() const ABSL_NO_THREAD_SAFETY_ANALYSIS { + return central_cache_; + } + // Returns the central cache, bypassing thread analysis. // // This is used only when passing references to maps held in the central @@ -441,6 +454,7 @@ class ThreadLocalStoreImpl : Logger::Loggable, public StoreRo const uint64_t scope_id_; ThreadLocalStoreImpl& parent_; + const bool evictable_{}; private: StatNameStorage prefix_; diff --git a/source/common/stream_info/bool_accessor_impl.h b/source/common/stream_info/bool_accessor_impl.h index 8de1563fae48e..274a95a4c719b 100644 --- a/source/common/stream_info/bool_accessor_impl.h +++ b/source/common/stream_info/bool_accessor_impl.h @@ -14,7 +14,7 @@ class BoolAccessorImpl : public BoolAccessor { // From FilterState::Object ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(value_); return message; } diff --git a/source/common/stream_info/stream_info_impl.h b/source/common/stream_info/stream_info_impl.h index f83f314a3961b..6b4a538bae80f 100644 --- a/source/common/stream_info/stream_info_impl.h +++ b/source/common/stream_info/stream_info_impl.h @@ -306,11 +306,11 @@ struct StreamInfoImpl : public StreamInfo { envoy::config::core::v3::Metadata& dynamicMetadata() override { return metadata_; }; const envoy::config::core::v3::Metadata& dynamicMetadata() const override { return metadata_; }; - void setDynamicMetadata(const std::string& name, const ProtobufWkt::Struct& value) override { + void setDynamicMetadata(const std::string& name, const Protobuf::Struct& value) override { (*metadata_.mutable_filter_metadata())[name].MergeFrom(value); }; - void setDynamicTypedMetadata(const std::string& name, const ProtobufWkt::Any& value) override { + void setDynamicTypedMetadata(const std::string& name, const Protobuf::Any& value) override { (*metadata_.mutable_typed_filter_metadata())[name].MergeFrom(value); } diff --git a/source/common/stream_info/uint32_accessor_impl.h b/source/common/stream_info/uint32_accessor_impl.h index 7c725302bfcad..0e7c4b2e458d7 100644 --- a/source/common/stream_info/uint32_accessor_impl.h +++ b/source/common/stream_info/uint32_accessor_impl.h @@ -14,7 +14,7 @@ class UInt32AccessorImpl : public UInt32Accessor { // From FilterState::Object ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(value_); return message; } diff --git a/source/common/stream_info/uint64_accessor_impl.h b/source/common/stream_info/uint64_accessor_impl.h index ff14bbe7844c8..c9d724fd04cce 100644 --- a/source/common/stream_info/uint64_accessor_impl.h +++ b/source/common/stream_info/uint64_accessor_impl.h @@ -14,7 +14,7 @@ class UInt64AccessorImpl : public UInt64Accessor { // From FilterState::Object ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(value_); return message; } diff --git a/source/common/stream_info/utility.cc b/source/common/stream_info/utility.cc index 387ed26b0de0b..fd5cce74ce65f 100644 --- a/source/common/stream_info/utility.cc +++ b/source/common/stream_info/utility.cc @@ -445,41 +445,28 @@ ProxyStatusUtils::fromStreamInfo(const StreamInfo& stream_info) { return ProxyStatusError::HttpResponseTimeout; } - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.proxy_status_mapping_more_core_response_flags")) { - if (stream_info.hasResponseFlag(CoreResponseFlag::DurationTimeout)) { - return ProxyStatusError::ConnectionTimeout; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::LocalReset)) { - return ProxyStatusError::ConnectionTimeout; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamRemoteReset)) { - return ProxyStatusError::ConnectionTerminated; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamConnectionFailure)) { - return ProxyStatusError::ConnectionRefused; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UnauthorizedExternalService)) { - return ProxyStatusError::ConnectionRefused; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamConnectionTermination)) { - return ProxyStatusError::ConnectionTerminated; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::OverloadManager)) { - return ProxyStatusError::ConnectionLimitReached; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::DropOverLoad)) { - return ProxyStatusError::ConnectionLimitReached; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::FaultInjected)) { - return ProxyStatusError::HttpRequestError; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::DownstreamConnectionTermination)) { - return ProxyStatusError::ConnectionTerminated; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::DownstreamRemoteReset)) { - return ProxyStatusError::ConnectionTerminated; - } - } else { - if (stream_info.hasResponseFlag(CoreResponseFlag::LocalReset)) { - return ProxyStatusError::ConnectionTimeout; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamRemoteReset)) { - return ProxyStatusError::ConnectionTerminated; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamConnectionFailure)) { - return ProxyStatusError::ConnectionRefused; - } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamConnectionTermination)) { - return ProxyStatusError::ConnectionTerminated; - } + if (stream_info.hasResponseFlag(CoreResponseFlag::DurationTimeout)) { + return ProxyStatusError::ConnectionTimeout; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::LocalReset)) { + return ProxyStatusError::ConnectionTimeout; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamRemoteReset)) { + return ProxyStatusError::ConnectionTerminated; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamConnectionFailure)) { + return ProxyStatusError::ConnectionRefused; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::UnauthorizedExternalService)) { + return ProxyStatusError::ConnectionRefused; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamConnectionTermination)) { + return ProxyStatusError::ConnectionTerminated; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::OverloadManager)) { + return ProxyStatusError::ConnectionLimitReached; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::DropOverLoad)) { + return ProxyStatusError::ConnectionLimitReached; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::FaultInjected)) { + return ProxyStatusError::HttpRequestError; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::DownstreamConnectionTermination)) { + return ProxyStatusError::ConnectionTerminated; + } else if (stream_info.hasResponseFlag(CoreResponseFlag::DownstreamRemoteReset)) { + return ProxyStatusError::ConnectionTerminated; } if (stream_info.hasResponseFlag(CoreResponseFlag::UpstreamOverflow)) { diff --git a/source/common/tcp_proxy/BUILD b/source/common/tcp_proxy/BUILD index 27034f0f8d883..c6b30b08d03d1 100644 --- a/source/common/tcp_proxy/BUILD +++ b/source/common/tcp_proxy/BUILD @@ -18,6 +18,7 @@ envoy_cc_library( ], deps = [ "//envoy/http:header_map_interface", + "//envoy/http:request_id_extension_interface", "//envoy/router:router_ratelimit_interface", "//envoy/tcp:conn_pool_interface", "//envoy/tcp:upstream_interface", @@ -73,6 +74,7 @@ envoy_cc_library( "//source/common/config:well_known_names", "//source/common/formatter:substitution_format_string_lib", "//source/common/http:codec_client_lib", + "//source/common/http:request_id_extension_lib", "//source/common/network:application_protocol_lib", "//source/common/network:cidr_range_lib", "//source/common/network:filter_lib", @@ -90,8 +92,10 @@ envoy_cc_library( "//source/common/stream_info:uint64_accessor_lib", "//source/common/upstream:load_balancer_context_base_lib", "//source/common/upstream:od_cds_api_lib", + "//source/extensions/request_id/uuid:config", "//source/extensions/upstreams/tcp/generic:config", "@envoy_api//envoy/config/accesslog/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/tcp_proxy/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/request_id/uuid/v3:pkg_cc_proto", ], ) diff --git a/source/common/tcp_proxy/tcp_proxy.cc b/source/common/tcp_proxy/tcp_proxy.cc index 656ba58a4f186..42e574e5c4d05 100644 --- a/source/common/tcp_proxy/tcp_proxy.cc +++ b/source/common/tcp_proxy/tcp_proxy.cc @@ -10,6 +10,7 @@ #include "envoy/event/timer.h" #include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.h" #include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.validate.h" +#include "envoy/extensions/request_id/uuid/v3/uuid.pb.h" #include "envoy/registry/registry.h" #include "envoy/stats/scope.h" #include "envoy/stream_info/bool_accessor.h" @@ -26,6 +27,7 @@ #include "source/common/config/metadata.h" #include "source/common/config/utility.h" #include "source/common/config/well_known_names.h" +#include "source/common/http/request_id_extension_impl.h" #include "source/common/network/application_protocol.h" #include "source/common/network/proxy_protocol_filter_state.h" #include "source/common/network/socket_option_factory.h" @@ -635,10 +637,9 @@ bool Filter::maybeTunnel(Upstream::ThreadLocalCluster& cluster) { "envoy.restart_features.upstream_http_filters_with_tcp_proxy")) { // TODO(vikaschoudhary16): Initialize route_ once per cluster. upstream_decoder_filter_callbacks_.route_ = THROW_OR_RETURN_VALUE( - Http::NullRouteImpl::create( - cluster.info()->name(), - *std::unique_ptr{new Router::RetryPolicyImpl()}, - config_->regexEngine()), + Http::NullRouteImpl::create(cluster.info()->name(), + Router::RetryPolicyImpl::DefaultRetryPolicy, + config_->regexEngine()), std::unique_ptr); } Upstream::HostConstSharedPtr host = @@ -788,6 +789,17 @@ TunnelingConfigHelperImpl::TunnelingConfigHelperImpl( hostname_fmt_ = THROW_OR_RETURN_VALUE(Formatter::SubstitutionFormatStringUtils::fromProtoConfig( substitution_format_config, context), Formatter::FormatterPtr); + + // Initialize request ID extension if explicitly configured. + const auto& rid_config = config_message.tunneling_config().request_id_extension(); + if (rid_config.has_typed_config()) { + auto extension_or_error = Http::RequestIDExtensionFactory::fromProto(rid_config, context); + if (!extension_or_error.ok()) { + throw EnvoyException(absl::StrCat("Failed to create request ID extension: ", + extension_or_error.status().ToString())); + } + request_id_extension_ = extension_or_error.value(); + } } std::string TunnelingConfigHelperImpl::host(const StreamInfo::StreamInfo& stream_info) const { @@ -875,6 +887,29 @@ Network::FilterStatus Filter::onNewConnection() { resetAccessLogFlushTimer(); } + idle_timeout_ = config_->idleTimeout(); + if (const auto* per_connection_idle_timeout = + getStreamInfo().filterState()->getDataReadOnly( + PerConnectionIdleTimeoutMs); + per_connection_idle_timeout != nullptr) { + idle_timeout_ = std::chrono::milliseconds(per_connection_idle_timeout->value()); + } + + if (idle_timeout_) { + // The idle_timer_ can be moved to a Drainer, so related callbacks call into + // the UpstreamCallbacks, which has the same lifetime as the timer, and can dispatch + // the call to either TcpProxy or to Drainer, depending on the current state. + idle_timer_ = read_callbacks_->connection().dispatcher().createTimer( + [upstream_callbacks = upstream_callbacks_]() { upstream_callbacks->onIdleTimeout(); }); + + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.tcp_proxy_set_idle_timer_immediately_on_new_connection")) { + // Start the idle timer immediately so that if no response is received from the upstream, + // the downstream connection will time out. + resetIdleTimer(); + } + } + // Set UUID for the connection. This is used for logging and tracing. getStreamInfo().setStreamIdProvider( std::make_shared(config_->randomGenerator().uuid())); @@ -1032,20 +1067,7 @@ void Filter::onUpstreamConnection() { read_callbacks_->connection(), getStreamInfo().downstreamAddressProvider().requestedServerName()); - idle_timeout_ = config_->idleTimeout(); - if (const auto* per_connection_idle_timeout = - getStreamInfo().filterState()->getDataReadOnly( - PerConnectionIdleTimeoutMs); - per_connection_idle_timeout != nullptr) { - idle_timeout_ = std::chrono::milliseconds(per_connection_idle_timeout->value()); - } - if (idle_timeout_) { - // The idle_timer_ can be moved to a Drainer, so related callbacks call into - // the UpstreamCallbacks, which has the same lifetime as the timer, and can dispatch - // the call to either TcpProxy or to Drainer, depending on the current state. - idle_timer_ = read_callbacks_->connection().dispatcher().createTimer( - [upstream_callbacks = upstream_callbacks_]() { upstream_callbacks->onIdleTimeout(); }); resetIdleTimer(); read_callbacks_->connection().addBytesSentCallback([this](uint64_t) { resetIdleTimer(); diff --git a/source/common/tcp_proxy/tcp_proxy.h b/source/common/tcp_proxy/tcp_proxy.h index a4cd362afdda8..57e930a0fb0b4 100644 --- a/source/common/tcp_proxy/tcp_proxy.h +++ b/source/common/tcp_proxy/tcp_proxy.h @@ -12,6 +12,7 @@ #include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.h" #include "envoy/http/codec.h" #include "envoy/http/header_evaluator.h" +#include "envoy/http/request_id_extension.h" #include "envoy/network/connection.h" #include "envoy/network/filter.h" #include "envoy/runtime/runtime.h" @@ -157,6 +158,7 @@ class TunnelResponseTrailers : public Http::TunnelResponseHeadersOrTrailersImpl private: const Http::ResponseTrailerMapPtr response_trailers_; }; + class Config; class TunnelingConfigHelperImpl : public TunnelingConfigHelper, protected Logger::Loggable { @@ -169,6 +171,9 @@ class TunnelingConfigHelperImpl : public TunnelingConfigHelper, bool usePost() const override { return !post_path_.empty(); } const std::string& postPath() const override { return post_path_; } Envoy::Http::HeaderEvaluator& headerEvaluator() const override { return *header_parser_; } + const Envoy::Http::RequestIDExtensionSharedPtr& requestIDExtension() const override { + return request_id_extension_; + } const Envoy::Router::FilterConfig& routerFilterConfig() const override { return router_config_; } void @@ -187,6 +192,8 @@ class TunnelingConfigHelperImpl : public TunnelingConfigHelper, const bool propagate_response_headers_; const bool propagate_response_trailers_; std::string post_path_; + // Request ID extension for tunneling requests. If null, no request ID is generated. + Envoy::Http::RequestIDExtensionSharedPtr request_id_extension_; Stats::StatNameManagedStorage route_stat_name_storage_; const Router::FilterConfig router_config_; Server::Configuration::ServerFactoryContext& server_factory_context_; @@ -548,8 +555,8 @@ class Filter : public Network::ReadFilter, } void addDownstreamWatermarkCallbacks(Http::DownstreamWatermarkCallbacks&) override {} void removeDownstreamWatermarkCallbacks(Http::DownstreamWatermarkCallbacks&) override {} - void setDecoderBufferLimit(uint32_t) override {} - uint32_t decoderBufferLimit() override { return 0; } + void setDecoderBufferLimit(uint64_t) override {} + uint64_t decoderBufferLimit() override { return 0; } bool recreateStream(const Http::ResponseHeaderMap*) override { return false; } void addUpstreamSocketOptions(const Network::Socket::OptionsSharedPtr&) override {} Network::Socket::OptionsSharedPtr getUpstreamSocketOptions() const override { return nullptr; } @@ -582,6 +589,7 @@ class Filter : public Network::ReadFilter, os << spaces << "TcpProxy " << this << DUMP_MEMBER(streamId()) << "\n"; DUMP_DETAILS(parent_->getStreamInfo().upstreamInfo()); } + Filter* parent_{}; Http::RequestTrailerMapPtr request_trailer_map_; std::shared_ptr route_; diff --git a/source/common/tcp_proxy/upstream.cc b/source/common/tcp_proxy/upstream.cc index 6c9e93c948d81..8fd13df1b3518 100644 --- a/source/common/tcp_proxy/upstream.cc +++ b/source/common/tcp_proxy/upstream.cc @@ -18,6 +18,34 @@ namespace TcpProxy { using TunnelingConfig = envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy_TunnelingConfig; +// Constants for tunnel request ID metadata. +const std::string& tunnelRequestIdMetadataNamespace() { + CONSTRUCT_ON_FIRST_USE(std::string, "envoy.filters.network.tcp_proxy"); +} + +const std::string& tunnelRequestIdMetadataKey() { + CONSTRUCT_ON_FIRST_USE(std::string, "tunnel_request_id"); +} + +// Helper function to generate and store request ID in dynamic metadata. +void generateAndStoreRequestId(const TunnelingConfigHelper& config, Http::RequestHeaderMap& headers, + StreamInfo::StreamInfo& downstream_info) { + if (config.requestIDExtension() != nullptr) { + // For tunneling requests there is no way to get the external request ID as the incoming + // traffic could be anything - HTTPS, MySQL, Postgres, etc. + config.requestIDExtension()->set(headers, /*edge_request=*/true, + /*keep_external_id=*/false); + // Also store the request ID in dynamic metadata to allow TCP access logs to format it. + const auto rid = headers.getRequestIdValue(); + if (!rid.empty()) { + Protobuf::Struct md; + auto& fields = *md.mutable_fields(); + fields[tunnelRequestIdMetadataKey()].mutable_string_value()->assign(rid.data(), rid.size()); + downstream_info.setDynamicMetadata(tunnelRequestIdMetadataNamespace(), md); + } + } +} + TcpUpstream::TcpUpstream(Tcp::ConnectionPool::ConnectionDataPtr&& data, Tcp::ConnectionPool::UpstreamCallbacks& upstream_callbacks) : upstream_conn_data_(std::move(data)) { @@ -121,6 +149,10 @@ void HttpUpstream::setRequestEncoder(Http::RequestEncoder& request_encoder, bool } } + // Optionally generate a request ID before evaluating configured headers so + // it is available to header formatters. + generateAndStoreRequestId(config_, *headers, downstream_info_); + config_.headerEvaluator().evaluateHeaders(*headers, {downstream_info_.getRequestHeaders()}, downstream_info_); const auto status = request_encoder_->encodeHeaders(*headers, false); @@ -309,7 +341,7 @@ std::unique_ptr HttpConnPool::createConnPool( return nullptr; } - ProtobufWkt::Any message; + Protobuf::Any message; if (cluster.info()->upstreamConfig()) { message = cluster.info()->upstreamConfig()->typed_config(); } @@ -423,6 +455,8 @@ CombinedUpstream::CombinedUpstream(HttpConnPool& http_conn_pool, downstream_headers_->addReference(Http::Headers::get().Path, config_.postPath()); } + generateAndStoreRequestId(config_, *downstream_headers_, downstream_info_); + config_.headerEvaluator().evaluateHeaders( *downstream_headers_, {downstream_info_.getRequestHeaders()}, downstream_info_); } diff --git a/source/common/tcp_proxy/upstream.h b/source/common/tcp_proxy/upstream.h index 661797dc65de0..e6932574b3abf 100644 --- a/source/common/tcp_proxy/upstream.h +++ b/source/common/tcp_proxy/upstream.h @@ -322,6 +322,8 @@ class CombinedUpstream : public GenericUpstream, public Envoy::Router::RouterFil void onPerTryTimeout(UpstreamRequest&) override {} void onPerTryIdleTimeout(UpstreamRequest&) override {} void onStreamMaxDurationReached(UpstreamRequest&) override {} + void setupRouteTimeoutForWebsocketUpgrade() override {} + void disableRouteTimeoutForWebsocketUpgrade() override {} Http::StreamDecoderFilterCallbacks* callbacks() override { return &decoder_filter_callbacks_; } Upstream::ClusterInfoConstSharedPtr cluster() override { return decoder_filter_callbacks_.clusterInfo(); diff --git a/source/common/tls/context_impl.cc b/source/common/tls/context_impl.cc index aace5f7706ea7..e32186ddb4c44 100644 --- a/source/common/tls/context_impl.cc +++ b/source/common/tls/context_impl.cc @@ -624,9 +624,9 @@ std::vector ContextImpl::getCertChainInformat auto ocsp_resp = ctx.ocsp_response_.get(); if (ocsp_resp) { auto* ocsp_details = detail->mutable_ocsp_details(); - ProtobufWkt::Timestamp* valid_from = ocsp_details->mutable_valid_from(); + Protobuf::Timestamp* valid_from = ocsp_details->mutable_valid_from(); TimestampUtil::systemClockToTimestamp(ocsp_resp->getThisUpdate(), *valid_from); - ProtobufWkt::Timestamp* expiration = ocsp_details->mutable_expiration(); + Protobuf::Timestamp* expiration = ocsp_details->mutable_expiration(); TimestampUtil::systemClockToTimestamp(ocsp_resp->getNextUpdate(), *expiration); } cert_details.push_back(std::move(detail)); diff --git a/source/common/tls/default_tls_certificate_selector.h b/source/common/tls/default_tls_certificate_selector.h index 1fdb290020ba6..f741814c9c1db 100644 --- a/source/common/tls/default_tls_certificate_selector.h +++ b/source/common/tls/default_tls_certificate_selector.h @@ -70,7 +70,7 @@ class TlsCertificateSelectorConfigFactoryImpl : public Ssl::TlsCertificateSelect }; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } static Ssl::TlsCertificateSelectorConfigFactory* getDefaultTlsCertificateSelectorConfigFactory() { diff --git a/source/common/tls/server_context_config_impl.cc b/source/common/tls/server_context_config_impl.cc index fc42a6ed041af..b7d303ae01017 100644 --- a/source/common/tls/server_context_config_impl.cc +++ b/source/common/tls/server_context_config_impl.cc @@ -180,7 +180,7 @@ ServerContextConfigImpl::ServerContextConfigImpl( auto factory = TlsCertificateSelectorConfigFactoryImpl::getDefaultTlsCertificateSelectorConfigFactory(); - const ProtobufWkt::Any any; + const Protobuf::Any any; tls_certificate_selector_factory_ = factory->createTlsCertificateSelectorFactory( any, factory_context.serverFactoryContext(), ProtobufMessage::getNullValidationVisitor(), creation_status, for_quic); diff --git a/source/common/tls/ssl_handshaker.h b/source/common/tls/ssl_handshaker.h index 3a5e162d99ada..fd42390ce1611 100644 --- a/source/common/tls/ssl_handshaker.h +++ b/source/common/tls/ssl_handshaker.h @@ -174,7 +174,7 @@ class HandshakerFactoryImpl : public Ssl::HandshakerFactory { std::string name() const override { return "envoy.default_tls_handshaker"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } Ssl::HandshakerFactoryCb createHandshakerCb(const Protobuf::Message&, diff --git a/source/common/tls/utility.cc b/source/common/tls/utility.cc index 6024c9fe97e5f..24b2ba40e575d 100644 --- a/source/common/tls/utility.cc +++ b/source/common/tls/utility.cc @@ -30,9 +30,9 @@ Envoy::Ssl::CertificateDetailsPtr Utility::certificateDetails(X509* cert, const const auto days_until_expiry = Utility::getDaysUntilExpiration(cert, time_source).value_or(0); certificate_details->set_days_until_expiration(days_until_expiry); - ProtobufWkt::Timestamp* valid_from = certificate_details->mutable_valid_from(); + Protobuf::Timestamp* valid_from = certificate_details->mutable_valid_from(); TimestampUtil::systemClockToTimestamp(Utility::getValidFrom(*cert), *valid_from); - ProtobufWkt::Timestamp* expiration_time = certificate_details->mutable_expiration_time(); + Protobuf::Timestamp* expiration_time = certificate_details->mutable_expiration_time(); TimestampUtil::systemClockToTimestamp(Utility::getExpirationTime(*cert), *expiration_time); for (auto& dns_san : Utility::getSubjectAltNames(*cert, GEN_DNS)) { @@ -112,7 +112,7 @@ std::string getRFC2253NameFromCertificate(X509& cert, CertName desired_name) { bssl::UniquePtr buf(BIO_new(BIO_s_mem())); RELEASE_ASSERT(buf != nullptr, ""); - X509_NAME* name = nullptr; + const X509_NAME* name = nullptr; switch (desired_name) { case CertName::Issuer: name = X509_get_issuer_name(&cert); @@ -143,7 +143,7 @@ std::string getRFC2253NameFromCertificate(X509& cert, CertName desired_name) { * @return Envoy::Ssl::ParsedX509NamePtr returns the struct contains the parsed values. */ Envoy::Ssl::ParsedX509NamePtr parseX509NameFromCertificate(X509& cert, CertName desired_name) { - X509_NAME* name = nullptr; + const X509_NAME* name = nullptr; switch (desired_name) { case CertName::Issuer: name = X509_get_issuer_name(&cert); @@ -156,8 +156,7 @@ Envoy::Ssl::ParsedX509NamePtr parseX509NameFromCertificate(X509& cert, CertName auto parsed = std::make_unique(); int cnt = X509_NAME_entry_count(name); for (int i = 0; i < cnt; i++) { - const X509_NAME_ENTRY* ent; - ent = X509_NAME_get_entry(name, i); + const X509_NAME_ENTRY* ent = X509_NAME_get_entry(name, i); const ASN1_OBJECT* fn = X509_NAME_ENTRY_get_object(ent); int fn_nid = OBJ_obj2nid(fn); @@ -237,19 +236,19 @@ std::vector Utility::getSubjectAltNames(X509& cert, int type) { std::string Utility::generalNameAsString(const GENERAL_NAME* general_name) { std::string san; - ASN1_STRING* str = nullptr; + const ASN1_STRING* str = nullptr; switch (general_name->type) { case GEN_DNS: str = general_name->d.dNSName; - san.assign(reinterpret_cast(ASN1_STRING_data(str)), ASN1_STRING_length(str)); + san.assign(reinterpret_cast(ASN1_STRING_get0_data(str)), ASN1_STRING_length(str)); break; case GEN_URI: str = general_name->d.uniformResourceIdentifier; - san.assign(reinterpret_cast(ASN1_STRING_data(str)), ASN1_STRING_length(str)); + san.assign(reinterpret_cast(ASN1_STRING_get0_data(str)), ASN1_STRING_length(str)); break; case GEN_EMAIL: str = general_name->d.rfc822Name; - san.assign(reinterpret_cast(ASN1_STRING_data(str)), ASN1_STRING_length(str)); + san.assign(reinterpret_cast(ASN1_STRING_get0_data(str)), ASN1_STRING_length(str)); break; case GEN_IPADD: { if (general_name->d.ip->length == 4) { @@ -270,7 +269,7 @@ std::string Utility::generalNameAsString(const GENERAL_NAME* general_name) { break; } case GEN_OTHERNAME: { - ASN1_TYPE* value = general_name->d.otherName->value; + const ASN1_TYPE* value = general_name->d.otherName->value; if (value == nullptr) { break; } @@ -304,38 +303,38 @@ std::string Utility::generalNameAsString(const GENERAL_NAME* general_name) { break; } case V_ASN1_BIT_STRING: { - ASN1_BIT_STRING* tmp_str = value->value.bit_string; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_BIT_STRING* tmp_str = value->value.bit_string; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_OCTET_STRING: { - ASN1_OCTET_STRING* tmp_str = value->value.octet_string; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_OCTET_STRING* tmp_str = value->value.octet_string; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_PRINTABLESTRING: { - ASN1_PRINTABLESTRING* tmp_str = value->value.printablestring; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_PRINTABLESTRING* tmp_str = value->value.printablestring; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_T61STRING: { - ASN1_T61STRING* tmp_str = value->value.t61string; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_T61STRING* tmp_str = value->value.t61string; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_IA5STRING: { - ASN1_IA5STRING* tmp_str = value->value.ia5string; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_IA5STRING* tmp_str = value->value.ia5string; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_GENERALSTRING: { - ASN1_GENERALSTRING* tmp_str = value->value.generalstring; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_GENERALSTRING* tmp_str = value->value.generalstring; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } @@ -360,38 +359,38 @@ std::string Utility::generalNameAsString(const GENERAL_NAME* general_name) { break; } case V_ASN1_UTCTIME: { - ASN1_UTCTIME* tmp_str = value->value.utctime; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_UTCTIME* tmp_str = value->value.utctime; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_GENERALIZEDTIME: { - ASN1_GENERALIZEDTIME* tmp_str = value->value.generalizedtime; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_GENERALIZEDTIME* tmp_str = value->value.generalizedtime; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_VISIBLESTRING: { - ASN1_VISIBLESTRING* tmp_str = value->value.visiblestring; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_VISIBLESTRING* tmp_str = value->value.visiblestring; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_UTF8STRING: { - ASN1_UTF8STRING* tmp_str = value->value.utf8string; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_UTF8STRING* tmp_str = value->value.utf8string; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_SET: { - ASN1_STRING* tmp_str = value->value.set; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_STRING* tmp_str = value->value.set; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } case V_ASN1_SEQUENCE: { - ASN1_STRING* tmp_str = value->value.sequence; - san.assign(reinterpret_cast(ASN1_STRING_data(tmp_str)), + const ASN1_STRING* tmp_str = value->value.sequence; + san.assign(reinterpret_cast(ASN1_STRING_get0_data(tmp_str)), ASN1_STRING_length(tmp_str)); break; } @@ -450,7 +449,7 @@ std::vector Utility::getCertificateExtensionOids(X509& cert) { int count = X509_get_ext_count(&cert); for (int pos = 0; pos < count; pos++) { - X509_EXTENSION* extension = X509_get_ext(&cert, pos); + const X509_EXTENSION* extension = X509_get_ext(&cert, pos); RELEASE_ASSERT(extension != nullptr, ""); char oid[MAX_OID_LENGTH]; @@ -476,7 +475,7 @@ absl::string_view Utility::getCertificateExtensionValue(X509& cert, return {}; } - X509_EXTENSION* extension = X509_get_ext(&cert, pos); + const X509_EXTENSION* extension = X509_get_ext(&cert, pos); if (extension == nullptr) { return {}; } diff --git a/source/common/tracing/custom_tag_impl.cc b/source/common/tracing/custom_tag_impl.cc index 3b9c8e5fc0a6a..d221d0ec668c5 100644 --- a/source/common/tracing/custom_tag_impl.cc +++ b/source/common/tracing/custom_tag_impl.cc @@ -95,17 +95,17 @@ MetadataCustomTag::metadataToString(const envoy::config::core::v3::Metadata* met return absl::nullopt; } - const ProtobufWkt::Value& value = Envoy::Config::Metadata::metadataValue(metadata, metadata_key_); + const Protobuf::Value& value = Envoy::Config::Metadata::metadataValue(metadata, metadata_key_); switch (value.kind_case()) { - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: return value.bool_value() ? "true" : "false"; - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: return absl::StrCat(value.number_value()); - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: return value.string_value(); - case ProtobufWkt::Value::kListValue: + case Protobuf::Value::kListValue: return jsonOrNullopt(value.list_value()); - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: return jsonOrNullopt(value.struct_value()); default: break; diff --git a/source/common/tracing/http_tracer_impl.cc b/source/common/tracing/http_tracer_impl.cc index da26e1ddd1455..c5c0e7144b218 100644 --- a/source/common/tracing/http_tracer_impl.cc +++ b/source/common/tracing/http_tracer_impl.cc @@ -256,15 +256,7 @@ void HttpTracerUtility::setCommonTags(Span& span, const StreamInfo::StreamInfo& span.setTag(Tracing::Tags::get().Error, Tracing::Tags::get().True); } - ReadOnlyHttpTraceContext trace_context{stream_info.getRequestHeaders() != nullptr - ? *stream_info.getRequestHeaders() - : *Http::StaticEmptyHeaders::get().request_headers}; - CustomTagContext ctx{trace_context, stream_info}; - if (const CustomTagMap* custom_tag_map = tracing_config.customTags(); custom_tag_map) { - for (const auto& it : *custom_tag_map) { - it.second->applySpan(span, ctx); - } - } + tracing_config.modifySpan(span); } } // namespace Tracing diff --git a/source/common/tracing/null_span_impl.h b/source/common/tracing/null_span_impl.h index cba151272db4e..1de48d2e437e8 100644 --- a/source/common/tracing/null_span_impl.h +++ b/source/common/tracing/null_span_impl.h @@ -31,6 +31,7 @@ class NullSpan : public Span { return SpanPtr{new NullSpan()}; } void setSampled(bool) override {} + bool useLocalDecision() const override { return false; } }; } // namespace Tracing diff --git a/source/common/tracing/trace_context_impl.cc b/source/common/tracing/trace_context_impl.cc index 5eb4075dac59f..8dd85e1906807 100644 --- a/source/common/tracing/trace_context_impl.cc +++ b/source/common/tracing/trace_context_impl.cc @@ -79,6 +79,36 @@ TraceContextHandler::get(const TraceContext& trace_context) const { } } +TraceContextHandler::GetAllResult +TraceContextHandler::getAll(const TraceContext& trace_context) const { + auto header_map = trace_context.requestHeaders(); + if (!header_map.has_value()) { + if (const auto value = trace_context.get(key_); value.has_value()) { + return {value.value()}; + } + return {}; + } + + if (handle_.has_value()) { + auto* entry = header_map->getInline(handle_.value()); + if (entry == nullptr) { + return {}; + } + return {entry->value().getStringView()}; + } else { + auto results = header_map->get(key_); + if (results.empty()) { + return {}; + } + GetAllResult all_values; + all_values.reserve(results.size()); + for (size_t i = 0; i < results.size(); ++i) { + all_values.push_back(results[i]->value().getStringView()); + } + return all_values; + } +} + void TraceContextHandler::remove(TraceContext& trace_context) const { auto header_map = trace_context.requestHeaders(); if (!header_map.has_value()) { diff --git a/source/common/tracing/trace_context_impl.h b/source/common/tracing/trace_context_impl.h index 8ee2c4aeb83f3..31f6892726a8a 100644 --- a/source/common/tracing/trace_context_impl.h +++ b/source/common/tracing/trace_context_impl.h @@ -43,6 +43,16 @@ class TraceContextHandler { */ absl::optional get(const TraceContext& trace_context) const; + using GetAllResult = absl::InlinedVector; + + /** + * Get all values from the trace context by the key. If the underlying trace context is HTTP + * header map, then there may be multiple values for the same key and the get() method will + * return the first value only. This method will return all values for the key. + * @param trace_context the trace context to get the values. + */ + GetAllResult getAll(const TraceContext& trace_context) const; + /* * Set the key/value pair in the trace context. * @param trace_context the trace context to set the key/value pair. diff --git a/source/common/tracing/tracer_config_impl.h b/source/common/tracing/tracer_config_impl.h index 9ea8e1fe58b9c..26154b3c8d8c0 100644 --- a/source/common/tracing/tracer_config_impl.h +++ b/source/common/tracing/tracer_config_impl.h @@ -30,10 +30,10 @@ class TracerFactoryContextImpl : public Server::Configuration::TracerFactoryCont ProtobufMessage::ValidationVisitor& validation_visitor_; }; -class ConnectionManagerTracingConfigImpl : public ConnectionManagerTracingConfig { +class ConnectionManagerTracingConfig { public: - ConnectionManagerTracingConfigImpl(envoy::config::core::v3::TrafficDirection traffic_direction, - const ConnectionManagerTracingConfigProto& tracing_config) { + ConnectionManagerTracingConfig(envoy::config::core::v3::TrafficDirection traffic_direction, + const ConnectionManagerTracingConfigProto& tracing_config) { // Listener level traffic direction overrides the operation name switch (traffic_direction) { @@ -77,34 +77,28 @@ class ConnectionManagerTracingConfigImpl : public ConnectionManagerTracingConfig Tracing::DefaultMaxPathTagLength); } - ConnectionManagerTracingConfigImpl(Tracing::OperationName operation_name, - Tracing::CustomTagMap custom_tags, - envoy::type::v3::FractionalPercent client_sampling, - envoy::type::v3::FractionalPercent random_sampling, - envoy::type::v3::FractionalPercent overall_sampling, - bool verbose, uint32_t max_path_tag_length) - : operation_name_(operation_name), custom_tags_(custom_tags), - client_sampling_(client_sampling), random_sampling_(random_sampling), - overall_sampling_(overall_sampling), verbose_(verbose), + ConnectionManagerTracingConfig(Tracing::OperationName operation_name, + Tracing::CustomTagMap custom_tags, + envoy::type::v3::FractionalPercent client_sampling, + envoy::type::v3::FractionalPercent random_sampling, + envoy::type::v3::FractionalPercent overall_sampling, bool verbose, + uint32_t max_path_tag_length) + : operation_name_(operation_name), custom_tags_(std::move(custom_tags)), + client_sampling_(std::move(client_sampling)), random_sampling_(std::move(random_sampling)), + overall_sampling_(std::move(overall_sampling)), verbose_(verbose), max_path_tag_length_(max_path_tag_length) {} - ConnectionManagerTracingConfigImpl() = default; + ConnectionManagerTracingConfig() = default; - const envoy::type::v3::FractionalPercent& getClientSampling() const override { - return client_sampling_; - } - const envoy::type::v3::FractionalPercent& getRandomSampling() const override { - return random_sampling_; - } - const envoy::type::v3::FractionalPercent& getOverallSampling() const override { - return overall_sampling_; - } - const Tracing::CustomTagMap& getCustomTags() const override { return custom_tags_; } + const envoy::type::v3::FractionalPercent& getClientSampling() const { return client_sampling_; } + const envoy::type::v3::FractionalPercent& getRandomSampling() const { return random_sampling_; } + const envoy::type::v3::FractionalPercent& getOverallSampling() const { return overall_sampling_; } + const Tracing::CustomTagMap& getCustomTags() const { return custom_tags_; } - Tracing::OperationName operationName() const override { return operation_name_; } - bool verbose() const override { return verbose_; } - uint32_t maxPathTagLength() const override { return max_path_tag_length_; } - bool spawnUpstreamSpan() const override { return spawn_upstream_span_; } + Tracing::OperationName operationName() const { return operation_name_; } + bool verbose() const { return verbose_; } + uint32_t maxPathTagLength() const { return max_path_tag_length_; } + bool spawnUpstreamSpan() const { return spawn_upstream_span_; } // TODO(wbpcode): keep this field be public for compatibility. Then the HCM needn't change much // code to use this config. @@ -118,5 +112,7 @@ class ConnectionManagerTracingConfigImpl : public ConnectionManagerTracingConfig bool spawn_upstream_span_{}; }; +using ConnectionManagerTracingConfigPtr = std::unique_ptr; + } // namespace Tracing } // namespace Envoy diff --git a/source/common/tracing/tracer_impl.cc b/source/common/tracing/tracer_impl.cc index 8205d0d2f8cd0..c012dd76ed973 100644 --- a/source/common/tracing/tracer_impl.cc +++ b/source/common/tracing/tracer_impl.cc @@ -84,8 +84,7 @@ Decision TracerUtility::shouldTraceRequest(const StreamInfo::StreamInfo& stream_ } } -void TracerUtility::finalizeSpan(Span& span, const TraceContext& trace_context, - const StreamInfo::StreamInfo& stream_info, +void TracerUtility::finalizeSpan(Span& span, const StreamInfo::StreamInfo& stream_info, const Config& tracing_config, bool upstream_span) { span.setTag(Tracing::Tags::get().Component, Tracing::Tags::get().Proxy); @@ -123,14 +122,7 @@ void TracerUtility::finalizeSpan(Span& span, const TraceContext& trace_context, if (tracing_config.verbose()) { annotateVerbose(span, stream_info); } - - // Custom tag from configuration. - CustomTagContext ctx{trace_context, stream_info}; - if (const CustomTagMap* custom_tag_map = tracing_config.customTags(); custom_tag_map) { - for (const auto& it : *custom_tag_map) { - it.second->applySpan(span, ctx); - } - } + tracing_config.modifySpan(span); // Finish the span. span.finishSpan(); diff --git a/source/common/tracing/tracer_impl.h b/source/common/tracing/tracer_impl.h index 70354ef4ba8c5..446d532513bf3 100644 --- a/source/common/tracing/tracer_impl.h +++ b/source/common/tracing/tracer_impl.h @@ -34,14 +34,12 @@ class TracerUtility { /** * Finalize span and set protocol independent tags to the span. * @param span the downstream or upstream span. - * @param context traceable stream context. * @param stream_info stream info. * @param config tracing configuration. * @param upstream_span true if the span is an upstream span. */ - static void finalizeSpan(Span& span, const TraceContext& context, - const StreamInfo::StreamInfo& stream_info, const Config& config, - bool upstream_span); + static void finalizeSpan(Span& span, const StreamInfo::StreamInfo& stream_info, + const Config& config, bool upstream_span); private: static const std::string IngressOperation; @@ -52,7 +50,7 @@ class EgressConfigImpl : public Config { public: // Tracing::Config Tracing::OperationName operationName() const override { return Tracing::OperationName::Egress; } - const CustomTagMap* customTags() const override { return nullptr; } + void modifySpan(Tracing::Span&) const override {} bool verbose() const override { return false; } uint32_t maxPathTagLength() const override { return Tracing::DefaultMaxPathTagLength; } // This EgressConfigImpl is only used for async client tracing. Return false here is OK. diff --git a/source/common/upstream/BUILD b/source/common/upstream/BUILD index a4066b6d89f23..061909f4e1cbf 100644 --- a/source/common/upstream/BUILD +++ b/source/common/upstream/BUILD @@ -16,6 +16,7 @@ envoy_cc_library( deps = [ "//envoy/config:grpc_mux_interface", "//envoy/config:subscription_interface", + "//envoy/config:xds_manager_interface", "//envoy/upstream:cluster_manager_interface", "//source/common/common:minimal_logger_lib", "//source/common/config:resource_name_lib", @@ -437,7 +438,6 @@ envoy_cc_library( deps = [ ":load_balancer_context_base_lib", ":resource_manager_lib", - ":scheduler_lib", ":upstream_factory_context_lib", "//envoy/event:timer_interface", "//envoy/filter:config_provider_manager_interface", diff --git a/source/common/upstream/cds_api_helper.cc b/source/common/upstream/cds_api_helper.cc index 591e71b643a7d..f3aece06267a9 100644 --- a/source/common/upstream/cds_api_helper.cc +++ b/source/common/upstream/cds_api_helper.cc @@ -18,15 +18,12 @@ std::pair> CdsApiHelper::onConfigUpdate(const std::vector& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& system_version_info) { - Config::ScopedResume maybe_resume_eds_leds_sds; - if (cm_.adsMux()) { - // A cluster update pauses sending EDS and LEDS requests. - const std::vector paused_xds_types{ - Config::getTypeUrl(), - Config::getTypeUrl(), - Config::getTypeUrl()}; - maybe_resume_eds_leds_sds = cm_.adsMux()->pause(paused_xds_types); - } + // A cluster update pauses sending EDS and LEDS requests. + const std::vector paused_xds_types{ + Config::getTypeUrl(), + Config::getTypeUrl(), + Config::getTypeUrl()}; + Config::ScopedResume resume_eds_leds_sds = xds_manager_.pause(paused_xds_types); ENVOY_LOG( info, diff --git a/source/common/upstream/cds_api_helper.h b/source/common/upstream/cds_api_helper.h index 5fc45186320e3..0c0a14f513282 100644 --- a/source/common/upstream/cds_api_helper.h +++ b/source/common/upstream/cds_api_helper.h @@ -4,6 +4,7 @@ #include #include "envoy/config/subscription.h" +#include "envoy/config/xds_manager.h" #include "envoy/upstream/cluster_manager.h" #include "source/common/common/logger.h" @@ -18,7 +19,8 @@ namespace Upstream { */ class CdsApiHelper : Logger::Loggable { public: - CdsApiHelper(ClusterManager& cm, std::string name) : cm_(cm), name_(std::move(name)) {} + CdsApiHelper(ClusterManager& cm, Config::XdsManager& xds_manager, std::string name) + : cm_(cm), xds_manager_(xds_manager), name_(std::move(name)) {} /** * onConfigUpdate handles the addition and removal of clusters by notifying the ClusterManager * about the cluster changes. It closely follows the onConfigUpdate API from @@ -38,6 +40,7 @@ class CdsApiHelper : Logger::Loggable { private: ClusterManager& cm_; + Config::XdsManager& xds_manager_; const std::string name_; std::string system_version_info_; }; diff --git a/source/common/upstream/cds_api_impl.cc b/source/common/upstream/cds_api_impl.cc index 1161437fbf857..71eefecfd82f6 100644 --- a/source/common/upstream/cds_api_impl.cc +++ b/source/common/upstream/cds_api_impl.cc @@ -12,10 +12,12 @@ absl::StatusOr CdsApiImpl::create(const envoy::config::core::v3::ConfigSource& cds_config, const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm, Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, - Server::Configuration::ServerFactoryContext& factory_context) { + Server::Configuration::ServerFactoryContext& factory_context, + bool support_multi_ads_sources) { absl::Status creation_status = absl::OkStatus(); - auto ret = CdsApiPtr{new CdsApiImpl(cds_config, cds_resources_locator, cm, scope, - validation_visitor, factory_context, creation_status)}; + auto ret = + CdsApiPtr{new CdsApiImpl(cds_config, cds_resources_locator, cm, scope, validation_visitor, + factory_context, support_multi_ads_sources, creation_status)}; RETURN_IF_NOT_OK(creation_status); return ret; } @@ -25,12 +27,13 @@ CdsApiImpl::CdsApiImpl(const envoy::config::core::v3::ConfigSource& cds_config, ClusterManager& cm, Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, Server::Configuration::ServerFactoryContext& factory_context, - absl::Status& creation_status) + bool support_multi_ads_sources, absl::Status& creation_status) : Envoy::Config::SubscriptionBase(validation_visitor, "name"), - helper_(cm, "cds"), cm_(cm), scope_(scope.createScope("cluster_manager.cds.")), - factory_context_(factory_context), - stats_({ALL_CDS_STATS(POOL_COUNTER(*scope_), POOL_GAUGE(*scope_))}) { + helper_(cm, factory_context.xdsManager(), "cds"), cm_(cm), + scope_(scope.createScope("cluster_manager.cds.")), factory_context_(factory_context), + stats_({ALL_CDS_STATS(POOL_COUNTER(*scope_), POOL_GAUGE(*scope_))}), + support_multi_ads_sources_(support_multi_ads_sources) { const auto resource_name = getResourceName(); absl::StatusOr subscription_or_error; if (cds_resources_locator == nullptr) { @@ -46,6 +49,34 @@ CdsApiImpl::CdsApiImpl(const envoy::config::core::v3::ConfigSource& cds_config, absl::Status CdsApiImpl::onConfigUpdate(const std::vector& resources, const std::string& version_info) { + // If another source may be adding clusters to the cluster-manager, Envoy needs to + // track which clusters are received via the SotW CDS configuration, so only + // clusters that were added through SotW CDS and are not updated will be removed. + if (support_multi_ads_sources_) { + // The input resources will be the next sotw_resource_names_. + absl::flat_hash_set next_sotw_resource_names; + next_sotw_resource_names.reserve(resources.size()); + std::transform(resources.cbegin(), resources.cend(), + std::inserter(next_sotw_resource_names, next_sotw_resource_names.begin()), + [](const Config::DecodedResourceRef resource) { return resource.get().name(); }); + // Find all the clusters that are currently used, but no longer appear in + // the next step. + Protobuf::RepeatedPtrField to_remove; + for (const std::string& cluster_name : sotw_resource_names_) { + if (!next_sotw_resource_names.contains(cluster_name)) { + to_remove.Add(std::string(cluster_name)); + } + } + absl::Status status = onConfigUpdate(resources, to_remove, version_info); + // Even if the onConfigUpdate() above returns an error, some of the clusters + // may have been updated. Either way, we use the new update to override the + // contents. + // TODO(adisuissa): This will not be needed once the xDS-Cache layer is + // introduced, as it will keep track of only the valid resources. + sotw_resource_names_ = std::move(next_sotw_resource_names); + return status; + } + auto all_existing_clusters = cm_.clusters(); // Exclude the clusters which CDS wants to add. for (const auto& resource : resources) { diff --git a/source/common/upstream/cds_api_impl.h b/source/common/upstream/cds_api_impl.h index 13b04a5ca156a..d6614f012e2e4 100644 --- a/source/common/upstream/cds_api_impl.h +++ b/source/common/upstream/cds_api_impl.h @@ -30,6 +30,7 @@ struct CdsStats { /** * CDS API implementation that fetches via Subscription. + * This supports the wildcard subscription to a single source. */ class CdsApiImpl : public CdsApi, Envoy::Config::SubscriptionBase { @@ -38,7 +39,8 @@ class CdsApiImpl : public CdsApi, create(const envoy::config::core::v3::ConfigSource& cds_config, const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm, Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, - Server::Configuration::ServerFactoryContext& factory_context); + Server::Configuration::ServerFactoryContext& factory_context, + bool support_multi_ads_sources); // Upstream::CdsApi void initialize() override { subscription_->start({}); } @@ -60,7 +62,7 @@ class CdsApiImpl : public CdsApi, const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm, Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, Server::Configuration::ServerFactoryContext& factory_context, - absl::Status& creation_status); + bool support_multi_ads_sources, absl::Status& creation_status); void runInitializeCallbackIfAny(); CdsApiHelper helper_; @@ -68,6 +70,13 @@ class CdsApiImpl : public CdsApi, Stats::ScopeSharedPtr scope_; Server::Configuration::ServerFactoryContext& factory_context_; CdsStats stats_; + // This enables tracking the resources via SotW for wildcard-CDS, so concurrent OD-CDS + // resources won't get overridden when a CDS-wildcard update arrives. + // TODO(adisuissa): once proper support for an xDS-Caching layer is added, + // this will not be relevant, as the callbacks will be similar to delta-xDS + // from each collection source. + const bool support_multi_ads_sources_; + absl::flat_hash_set sotw_resource_names_; Config::SubscriptionPtr subscription_; std::function initialize_callback_; }; diff --git a/source/common/upstream/cluster_factory_impl.cc b/source/common/upstream/cluster_factory_impl.cc index 8c4d1e5fe5463..556e1a19057c1 100644 --- a/source/common/upstream/cluster_factory_impl.cc +++ b/source/common/upstream/cluster_factory_impl.cc @@ -27,7 +27,7 @@ ClusterFactoryImplBase::create(const envoy::config::cluster::v3::Cluster& cluste // try to look up by typed_config if (cluster.has_cluster_type() && cluster.cluster_type().has_typed_config() && (TypeUtil::typeUrlToDescriptorFullName(cluster.cluster_type().typed_config().type_url()) != - ProtobufWkt::Struct::GetDescriptor()->full_name())) { + Protobuf::Struct::GetDescriptor()->full_name())) { cluster_config_type_name = TypeUtil::typeUrlToDescriptorFullName(cluster.cluster_type().typed_config().type_url()); factory = Registry::FactoryRegistry::getFactoryByType(cluster_config_type_name); @@ -105,6 +105,19 @@ ClusterFactoryImplBase::selectDnsResolver(const envoy::config::cluster::v3::Clus return context.dnsResolver(); } +absl::StatusOr ClusterFactoryImplBase::selectDnsResolver( + const envoy::config::core::v3::TypedExtensionConfig& typed_dns_resolver_config, + ClusterFactoryContext& context) { + if (typed_dns_resolver_config.has_typed_config()) { + Network::DnsResolverFactory& dns_resolver_factory = + Network::createDnsResolverFactoryFromTypedConfig(typed_dns_resolver_config); + auto& server_context = context.serverFactoryContext(); + return dns_resolver_factory.createDnsResolver(server_context.mainThreadDispatcher(), + server_context.api(), typed_dns_resolver_config); + } + return context.dnsResolver(); +} + absl::StatusOr> ClusterFactoryImplBase::create(const envoy::config::cluster::v3::Cluster& cluster, ClusterFactoryContext& context) { diff --git a/source/common/upstream/cluster_factory_impl.h b/source/common/upstream/cluster_factory_impl.h index b4cd69ecadfbf..be21c1d08a2c6 100644 --- a/source/common/upstream/cluster_factory_impl.h +++ b/source/common/upstream/cluster_factory_impl.h @@ -111,6 +111,10 @@ class ClusterFactoryImplBase : public ClusterFactory { selectDnsResolver(const envoy::config::cluster::v3::Cluster& cluster, ClusterFactoryContext& context); + absl::StatusOr + selectDnsResolver(const envoy::config::core::v3::TypedExtensionConfig& typed_dns_resolver_config, + ClusterFactoryContext& context); + // Upstream::ClusterFactory absl::StatusOr> create(const envoy::config::cluster::v3::Cluster& cluster, diff --git a/source/common/upstream/cluster_manager_impl.cc b/source/common/upstream/cluster_manager_impl.cc index 7d0a3dc6d14d8..11cd469e0c67b 100644 --- a/source/common/upstream/cluster_manager_impl.cc +++ b/source/common/upstream/cluster_manager_impl.cc @@ -13,6 +13,7 @@ #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/config/core/v3/protocol.pb.h" #include "envoy/event/dispatcher.h" +#include "envoy/grpc/async_client.h" #include "envoy/network/dns.h" #include "envoy/runtime/runtime.h" #include "envoy/stats/scope.h" @@ -42,6 +43,8 @@ #include "source/common/upstream/load_balancer_context_base.h" #include "source/common/upstream/priority_conn_pool_map_impl.h" +#include "absl/status/status.h" + #ifdef ENVOY_ENABLE_QUIC #include "source/common/http/conn_pool_grid.h" #include "source/common/http/http3/conn_pool.h" @@ -223,14 +226,11 @@ void ClusterManagerInitHelper::maybeFinishInitialize() { // If the first CDS response doesn't have any primary cluster, ClusterLoadAssignment // should be already paused by CdsApiImpl::onConfigUpdate(). Need to check that to // avoid double pause ClusterLoadAssignment. - Config::ScopedResume maybe_resume_eds_leds_sds; - if (cm_.adsMux()) { - const std::vector paused_xds_types{ - Config::getTypeUrl(), - Config::getTypeUrl(), - Config::getTypeUrl()}; - maybe_resume_eds_leds_sds = cm_.adsMux()->pause(paused_xds_types); - } + const std::vector paused_xds_types{ + Config::getTypeUrl(), + Config::getTypeUrl(), + Config::getTypeUrl()}; + Config::ScopedResume resume_eds_leds_sds = xds_manager_.pause(paused_xds_types); initializeSecondaryClusters(); } return; @@ -301,52 +301,50 @@ void ClusterManagerInitHelper::setPrimaryClustersInitializedCb( } } -ClusterManagerImpl::ClusterManagerImpl( - const envoy::config::bootstrap::v3::Bootstrap& bootstrap, ClusterManagerFactory& factory, - Server::Configuration::CommonFactoryContext& context, Stats::Store& stats, - ThreadLocal::Instance& tls, Runtime::Loader& runtime, const LocalInfo::LocalInfo& local_info, - AccessLog::AccessLogManager& log_manager, Event::Dispatcher& main_thread_dispatcher, - OptRef admin, Api::Api& api, Http::Context& http_context, - Grpc::Context& grpc_context, Router::Context& router_context, Server::Instance& server, - Config::XdsManager& xds_manager, absl::Status& creation_status) - : server_(server), factory_(factory), runtime_(runtime), stats_(stats), tls_(tls), - xds_manager_(xds_manager), random_(api.randomGenerator()), +ClusterManagerImpl::ClusterManagerImpl(const envoy::config::bootstrap::v3::Bootstrap& bootstrap, + ClusterManagerFactory& factory, + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status) + : context_(context), factory_(factory), runtime_(context.runtime()), + stats_(context.serverScope().store()), tls_(context.threadLocal()), + xds_manager_(context.xdsManager()), random_(context.api().randomGenerator()), deferred_cluster_creation_(bootstrap.cluster_manager().enable_deferred_cluster_creation()), bind_config_(bootstrap.cluster_manager().has_upstream_bind_config() ? absl::make_optional(bootstrap.cluster_manager().upstream_bind_config()) : absl::nullopt), - local_info_(local_info), cm_stats_(generateStats(*stats.rootScope())), - init_helper_(*this, + local_info_(context.localInfo()), cm_stats_(generateStats(*stats_.rootScope())), + init_helper_(xds_manager_, [this](ClusterManagerCluster& cluster) { return onClusterInit(cluster); }), - time_source_(main_thread_dispatcher.timeSource()), dispatcher_(main_thread_dispatcher), - http_context_(http_context), router_context_(router_context), - cluster_stat_names_(stats.symbolTable()), - cluster_config_update_stat_names_(stats.symbolTable()), - cluster_lb_stat_names_(stats.symbolTable()), - cluster_endpoint_stat_names_(stats.symbolTable()), - cluster_load_report_stat_names_(stats.symbolTable()), - cluster_circuit_breakers_stat_names_(stats.symbolTable()), - cluster_request_response_size_stat_names_(stats.symbolTable()), - cluster_timeout_budget_stat_names_(stats.symbolTable()), + time_source_(context.timeSource()), dispatcher_(context.mainThreadDispatcher()), + http_context_(context.httpContext()), router_context_(context.routerContext()), + cluster_stat_names_(stats_.symbolTable()), + cluster_config_update_stat_names_(stats_.symbolTable()), + cluster_lb_stat_names_(stats_.symbolTable()), + cluster_endpoint_stat_names_(stats_.symbolTable()), + cluster_load_report_stat_names_(stats_.symbolTable()), + cluster_circuit_breakers_stat_names_(stats_.symbolTable()), + cluster_request_response_size_stat_names_(stats_.symbolTable()), + cluster_timeout_budget_stat_names_(stats_.symbolTable()), common_lb_config_pool_( std::make_shared>( - main_thread_dispatcher)), + dispatcher_)), shutdown_(false) { - if (admin.has_value()) { + if (auto admin = context.admin(); admin.has_value()) { config_tracker_entry_ = admin->getConfigTracker().add( "clusters", [this](const Matchers::StringMatcher& name_matcher) { return dumpClusterConfigs(name_matcher); }); } async_client_manager_ = std::make_unique( - *this, tls, context, grpc_context.statNames(), bootstrap.grpc_async_client_manager_config()); + *this, context.threadLocal(), context, context.grpcContext().statNames(), + bootstrap.grpc_async_client_manager_config()); const auto& cm_config = bootstrap.cluster_manager(); if (cm_config.has_outlier_detection()) { const std::string event_log_file_path = cm_config.outlier_detection().event_log_path(); if (!event_log_file_path.empty()) { - auto outlier_or_error = - Outlier::EventLoggerImpl::create(log_manager, event_log_file_path, time_source_); + auto outlier_or_error = Outlier::EventLoggerImpl::create(context.accessLogManager(), + event_log_file_path, time_source_); SET_AND_RETURN_IF_NOT_OK(outlier_or_error.status(), creation_status); outlier_event_logger_ = std::move(*outlier_or_error); } @@ -458,8 +456,14 @@ ClusterManagerImpl::initialize(const envoy::config::bootstrap::v3::Bootstrap& bo cds_resources_locator = std::make_unique(std::move(url_or_error.value())); } - auto cds_or_error = - factory_.createCds(dyn_resources.cds_config(), cds_resources_locator.get(), *this); + // In case cds_config is configured and the new xDS-TP configs are used, + // then the CdsApi will need to track the resources, as the xDS-TP configs + // may be used for OD-CDS. If this is not set, the SotW update may override + // the OD-CDS resources. + const bool support_multi_ads_sources = + bootstrap.has_default_config_source() || !bootstrap.config_sources().empty(); + auto cds_or_error = factory_.createCds(dyn_resources.cds_config(), cds_resources_locator.get(), + *this, support_multi_ads_sources); RETURN_IF_NOT_OK_REF(cds_or_error.status()) cds_api_ = std::move(*cds_or_error); init_helper_.setCds(cds_api_.get()); @@ -505,11 +509,11 @@ absl::Status ClusterManagerImpl::initializeSecondaryClusters( auto factory_or_error = Config::Utility::factoryForGrpcApiConfigSource( *async_client_manager_, load_stats_config, *stats_.rootScope(), false, 0, false); RETURN_IF_NOT_OK_REF(factory_or_error.status()); - absl::StatusOr client_or_error = + absl::StatusOr client_or_error = factory_or_error.value()->createUncachedRawAsyncClient(); RETURN_IF_NOT_OK_REF(client_or_error.status()); load_stats_reporter_ = std::make_unique( - local_info_, *this, *stats_.rootScope(), std::move(*client_or_error), dispatcher_); + local_info_, *this, *stats_.rootScope(), std::move(client_or_error.value()), dispatcher_); } return absl::OkStatus(); } @@ -551,7 +555,7 @@ absl::Status ClusterManagerImpl::onClusterInit(ClusterManagerCluster& cm_cluster // This is used by cluster types such as EDS clusters to drain the connection pools of removed // hosts. cluster_data->second->member_update_cb_ = cluster.prioritySet().addMemberUpdateCb( - [&cluster, this](const HostVector&, const HostVector& hosts_removed) -> absl::Status { + [&cluster, this](const HostVector&, const HostVector& hosts_removed) { if (cluster.info()->lbConfig().close_connections_on_host_set_change()) { for (const auto& host_set : cluster.prioritySet().hostSetsPerPriority()) { // This will drain all tcp and http connection pools. @@ -568,7 +572,6 @@ absl::Status ClusterManagerImpl::onClusterInit(ClusterManagerCluster& cm_cluster postThreadLocalRemoveHosts(cluster, hosts_removed); } } - return absl::OkStatus(); }); // This is used by cluster types such as EDS clusters to update the cluster @@ -609,7 +612,6 @@ absl::Status ClusterManagerImpl::onClusterInit(ClusterManagerCluster& cm_cluster postThreadLocalClusterUpdate( cm_cluster, ThreadLocalClusterUpdateParams(priority, hosts_added, hosts_removed)); } - return absl::OkStatus(); }); // Finally, post updates cross-thread so the per-thread load balancers are ready. First we @@ -954,7 +956,7 @@ void ClusterManagerImpl::updateClusterCounts() { if (all_clusters_initialized && xds_manager_.adsMux()) { const auto type_url = Config::getTypeUrl(); if (resume_cds_ == nullptr && !warming_clusters_.empty()) { - resume_cds_ = xds_manager_.adsMux()->pause(type_url); + resume_cds_ = xds_manager_.pause(type_url); } else if (warming_clusters_.empty()) { resume_cds_.reset(); } @@ -1075,15 +1077,16 @@ void ClusterManagerImpl::drainConnections(const std::string& cluster, }); } -void ClusterManagerImpl::drainConnections(DrainConnectionsHostPredicate predicate) { +void ClusterManagerImpl::drainConnections(DrainConnectionsHostPredicate predicate, + ConnectionPool::DrainBehavior drain_behavior) { ENVOY_LOG_EVENT(debug, "drain_connections_call_for_all_clusters", "drainConnections called for all clusters"); - tls_.runOnAllThreads([predicate](OptRef cluster_manager) { - for (const auto& cluster_entry : cluster_manager->thread_local_clusters_) { - cluster_entry.second->drainConnPools(predicate, - ConnectionPool::DrainBehavior::DrainExistingConnections); - } - }); + tls_.runOnAllThreads( + [predicate, drain_behavior](OptRef cluster_manager) { + for (const auto& cluster_entry : cluster_manager->thread_local_clusters_) { + cluster_entry.second->drainConnPools(predicate, drain_behavior); + } + }); } absl::Status ClusterManagerImpl::checkActiveStaticCluster(const std::string& cluster) { @@ -1464,7 +1467,7 @@ ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::httpAsyncClient if (lazy_http_async_client_ == nullptr) { lazy_http_async_client_ = std::make_unique( cluster_info_, parent_.parent_.stats_, parent_.thread_local_dispatcher_, parent_.parent_, - parent_.parent_.server_.serverFactoryContext(), + parent_.parent_.context_, Router::ShadowWriterPtr{new Router::ShadowWriterImpl(parent_.parent_)}, parent_.parent_.http_context_, parent_.parent_.router_context_); } @@ -1482,13 +1485,13 @@ void ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::updateHost const std::string& name, uint32_t priority, PrioritySet::UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, absl::optional weighted_priority_health, + const HostVector& hosts_removed, absl::optional weighted_priority_health, absl::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map) { ENVOY_LOG(debug, "membership update for TLS cluster {} added {} removed {}", name, hosts_added.size(), hosts_removed.size()); priority_set_.updateHosts(priority, std::move(update_hosts_params), std::move(locality_weights), - hosts_added, hosts_removed, seed, weighted_priority_health, + hosts_added, hosts_removed, weighted_priority_health, overprovisioning_factor, std::move(cross_priority_host_map)); // If an LB is thread aware, create a new worker local LB on membership changes. if (lb_factory_ != nullptr && lb_factory_->recreateOnHostChange()) { @@ -1537,8 +1540,9 @@ ClusterManagerImpl::allocateOdCdsApi(OdCdsCreationFunction creation_function, // TODO(krnowak): Instead of creating a new handle every time, store the handles internally and // return an already existing one if the config or locator matches. Note that this may need a // way to clean up the unused handles, so we can close the unnecessary connections. - auto odcds_or_error = creation_function(odcds_config, odcds_resources_locator, *this, *this, - *stats_.rootScope(), validation_visitor); + auto odcds_or_error = + creation_function(odcds_config, odcds_resources_locator, xds_manager_, *this, *this, + *stats_.rootScope(), validation_visitor, context_); RETURN_IF_NOT_OK_REF(odcds_or_error.status()); return OdCdsApiHandleImpl::create(*this, std::move(*odcds_or_error)); } @@ -1794,8 +1798,8 @@ void ClusterManagerImpl::ThreadLocalClusterManagerImpl::updateClusterMembership( const auto& cluster_entry = thread_local_clusters_[name]; cluster_entry->updateHosts(name, priority, std::move(update_hosts_params), std::move(locality_weights), hosts_added, hosts_removed, - parent_.random_.random(), weighted_priority_health, - overprovisioning_factor, std::move(cross_priority_host_map)); + weighted_priority_health, overprovisioning_factor, + std::move(cross_priority_host_map)); } void ClusterManagerImpl::ThreadLocalClusterManagerImpl::onHostHealthFailure( @@ -2146,11 +2150,8 @@ void ClusterManagerImpl::ThreadLocalClusterManagerImpl::tcpConnPoolIsIdle( absl::StatusOr ProdClusterManagerFactory::clusterManagerFromProto( const envoy::config::bootstrap::v3::Bootstrap& bootstrap) { absl::Status creation_status = absl::OkStatus(); - auto cluster_manager_impl = std::unique_ptr{new ClusterManagerImpl( - bootstrap, *this, context_, stats_, tls_, context_.runtime(), context_.localInfo(), - context_.accessLogManager(), context_.mainThreadDispatcher(), context_.admin(), - context_.api(), http_context_, context_.grpcContext(), context_.routerContext(), server_, - context_.xdsManager(), creation_status)}; + auto cluster_manager_impl = std::unique_ptr{ + new ClusterManagerImpl(bootstrap, *this, context_, creation_status)}; RETURN_IF_NOT_OK(creation_status); return cluster_manager_impl; } @@ -2201,7 +2202,7 @@ Http::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateConnPool( dispatcher, context_.api().randomGenerator(), host, priority, options, transport_socket_options, state, source, alternate_protocols_cache, coptions, quic_stat_names_, *stats_.rootScope(), *quic_info, network_observer_registry, - server_.overloadManager()); + context_.overloadManager()); #else (void)quic_info; (void)network_observer_registry; @@ -2221,13 +2222,13 @@ Http::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateConnPool( return std::make_unique( dispatcher, context_.api().randomGenerator(), host, priority, options, transport_socket_options, state, origin, alternate_protocols_cache, - server_.overloadManager()); + context_.overloadManager()); } if (protocols.size() == 1 && protocols[0] == Http::Protocol::Http2 && context_.runtime().snapshot().featureEnabled("upstream.use_http2", 100)) { return Http::Http2::allocateConnPool(dispatcher, context_.api().randomGenerator(), host, priority, options, transport_socket_options, state, - server_.overloadManager(), origin, + context_.overloadManager(), origin, alternate_protocols_cache); } if (protocols.size() == 1 && protocols[0] == Http::Protocol::Http3 && @@ -2239,7 +2240,7 @@ Http::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateConnPool( return Http::Http3::allocateConnPool( dispatcher, context_.api().randomGenerator(), host, priority, options, transport_socket_options, state, quic_stat_names_, {}, *stats_.rootScope(), {}, *quic_info, - network_observer_registry, server_.overloadManager(), false); + network_observer_registry, context_.overloadManager(), false); #else UNREFERENCED_PARAMETER(source); // Should be blocked by configuration checking at an earlier point. @@ -2249,7 +2250,7 @@ Http::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateConnPool( ASSERT(protocols.size() == 1 && protocols[0] == Http::Protocol::Http11); return Http::Http1::allocateConnPool(dispatcher, context_.api().randomGenerator(), host, priority, options, transport_socket_options, state, - server_.overloadManager()); + context_.overloadManager()); } Tcp::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateTcpConnPool( @@ -2261,7 +2262,7 @@ Tcp::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateTcpConnPool( ENVOY_LOG_MISC(debug, "Allocating TCP conn pool"); return std::make_unique(dispatcher, host, priority, options, transport_socket_options, state, tcp_pool_idle_timeout, - server_.overloadManager()); + context_.overloadManager()); } absl::StatusOr> @@ -2275,11 +2276,11 @@ ProdClusterManagerFactory::clusterFromProto(const envoy::config::cluster::v3::Cl absl::StatusOr ProdClusterManagerFactory::createCds(const envoy::config::core::v3::ConfigSource& cds_config, const xds::core::v3::ResourceLocator* cds_resources_locator, - ClusterManager& cm) { + ClusterManager& cm, bool support_multi_ads_sources) { // TODO(htuch): Differentiate static vs. dynamic validation visitors. return CdsApiImpl::create(cds_config, cds_resources_locator, cm, *stats_.rootScope(), context_.messageValidationContext().dynamicValidationVisitor(), - context_); + context_, support_multi_ads_sources); } } // namespace Upstream diff --git a/source/common/upstream/cluster_manager_impl.h b/source/common/upstream/cluster_manager_impl.h index 4081b53e36488..2090e81bd82d4 100644 --- a/source/common/upstream/cluster_manager_impl.h +++ b/source/common/upstream/cluster_manager_impl.h @@ -53,15 +53,11 @@ class ProdClusterManagerFactory : public ClusterManagerFactory { using LazyCreateDnsResolver = std::function; ProdClusterManagerFactory(Server::Configuration::ServerFactoryContext& context, - Stats::Store& stats, ThreadLocal::Instance& tls, - Http::Context& http_context, LazyCreateDnsResolver dns_resolver_fn, - Ssl::ContextManager& ssl_context_manager, - Quic::QuicStatNames& quic_stat_names, Server::Instance& server) - : context_(context), stats_(stats), tls_(tls), http_context_(http_context), - dns_resolver_fn_(dns_resolver_fn), ssl_context_manager_(ssl_context_manager), + LazyCreateDnsResolver dns_resolver_fn, + Quic::QuicStatNames& quic_stat_names) + : context_(context), stats_(context.serverScope().store()), dns_resolver_fn_(dns_resolver_fn), quic_stat_names_(quic_stat_names), - alternate_protocols_cache_manager_(context.httpServerPropertiesCacheManager()), - server_(server) {} + alternate_protocols_cache_manager_(context.httpServerPropertiesCacheManager()) {} // Upstream::ClusterManagerFactory absl::StatusOr @@ -88,19 +84,14 @@ class ProdClusterManagerFactory : public ClusterManagerFactory { Outlier::EventLoggerSharedPtr outlier_event_logger, bool added_via_api) override; absl::StatusOr createCds(const envoy::config::core::v3::ConfigSource& cds_config, const xds::core::v3::ResourceLocator* cds_resources_locator, - ClusterManager& cm) override; + ClusterManager& cm, bool support_multi_ads_sources) override; protected: Server::Configuration::ServerFactoryContext& context_; Stats::Store& stats_; - ThreadLocal::Instance& tls_; - Http::Context& http_context_; - LazyCreateDnsResolver dns_resolver_fn_; - Ssl::ContextManager& ssl_context_manager_; Quic::QuicStatNames& quic_stat_names_; Http::HttpServerPropertiesCacheManager& alternate_protocols_cache_manager_; - Server::Instance& server_; }; // For friend declaration in ClusterManagerInitHelper. @@ -142,9 +133,9 @@ class ClusterManagerInitHelper : Logger::Loggable { * initialized. The cluster manager can use this for post-init processing. */ ClusterManagerInitHelper( - ClusterManager& cm, + Config::XdsManager& xds_manager, const std::function& per_cluster_init_callback) - : cm_(cm), per_cluster_init_callback_(per_cluster_init_callback) {} + : xds_manager_(xds_manager), per_cluster_init_callback_(per_cluster_init_callback) {} enum class State { // Initial state. During this state all static clusters are loaded. Any primary clusters @@ -188,7 +179,7 @@ class ClusterManagerInitHelper : Logger::Loggable { void maybeFinishInitialize(); absl::Status onClusterInit(ClusterManagerCluster& cluster); - ClusterManager& cm_; + Config::XdsManager& xds_manager_; std::function per_cluster_init_callback_; CdsApi* cds_{}; ClusterManager::PrimaryClustersReadyCallback primary_clusters_initialized_callback_; @@ -370,7 +361,8 @@ class ClusterManagerImpl : public ClusterManager, void drainConnections(const std::string& cluster, DrainConnectionsHostPredicate predicate) override; - void drainConnections(DrainConnectionsHostPredicate predicate) override; + void drainConnections(DrainConnectionsHostPredicate predicate, + ConnectionPool::DrainBehavior drain_behavior) override; absl::Status checkActiveStaticCluster(const std::string& cluster) override; @@ -398,14 +390,8 @@ class ClusterManagerImpl : public ClusterManager, // clusterManagerFromProto() static method. The init() method must be called after construction. ClusterManagerImpl(const envoy::config::bootstrap::v3::Bootstrap& bootstrap, ClusterManagerFactory& factory, - Server::Configuration::CommonFactoryContext& context, Stats::Store& stats, - ThreadLocal::Instance& tls, Runtime::Loader& runtime, - const LocalInfo::LocalInfo& local_info, - AccessLog::AccessLogManager& log_manager, - Event::Dispatcher& main_thread_dispatcher, OptRef admin, - Api::Api& api, Http::Context& http_context, Grpc::Context& grpc_context, - Router::Context& router_context, Server::Instance& server, - Config::XdsManager& xds_manager, absl::Status& creation_status); + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status); virtual void postThreadLocalRemoveHosts(const Cluster& cluster, const HostVector& hosts_removed); @@ -618,7 +604,7 @@ class ClusterManagerImpl : public ClusterManager, PrioritySet::UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - uint64_t seed, absl::optional weighted_priority_health, + absl::optional weighted_priority_health, absl::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map); @@ -922,7 +908,7 @@ class ClusterManagerImpl : public ClusterManager, bool deferralIsSupportedForCluster(const ClusterInfoConstSharedPtr& info) const; - Server::Instance& server_; + Server::Configuration::ServerFactoryContext& context_; ClusterManagerFactory& factory_; Runtime::Loader& runtime_; Stats::Store& stats_; diff --git a/source/common/upstream/health_checker_impl.cc b/source/common/upstream/health_checker_impl.cc index 86f08d1467308..f66786a63a5b7 100644 --- a/source/common/upstream/health_checker_impl.cc +++ b/source/common/upstream/health_checker_impl.cc @@ -26,6 +26,7 @@ #include "source/common/runtime/runtime_features.h" #include "source/common/upstream/host_utility.h" +#include "absl/status/statusor.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" @@ -91,26 +92,42 @@ HealthCheckerFactory::create(const envoy::config::core::v3::HealthCheck& health_ return factory->createCustomHealthChecker(health_check_config, *context); } +absl::StatusOr> +PayloadMatcher::decodePayload(const envoy::config::core::v3::HealthCheck::Payload& payload) { + std::vector decoded; + if (payload.has_text()) { + decoded = Hex::decode(payload.text()); + if (decoded.empty()) { + return absl::InvalidArgumentError(fmt::format("invalid hex string '{}'", payload.text())); + } + } else { + decoded.assign(payload.binary().begin(), payload.binary().end()); + } + return decoded; +} + absl::StatusOr PayloadMatcher::loadProtoBytes( const Protobuf::RepeatedPtrField& byte_array) { - MatchSegments result; - - for (const auto& entry : byte_array) { - std::vector decoded; - if (entry.has_text()) { - decoded = Hex::decode(entry.text()); - if (decoded.empty()) { - return absl::InvalidArgumentError(fmt::format("invalid hex string '{}'", entry.text())); - } - } else { - decoded.assign(entry.binary().begin(), entry.binary().end()); + MatchSegments segments; + for (const auto& payload : byte_array) { + auto decoded_or_error = decodePayload(payload); + if (!decoded_or_error.ok()) { + return decoded_or_error.status(); } - if (!decoded.empty()) { - result.push_back(decoded); + if (!decoded_or_error.value().empty()) { + segments.emplace_back(std::move(decoded_or_error.value())); } } + return segments; +} - return result; +absl::StatusOr PayloadMatcher::loadProtoBytes( + const envoy::config::core::v3::HealthCheck::Payload& single_payload) { + auto decoded_or_error = decodePayload(single_payload); + if (!decoded_or_error.ok()) { + return decoded_or_error.status(); + } + return MatchSegments{std::move(decoded_or_error.value())}; } bool PayloadMatcher::match(const MatchSegments& expected, const Buffer::Instance& buffer) { diff --git a/source/common/upstream/health_checker_impl.h b/source/common/upstream/health_checker_impl.h index 040173b0d95a4..259e9df96198f 100644 --- a/source/common/upstream/health_checker_impl.h +++ b/source/common/upstream/health_checker_impl.h @@ -161,7 +161,13 @@ class PayloadMatcher { static absl::StatusOr loadProtoBytes( const Protobuf::RepeatedPtrField& byte_array); + static absl::StatusOr + loadProtoBytes(const envoy::config::core::v3::HealthCheck::Payload& single_payload); static bool match(const MatchSegments& expected, const Buffer::Instance& buffer); + +private: + static absl::StatusOr> + decodePayload(const envoy::config::core::v3::HealthCheck::Payload& payload); }; } // namespace Upstream diff --git a/source/common/upstream/health_discovery_service.cc b/source/common/upstream/health_discovery_service.cc index e27918cae5eaa..e3c0d9e2dcf61 100644 --- a/source/common/upstream/health_discovery_service.cc +++ b/source/common/upstream/health_discovery_service.cc @@ -527,9 +527,8 @@ void HdsCluster::updateHosts( // Update the priority set. hosts_per_locality_ = std::make_shared(std::move(hosts_by_locality), false); - priority_set_.updateHosts( - 0, HostSetImpl::partitionHosts(hosts_, hosts_per_locality_), {}, hosts_added, hosts_removed, - server_context_.api().randomGenerator().random(), absl::nullopt, absl::nullopt); + priority_set_.updateHosts(0, HostSetImpl::partitionHosts(hosts_, hosts_per_locality_), {}, + hosts_added, hosts_removed, absl::nullopt, absl::nullopt); } ClusterSharedPtr HdsCluster::create() { return nullptr; } @@ -558,8 +557,7 @@ void HdsCluster::initialize(std::function callback) { } // Use the ungrouped and grouped hosts lists to retain locality structure in the priority set. priority_set_.updateHosts(0, HostSetImpl::partitionHosts(hosts_, hosts_per_locality_), {}, - *hosts_, {}, server_context_.api().randomGenerator().random(), - absl::nullopt, absl::nullopt); + *hosts_, {}, absl::nullopt, absl::nullopt); initialized_ = true; } diff --git a/source/common/upstream/load_stats_reporter.cc b/source/common/upstream/load_stats_reporter.cc index c77d099caf9b5..6cc62f14709ff 100644 --- a/source/common/upstream/load_stats_reporter.cc +++ b/source/common/upstream/load_stats_reporter.cc @@ -3,6 +3,7 @@ #include "envoy/service/load_stats/v3/lrs.pb.h" #include "envoy/stats/scope.h" +#include "source/common/network/utility.h" #include "source/common/protobuf/protobuf.h" namespace Envoy { @@ -10,7 +11,7 @@ namespace Upstream { LoadStatsReporter::LoadStatsReporter(const LocalInfo::LocalInfo& local_info, ClusterManager& cluster_manager, Stats::Scope& scope, - Grpc::RawAsyncClientPtr async_client, + Grpc::RawAsyncClientSharedPtr&& async_client, Event::Dispatcher& dispatcher) : cm_(cluster_manager), stats_{ALL_LOAD_REPORTER_STATS(POOL_COUNTER_PREFIX(scope, "load_reporter."))}, @@ -84,48 +85,84 @@ void LoadStatsReporter::sendLoadStatsRequest() { uint64_t rq_active = 0; uint64_t rq_issued = 0; LoadMetricStats::StatMap load_metrics; + + envoy::config::endpoint::v3::UpstreamLocalityStats locality_stats; + locality_stats.mutable_locality()->MergeFrom(hosts[0]->locality()); + locality_stats.set_priority(host_set->priority()); + for (const HostSharedPtr& host : hosts) { uint64_t host_rq_success = host->stats().rq_success_.latch(); uint64_t host_rq_error = host->stats().rq_error_.latch(); uint64_t host_rq_active = host->stats().rq_active_.value(); uint64_t host_rq_issued = host->stats().rq_total_.latch(); - rq_success += host_rq_success; - rq_error += host_rq_error; - rq_active += host_rq_active; - rq_issued += host_rq_issued; - if (host_rq_success + host_rq_error + host_rq_active != 0) { + + // Check if the host has any load stats updates. If the host has no load stats updates, we + // skip it. + bool endpoint_has_updates = + (host_rq_success + host_rq_error + host_rq_active + host_rq_issued) != 0; + + if (endpoint_has_updates) { + rq_success += host_rq_success; + rq_error += host_rq_error; + rq_active += host_rq_active; + rq_issued += host_rq_issued; + + envoy::config::endpoint::v3::UpstreamEndpointStats* upstream_endpoint_stats = nullptr; + // Set the upstream endpoint stats if we are reporting endpoint granularity. + if (message_ && message_->report_endpoint_granularity()) { + upstream_endpoint_stats = locality_stats.add_upstream_endpoint_stats(); + Network::Utility::addressToProtobufAddress( + *host->address(), *upstream_endpoint_stats->mutable_address()); + upstream_endpoint_stats->set_total_successful_requests(host_rq_success); + upstream_endpoint_stats->set_total_error_requests(host_rq_error); + upstream_endpoint_stats->set_total_requests_in_progress(host_rq_active); + upstream_endpoint_stats->set_total_issued_requests(host_rq_issued); + } + const std::unique_ptr latched_stats = host->loadMetricStats().latch(); if (latched_stats != nullptr) { for (const auto& metric : *latched_stats) { - const std::string& name = metric.first; - LoadMetricStats::Stat& stat = load_metrics[name]; - stat.num_requests_with_metric += metric.second.num_requests_with_metric; - stat.total_metric_value += metric.second.total_metric_value; + const auto& metric_name = metric.first; + const auto& metric_value = metric.second; + + // Add the metric to the load metrics map. + LoadMetricStats::Stat& stat = load_metrics[metric_name]; + stat.num_requests_with_metric += metric_value.num_requests_with_metric; + stat.total_metric_value += metric_value.total_metric_value; + + // If we are reporting endpoint granularity, add the metric to the upstream endpoint + // stats. + if (upstream_endpoint_stats != nullptr) { + auto* endpoint_load_metric = upstream_endpoint_stats->add_load_metric_stats(); + endpoint_load_metric->set_metric_name(metric_name); + endpoint_load_metric->set_num_requests_finished_with_metric( + metric_value.num_requests_with_metric); + endpoint_load_metric->set_total_metric_value(metric_value.total_metric_value); + } } } } } + bool should_send_locality_stats = rq_success + rq_error + rq_active != 0; if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.report_load_with_rq_issued")) { should_send_locality_stats = rq_issued != 0; } if (should_send_locality_stats) { - auto* locality_stats = cluster_stats->add_upstream_locality_stats(); - locality_stats->mutable_locality()->MergeFrom(hosts[0]->locality()); - locality_stats->set_priority(host_set->priority()); - locality_stats->set_total_successful_requests(rq_success); - locality_stats->set_total_error_requests(rq_error); - locality_stats->set_total_requests_in_progress(rq_active); - locality_stats->set_total_issued_requests(rq_issued); + locality_stats.set_total_successful_requests(rq_success); + locality_stats.set_total_error_requests(rq_error); + locality_stats.set_total_requests_in_progress(rq_active); + locality_stats.set_total_issued_requests(rq_issued); for (const auto& metric : load_metrics) { - auto* load_metric_stats = locality_stats->add_load_metric_stats(); + auto* load_metric_stats = locality_stats.add_load_metric_stats(); load_metric_stats->set_metric_name(metric.first); load_metric_stats->set_num_requests_finished_with_metric( metric.second.num_requests_with_metric); load_metric_stats->set_total_metric_value(metric.second.total_metric_value); } + cluster_stats->add_upstream_locality_stats()->MergeFrom(locality_stats); } } } @@ -150,8 +187,8 @@ void LoadStatsReporter::sendLoadStatsRequest() { ENVOY_LOG(trace, "Sending LoadStatsRequest: {}", request_.DebugString()); stream_->sendMessage(request_, false); stats_.responses_.inc(); - // When the connection is established, the message has not yet been read so we - // will not have a load reporting period. + // When the connection is established, the message has not yet been read so we will not have a + // load reporting period. if (message_) { startLoadReportPeriod(); } @@ -261,6 +298,5 @@ void LoadStatsReporter::onRemoteClose(Grpc::Status::GrpcStatus status, const std setRetryTimer(); } } - } // namespace Upstream } // namespace Envoy diff --git a/source/common/upstream/load_stats_reporter.h b/source/common/upstream/load_stats_reporter.h index 4f85f158a6f38..eeae2b6050c39 100644 --- a/source/common/upstream/load_stats_reporter.h +++ b/source/common/upstream/load_stats_reporter.h @@ -34,7 +34,7 @@ class LoadStatsReporter Logger::Loggable { public: LoadStatsReporter(const LocalInfo::LocalInfo& local_info, ClusterManager& cluster_manager, - Stats::Scope& scope, Grpc::RawAsyncClientPtr async_client, + Stats::Scope& scope, Grpc::RawAsyncClientSharedPtr&& async_client, Event::Dispatcher& dispatcher); // Grpc::AsyncStreamCallbacks diff --git a/source/common/upstream/od_cds_api_impl.cc b/source/common/upstream/od_cds_api_impl.cc index da4f452f924eb..9e7cd340eadda 100644 --- a/source/common/upstream/od_cds_api_impl.cc +++ b/source/common/upstream/od_cds_api_impl.cc @@ -11,31 +11,34 @@ namespace Upstream { absl::StatusOr OdCdsApiImpl::create(const envoy::config::core::v3::ConfigSource& odcds_config, OptRef odcds_resources_locator, - ClusterManager& cm, MissingClusterNotifier& notifier, Stats::Scope& scope, - ProtobufMessage::ValidationVisitor& validation_visitor) { + Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, + ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::ServerFactoryContext&) { absl::Status creation_status = absl::OkStatus(); - auto ret = OdCdsApiSharedPtr(new OdCdsApiImpl(odcds_config, odcds_resources_locator, cm, notifier, - scope, validation_visitor, creation_status)); + auto ret = + OdCdsApiSharedPtr(new OdCdsApiImpl(odcds_config, odcds_resources_locator, xds_manager, cm, + notifier, scope, validation_visitor, creation_status)); RETURN_IF_NOT_OK(creation_status); return ret; } OdCdsApiImpl::OdCdsApiImpl(const envoy::config::core::v3::ConfigSource& odcds_config, OptRef odcds_resources_locator, - ClusterManager& cm, MissingClusterNotifier& notifier, - Stats::Scope& scope, + Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, absl::Status& creation_status) : Envoy::Config::SubscriptionBase(validation_visitor, "name"), - helper_(cm, "odcds"), cm_(cm), notifier_(notifier), + helper_(cm, xds_manager, "odcds"), notifier_(notifier), scope_(scope.createScope("cluster_manager.odcds.")) { // TODO(krnowak): Move the subscription setup to CdsApiHelper. Maybe make CdsApiHelper a base // class for CDS and ODCDS. const auto resource_name = getResourceName(); absl::StatusOr subscription_or_error; if (!odcds_resources_locator.has_value()) { - subscription_or_error = cm_.subscriptionFactory().subscriptionFromConfigSource( + subscription_or_error = cm.subscriptionFactory().subscriptionFromConfigSource( odcds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, {}); } else { subscription_or_error = cm.subscriptionFactory().collectionSubscriptionFromUrl( @@ -114,8 +117,236 @@ void OdCdsApiImpl::updateOnDemand(std::string cluster_name) { subscription_->requestOnDemandUpdate({std::move(cluster_name)}); return; } - PANIC("corrupt enum"); } +// A class that maintains all the od-cds xDS-TP based singleton subscriptions, +// and update the cluster-manager when the resources are updated. +// The object will only be accessed by the main thread. It should also be a +// singleton object that is used by all the filters that need to access od-cds +// over xdstp-based config sources, and will only be allocated for the first +// occurrence of the filter. +class XdstpOdCdsApiImpl::XdstpOdcdsSubscriptionsManager : public Singleton::Instance, + Logger::Loggable { +public: + XdstpOdcdsSubscriptionsManager(Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, + ProtobufMessage::ValidationVisitor& validation_visitor) + : xds_manager_(xds_manager), helper_(cm, xds_manager, "odcds-xdstp"), notifier_(notifier), + scope_(scope.createScope("cluster_manager.odcds.")), + validation_visitor_(validation_visitor) {} + + absl::Status onResourceUpdate(absl::string_view resource_name, + const Config::DecodedResourceRef& resource, + const std::string& system_version_info) { + auto [_, exception_msgs] = helper_.onConfigUpdate({resource}, {}, system_version_info); + if (!exception_msgs.empty()) { + return absl::InvalidArgumentError(fmt::format("Error adding/updating cluster {} - {}", + resource_name, + absl::StrJoin(exception_msgs, ", "))); + } + return absl::OkStatus(); + } + + absl::Status onResourceRemoved(absl::string_view resource_name, + const std::string& system_version_info) { + // TODO(adisuissa): add direct `onResourceRemove(resource_name)` to `helper_`. + Protobuf::RepeatedPtrField removed_resource_list; + removed_resource_list.Add(std::string(resource_name)); + auto [_, exception_msgs] = + helper_.onConfigUpdate({}, removed_resource_list, system_version_info); + // Removal of a cluster should not result in an error. + ASSERT(exception_msgs.empty()); + notifier_.notifyMissingCluster(resource_name); + return absl::OkStatus(); + } + + void onFailure(absl::string_view resource_name) { + ENVOY_LOG(trace, "ODCDS-manager: failure for resource: {}", resource_name); + // This function will only be invoked if the resource wasn't previously updated or removed. + // Remove the resource, so if there are other resources waiting for it, + // their initialization can proceed. + notifier_.notifyMissingCluster(resource_name); + } + + void addSubscription(absl::string_view resource_name, bool old_ads) { + if (subscriptions_.contains(resource_name)) { + ENVOY_LOG(debug, "ODCDS-manager: resource {} is already subscribed to, skipping", + resource_name); + return; + } + ENVOY_LOG(trace, "ODCDS-manager: adding a subscription for resource {}", resource_name); + // Subscribe using the xds-manager. + auto subscription = + std::make_unique(*this, resource_name, validation_visitor_); + absl::Status status = subscription->initializeSubscription(old_ads); + if (status.ok()) { + subscriptions_.emplace(std::string(resource_name), std::move(subscription)); + } else { + // There was an error while subscribing. This could be, for example, when + // the cluster_name isn't a valid xdstp resource, or its config-source was + // not added to the bootstrap's config_sources. + ENVOY_LOG(info, + "ODCDS-manager: xDS-TP resource {} could not be registered: {}. Treating as " + "missing cluster", + resource_name, status.message()); + onFailure(resource_name); + } + } + +private: + // A singleton subscription handler. + class PerSubscriptionData : Envoy::Config::SubscriptionBase { + public: + PerSubscriptionData(XdstpOdcdsSubscriptionsManager& parent, absl::string_view resource_name, + ProtobufMessage::ValidationVisitor& validation_visitor) + : Envoy::Config::SubscriptionBase(validation_visitor, + "name"), + parent_(parent), resource_name_(resource_name) {} + + absl::Status initializeSubscription(bool old_ads) { + const auto resource_type = getResourceName(); + // If old_ads is set, creates a subscription using the staticAdsConfigSource. + // Otherwise, the subscribeToSingletonResource will take care of + // subscription via the ADS source. + absl::StatusOr subscription_or_error = + parent_.xds_manager_.subscribeToSingletonResource( + resource_name_, + old_ads + ? makeOptRef(staticAdsConfigSource()) + : absl::nullopt, + Grpc::Common::typeUrl(resource_type), *parent_.scope_, *this, resource_decoder_, {}); + RETURN_IF_NOT_OK_REF(subscription_or_error.status()); + subscription_ = std::move(subscription_or_error.value()); + subscription_->start({resource_name_}); + return absl::OkStatus(); + } + + private: + const envoy::config::core::v3::ConfigSource& staticAdsConfigSource() { + CONSTRUCT_ON_FIRST_USE(envoy::config::core::v3::ConfigSource, + []() -> envoy::config::core::v3::ConfigSource { + envoy::config::core::v3::ConfigSource ads; + ads.mutable_ads(); + return ads; + }()); + } + + // Config::SubscriptionCallbacks + absl::Status onConfigUpdate(const std::vector& resources, + const std::string& version_info) override { + // As this is a singleton subscription, the response can either contain 1 + // resource that should be updated or 0 (implying the resource should be + // removed). + ASSERT(resources.empty() || (resources.size() == 1)); + resource_was_updated_ = true; + if (resources.empty()) { + ENVOY_LOG(trace, "ODCDS-manager: removing a single resource: {}", resource_name_); + return parent_.onResourceRemoved(resource_name_, version_info); + } + // A single cluster update. + ENVOY_LOG(trace, "ODCDS-manager: updating a single resource: {}", resource_name_); + return parent_.onResourceUpdate(resource_name_, resources[0], version_info); + } + absl::Status onConfigUpdate(const std::vector& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override { + // As this is a singleton subscription, the update can either contain 1 + // added resource, or remove the resource. + ASSERT(added_resources.size() + removed_resources.size() == 1); + resource_was_updated_ = true; + if (!removed_resources.empty()) { + ENVOY_LOG(trace, "ODCDS-manager: removing a single resource: {}", resource_name_); + return parent_.onResourceRemoved(resource_name_, system_version_info); + } + // A single cluster update. + ENVOY_LOG(trace, "ODCDS-manager: updating a single resource: {}", resource_name_); + return parent_.onResourceUpdate(resource_name_, added_resources[0], system_version_info); + } + void onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason reason, + const EnvoyException* e) override { + ASSERT(reason != Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure); + ENVOY_LOG(trace, "ODCDS-manager: error while fetching a single resource {}: {}", + resource_name_, e->what()); + // If the resource wasn't previously updated, this sends a notification that it was removed, + // so if there are any resources waiting for this one, they can proceed. + if (!resource_was_updated_) { + resource_was_updated_ = true; + parent_.onFailure(resource_name_); + } + } + + XdstpOdcdsSubscriptionsManager& parent_; + // TODO(adisuissa): this can be converted to an absl::string_view and point to the + // subscriptions_ map key. + const std::string resource_name_; + Config::SubscriptionPtr subscription_; + bool resource_was_updated_{false}; + }; + using PerSubscriptionDataPtr = std::unique_ptr; + + Config::XdsManager& xds_manager_; + CdsApiHelper helper_; + MissingClusterNotifier& notifier_; + Stats::ScopeSharedPtr scope_; + ProtobufMessage::ValidationVisitor& validation_visitor_; + // Maps a resource name to its subscription data. + absl::flat_hash_map subscriptions_; +}; + +// Register the XdstpOdcdsSubscriptionsManager singleton. +SINGLETON_MANAGER_REGISTRATION(xdstp_odcds_subscriptions_manager); + +absl::StatusOr +XdstpOdCdsApiImpl::create(const envoy::config::core::v3::ConfigSource& config_source, + OptRef, Config::XdsManager& xds_manager, + ClusterManager& cm, MissingClusterNotifier& notifier, Stats::Scope& scope, + ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::ServerFactoryContext& server_factory_context) { + absl::Status creation_status = absl::OkStatus(); + // TODO(adisuissa): convert the config_source to optional. + const bool old_ads = config_source.config_source_specifier_case() == + envoy::config::core::v3::ConfigSource::ConfigSourceSpecifierCase::kAds; + auto ret = OdCdsApiSharedPtr(new XdstpOdCdsApiImpl(xds_manager, cm, notifier, scope, + server_factory_context, old_ads, + validation_visitor, creation_status)); + RETURN_IF_NOT_OK(creation_status); + return ret; +} + +XdstpOdCdsApiImpl::XdstpOdCdsApiImpl(Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, + Server::Configuration::ServerFactoryContext& server_context, + bool old_ads, + ProtobufMessage::ValidationVisitor& validation_visitor, + absl::Status& creation_status) + : old_ads_(old_ads) { + // Create a singleton xdstp-based od-cds handler. This will be accessed by + // the main thread and used by all the filters that need to access od-cds + // over xdstp-based config sources. + // The singleton object will handle all the subscriptions to OD-CDS + // resources, and will apply the updates to the cluster-manager. + subscriptions_manager_ = + subscriptionsManager(server_context, xds_manager, cm, notifier, scope, validation_visitor); + // This will always succeed as the xDS-TP config-source matching the resource name + // will only be known when that resource is subscribed to. + creation_status = absl::OkStatus(); +} + +XdstpOdCdsApiImpl::XdstpOdcdsSubscriptionsManagerSharedPtr +XdstpOdCdsApiImpl::subscriptionsManager(Server::Configuration::ServerFactoryContext& server_context, + Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, + ProtobufMessage::ValidationVisitor& validation_visitor) { + return server_context.singletonManager().getTyped( + SINGLETON_MANAGER_REGISTERED_NAME(xdstp_odcds_subscriptions_manager), + [&xds_manager, &cm, ¬ifier, &scope, &validation_visitor] { + return std::make_shared(xds_manager, cm, notifier, scope, + validation_visitor); + }); +} + +void XdstpOdCdsApiImpl::updateOnDemand(std::string cluster_name) { + subscriptions_manager_->addSubscription(cluster_name, old_ads_); +} } // namespace Upstream } // namespace Envoy diff --git a/source/common/upstream/od_cds_api_impl.h b/source/common/upstream/od_cds_api_impl.h index 4bb7082f81391..577d37be06a0b 100644 --- a/source/common/upstream/od_cds_api_impl.h +++ b/source/common/upstream/od_cds_api_impl.h @@ -8,6 +8,7 @@ #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/config/subscription.h" #include "envoy/protobuf/message_validator.h" +#include "envoy/server/factory_context.h" #include "envoy/stats/scope.h" #include "envoy/upstream/cluster_manager.h" @@ -36,9 +37,10 @@ class OdCdsApiImpl : public OdCdsApi, public: static absl::StatusOr create(const envoy::config::core::v3::ConfigSource& odcds_config, - OptRef odcds_resources_locator, ClusterManager& cm, - MissingClusterNotifier& notifier, Stats::Scope& scope, - ProtobufMessage::ValidationVisitor& validation_visitor); + OptRef odcds_resources_locator, + Config::XdsManager& xds_manager, ClusterManager& cm, MissingClusterNotifier& notifier, + Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::ServerFactoryContext& server_factory_context); // Upstream::OdCdsApi void updateOnDemand(std::string cluster_name) override; @@ -54,14 +56,14 @@ class OdCdsApiImpl : public OdCdsApi, const EnvoyException* e) override; OdCdsApiImpl(const envoy::config::core::v3::ConfigSource& odcds_config, - OptRef odcds_resources_locator, ClusterManager& cm, + OptRef odcds_resources_locator, + Config::XdsManager& xds_manager, ClusterManager& cm, MissingClusterNotifier& notifier, Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, absl::Status& creation_status); void sendAwaiting(); CdsApiHelper helper_; - ClusterManager& cm_; MissingClusterNotifier& notifier_; Stats::ScopeSharedPtr scope_; StartStatus status_{StartStatus::NotStarted}; @@ -69,5 +71,44 @@ class OdCdsApiImpl : public OdCdsApi, Config::SubscriptionPtr subscription_; }; +/** + * ODCDS API implementation that fetches via Subscription for xDS-TP based + * configs and resources. + */ +class XdstpOdCdsApiImpl : public OdCdsApi { +public: + static absl::StatusOr + create(const envoy::config::core::v3::ConfigSource&, OptRef, + Config::XdsManager& xds_manager, ClusterManager& cm, MissingClusterNotifier& notifier, + Stats::Scope& scope, ProtobufMessage::ValidationVisitor& validation_visitor, + Server::Configuration::ServerFactoryContext& server_factory_context); + + // Upstream::OdCdsApi + void updateOnDemand(std::string cluster_name) override; + +private: + class XdstpOdcdsSubscriptionsManager; + using XdstpOdcdsSubscriptionsManagerSharedPtr = std::shared_ptr; + + XdstpOdCdsApiImpl(Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, + Server::Configuration::ServerFactoryContext& server_context, bool old_ads, + ProtobufMessage::ValidationVisitor& validation_visitor, + absl::Status& creation_status); + + // Fetches, and potentially creates, the singleton subscriptions manager. + // The arguments will be passed to the subscriptions manager's constructor, if + // it is the first time it is initialized. + static XdstpOdcdsSubscriptionsManagerSharedPtr + subscriptionsManager(Server::Configuration::ServerFactoryContext& context, + Config::XdsManager& xds_manager, ClusterManager& cm, + MissingClusterNotifier& notifier, Stats::Scope& scope, + ProtobufMessage::ValidationVisitor& validation_visitor); + + // A singleton through which all subscriptions will be processed. + XdstpOdcdsSubscriptionsManagerSharedPtr subscriptions_manager_; + const bool old_ads_; +}; + } // namespace Upstream } // namespace Envoy diff --git a/source/common/upstream/outlier_detection_impl.cc b/source/common/upstream/outlier_detection_impl.cc index 2135f8f016da5..1a8bed06adeb8 100644 --- a/source/common/upstream/outlier_detection_impl.cc +++ b/source/common/upstream/outlier_detection_impl.cc @@ -324,7 +324,7 @@ void DetectorImpl::initialize(Cluster& cluster) { }); } member_update_cb_ = cluster.prioritySet().addMemberUpdateCb( - [this](const HostVector& hosts_added, const HostVector& hosts_removed) -> absl::Status { + [this](const HostVector& hosts_added, const HostVector& hosts_removed) { for (const HostSharedPtr& host : hosts_added) { addHostMonitor(host); } @@ -338,7 +338,6 @@ void DetectorImpl::initialize(Cluster& cluster) { host_monitors_.erase(host); } - return absl::OkStatus(); }); armIntervalTimer(); diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 0b7f10d06dc5e..02d3a076df239 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -94,7 +94,7 @@ parseTcpKeepaliveConfig(const envoy::config::cluster::v3::Cluster& config) { } absl::StatusOr -createProtocolOptionsConfig(const std::string& name, const ProtobufWkt::Any& typed_config, +createProtocolOptionsConfig(const std::string& name, const Protobuf::Any& typed_config, Server::Configuration::ProtocolOptionsFactoryContext& factory_context) { Server::Configuration::ProtocolOptionsFactory* factory = Registry::FactoryRegistry::getFactory( @@ -360,6 +360,28 @@ createUpstreamLocalAddressSelector( !(cluster_name.has_value()) ? "Bootstrap" : fmt::format("Cluster {}", cluster_name.value()))); } + +#if !defined(__linux__) + auto fail_status = absl::InvalidArgumentError(fmt::format( + "{}'s upstream binding config contains addresses with network namespace filepaths, but the " + "OS is not Linux. Network namespaces can only be used on Linux.", + !(cluster_name.has_value()) ? "Bootstrap" + : fmt::format("Cluster {}", cluster_name.value()))); + if (bind_config->has_source_address() && + !bind_config->source_address().network_namespace_filepath().empty()) { + return fail_status; + }; + for (const auto& addr : bind_config->extra_source_addresses()) { + if (addr.has_address() && !addr.address().network_namespace_filepath().empty()) { + return fail_status; + } + } + for (const auto& addr : bind_config->additional_source_addresses()) { + if (!addr.network_namespace_filepath().empty()) { + return fail_status; + } + } +#endif } UpstreamLocalAddressSelectorFactory* local_address_selector_factory; const envoy::config::core::v3::TypedExtensionConfig* const local_address_selector_config = @@ -473,6 +495,9 @@ HostDescriptionImplBase::HostDescriptionImplBase( // for a pipe address is a misconfiguration. creation_status = absl::InvalidArgumentError( fmt::format("Invalid host configuration: non-zero port for non-IP address")); + } else if (dest_address && dest_address->networkNamespace().has_value()) { + creation_status = absl::InvalidArgumentError( + "Invalid host configuration: hosts cannot specify network namespaces with their address"); } } @@ -616,17 +641,13 @@ Host::CreateConnectionData HostImplBase::createConnection( socket_factory.createTransportSocket(transport_socket_options, host), upstream_local_address.socket_options_, transport_socket_options); } else if (address_list_or_null != nullptr && address_list_or_null->size() > 1) { - // TODO(adisuissa): convert from OptRef to reference once the runtime flag - // envoy.reloadable_features.use_config_in_happy_eyeballs is deprecated. + ENVOY_LOG(debug, "Upstream using happy eyeballs config."); OptRef happy_eyeballs_config; - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.use_config_in_happy_eyeballs")) { - ENVOY_LOG(debug, "Upstream using happy eyeballs config."); - if (cluster.happyEyeballsConfig().has_value()) { - happy_eyeballs_config = cluster.happyEyeballsConfig(); - } else { - happy_eyeballs_config = defaultHappyEyeballsConfig(); - } + if (cluster.happyEyeballsConfig().has_value()) { + happy_eyeballs_config = cluster.happyEyeballsConfig(); + } else { + happy_eyeballs_config = defaultHappyEyeballsConfig(); } connection = std::make_unique( dispatcher, *address_list_or_null, source_address_selector, socket_factory, @@ -708,7 +729,7 @@ std::vector HostsPerLocalityImpl::filter( void HostSetImpl::updateHosts(PrioritySet::UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - uint64_t seed, absl::optional weighted_priority_health, + absl::optional weighted_priority_health, absl::optional overprovisioning_factor) { if (weighted_priority_health.has_value()) { weighted_priority_health_ = weighted_priority_health.value(); @@ -727,84 +748,7 @@ void HostSetImpl::updateHosts(PrioritySet::UpdateHostsParams&& update_hosts_para excluded_hosts_per_locality_ = std::move(update_hosts_params.excluded_hosts_per_locality); locality_weights_ = std::move(locality_weights); - // TODO(ggreenway): implement `weighted_priority_health` support in `rebuildLocalityScheduler`. - rebuildLocalityScheduler(healthy_locality_scheduler_, healthy_locality_entries_, - *healthy_hosts_per_locality_, healthy_hosts_->get(), hosts_per_locality_, - excluded_hosts_per_locality_, locality_weights_, - overprovisioning_factor_, seed); - rebuildLocalityScheduler(degraded_locality_scheduler_, degraded_locality_entries_, - *degraded_hosts_per_locality_, degraded_hosts_->get(), - hosts_per_locality_, excluded_hosts_per_locality_, locality_weights_, - overprovisioning_factor_, seed); - - THROW_IF_NOT_OK(runUpdateCallbacks(hosts_added, hosts_removed)); -} - -void HostSetImpl::rebuildLocalityScheduler( - std::unique_ptr>& locality_scheduler, - std::vector>& locality_entries, - const HostsPerLocality& eligible_hosts_per_locality, const HostVector& eligible_hosts, - HostsPerLocalityConstSharedPtr all_hosts_per_locality, - HostsPerLocalityConstSharedPtr excluded_hosts_per_locality, - LocalityWeightsConstSharedPtr locality_weights, uint32_t overprovisioning_factor, - uint64_t seed) { - // Rebuild the locality scheduler by computing the effective weight of each - // locality in this priority. The scheduler is reset by default, and is rebuilt only if we have - // locality weights (i.e. using EDS) and there is at least one eligible host in this priority. - // - // We omit building a scheduler when there are zero eligible hosts in the priority as - // all the localities will have zero effective weight. At selection time, we'll either select - // from a different scheduler or there will be no available hosts in the priority. At that point - // we'll rely on other mechanisms such as panic mode to select a host, none of which rely on the - // scheduler. - // - // TODO(htuch): if the underlying locality index -> - // envoy::config::core::v3::Locality hasn't changed in hosts_/healthy_hosts_/degraded_hosts_, we - // could just update locality_weight_ without rebuilding. Similar to how host - // level WRR works, we would age out the existing entries via picks and lazily - // apply the new weights. - locality_scheduler = nullptr; - if (all_hosts_per_locality != nullptr && locality_weights != nullptr && - !locality_weights->empty() && !eligible_hosts.empty()) { - locality_entries.clear(); - for (uint32_t i = 0; i < all_hosts_per_locality->get().size(); ++i) { - const double effective_weight = effectiveLocalityWeight( - i, eligible_hosts_per_locality, *excluded_hosts_per_locality, *all_hosts_per_locality, - *locality_weights, overprovisioning_factor); - if (effective_weight > 0) { - locality_entries.emplace_back(std::make_shared(i, effective_weight)); - } - } - // If not all effective weights were zero, create the scheduler. - if (!locality_entries.empty()) { - locality_scheduler = std::make_unique>( - EdfScheduler::createWithPicks( - locality_entries, [](const LocalityEntry& entry) { return entry.effective_weight_; }, - seed)); - } - } -} - -absl::optional HostSetImpl::chooseHealthyLocality() { - return chooseLocality(healthy_locality_scheduler_.get()); -} - -absl::optional HostSetImpl::chooseDegradedLocality() { - return chooseLocality(degraded_locality_scheduler_.get()); -} - -absl::optional -HostSetImpl::chooseLocality(EdfScheduler* locality_scheduler) { - if (locality_scheduler == nullptr) { - return {}; - } - const std::shared_ptr locality = locality_scheduler->pickAndAdd( - [](const LocalityEntry& locality) { return locality.effective_weight_; }); - // We don't build a schedule if there are no weighted localities, so we should always succeed. - ASSERT(locality != nullptr); - // If we picked it before, its weight must have been positive. - ASSERT(locality->effective_weight_ > 0); - return locality->index_; + runUpdateCallbacks(hosts_added, hosts_removed); } PrioritySet::UpdateHostsParams @@ -848,29 +792,6 @@ HostSetImpl::partitionHosts(HostVectorConstSharedPtr hosts, std::move(std::get<2>(healthy_degraded_excluded_hosts_per_locality))); } -double HostSetImpl::effectiveLocalityWeight(uint32_t index, - const HostsPerLocality& eligible_hosts_per_locality, - const HostsPerLocality& excluded_hosts_per_locality, - const HostsPerLocality& all_hosts_per_locality, - const LocalityWeights& locality_weights, - uint32_t overprovisioning_factor) { - const auto& locality_eligible_hosts = eligible_hosts_per_locality.get()[index]; - const uint32_t excluded_count = excluded_hosts_per_locality.get().size() > index - ? excluded_hosts_per_locality.get()[index].size() - : 0; - const auto host_count = all_hosts_per_locality.get()[index].size() - excluded_count; - if (host_count == 0) { - return 0.0; - } - const double locality_availability_ratio = 1.0 * locality_eligible_hosts.size() / host_count; - const uint32_t weight = locality_weights[index]; - // Availability ranges from 0-1.0, and is the ratio of eligible hosts to total hosts, modified - // by the overprovisioning factor. - const double effective_locality_availability_ratio = - std::min(1.0, (overprovisioning_factor / 100.0) * locality_availability_ratio); - return weight * effective_locality_availability_ratio; -} - const HostSet& PrioritySetImpl::getOrCreateHostSet(uint32_t priority, absl::optional weighted_priority_health, @@ -892,7 +813,7 @@ PrioritySetImpl::getOrCreateHostSet(uint32_t priority, void PrioritySetImpl::updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - uint64_t seed, absl::optional weighted_priority_health, + absl::optional weighted_priority_health, absl::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map) { // Update cross priority host map first. In this way, when the update callbacks of the priority @@ -905,10 +826,10 @@ void PrioritySetImpl::updateHosts(uint32_t priority, UpdateHostsParams&& update_ getOrCreateHostSet(priority, weighted_priority_health, overprovisioning_factor); static_cast(host_sets_[priority].get()) ->updateHosts(std::move(update_hosts_params), std::move(locality_weights), hosts_added, - hosts_removed, seed, weighted_priority_health, overprovisioning_factor); + hosts_removed, weighted_priority_health, overprovisioning_factor); if (!batch_update_) { - THROW_IF_NOT_OK(runUpdateCallbacks(hosts_added, hosts_removed)); + runUpdateCallbacks(hosts_added, hosts_removed); } } @@ -922,13 +843,13 @@ void PrioritySetImpl::batchHostUpdate(BatchUpdateCb& callback) { HostVector net_hosts_added = filterHosts(scope.all_hosts_added_, scope.all_hosts_removed_); HostVector net_hosts_removed = filterHosts(scope.all_hosts_removed_, scope.all_hosts_added_); - THROW_IF_NOT_OK(runUpdateCallbacks(net_hosts_added, net_hosts_removed)); + runUpdateCallbacks(net_hosts_added, net_hosts_removed); } void PrioritySetImpl::BatchUpdateScope::updateHosts( uint32_t priority, PrioritySet::UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, absl::optional weighted_priority_health, + const HostVector& hosts_removed, absl::optional weighted_priority_health, absl::optional overprovisioning_factor) { // We assume that each call updates a different priority. ASSERT(priorities_.find(priority) == priorities_.end()); @@ -943,13 +864,13 @@ void PrioritySetImpl::BatchUpdateScope::updateHosts( } parent_.updateHosts(priority, std::move(update_hosts_params), locality_weights, hosts_added, - hosts_removed, seed, weighted_priority_health, overprovisioning_factor); + hosts_removed, weighted_priority_health, overprovisioning_factor); } void MainPrioritySetImpl::updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, + const HostVector& hosts_removed, absl::optional weighted_priority_health, absl::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map) { @@ -958,7 +879,7 @@ void MainPrioritySetImpl::updateHosts(uint32_t priority, UpdateHostsParams&& upd updateCrossPriorityHostMap(priority, hosts_added, hosts_removed); PrioritySetImpl::updateHosts(priority, std::move(update_hosts_params), locality_weights, - hosts_added, hosts_removed, seed, weighted_priority_health, + hosts_added, hosts_removed, weighted_priority_health, overprovisioning_factor); } @@ -1688,7 +1609,6 @@ ClusterImplBase::ClusterImplBase(const envoy::config::cluster::v3::Cluster& clus info_->endpointStats().membership_healthy_.set(healthy_hosts); info_->endpointStats().membership_degraded_.set(degraded_hosts); info_->endpointStats().membership_excluded_.set(excluded_hosts); - return absl::OkStatus(); }); // Drop overload configuration parsing. SET_AND_RETURN_IF_NOT_OK(parseDropOverloadConfig(cluster.load_assignment()), creation_status); @@ -1918,9 +1838,9 @@ void ClusterImplBase::reloadHealthyHostsHelper(const HostSharedPtr&) { HostVectorConstSharedPtr hosts_copy = std::make_shared(host_set->hosts()); HostsPerLocalityConstSharedPtr hosts_per_locality_copy = host_set->hostsPerLocality().clone(); - prioritySet().updateHosts( - priority, HostSetImpl::partitionHosts(hosts_copy, hosts_per_locality_copy), - host_set->localityWeights(), {}, {}, random_.random(), absl::nullopt, absl::nullopt); + prioritySet().updateHosts(priority, + HostSetImpl::partitionHosts(hosts_copy, hosts_per_locality_copy), + host_set->localityWeights(), {}, {}, absl::nullopt, absl::nullopt); } } @@ -1946,11 +1866,21 @@ ClusterImplBase::resolveProtoAddress(const envoy::config::core::v3::Address& add return resolve_status; } -absl::Status ClusterImplBase::validateEndpointsForZoneAwareRouting( - const envoy::config::endpoint::v3::LocalityLbEndpoints& endpoints) const { - if (local_cluster_ && endpoints.priority() > 0) { - return absl::InvalidArgumentError( - fmt::format("Unexpected non-zero priority for local cluster '{}'.", info()->name())); +absl::Status ClusterImplBase::validateEndpoints( + absl::Span localities, + OptRef priorities) const { + for (const auto* endpoints : localities) { + if (local_cluster_ && endpoints->priority() > 0) { + return absl::InvalidArgumentError( + fmt::format("Unexpected non-zero priority for local cluster '{}'.", info()->name())); + } + } + + if (priorities.has_value()) { + OptRef lb_config = info_->loadBalancerConfig(); + if (lb_config.has_value()) { + return lb_config->validateEndpoints(*priorities); + } } return absl::OkStatus(); } @@ -2156,10 +2086,8 @@ ClusterInfoImpl::ResourceManagers::load(const envoy::config::cluster::v3::Cluste PriorityStateManager::PriorityStateManager(ClusterImplBase& cluster, const LocalInfo::LocalInfo& local_info, - PrioritySet::HostUpdateCb* update_cb, - Random::RandomGenerator& random) - : parent_(cluster), local_info_node_(local_info.node()), update_cb_(update_cb), - random_(random) {} + PrioritySet::HostUpdateCb* update_cb) + : parent_(cluster), local_info_node_(local_info.node()), update_cb_(update_cb) {} void PriorityStateManager::initializePriorityFor( const envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint) { @@ -2273,14 +2201,13 @@ void PriorityStateManager::updateClusterPrioritySet( if (update_cb_ != nullptr) { update_cb_->updateHosts(priority, HostSetImpl::partitionHosts(hosts, per_locality_shared), std::move(locality_weights), hosts_added.value_or(*hosts), - hosts_removed.value_or({}), random_.random(), - weighted_priority_health, overprovisioning_factor); + hosts_removed.value_or({}), weighted_priority_health, + overprovisioning_factor); } else { - parent_.prioritySet().updateHosts(priority, - HostSetImpl::partitionHosts(hosts, per_locality_shared), - std::move(locality_weights), hosts_added.value_or(*hosts), - hosts_removed.value_or({}), random_.random(), - weighted_priority_health, overprovisioning_factor); + parent_.prioritySet().updateHosts( + priority, HostSetImpl::partitionHosts(hosts, per_locality_shared), + std::move(locality_weights), hosts_added.value_or(*hosts), + hosts_removed.value_or({}), weighted_priority_health, overprovisioning_factor); } } diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index d3e34a0f87ab1..d91e610716679 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -58,7 +58,6 @@ #include "source/common/orca/orca_load_metrics.h" #include "source/common/shared_pool/shared_pool.h" #include "source/common/stats/isolated_store_impl.h" -#include "source/common/upstream/edf_scheduler.h" #include "source/common/upstream/load_balancer_context_base.h" #include "source/common/upstream/resource_manager_impl.h" #include "source/common/upstream/transport_socket_match_impl.h" @@ -598,8 +597,6 @@ class HostSetImpl : public HostSet { return excluded_hosts_per_locality_; } LocalityWeightsConstSharedPtr localityWeights() const override { return locality_weights_; } - absl::optional chooseHealthyLocality() override; - absl::optional chooseDegradedLocality() override; uint32_t priority() const override { return priority_; } uint32_t overprovisioningFactor() const override { return overprovisioning_factor_; } bool weightedPriorityHealth() const override { return weighted_priority_health_; } @@ -619,26 +616,16 @@ class HostSetImpl : public HostSet { void updateHosts(PrioritySet::UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, + const HostVector& hosts_removed, absl::optional weighted_priority_health = absl::nullopt, absl::optional overprovisioning_factor = absl::nullopt); protected: - virtual absl::Status runUpdateCallbacks(const HostVector& hosts_added, - const HostVector& hosts_removed) { - return member_update_cb_helper_.runCallbacks(priority_, hosts_added, hosts_removed); + virtual void runUpdateCallbacks(const HostVector& hosts_added, const HostVector& hosts_removed) { + member_update_cb_helper_.runCallbacks(priority_, hosts_added, hosts_removed); } private: - // Weight for a locality taking into account health status using the provided eligible hosts per - // locality. - static double effectiveLocalityWeight(uint32_t index, - const HostsPerLocality& eligible_hosts_per_locality, - const HostsPerLocality& excluded_hosts_per_locality, - const HostsPerLocality& all_hosts_per_locality, - const LocalityWeights& locality_weights, - uint32_t overprovisioning_factor); - const uint32_t priority_; uint32_t overprovisioning_factor_; bool weighted_priority_health_; @@ -651,50 +638,10 @@ class HostSetImpl : public HostSet { HostsPerLocalityConstSharedPtr degraded_hosts_per_locality_{HostsPerLocalityImpl::empty()}; HostsPerLocalityConstSharedPtr excluded_hosts_per_locality_{HostsPerLocalityImpl::empty()}; // TODO(mattklein123): Remove mutable. - mutable Common::CallbackManager + mutable Common::CallbackManager member_update_cb_helper_; - // Locality weights (used to build WRR locality_scheduler_); + // Locality weights. LocalityWeightsConstSharedPtr locality_weights_; - // WRR locality scheduler state. - struct LocalityEntry { - LocalityEntry(uint32_t index, double effective_weight) - : index_(index), effective_weight_(effective_weight) {} - const uint32_t index_; - const double effective_weight_; - }; - - // Rebuilds the provided locality scheduler with locality entries based on the locality weights - // and eligible hosts. - // - // @param locality_scheduler the locality scheduler to rebuild. Will be set to nullptr if no - // localities are eligible. - // @param locality_entries the vector that holds locality entries. Will be reset and populated - // with entries corresponding to the new scheduler. - // @param eligible_hosts_per_locality eligible hosts for this scheduler grouped by locality. - // @param eligible_hosts all eligible hosts for this scheduler. - // @param all_hosts_per_locality all hosts for this HostSet grouped by locality. - // @param locality_weights the weighting of each locality. - // @param overprovisioning_factor the overprovisioning factor to use when computing the effective - // weight of a locality. - // @param seed a random number of initial picks to "invoke" on the locality scheduler. This - // allows to distribute the load between different localities across worker threads and a fleet - // of Envoys. - static void - rebuildLocalityScheduler(std::unique_ptr>& locality_scheduler, - std::vector>& locality_entries, - const HostsPerLocality& eligible_hosts_per_locality, - const HostVector& eligible_hosts, - HostsPerLocalityConstSharedPtr all_hosts_per_locality, - HostsPerLocalityConstSharedPtr excluded_hosts_per_locality, - LocalityWeightsConstSharedPtr locality_weights, - uint32_t overprovisioning_factor, uint64_t seed); - - static absl::optional chooseLocality(EdfScheduler* locality_scheduler); - - std::vector> healthy_locality_entries_; - std::unique_ptr> healthy_locality_scheduler_; - std::vector> degraded_locality_entries_; - std::unique_ptr> degraded_locality_scheduler_; }; using HostSetImplPtr = std::unique_ptr; @@ -725,7 +672,7 @@ class PrioritySetImpl : public PrioritySet { void updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, + const HostVector& hosts_removed, absl::optional weighted_priority_health = absl::nullopt, absl::optional overprovisioning_factor = absl::nullopt, HostMapConstSharedPtr cross_priority_host_map = nullptr) override; @@ -745,13 +692,12 @@ class PrioritySetImpl : public PrioritySet { overprovisioning_factor); } - virtual absl::Status runUpdateCallbacks(const HostVector& hosts_added, - const HostVector& hosts_removed) { - return member_update_cb_helper_.runCallbacks(hosts_added, hosts_removed); + virtual void runUpdateCallbacks(const HostVector& hosts_added, const HostVector& hosts_removed) { + member_update_cb_helper_.runCallbacks(hosts_added, hosts_removed); } - virtual absl::Status runReferenceUpdateCallbacks(uint32_t priority, const HostVector& hosts_added, - const HostVector& hosts_removed) { - return priority_update_cb_helper_.runCallbacks(priority, hosts_added, hosts_removed); + virtual void runReferenceUpdateCallbacks(uint32_t priority, const HostVector& hosts_added, + const HostVector& hosts_removed) { + priority_update_cb_helper_.runCallbacks(priority, hosts_added, hosts_removed); } // This vector will generally have at least one member, for priority level 0. // It will expand as host sets are added but currently does not shrink to @@ -766,8 +712,9 @@ class PrioritySetImpl : public PrioritySet { // because host_sets_ is directly returned so we avoid translation. std::vector host_sets_priority_update_cbs_; // TODO(mattklein123): Remove mutable. - mutable Common::CallbackManager member_update_cb_helper_; - mutable Common::CallbackManager + mutable Common::CallbackManager + member_update_cb_helper_; + mutable Common::CallbackManager priority_update_cb_helper_; bool batch_update_ : 1; @@ -784,8 +731,7 @@ class PrioritySetImpl : public PrioritySet { void updateHosts(uint32_t priority, PrioritySet::UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, - absl::optional weighted_priority_health, + const HostVector& hosts_removed, absl::optional weighted_priority_health, absl::optional overprovisioning_factor) override; absl::node_hash_set all_hosts_added_; @@ -806,7 +752,7 @@ class MainPrioritySetImpl : public PrioritySetImpl, public Logger::Loggable weighted_priority_health = absl::nullopt, absl::optional overprovisioning_factor = absl::nullopt, HostMapConstSharedPtr cross_priority_host_map = nullptr) override; @@ -1287,8 +1233,9 @@ class ClusterImplBase : public Cluster, protected Logger::Loggable endpoints, + OptRef priorities) const; private: static const absl::string_view DoNotValidateAlpnRuntimeKey; @@ -1318,7 +1265,7 @@ using ClusterImplBaseSharedPtr = std::shared_ptr; class PriorityStateManager : protected Logger::Loggable { public: PriorityStateManager(ClusterImplBase& cluster, const LocalInfo::LocalInfo& local_info, - PrioritySet::HostUpdateCb* update_cb, Random::RandomGenerator& random); + PrioritySet::HostUpdateCb* update_cb); // Initializes the PriorityState vector based on the priority specified in locality_lb_endpoint. void initializePriorityFor( @@ -1355,7 +1302,6 @@ class PriorityStateManager : protected Logger::Loggable { PriorityState priority_state_; const envoy::config::core::v3::Node& local_info_node_; PrioritySet::HostUpdateCb* update_cb_; - Random::RandomGenerator& random_; }; using PriorityStateManagerPtr = std::unique_ptr; diff --git a/source/docs/subset_load_balancer.md b/source/docs/subset_load_balancer.md index 23220d79e1a39..6ba8fde0783ac 100644 --- a/source/docs/subset_load_balancer.md +++ b/source/docs/subset_load_balancer.md @@ -64,12 +64,12 @@ The CDS configuration for the subset selectors is meant to allow future extensio Subsets are stored in a trie-like fashion. Keys in the selectors are lexically sorted. An `LbSubsetMap` is an `unordered_map` of string keys to `ValueSubsetMap`. `ValueSubsetMap` is an -`unordered_map` of (wrapped, see below) `ProtobufWkt::Value` to `LbSubsetEntry`. The +`unordered_map` of (wrapped, see below) `Protobuf::Value` to `LbSubsetEntry`. The `LbSubsetEntry` may contain an `LbSubsetMap` of additional keys or a `Subset`. `Subset` encapsulates the filtered `Upstream::HostSet` and `Upstream::LoadBalancer` for a subset. -`ProtobufWkt::Value` is wrapped to provide a cached hash value for the value. Currently, -`ProtobufWkt::Value` is hashed by first encoding the value as a string and then hashing the +`Protobuf::Value` is wrapped to provide a cached hash value for the value. Currently, +`Protobuf::Value` is hashed by first encoding the value as a string and then hashing the string. By wrapping it, we can compute the hash value outside the request path for both the metadata values provided in `LoadBalancerContext` and those used internally by the SLB. diff --git a/source/extensions/access_loggers/common/stream_access_log_common_impl.h b/source/extensions/access_loggers/common/stream_access_log_common_impl.h index 010abac65a7d1..5e6e44dfe3854 100644 --- a/source/extensions/access_loggers/common/stream_access_log_common_impl.h +++ b/source/extensions/access_loggers/common/stream_access_log_common_impl.h @@ -13,7 +13,7 @@ namespace AccessLoggers { template AccessLog::InstanceSharedPtr createStreamAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) { const auto& fal_config = MessageUtil::downcastAndValidate(config, context.messageValidationVisitor()); diff --git a/source/extensions/access_loggers/file/config.cc b/source/extensions/access_loggers/file/config.cc index 7aa65f9c47c78..7d486ff006170 100644 --- a/source/extensions/access_loggers/file/config.cc +++ b/source/extensions/access_loggers/file/config.cc @@ -21,7 +21,7 @@ namespace File { AccessLog::InstanceSharedPtr FileAccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers) { const auto& fal_config = MessageUtil::downcastAndValidate< const envoy::extensions::access_loggers::file::v3::FileAccessLog&>( diff --git a/source/extensions/access_loggers/file/config.h b/source/extensions/access_loggers/file/config.h index 2c2b17997034d..af5727823ffc0 100644 --- a/source/extensions/access_loggers/file/config.h +++ b/source/extensions/access_loggers/file/config.h @@ -14,7 +14,7 @@ class FileAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/access_loggers/filters/cel/cel.cc b/source/extensions/access_loggers/filters/cel/cel.cc index 20f80b70e2a04..009bcd3aa7924 100644 --- a/source/extensions/access_loggers/filters/cel/cel.cc +++ b/source/extensions/access_loggers/filters/cel/cel.cc @@ -9,23 +9,30 @@ namespace CEL { namespace Expr = Envoy::Extensions::Filters::Common::Expr; CELAccessLogExtensionFilter::CELAccessLogExtensionFilter( - const ::Envoy::LocalInfo::LocalInfo& local_info, Expr::BuilderInstanceSharedPtr builder, - const google::api::expr::v1alpha1::Expr& input_expr) - : local_info_(local_info), builder_(builder), parsed_expr_(input_expr) { - compiled_expr_ = Expr::createExpression(builder_->builder(), parsed_expr_); -} + const ::Envoy::LocalInfo::LocalInfo& local_info, + Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr builder, + const cel::expr::Expr& input_expr) + : local_info_(local_info), expr_([&]() { + auto compiled_expr = + Extensions::Filters::Common::Expr::CompiledExpression::Create(builder, input_expr); + if (!compiled_expr.ok()) { + throw EnvoyException( + absl::StrCat("failed to create an expression: ", compiled_expr.status().message())); + } + return std::move(compiled_expr.value()); + }()) {} bool CELAccessLogExtensionFilter::evaluate(const Formatter::HttpFormatterContext& log_context, const StreamInfo::StreamInfo& stream_info) const { Protobuf::Arena arena; - auto eval_status = Expr::evaluate(*compiled_expr_, arena, &local_info_, stream_info, - &log_context.requestHeaders(), &log_context.responseHeaders(), - &log_context.responseTrailers()); - if (!eval_status.has_value() || eval_status.value().IsError()) { + const auto result = + expr_.evaluate(arena, &local_info_, stream_info, &log_context.requestHeaders(), + &log_context.responseHeaders(), &log_context.responseTrailers()); + if (!result.has_value() || result.value().IsError()) { return false; } - auto result = eval_status.value(); - return result.IsBool() ? result.BoolOrDie() : false; + auto eval_result = result.value(); + return eval_result.IsBool() ? eval_result.BoolOrDie() : false; } } // namespace CEL diff --git a/source/extensions/access_loggers/filters/cel/cel.h b/source/extensions/access_loggers/filters/cel/cel.h index 74247d5187d6e..681515db90aec 100644 --- a/source/extensions/access_loggers/filters/cel/cel.h +++ b/source/extensions/access_loggers/filters/cel/cel.h @@ -20,17 +20,15 @@ namespace CEL { class CELAccessLogExtensionFilter : public AccessLog::Filter { public: CELAccessLogExtensionFilter(const ::Envoy::LocalInfo::LocalInfo& local_info, - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr, - const google::api::expr::v1alpha1::Expr&); + Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr, + const cel::expr::Expr&); bool evaluate(const Formatter::HttpFormatterContext& log_context, const StreamInfo::StreamInfo& stream_info) const override; private: const ::Envoy::LocalInfo::LocalInfo& local_info_; - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder_; - const google::api::expr::v1alpha1::Expr parsed_expr_; - Extensions::Filters::Common::Expr::ExpressionPtr compiled_expr_; + const Extensions::Filters::Common::Expr::CompiledExpression expr_; }; } // namespace CEL diff --git a/source/extensions/access_loggers/filters/cel/config.cc b/source/extensions/access_loggers/filters/cel/config.cc index 9a1a1be32435b..f83565b141b67 100644 --- a/source/extensions/access_loggers/filters/cel/config.cc +++ b/source/extensions/access_loggers/filters/cel/config.cc @@ -16,7 +16,7 @@ namespace CEL { Envoy::AccessLog::FilterPtr CELAccessLogExtensionFilterFactory::createFilter( const envoy::config::accesslog::v3::ExtensionFilter& config, - Server::Configuration::FactoryContext& context) { + Server::Configuration::GenericFactoryContext& context) { auto factory_config = Config::Utility::translateToFactoryConfig(config, context.messageValidationVisitor(), *this); diff --git a/source/extensions/access_loggers/filters/cel/config.h b/source/extensions/access_loggers/filters/cel/config.h index 62dfb342491c0..2e915316868cf 100644 --- a/source/extensions/access_loggers/filters/cel/config.h +++ b/source/extensions/access_loggers/filters/cel/config.h @@ -22,7 +22,7 @@ class CELAccessLogExtensionFilterFactory : public Envoy::AccessLog::ExtensionFil public: Envoy::AccessLog::FilterPtr createFilter(const envoy::config::accesslog::v3::ExtensionFilter& config, - Server::Configuration::FactoryContext& context) override; + Server::Configuration::GenericFactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; std::string name() const override { return "envoy.access_loggers.extension_filters.cel"; } }; diff --git a/source/extensions/access_loggers/fluentd/config.cc b/source/extensions/access_loggers/fluentd/config.cc index 15a96c6252f8f..d71fdc04b7666 100644 --- a/source/extensions/access_loggers/fluentd/config.cc +++ b/source/extensions/access_loggers/fluentd/config.cc @@ -33,7 +33,7 @@ getAccessLoggerCacheSingleton(Server::Configuration::ServerFactoryContext& conte AccessLog::InstanceSharedPtr FluentdAccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers) { const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::access_loggers::fluentd::v3::FluentdAccessLogConfig&>( diff --git a/source/extensions/access_loggers/fluentd/config.h b/source/extensions/access_loggers/fluentd/config.h index a098d9129179a..f1be35f13c658 100644 --- a/source/extensions/access_loggers/fluentd/config.h +++ b/source/extensions/access_loggers/fluentd/config.h @@ -14,7 +14,7 @@ class FluentdAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc b/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc index 017ca04259aa0..d57565805aed7 100644 --- a/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc +++ b/source/extensions/access_loggers/grpc/grpc_access_log_utils.cc @@ -357,9 +357,9 @@ bool extractFilterStateData(const StreamInfo::FilterState& filter_state, const s ProtobufTypes::MessagePtr serialized_proto = state->serializeAsProto(); if (serialized_proto != nullptr) { auto& filter_state_objects = *common_access_log.mutable_filter_state_objects(); - ProtobufWkt::Any& any = filter_state_objects[key]; - if (dynamic_cast(serialized_proto.get()) != nullptr) { - any.Swap(dynamic_cast(serialized_proto.get())); + Protobuf::Any& any = filter_state_objects[key]; + if (dynamic_cast(serialized_proto.get()) != nullptr) { + any.Swap(dynamic_cast(serialized_proto.get())); } else { any.PackFrom(*serialized_proto); } diff --git a/source/extensions/access_loggers/grpc/http_config.cc b/source/extensions/access_loggers/grpc/http_config.cc index 696b3986c0377..e15a07c42f001 100644 --- a/source/extensions/access_loggers/grpc/http_config.cc +++ b/source/extensions/access_loggers/grpc/http_config.cc @@ -20,7 +20,8 @@ namespace HttpGrpc { AccessLog::InstanceSharedPtr HttpGrpcAccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, std::vector&&) { + Server::Configuration::GenericFactoryContext& context, + std::vector&&) { GrpcCommon::validateProtoDescriptors(); const auto& proto_config = MessageUtil::downcastAndValidate< diff --git a/source/extensions/access_loggers/grpc/http_config.h b/source/extensions/access_loggers/grpc/http_config.h index a262b2e29b4c5..45447f4a7a3d3 100644 --- a/source/extensions/access_loggers/grpc/http_config.h +++ b/source/extensions/access_loggers/grpc/http_config.h @@ -16,7 +16,7 @@ class HttpGrpcAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/access_loggers/grpc/tcp_config.cc b/source/extensions/access_loggers/grpc/tcp_config.cc index 744a0ef733008..0a8b4c3744f47 100644 --- a/source/extensions/access_loggers/grpc/tcp_config.cc +++ b/source/extensions/access_loggers/grpc/tcp_config.cc @@ -20,7 +20,8 @@ namespace TcpGrpc { AccessLog::InstanceSharedPtr TcpGrpcAccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, std::vector&&) { + Server::Configuration::GenericFactoryContext& context, + std::vector&&) { GrpcCommon::validateProtoDescriptors(); const auto& proto_config = MessageUtil::downcastAndValidate< diff --git a/source/extensions/access_loggers/grpc/tcp_config.h b/source/extensions/access_loggers/grpc/tcp_config.h index fce3a662f4e49..53ca1ececb37d 100644 --- a/source/extensions/access_loggers/grpc/tcp_config.h +++ b/source/extensions/access_loggers/grpc/tcp_config.h @@ -16,7 +16,7 @@ class TcpGrpcAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/access_loggers/open_telemetry/config.cc b/source/extensions/access_loggers/open_telemetry/config.cc index a6b656d8078d0..7ca56443c4618 100644 --- a/source/extensions/access_loggers/open_telemetry/config.cc +++ b/source/extensions/access_loggers/open_telemetry/config.cc @@ -33,7 +33,7 @@ getAccessLoggerCacheSingleton(Server::Configuration::CommonFactoryContext& conte ::Envoy::AccessLog::InstanceSharedPtr AccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, ::Envoy::AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers) { validateProtoDescriptors(); diff --git a/source/extensions/access_loggers/open_telemetry/config.h b/source/extensions/access_loggers/open_telemetry/config.h index 13a85debc2633..a0c31639cf672 100644 --- a/source/extensions/access_loggers/open_telemetry/config.h +++ b/source/extensions/access_loggers/open_telemetry/config.h @@ -16,7 +16,7 @@ class AccessLogFactory : public Envoy::AccessLog::AccessLogInstanceFactory { public: ::Envoy::AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, ::Envoy::AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/access_loggers/open_telemetry/grpc_access_log_impl.h b/source/extensions/access_loggers/open_telemetry/grpc_access_log_impl.h index 215fb22b3763b..49e102a1bab86 100644 --- a/source/extensions/access_loggers/open_telemetry/grpc_access_log_impl.h +++ b/source/extensions/access_loggers/open_telemetry/grpc_access_log_impl.h @@ -33,7 +33,7 @@ class GrpcAccessLoggerImpl // OpenTelemetry logging uses LogRecord for both HTTP and TCP, so protobuf::Empty is used // as an empty placeholder for the non-used addEntry method. // TODO(itamarkam): Don't cache OpenTelemetry loggers by type (HTTP/TCP). - ProtobufWkt::Empty, opentelemetry::proto::collector::logs::v1::ExportLogsServiceRequest, + Protobuf::Empty, opentelemetry::proto::collector::logs::v1::ExportLogsServiceRequest, opentelemetry::proto::collector::logs::v1::ExportLogsServiceResponse> { public: GrpcAccessLoggerImpl( @@ -88,7 +88,7 @@ class GrpcAccessLoggerImpl // Extensions::AccessLoggers::GrpcCommon::GrpcAccessLogger void addEntry(opentelemetry::proto::logs::v1::LogRecord&& entry) override; // Non used addEntry method (the above is used for both TCP and HTTP). - void addEntry(ProtobufWkt::Empty&& entry) override { (void)entry; }; + void addEntry(Protobuf::Empty&& entry) override { (void)entry; }; bool isEmpty() override; void initMessage() override; void clearMessage() override; diff --git a/source/extensions/access_loggers/stream/config.cc b/source/extensions/access_loggers/stream/config.cc index 0f78a10a5d845..98d54a3326170 100644 --- a/source/extensions/access_loggers/stream/config.cc +++ b/source/extensions/access_loggers/stream/config.cc @@ -22,7 +22,7 @@ namespace File { AccessLog::InstanceSharedPtr StdoutAccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers) { return AccessLoggers::createStreamAccessLogInstance< envoy::extensions::access_loggers::stream::v3::StdoutAccessLog, @@ -45,7 +45,7 @@ LEGACY_REGISTER_FACTORY(StdoutAccessLogFactory, AccessLog::AccessLogInstanceFact AccessLog::InstanceSharedPtr StderrAccessLogFactory::createAccessLogInstance( const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers) { return createStreamAccessLogInstance< envoy::extensions::access_loggers::stream::v3::StderrAccessLog, diff --git a/source/extensions/access_loggers/stream/config.h b/source/extensions/access_loggers/stream/config.h index 1be10c2df49df..d60ace378308a 100644 --- a/source/extensions/access_loggers/stream/config.h +++ b/source/extensions/access_loggers/stream/config.h @@ -14,7 +14,7 @@ class StdoutAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; @@ -29,7 +29,7 @@ class StderrAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& command_parsers = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/access_loggers/wasm/config.cc b/source/extensions/access_loggers/wasm/config.cc index d2ccf7d9c72e0..75c87134e7f30 100644 --- a/source/extensions/access_loggers/wasm/config.cc +++ b/source/extensions/access_loggers/wasm/config.cc @@ -14,9 +14,11 @@ namespace Extensions { namespace AccessLoggers { namespace Wasm { -AccessLog::InstanceSharedPtr WasmAccessLogFactory::createAccessLogInstance( - const Protobuf::Message& proto_config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, std::vector&&) { +AccessLog::InstanceSharedPtr +WasmAccessLogFactory::createAccessLogInstance(const Protobuf::Message& proto_config, + AccessLog::FilterPtr&& filter, + Server::Configuration::GenericFactoryContext& context, + std::vector&&) { const auto& config = MessageUtil::downcastAndValidate< const envoy::extensions::access_loggers::wasm::v3::WasmAccessLog&>( proto_config, context.messageValidationVisitor()); diff --git a/source/extensions/access_loggers/wasm/config.h b/source/extensions/access_loggers/wasm/config.h index d85ad871c2299..e297d5ea98e8e 100644 --- a/source/extensions/access_loggers/wasm/config.h +++ b/source/extensions/access_loggers/wasm/config.h @@ -17,7 +17,7 @@ class WasmAccessLogFactory : public AccessLog::AccessLogInstanceFactory, public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message& config, AccessLog::FilterPtr&& filter, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext& context, std::vector&& = {}) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override; @@ -25,7 +25,7 @@ class WasmAccessLogFactory : public AccessLog::AccessLogInstanceFactory, std::string name() const override; private: - absl::flat_hash_map convertJsonFormatToMap(ProtobufWkt::Struct config); + absl::flat_hash_map convertJsonFormatToMap(Protobuf::Struct config); }; } // namespace Wasm diff --git a/source/extensions/api_listeners/default_api_listener/api_listener_impl.h b/source/extensions/api_listeners/default_api_listener/api_listener_impl.h index 01ec2f3015b7e..e6053dd0c22c0 100644 --- a/source/extensions/api_listeners/default_api_listener/api_listener_impl.h +++ b/source/extensions/api_listeners/default_api_listener/api_listener_impl.h @@ -118,6 +118,7 @@ class ApiListenerImplBase : public Server::ApiListener, void removeConnectionCallbacks(Network::ConnectionCallbacks& cb) override { callbacks_.remove(&cb); } + const Network::ConnectionSocketPtr& getSocket() const override { PANIC("not implemented"); } void addBytesSentCallback(Network::Connection::BytesSentCb) override { IS_ENVOY_BUG("Unexpected function call"); } diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/factory_base.h b/source/extensions/bootstrap/reverse_tunnel/backup_files/factory_base.h new file mode 100644 index 0000000000000..564e00b7d8b29 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/factory_base.h @@ -0,0 +1,128 @@ +#pragma once + +#include "envoy/server/bootstrap_extension_config.h" + +#include "source/common/common/logger.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Common base class for reverse connection bootstrap factory registrations. + * Removes a substantial amount of boilerplate and follows Envoy's factory patterns. + * Template parameters: + * ConfigProto: Protobuf message type for the bootstrap configuration + * ExtensionImpl: The actual bootstrap extension implementation class + */ +template +class ReverseConnectionBootstrapFactoryBase + : public Server::Configuration::BootstrapExtensionFactory { +public: + // Server::Configuration::BootstrapExtensionFactory implementation + Server::BootstrapExtensionPtr + createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override { + return createBootstrapExtensionTyped(MessageUtil::downcastAndValidate( + config, context.messageValidationVisitor()), + context); + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { return name_; } + +protected: + /** + * Constructor for the factory base. + * @param name the name of the bootstrap extension factory + */ + explicit ReverseConnectionBootstrapFactoryBase(const std::string& name) : name_(name) {} + +private: + /** + * Create the typed bootstrap extension from the validated protobuf configuration. + * This method is implemented by derived classes to create the specific extension. + * @param proto_config the validated protobuf configuration + * @param context the server factory context + * @return unique pointer to the created bootstrap extension + */ + virtual Server::BootstrapExtensionPtr + createBootstrapExtensionTyped(const ConfigProto& proto_config, + Server::Configuration::ServerFactoryContext& context) PURE; + + const std::string name_; +}; + +/** + * Thread-safe factory utilities for reverse connection extensions. + * Provides common thread safety patterns and validation helpers. + */ +class ReverseConnectionFactoryUtils { +public: + /** + * Validate thread-local slot availability before using it. + * Follows Envoy's patterns for safe TLS access. + * @param tls_slot the thread-local slot to validate + * @param extension_name name of the extension for error messages + * @return true if the slot is safe to use, false otherwise + */ + template + static bool + validateThreadLocalSlot(const std::unique_ptr>& tls_slot, + const std::string& /* extension_name */) { + return tls_slot != nullptr; + } + + /** + * Safe access to thread-local registry with proper error handling. + * @param tls_slot the thread-local slot to access + * @param extension_name name of the extension for error messages + * @return pointer to the thread-local object, or nullptr if not available + */ + template + static TlsType* + safeGetThreadLocal(const std::unique_ptr>& tls_slot, + const std::string& extension_name) { + if (!validateThreadLocalSlot(tls_slot, extension_name)) { + return nullptr; + } + + try { + if (auto opt = tls_slot->get(); opt.has_value()) { + return &opt.value().get(); + } + } catch (const std::exception&) { + // Exception during TLS access - return nullptr for safety + } + + return nullptr; + } + + /** + * Create thread-local slot with proper error handling and validation. + * @param thread_local_manager the thread local manager + * @param extension_name name of the extension for logging + * @return unique pointer to the created slot, or nullptr on failure + */ + template + static std::unique_ptr> + createThreadLocalSlot(ThreadLocal::SlotAllocator& thread_local_manager, + const std::string& /* extension_name */) { + try { + return ThreadLocal::TypedSlot::makeUnique(thread_local_manager); + } catch (const std::exception&) { + // Failed to create slot - return nullptr + return nullptr; + } + } +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_client.cc b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_client.cc new file mode 100644 index 0000000000000..453c700101c71 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_client.cc @@ -0,0 +1,274 @@ +#include +#include + +#include "envoy/grpc/async_client.h" +#include "envoy/service/reverse_tunnel/v3/reverse_tunnel_handshake.pb.h" + +#include "source/common/common/logger.h" +#include "source/common/grpc/typed_async_client.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/grpc_reverse_tunnel_client.h" + +#include "absl/strings/str_cat.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +GrpcReverseTunnelClient::GrpcReverseTunnelClient( + Upstream::ClusterManager& cluster_manager, const std::string& cluster_name, + const envoy::service::reverse_tunnel::v3::ReverseTunnelGrpcConfig& config, + GrpcReverseTunnelCallbacks& callbacks) + : cluster_manager_(cluster_manager), cluster_name_(cluster_name), config_(config), + callbacks_(callbacks), + service_method_(Protobuf::DescriptorPool::generated_pool()->FindMethodByName( + "envoy.service.reverse_tunnel.v3.ReverseTunnelHandshakeService.EstablishTunnel")) { + + // Generate unique correlation ID for this handshake session + correlation_id_ = + absl::StrCat("handshake_", std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); + + ENVOY_LOG(debug, "Created GrpcReverseTunnelClient with correlation ID: {}", correlation_id_); + + // Create gRPC client immediately + if (auto status = createGrpcClient(); !status.ok()) { + ENVOY_LOG(error, "Failed to create gRPC client for reverse tunnel handshake: {}", + status.message()); + throw EnvoyException(fmt::format( + "Failed to create gRPC client for reverse tunnel handshake: {}", status.message())); + } +} + +GrpcReverseTunnelClient::~GrpcReverseTunnelClient() { + ENVOY_LOG(debug, "Destroying GrpcReverseTunnelClient with correlation ID: {}", correlation_id_); + cancel(); +} + +absl::Status GrpcReverseTunnelClient::createGrpcClient() { + try { + // Verify cluster name is provided + if (cluster_name_.empty()) { + return absl::InvalidArgumentError( + "Cluster name cannot be empty for gRPC reverse tunnel handshake"); + } + + auto thread_local_cluster = cluster_manager_.getThreadLocalCluster(cluster_name_); + if (!thread_local_cluster) { + return absl::NotFoundError( + fmt::format("Cluster '{}' not found for gRPC reverse tunnel handshake", cluster_name_)); + } + + // Create a basic gRPC service config for this cluster + envoy::config::core::v3::GrpcService grpc_service; + grpc_service.mutable_envoy_grpc()->set_cluster_name(cluster_name_); + if (config_.has_handshake_timeout()) { + *grpc_service.mutable_timeout() = config_.handshake_timeout(); + } + + // Create raw gRPC client + auto result = cluster_manager_.grpcAsyncClientManager().getOrCreateRawAsyncClient( + grpc_service, thread_local_cluster->info()->statsScope(), + false); // skip_cluster_check = false + + if (!result.ok()) { + return absl::InternalError( + fmt::format("Failed to create gRPC async client for cluster '{}': {}", cluster_name_, + result.status().message())); + } + + auto raw_client = result.value(); + + if (!raw_client) { + return absl::InternalError( + fmt::format("Failed to create gRPC async client for cluster '{}'", cluster_name_)); + } + + // Create typed client from raw client + client_ = + Grpc::AsyncClient(raw_client); + + ENVOY_LOG(debug, "Successfully created gRPC client for cluster '{}'", cluster_name_); + return absl::OkStatus(); + + } catch (const std::exception& e) { + return absl::InternalError(fmt::format("Exception creating gRPC client: {}", e.what())); + } +} + +bool GrpcReverseTunnelClient::initiateHandshake( + const std::string& tenant_id, const std::string& cluster_id, const std::string& node_id, + const absl::optional& metadata, Tracing::Span& span) { + + if (current_request_) { + ENVOY_LOG(warn, "Handshake already in progress - cancelling previous request."); + cancel(); + } + + // Check if client is available - typed client doesn't have a direct null check + // so we'll proceed with the request and let any errors be handled in the catch block + + try { + // Build the handshake request + auto request = buildHandshakeRequest(tenant_id, cluster_id, node_id, metadata); + + // Record handshake start time for metrics + handshake_start_time_ = std::chrono::steady_clock::now(); + + ENVOY_LOG(info, + "Initiating gRPC reverse tunnel handshake: tenant='{}', cluster='{}', node='{}', " + "correlation='{}'", + tenant_id, cluster_id, node_id, correlation_id_); + + // Create gRPC request with timeout options + Http::AsyncClient::RequestOptions options; + options.setTimeout(std::chrono::milliseconds(config_.handshake_timeout().seconds() * 1000 + + config_.handshake_timeout().nanos() / 1000000)); + + current_request_ = client_->send(*service_method_, request, *this, span, options); + + if (!current_request_) { + ENVOY_LOG(error, "Failed to send gRPC handshake request."); + callbacks_.onHandshakeFailure(Grpc::Status::WellKnownGrpcStatus::Internal, + "Failed to send gRPC request"); + return false; + } + + ENVOY_LOG(debug, "gRPC handshake request sent successfully with correlation ID: {}", + correlation_id_); + return true; + + } catch (const std::exception& e) { + ENVOY_LOG(error, "Exception initiating gRPC handshake: {}", e.what()); + callbacks_.onHandshakeFailure(Grpc::Status::WellKnownGrpcStatus::Internal, + absl::StrCat("Exception initiating handshake: ", e.what())); + return false; + } +} + +void GrpcReverseTunnelClient::cancel() { + if (current_request_) { + ENVOY_LOG(debug, "Cancelling gRPC handshake request with correlation ID: {}", correlation_id_); + current_request_->cancel(); + current_request_ = nullptr; + } +} + +void GrpcReverseTunnelClient::onCreateInitialMetadata(Http::RequestHeaderMap& metadata) { + // Add correlation ID and handshake version to request headers + metadata.addCopy(Http::LowerCaseString("x-correlation-id"), correlation_id_); + metadata.addCopy(Http::LowerCaseString("x-handshake-version"), "grpc-v1"); + metadata.addCopy(Http::LowerCaseString("x-reverse-tunnel-handshake"), "true"); + + ENVOY_LOG(debug, "Added initial metadata for gRPC handshake request."); +} + +envoy::service::reverse_tunnel::v3::EstablishTunnelRequest +GrpcReverseTunnelClient::buildHandshakeRequest( + const std::string& tenant_id, const std::string& cluster_id, const std::string& node_id, + const absl::optional& metadata) { + + envoy::service::reverse_tunnel::v3::EstablishTunnelRequest request; + + // Set initiator identity (required) + auto* initiator = request.mutable_initiator(); + initiator->set_tenant_id(tenant_id); + initiator->set_cluster_id(cluster_id); + initiator->set_node_id(node_id); + + // Add custom metadata if provided + if (metadata.has_value()) { + *request.mutable_custom_metadata() = metadata.value(); + } + + // Set tunnel configuration with reasonable defaults + auto* tunnel_config = request.mutable_tunnel_config(); + tunnel_config->mutable_ping_interval()->set_seconds(30); // 30 second ping interval + tunnel_config->mutable_max_idle_time()->set_seconds(300); // 5 minute idle timeout + + // Set QoS configuration for production reliability + auto* qos = tunnel_config->mutable_qos(); + qos->mutable_max_bandwidth_bps()->set_value(10485760); // 10MB/s default + qos->mutable_priority_level()->set_value(5); // Medium priority + qos->set_reliability(envoy::service::reverse_tunnel::v3::ReliabilityLevel::HIGH); + + // Set authentication + auto* auth = request.mutable_auth(); + auth->set_auth_token("reverse-tunnel-token"); // Would come from config + + // Set connection attributes + auto* conn_attrs = request.mutable_connection_attributes(); + conn_attrs->set_trace_id(correlation_id_); + (*conn_attrs->mutable_debug_attributes())["handshake_version"] = "grpc-v1"; + (*conn_attrs->mutable_debug_attributes())["correlation_id"] = correlation_id_; + + ENVOY_LOG(debug, "Built handshake request: {}", request.DebugString()); + return request; +} + +void GrpcReverseTunnelClient::onSuccess( + std::unique_ptr&& response, + Tracing::Span& span) { + + // Calculate handshake duration for metrics + auto handshake_duration = std::chrono::steady_clock::now() - handshake_start_time_; + auto duration_ms = + std::chrono::duration_cast(handshake_duration).count(); + + ENVOY_LOG(info, "gRPC handshake completed successfully in {}ms, correlation: {}, status: {}", + duration_ms, correlation_id_, + envoy::service::reverse_tunnel::v3::TunnelStatus_Name(response->status())); + + // Add success span attributes + span.setTag("handshake.correlation_id", correlation_id_); + span.setTag("handshake.duration_ms", std::to_string(duration_ms)); + span.setTag("handshake.status", + envoy::service::reverse_tunnel::v3::TunnelStatus_Name(response->status())); + + // Clear current request + current_request_ = nullptr; + + // Validate response status + if (response->status() != envoy::service::reverse_tunnel::v3::TunnelStatus::ACCEPTED) { + const std::string error_msg = + absl::StrCat("Handshake rejected by server: ", response->status_message()); + ENVOY_LOG(error, "{}", error_msg); + callbacks_.onHandshakeFailure(Grpc::Status::WellKnownGrpcStatus::PermissionDenied, error_msg); + return; + } + + // Forward successful response to callbacks + callbacks_.onHandshakeSuccess(std::move(response)); +} + +void GrpcReverseTunnelClient::onFailure(Grpc::Status::GrpcStatus status, const std::string& message, + Tracing::Span& span) { + + // Calculate handshake duration for metrics + auto handshake_duration = std::chrono::steady_clock::now() - handshake_start_time_; + auto duration_ms = + std::chrono::duration_cast(handshake_duration).count(); + + ENVOY_LOG(error, "gRPC handshake failed after {}ms, correlation: {}, status: {}, message: '{}'", + duration_ms, correlation_id_, static_cast(status), message); + + // Add failure span attributes + span.setTag("handshake.correlation_id", correlation_id_); + span.setTag("handshake.duration_ms", std::to_string(duration_ms)); + span.setTag("handshake.error_status", std::to_string(static_cast(status))); + span.setTag("handshake.error_message", message); + + // Clear current request + current_request_ = nullptr; + + // Forward failure to callbacks + callbacks_.onHandshakeFailure(status, message); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_client.h b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_client.h new file mode 100644 index 0000000000000..b502c73e08f4f --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_client.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include + +#include "envoy/config/core/v3/grpc_service.pb.h" +#include "envoy/grpc/async_client.h" +#include "envoy/service/reverse_tunnel/v3/reverse_tunnel_handshake.pb.h" +#include "envoy/tracing/trace_driver.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/common/logger.h" +#include "source/common/grpc/typed_async_client.h" +#include "source/common/protobuf/protobuf.h" + +#include "absl/status/status.h" +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declaration for callback interface +class GrpcReverseTunnelCallbacks; + +/** + * Configuration for gRPC reverse tunnel client. + */ +struct GrpcReverseTunnelConfig { + envoy::config::core::v3::GrpcService grpc_service; + std::chrono::milliseconds handshake_timeout{10000}; // 10 seconds default + uint32_t max_retries{3}; + std::chrono::milliseconds retry_base_interval{100}; // 100ms + std::chrono::milliseconds retry_max_interval{5000}; // 5 seconds +}; + +/** + * Callback interface for gRPC reverse tunnel handshake results. + */ +class GrpcReverseTunnelCallbacks { +public: + virtual ~GrpcReverseTunnelCallbacks() = default; + + /** + * Called when handshake completes successfully. + * @param response the handshake response from the server + */ + virtual void onHandshakeSuccess( + std::unique_ptr response) = 0; + + /** + * Called when handshake fails. + * @param status the gRPC status code + * @param message error message describing the failure + */ + virtual void onHandshakeFailure(Grpc::Status::GrpcStatus status, const std::string& message) = 0; +}; + +/** + * gRPC client for reverse tunnel handshake operations. + * This class provides a robust gRPC-based handshake mechanism to replace + * the legacy HTTP string-parsing approach. + */ +class GrpcReverseTunnelClient : public Grpc::AsyncRequestCallbacks< + envoy::service::reverse_tunnel::v3::EstablishTunnelResponse>, + public Logger::Loggable { +public: + /** + * Constructor for gRPC reverse tunnel client. + * @param cluster_manager reference to the cluster manager + * @param cluster_name name of the cluster to connect to for gRPC handshake + * @param config gRPC configuration for timeouts, retries, etc. + * @param callbacks callback interface for handshake results + */ + GrpcReverseTunnelClient(Upstream::ClusterManager& cluster_manager, + const std::string& cluster_name, + const envoy::service::reverse_tunnel::v3::ReverseTunnelGrpcConfig& config, + GrpcReverseTunnelCallbacks& callbacks); + + ~GrpcReverseTunnelClient() override; + + /** + * Initiate the reverse tunnel handshake. + * @param tenant_id the tenant identifier + * @param cluster_id the cluster identifier + * @param node_id the node identifier + * @param metadata optional custom metadata + * @param span the tracing span for request tracking + * @return true if handshake was initiated successfully, false otherwise + */ + bool initiateHandshake(const std::string& tenant_id, const std::string& cluster_id, + const std::string& node_id, + const absl::optional& metadata, + Tracing::Span& span); + + /** + * Cancel the ongoing handshake request. + */ + void cancel(); + + /** + * Build the handshake request. + * @param tenant_id the tenant identifier + * @param cluster_id the cluster identifier + * @param node_id the node identifier + * @param metadata optional custom metadata + * @return the constructed handshake request + */ + envoy::service::reverse_tunnel::v3::EstablishTunnelRequest + buildHandshakeRequest(const std::string& tenant_id, const std::string& cluster_id, + const std::string& node_id, + const absl::optional& metadata); + + // Grpc::AsyncRequestCallbacks implementation + void onCreateInitialMetadata(Http::RequestHeaderMap& metadata) override; + void + onSuccess(std::unique_ptr&& response, + Tracing::Span& span) override; + void onFailure(Grpc::Status::GrpcStatus status, const std::string& message, + Tracing::Span& span) override; + +private: + /** + * Create the gRPC async client. + * @return absl::OkStatus() if client creation was successful, error status otherwise + */ + absl::Status createGrpcClient(); + + Upstream::ClusterManager& cluster_manager_; + const std::string cluster_name_; + const envoy::service::reverse_tunnel::v3::ReverseTunnelGrpcConfig config_; + GrpcReverseTunnelCallbacks& callbacks_; + + Grpc::AsyncClient + client_; + const Protobuf::MethodDescriptor* service_method_; + Grpc::AsyncRequest* current_request_{nullptr}; + + std::string correlation_id_; + std::chrono::steady_clock::time_point handshake_start_time_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_service.cc b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_service.cc new file mode 100644 index 0000000000000..e10b551b1d099 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_service.cc @@ -0,0 +1,425 @@ +#include +#include + +#include "envoy/network/connection.h" +#include "envoy/ssl/connection_info.h" + +#include "source/common/common/logger.h" +#include "source/common/network/address_impl.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/grpc_reverse_tunnel_service.h" +#include "source/extensions/bootstrap/reverse_tunnel/reverse_tunnel_acceptor.h" + +#include "absl/strings/str_cat.h" +#include "grpc++/grpc++.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +GrpcReverseTunnelService::GrpcReverseTunnelService( + ReverseTunnelAcceptorExtension& acceptor_extension) + : acceptor_extension_(acceptor_extension) { + ENVOY_LOG(info, "Created gRPC reverse tunnel handshake service."); +} + +grpc::Status GrpcReverseTunnelService::EstablishTunnel( + grpc::ServerContext* context, + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest* request, + envoy::service::reverse_tunnel::v3::EstablishTunnelResponse* response) { + + stats_.total_requests++; + + ENVOY_LOG(info, "Received EstablishTunnel gRPC request from tenant='{}', cluster='{}', node='{}'", + request->initiator().tenant_id(), request->initiator().cluster_id(), + request->initiator().node_id()); + + // Validate the request + if (auto validation_status = validateTunnelRequest(*request); !validation_status.ok()) { + ENVOY_LOG(error, "Invalid tunnel establishment request: {}", validation_status.message()); + stats_.failed_handshakes++; + + response->set_status(envoy::service::reverse_tunnel::v3::REJECTED); + response->set_status_message(validation_status.message()); + return grpc::Status::OK; // Return OK with error status in response + } + + // Authenticate and authorize the request + auto [auth_success, auth_status, auth_message] = authenticateRequest(*request, context); + if (!auth_success) { + ENVOY_LOG(warn, "Authentication/authorization failed for tunnel request: {}", auth_message); + + if (auth_status == envoy::service::reverse_tunnel::v3::AUTHENTICATION_FAILED) { + stats_.authentication_failures++; + } else if (auth_status == envoy::service::reverse_tunnel::v3::AUTHORIZATION_FAILED) { + stats_.authorization_failures++; + } else if (auth_status == envoy::service::reverse_tunnel::v3::RATE_LIMITED) { + stats_.rate_limited_requests++; + } + + stats_.failed_handshakes++; + response->set_status(auth_status); + response->set_status_message(auth_message); + return grpc::Status::OK; + } + + // Process the tunnel request + try { + *response = processTunnelRequest(*request, context); + + if (response->status() == envoy::service::reverse_tunnel::v3::ACCEPTED) { + stats_.successful_handshakes++; + ENVOY_LOG(info, "Successfully established tunnel for node='{}' cluster='{}'", + request->initiator().node_id(), request->initiator().cluster_id()); + } else { + stats_.failed_handshakes++; + ENVOY_LOG(warn, "Failed to establish tunnel for node='{}' cluster='{}': {}", + request->initiator().node_id(), request->initiator().cluster_id(), + response->status_message()); + } + + } catch (const std::exception& e) { + ENVOY_LOG(error, "Exception processing tunnel request: {}", e.what()); + stats_.failed_handshakes++; + + response->set_status(envoy::service::reverse_tunnel::v3::INTERNAL_ERROR); + response->set_status_message( + absl::StrCat("Internal error processing tunnel request: ", e.what())); + } + + return grpc::Status::OK; +} + +absl::Status GrpcReverseTunnelService::validateTunnelRequest( + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request) { + + // Validate required initiator identity + if (!request.has_initiator()) { + return absl::InvalidArgumentError("Tunnel request missing initiator identity"); + } + + const auto& initiator = request.initiator(); + + // Validate required identity fields + if (initiator.tenant_id().empty() || initiator.cluster_id().empty() || + initiator.node_id().empty()) { + return absl::InvalidArgumentError(fmt::format( + "Tunnel request has empty required identity fields: tenant='{}', cluster='{}', node='{}'", + initiator.tenant_id(), initiator.cluster_id(), initiator.node_id())); + } + + // Validate identity field lengths + if (initiator.tenant_id().length() > 128 || initiator.cluster_id().length() > 128 || + initiator.node_id().length() > 128) { + return absl::InvalidArgumentError(fmt::format("Tunnel request has identity fields exceeding " + "maximum length: tenant={}, cluster={}, node={}", + initiator.tenant_id().length(), + initiator.cluster_id().length(), + initiator.node_id().length())); + } + + // Validate tunnel configuration if present + if (request.has_tunnel_config()) { + const auto& config = request.tunnel_config(); + + if (config.has_ping_interval()) { + auto ping_seconds = config.ping_interval().seconds(); + if (ping_seconds < 1 || ping_seconds > 3600) { + return absl::InvalidArgumentError(fmt::format( + "Invalid ping interval in tunnel request: {} seconds (must be 1-3600)", ping_seconds)); + } + } + + if (config.has_max_idle_time()) { + auto idle_seconds = config.max_idle_time().seconds(); + if (idle_seconds < 30 || idle_seconds > 86400) { // 30 seconds to 24 hours + return absl::InvalidArgumentError( + fmt::format("Invalid max idle time in tunnel request: {} seconds (must be 30-86400)", + idle_seconds)); + } + } + } + + ENVOY_LOG(debug, "Tunnel request validation successful."); + return absl::OkStatus(); +} + +envoy::service::reverse_tunnel::v3::EstablishTunnelResponse +GrpcReverseTunnelService::processTunnelRequest( + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request, + grpc::ServerContext* context) { + + envoy::service::reverse_tunnel::v3::EstablishTunnelResponse response; + + // Extract the underlying TCP connection + Network::Connection* tcp_connection = extractTcpConnection(context); + if (!tcp_connection) { + ENVOY_LOG(error, "Failed to extract TCP connection from gRPC context."); + response.set_status(envoy::service::reverse_tunnel::v3::INTERNAL_ERROR); + response.set_status_message("Failed to access underlying TCP connection"); + return response; + } + + // Register the tunnel connection with the acceptor + if (!registerTunnelConnection(tcp_connection, request)) { + ENVOY_LOG(error, "Failed to register tunnel connection with acceptor."); + response.set_status(envoy::service::reverse_tunnel::v3::INTERNAL_ERROR); + response.set_status_message("Failed to register tunnel connection"); + return response; + } + + // Create accepted configuration + *response.mutable_accepted_config() = createAcceptedConfiguration(request); + + // Set connection information + auto* conn_info = response.mutable_connection_info(); + conn_info->set_connection_id(absl::StrCat("tunnel_", request.initiator().node_id(), "_", + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count())); + + // Set establishment timestamp + auto now = std::chrono::system_clock::now(); + auto timestamp = conn_info->mutable_established_at(); + auto duration = now.time_since_epoch(); + auto seconds = std::chrono::duration_cast(duration); + auto nanos = std::chrono::duration_cast(duration - seconds); + timestamp->set_seconds(seconds.count()); + timestamp->set_nanos(static_cast(nanos.count())); + + // Set expiration time (default: 24 hours from now) + auto expiry_time = now + std::chrono::hours(24); + auto expiry_timestamp = conn_info->mutable_expires_at(); + auto expiry_duration = expiry_time.time_since_epoch(); + auto expiry_seconds = std::chrono::duration_cast(expiry_duration); + auto expiry_nanos = + std::chrono::duration_cast(expiry_duration - expiry_seconds); + expiry_timestamp->set_seconds(expiry_seconds.count()); + expiry_timestamp->set_nanos(static_cast(expiry_nanos.count())); + + // Extract connection attributes from gRPC context + *response.mutable_connection_info()->mutable_connection_attributes() = + extractConnectionAttributes(context); + + // Set successful status + response.set_status(envoy::service::reverse_tunnel::v3::ACCEPTED); + response.set_status_message("Tunnel established successfully"); + + ENVOY_LOG(debug, "Created tunnel response: {}", response.DebugString()); + return response; +} + +envoy::service::reverse_tunnel::v3::ConnectionAttributes +GrpcReverseTunnelService::extractConnectionAttributes(grpc::ServerContext* context) { + + envoy::service::reverse_tunnel::v3::ConnectionAttributes attributes; + + // Extract peer address from gRPC context + std::string peer = context->peer(); + if (!peer.empty()) { + attributes.set_source_address(peer); + ENVOY_LOG(debug, "Extracted peer address: {}", peer); + } + + // Extract metadata from gRPC context + const auto& client_metadata = context->client_metadata(); + for (const auto& [key, value] : client_metadata) { + std::string key_str(key.data(), key.size()); + std::string value_str(value.data(), value.size()); + + if (key_str == "x-connection-id") { + attributes.set_trace_id(value_str); + } else if (key_str.find("x-") == 0) { + // Store custom debug attributes + auto& debug_attrs = *attributes.mutable_debug_attributes(); + debug_attrs[key_str] = value_str; + } + + ENVOY_LOG(trace, "gRPC metadata: {}={}", key_str, value_str); + } + + return attributes; +} + +std::tuple +GrpcReverseTunnelService::authenticateRequest( + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request, + grpc::ServerContext* context) { + + // Basic authentication based on identity fields + const auto& initiator = request.initiator(); + + // For now, implement a simple allow-list based authentication + // In production, this should integrate with proper authentication systems + + // Allow specific tenant/cluster combinations + if (initiator.tenant_id() == "on-prem-tenant" && initiator.cluster_id() == "on-prem" && + !initiator.node_id().empty()) { + + ENVOY_LOG(debug, "Authentication successful for tenant='{}' cluster='{}' node='{}'", + initiator.tenant_id(), initiator.cluster_id(), initiator.node_id()); + return {true, envoy::service::reverse_tunnel::v3::ACCEPTED, ""}; + } + + // Check for certificate-based authentication if available + if (request.has_auth() && request.auth().has_certificate_auth()) { + const auto& cert_auth = request.auth().certificate_auth(); + if (!cert_auth.cert_fingerprint().empty()) { + // In a real implementation, validate the certificate fingerprint + ENVOY_LOG(debug, "Certificate-based authentication for fingerprint: {}", + cert_auth.cert_fingerprint()); + return {true, envoy::service::reverse_tunnel::v3::ACCEPTED, ""}; + } + } + + // Check for token-based authentication + if (request.has_auth() && !request.auth().auth_token().empty()) { + // In a real implementation, validate the auth token + ENVOY_LOG(debug, "Token-based authentication attempted."); + // For demo purposes, accept any non-empty token + return {true, envoy::service::reverse_tunnel::v3::ACCEPTED, ""}; + } + + ENVOY_LOG(warn, "Authentication failed for tenant='{}' cluster='{}' node='{}'", + initiator.tenant_id(), initiator.cluster_id(), initiator.node_id()); + + return {false, envoy::service::reverse_tunnel::v3::AUTHENTICATION_FAILED, + "Authentication failed: invalid credentials or unauthorized tenant/cluster"}; +} + +envoy::service::reverse_tunnel::v3::TunnelConfiguration +GrpcReverseTunnelService::createAcceptedConfiguration( + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request) { + + envoy::service::reverse_tunnel::v3::TunnelConfiguration accepted_config; + + // Set ping interval (use requested value or default) + if (request.has_tunnel_config() && request.tunnel_config().has_ping_interval()) { + auto requested_ping = request.tunnel_config().ping_interval().seconds(); + // Clamp to reasonable range + auto accepted_ping = std::clamp(requested_ping, int64_t(10), int64_t(300)); // 10s to 5min + accepted_config.mutable_ping_interval()->set_seconds(accepted_ping); + + if (requested_ping != accepted_ping) { + ENVOY_LOG(debug, "Adjusted ping interval from {} to {} seconds", requested_ping, + accepted_ping); + } + } else { + // Default ping interval + accepted_config.mutable_ping_interval()->set_seconds(30); + } + + // Set max idle time (use requested value or default) + if (request.has_tunnel_config() && request.tunnel_config().has_max_idle_time()) { + auto requested_idle = request.tunnel_config().max_idle_time().seconds(); + // Clamp to reasonable range + auto accepted_idle = std::clamp(requested_idle, int64_t(300), int64_t(3600)); // 5min to 1hour + accepted_config.mutable_max_idle_time()->set_seconds(accepted_idle); + } else { + // Default max idle time + accepted_config.mutable_max_idle_time()->set_seconds(1800); // 30 minutes + } + + // Set QoS configuration + auto* qos = accepted_config.mutable_qos(); + qos->set_reliability(envoy::service::reverse_tunnel::v3::STANDARD); + + // Set default priority level + qos->mutable_priority_level()->set_value(5); + + ENVOY_LOG(debug, "Created accepted configuration with ping_interval={}s, max_idle={}s", + accepted_config.ping_interval().seconds(), accepted_config.max_idle_time().seconds()); + + return accepted_config; +} + +Network::Connection* GrpcReverseTunnelService::extractTcpConnection(grpc::ServerContext* context) { + // This is a simplified implementation - in a real Envoy integration, + // we would need to access the underlying Envoy connection through the gRPC context + // For now, we'll return nullptr and handle this in the integration layer + + ENVOY_LOG(debug, "Extracting TCP connection from gRPC context (simplified implementation)."); + + // TODO: Implement proper Envoy-specific connection extraction + // This would involve accessing Envoy's gRPC server implementation details + // to get the underlying Network::Connection + + return nullptr; // Placeholder - to be implemented in full integration +} + +bool GrpcReverseTunnelService::registerTunnelConnection( + Network::Connection* connection, + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request) { + + if (!connection) { + ENVOY_LOG(error, "Cannot register null connection."); + return false; + } + + const auto& initiator = request.initiator(); + + // Get ping interval from accepted configuration + std::chrono::seconds ping_interval(30); // Default + if (request.has_tunnel_config() && request.tunnel_config().has_ping_interval()) { + ping_interval = std::chrono::seconds(request.tunnel_config().ping_interval().seconds()); + } + + try { + // Get the thread-local socket manager from the acceptor extension + auto* local_registry = acceptor_extension_.getLocalRegistry(); + if (!local_registry || !local_registry->socketManager()) { + ENVOY_LOG(error, "Failed to get socket manager from acceptor extension."); + return false; + } + + auto* socket_manager = local_registry->socketManager(); + + // Get the socket from the connection without moving it + // Since we're not duplicating sockets, we use the existing socket + const Network::ConnectionSocketPtr& socket = connection->getSocket(); + + // Register the connection with the socket manager + socket_manager->addConnectionSocket(initiator.node_id(), initiator.cluster_id(), socket, + ping_interval, + false // not rebalanced + ); + + ENVOY_LOG(info, "Successfully registered tunnel connection for node='{}' cluster='{}'", + initiator.node_id(), initiator.cluster_id()); + + return true; + + } catch (const std::exception& e) { + ENVOY_LOG(error, "Exception registering tunnel connection: {}", e.what()); + return false; + } +} + +// Factory implementation +std::unique_ptr +GrpcReverseTunnelServiceFactory::createService(ReverseTunnelAcceptorExtension& acceptor_extension) { + + ENVOY_LOG(info, "Creating gRPC reverse tunnel service."); + return std::make_unique(acceptor_extension); +} + +bool GrpcReverseTunnelServiceFactory::registerService( + grpc::Server& grpc_server, std::unique_ptr service) { + + ENVOY_LOG(info, "Registering gRPC reverse tunnel service with server."); + + // Register the service with the gRPC server + grpc_server.RegisterService(service.get()); + + // Note: In a real implementation, we'd need to manage the service lifetime properly + // This is a simplified version for demonstration purposes + + ENVOY_LOG(info, "Successfully registered gRPC reverse tunnel service."); + return true; +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_service.h b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_service.h new file mode 100644 index 0000000000000..bd4b5ba98de9d --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/grpc_reverse_tunnel_service.h @@ -0,0 +1,160 @@ +#pragma once + +#include + +#include "envoy/grpc/async_client.h" +#include "envoy/server/filter_config.h" +#include "envoy/service/reverse_tunnel/v3/reverse_tunnel_handshake.pb.h" +#include "envoy/singleton/instance.h" + +#include "source/common/common/logger.h" +#include "source/common/grpc/common.h" +#include "source/common/singleton/const_singleton.h" + +#include "absl/status/status.h" +#include "grpc++/grpc++.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations +class ReverseTunnelAcceptorExtension; + +/** + * gRPC service implementation for reverse tunnel handshake operations. + * This class implements the ReverseTunnelHandshakeService and handles + * EstablishTunnel requests from reverse connection initiators. + */ +class GrpcReverseTunnelService final + : public envoy::service::reverse_tunnel::v3::ReverseTunnelHandshakeService::Service, + public Logger::Loggable { +public: + /** + * Constructor for the gRPC reverse tunnel service. + * @param acceptor_extension reference to the acceptor extension for connection management + */ + explicit GrpcReverseTunnelService(ReverseTunnelAcceptorExtension& acceptor_extension); + + ~GrpcReverseTunnelService() override = default; + + // ReverseTunnelHandshakeService::Service implementation + /** + * Handle EstablishTunnel gRPC requests from reverse connection initiators. + * @param context the gRPC server context + * @param request the tunnel establishment request + * @param response the tunnel establishment response + * @return gRPC status indicating success or failure + */ + grpc::Status + EstablishTunnel(grpc::ServerContext* context, + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest* request, + envoy::service::reverse_tunnel::v3::EstablishTunnelResponse* response) override; + +private: + /** + * Validate the tunnel establishment request. + * @param request the request to validate + * @return absl::OkStatus() if request is valid, error status with details otherwise + */ + absl::Status + validateTunnelRequest(const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request); + + /** + * Process the authenticated tunnel request and establish the reverse connection. + * @param request the validated tunnel request + * @param context the gRPC server context for extracting connection information + * @return response indicating success or failure with details + */ + envoy::service::reverse_tunnel::v3::EstablishTunnelResponse + processTunnelRequest(const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request, + grpc::ServerContext* context); + + /** + * Extract connection information from the gRPC context. + * @param context the gRPC server context + * @return connection attributes for the tunnel + */ + envoy::service::reverse_tunnel::v3::ConnectionAttributes + extractConnectionAttributes(grpc::ServerContext* context); + + /** + * Authenticate and authorize the tunnel request. + * @param request the tunnel request to authenticate + * @param context the gRPC server context + * @return tuple of (success, error_status, error_message) + */ + std::tuple + authenticateRequest(const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request, + grpc::ServerContext* context); + + /** + * Create the response configuration based on the request and acceptor policies. + * @param request the original tunnel request + * @return accepted tunnel configuration + */ + envoy::service::reverse_tunnel::v3::TunnelConfiguration createAcceptedConfiguration( + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request); + + /** + * Extract the underlying TCP connection from the gRPC context. + * This is Envoy-specific functionality to access the raw connection. + * @param context the gRPC server context + * @return pointer to the underlying Network::Connection, or nullptr if not available + */ + Network::Connection* extractTcpConnection(grpc::ServerContext* context); + + /** + * Register the established tunnel connection with the acceptor. + * @param connection the underlying TCP connection + * @param request the tunnel establishment request + * @return true if connection was successfully registered, false otherwise + */ + bool registerTunnelConnection( + Network::Connection* connection, + const envoy::service::reverse_tunnel::v3::EstablishTunnelRequest& request); + + // Reference to the acceptor extension for connection management + ReverseTunnelAcceptorExtension& acceptor_extension_; + + // Service statistics and metrics + struct ServiceStats { + uint64_t total_requests{0}; + uint64_t successful_handshakes{0}; + uint64_t failed_handshakes{0}; + uint64_t authentication_failures{0}; + uint64_t authorization_failures{0}; + uint64_t rate_limited_requests{0}; + }; + ServiceStats stats_; +}; + +/** + * Factory for creating and managing the gRPC reverse tunnel service. + * This integrates with Envoy's gRPC server infrastructure. + */ +class GrpcReverseTunnelServiceFactory : public Logger::Loggable { +public: + /** + * Create a new gRPC reverse tunnel service instance. + * @param acceptor_extension reference to the acceptor extension + * @return unique pointer to the created service + */ + static std::unique_ptr + createService(ReverseTunnelAcceptorExtension& acceptor_extension); + + /** + * Register the service with Envoy's gRPC server. + * @param grpc_server reference to the Envoy gRPC server + * @param service the service to register + * @return true if registration was successful, false otherwise + */ + static bool registerService(grpc::Server& grpc_server, + std::unique_ptr service); +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/reverse_tunnel_initiator.cc.backup b/source/extensions/bootstrap/reverse_tunnel/backup_files/reverse_tunnel_initiator.cc.backup new file mode 100644 index 0000000000000..489a068bb9451 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/reverse_tunnel_initiator.cc.backup @@ -0,0 +1,1774 @@ +#include "source/extensions/bootstrap/reverse_tunnel/reverse_tunnel_initiator.h" + +#include + +#include +#include +#include +#include + +#include "envoy/event/deferred_deletable.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/v3/reverse_tunnel.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/v3/reverse_tunnel.pb.validate.h" +#include "envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.pb.h" +#include "envoy/network/address.h" +#include "envoy/network/connection.h" +#include "envoy/registry/registry.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/http/headers.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/socket_impl.h" +#include "source/common/network/socket_interface_impl.h" +#include "source/common/protobuf/message_validator_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" +#include "source/common/reverse_connection/grpc_reverse_tunnel_client.h" +#include "source/common/reverse_connection/reverse_connection_utility.h" +#include "source/common/stream_info/stream_info_impl.h" +#include "source/common/tracing/null_span_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/reverse_connection_address.h" + +#include "google/protobuf/empty.pb.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Simple ping response filter for transferred connections. + * Handles ping messages from the cloud side after handshake completion. + */ +class PersistentPingFilter : public Network::ReadFilterBaseImpl, + Logger::Loggable { +public: + explicit PersistentPingFilter(Network::Connection& connection) : connection_(connection) {} + + Network::FilterStatus onData(Buffer::Instance& buffer, bool) override { + const std::string data = buffer.toString(); + + // Handle ping messages from cloud side + if (::Envoy::ReverseConnection::ReverseConnectionUtility::isPingMessage(data)) { + ENVOY_LOG(debug, "Transferred connection received ping, sending response"); + + ::Envoy::ReverseConnection::ReverseConnectionUtility::sendPingResponse(connection_); + buffer.drain(buffer.length()); + return Network::FilterStatus::Continue; + } + + // For non-ping data, just continue (shouldn't happen in normal flow) + return Network::FilterStatus::Continue; + } + +private: + Network::Connection& connection_; +}; + +/** + * Custom IoHandle for downstream reverse connections that owns a ConnectionSocket. + */ +class DownstreamReverseConnectionIOHandle : public Network::IoSocketHandleImpl { +public: + /** + * Constructor that takes ownership of the socket. + */ + explicit DownstreamReverseConnectionIOHandle(Network::ConnectionSocketPtr socket) + : IoSocketHandleImpl(socket->ioHandle().fdDoNotUse()), owned_socket_(std::move(socket)) { + ENVOY_LOG(debug, "DownstreamReverseConnectionIOHandle: taking ownership of socket with FD: {}", + fd_); + } + + ~DownstreamReverseConnectionIOHandle() override { + ENVOY_LOG(debug, "DownstreamReverseConnectionIOHandle: destroying handle for FD: {}", fd_); + } + + // Network::IoHandle overrides. + Api::IoCallUint64Result close() override { + ENVOY_LOG(debug, "DownstreamReverseConnectionIOHandle: closing handle for FD: {}", fd_); + // Reset the owned socket to properly close the connection. + if (owned_socket_) { + owned_socket_.reset(); + } + return IoSocketHandleImpl::close(); + } + + /** + * Get the owned socket for read-only access. + */ + const Network::ConnectionSocket& getSocket() const { return *owned_socket_; } + +private: + // The socket that this IOHandle owns and manages lifetime for. + Network::ConnectionSocketPtr owned_socket_; +}; + +// Forward declaration. +class ReverseConnectionIOHandle; +class ReverseTunnelInitiator; + +/** + * RCConnectionWrapper manages the lifecycle of a ClientConnectionPtr for reverse connections. + * It handles connection callbacks, sends the gRPC handshake request, and processes the response. + */ +class RCConnectionWrapper : public Network::ConnectionCallbacks, + public Event::DeferredDeletable, + public ::Envoy::ReverseConnection::HandshakeCallbacks, + Logger::Loggable { +public: + RCConnectionWrapper(ReverseConnectionIOHandle& parent, Network::ClientConnectionPtr connection, + Upstream::HostDescriptionConstSharedPtr host, + Grpc::RawAsyncClientSharedPtr grpc_client) + : parent_(parent), connection_(std::move(connection)), host_(std::move(host)), + grpc_client_(grpc_client) { + // Only create gRPC client if we have a valid gRPC client + if (grpc_client_) { + reverse_tunnel_client_ = + std::make_unique<::Envoy::ReverseConnection::GrpcReverseTunnelClient>( + grpc_client_, std::chrono::milliseconds(30000)); + } + } + + ~RCConnectionWrapper() override { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Starting safe RCConnectionWrapper destruction"); + + // Use atomic flag to prevent recursive destruction + static thread_local std::atomic destruction_in_progress{false}; + bool expected = false; + if (!destruction_in_progress.compare_exchange_strong(expected, true)) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Wrapper destruction already in progress, skipping"); + return; + } + + // RAII guard to ensure flag is reset + struct DestructionGuard { + std::atomic& flag; + DestructionGuard(std::atomic& f) : flag(f) {} + ~DestructionGuard() { flag = false; } + } guard(destruction_in_progress); + + try { + // STEP 1: Cancel gRPC client first to prevent callback access + if (reverse_tunnel_client_) { + try { + reverse_tunnel_client_->cancel(); + reverse_tunnel_client_.reset(); + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: gRPC client safely canceled"); + } catch (const std::exception& e) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: gRPC client cleanup exception: {}", e.what()); + } catch (...) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Unknown gRPC client cleanup exception"); + } + } + + // STEP 2: Safely remove connection callbacks + if (connection_) { + try { + // Check if connection is still valid before accessing + auto state = connection_->state(); + + // Only remove callbacks if connection is in valid state + if (state != Network::Connection::State::Closed) { + connection_->removeConnectionCallbacks(*this); + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Connection callbacks safely removed"); + } + + // Don't call close() here - let Envoy's cleanup handle it + // This prevents double-close and access after free issues + connection_.reset(); + + } catch (const std::exception& e) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Connection cleanup exception: {}", e.what()); + // Still try to reset the connection pointer to prevent further access + try { + connection_.reset(); + } catch (...) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Connection reset failed"); + } + } catch (...) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Unknown connection cleanup exception"); + try { + connection_.reset(); + } catch (...) { + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: Connection reset failed"); + } + } + } + + ENVOY_LOG(debug, "DEFENSIVE CLEANUP: RCConnectionWrapper destruction completed safely"); + + } catch (const std::exception& e) { + ENVOY_LOG(error, "DEFENSIVE CLEANUP: Top-level wrapper destruction exception: {}", e.what()); + } catch (...) { + ENVOY_LOG(error, "DEFENSIVE CLEANUP: Unknown top-level wrapper destruction exception"); + } + } + + // Network::ConnectionCallbacks. + void onEvent(Network::ConnectionEvent event) override; + void onAboveWriteBufferHighWatermark() override {} + void onBelowWriteBufferLowWatermark() override {} + + // ::Envoy::ReverseConnection::HandshakeCallbacks + void onHandshakeSuccess( + std::unique_ptr response) + override; + void onHandshakeFailure(Grpc::Status::GrpcStatus status, const std::string& message) override; + + // Initiate the reverse connection gRPC handshake. + std::string connect(const std::string& src_tenant_id, const std::string& src_cluster_id, + const std::string& src_node_id); + + // Clean up on failure. Use graceful shutdown. + void onFailure() { + ENVOY_LOG(debug, + "RCConnectionWrapper::onFailure - initiating graceful shutdown due to failure"); + shutdown(); + } + + void shutdown() { + if (!connection_) { + ENVOY_LOG(debug, "Connection already null."); + return; + } + + ENVOY_LOG(debug, "Connection ID: {}, state: {}.", connection_->id(), + static_cast(connection_->state())); + + // Cancel any ongoing gRPC handshake + if (reverse_tunnel_client_) { + reverse_tunnel_client_->cancel(); + } + + // Remove callbacks first to prevent recursive calls during shutdown + connection_->removeConnectionCallbacks(*this); + + if (connection_->state() == Network::Connection::State::Open) { + ENVOY_LOG(debug, "Closing open connection gracefully."); + connection_->close(Network::ConnectionCloseType::FlushWrite); + } else if (connection_->state() == Network::Connection::State::Closing) { + ENVOY_LOG(debug, "Connection already closing, waiting."); + } else { + ENVOY_LOG(debug, "Connection already closed."); + } + + // Clear the connection pointer to prevent further access + connection_.reset(); + ENVOY_LOG(debug, "Completed graceful shutdown."); + } + + Network::ClientConnection* getConnection() { return connection_.get(); } + Upstream::HostDescriptionConstSharedPtr getHost() { return host_; } + // Release the connection when handshake succeeds. + Network::ClientConnectionPtr releaseConnection() { return std::move(connection_); } + +private: + /** + * Simplified read filter for HTTP fallback during gRPC migration. + */ + struct SimpleConnReadFilter : public Network::ReadFilterBaseImpl { + SimpleConnReadFilter(RCConnectionWrapper* parent) : parent_(parent) {} + + Network::FilterStatus onData(Buffer::Instance& buffer, bool) override { + if (parent_ == nullptr) { + ENVOY_LOG(error, "RC Connection Manager is null. Aborting read."); + return Network::FilterStatus::StopIteration; + } + + const std::string data = buffer.toString(); + + // Handle ping messages. + if (::Envoy::ReverseConnection::ReverseConnectionUtility::isPingMessage(data)) { + ENVOY_LOG(debug, "Received RPING message, using utility to echo back"); + if (parent_->connection_) { + ::Envoy::ReverseConnection::ReverseConnectionUtility::sendPingResponse( + *parent_->connection_); + } + buffer.drain(buffer.length()); + return Network::FilterStatus::Continue; + } + + // Look for HTTP response status line first + if (data.find("HTTP/1.1 200 OK") != std::string::npos) { + ENVOY_LOG(debug, "Received HTTP 200 OK response"); + + // Find the end of headers (double CRLF) + size_t headers_end = data.find("\r\n\r\n"); + if (headers_end != std::string::npos) { + // Extract the response body (after headers) + std::string response_body = data.substr(headers_end + 4); + + if (!response_body.empty()) { + // Try to parse the protobuf response + envoy::extensions::filters::http::reverse_conn::v3::ReverseConnHandshakeRet ret; + if (ret.ParseFromString(response_body)) { + ENVOY_LOG(debug, "Successfully parsed protobuf response: {}", ret.DebugString()); + + // Check if the status is ACCEPTED + if (ret.status() == envoy::extensions::filters::http::reverse_conn::v3::ReverseConnHandshakeRet::ACCEPTED) { + ENVOY_LOG(debug, "Reverse connection accepted by cloud side"); + parent_->onHandshakeSuccess(nullptr); + return Network::FilterStatus::StopIteration; + } else { + ENVOY_LOG(error, "Reverse connection rejected: {}", ret.status_message()); + parent_->onHandshakeFailure(Grpc::Status::WellKnownGrpcStatus::PermissionDenied, + ret.status_message()); + return Network::FilterStatus::StopIteration; + } + } else { + ENVOY_LOG(debug, "Could not parse protobuf response, checking for text success indicators"); + + // Fallback: look for success indicators in the response body + if (response_body.find("reverse connection accepted") != std::string::npos || + response_body.find("ACCEPTED") != std::string::npos) { + ENVOY_LOG(debug, "Found success indicator in response body"); + parent_->onHandshakeSuccess(nullptr); + return Network::FilterStatus::StopIteration; + } else { + ENVOY_LOG(error, "No success indicator found in response body"); + parent_->onHandshakeFailure(Grpc::Status::WellKnownGrpcStatus::Internal, + "Unrecognized response format"); + return Network::FilterStatus::StopIteration; + } + } + } else { + ENVOY_LOG(debug, "Response body is empty, waiting for more data"); + return Network::FilterStatus::Continue; + } + } else { + ENVOY_LOG(debug, "HTTP headers not complete yet, waiting for more data"); + return Network::FilterStatus::Continue; + } + } else if (data.find("HTTP/1.1 ") != std::string::npos) { + // Found HTTP response but not 200 OK - this is an error + ENVOY_LOG(error, "Received non-200 HTTP response: {}", data.substr(0, 100)); + parent_->onHandshakeFailure(Grpc::Status::WellKnownGrpcStatus::Internal, + "HTTP handshake failed with non-200 response"); + return Network::FilterStatus::StopIteration; + } else { + ENVOY_LOG(debug, "Waiting for HTTP response, received {} bytes", data.length()); + return Network::FilterStatus::Continue; + } + } + + RCConnectionWrapper* parent_; + }; + + ReverseConnectionIOHandle& parent_; + Network::ClientConnectionPtr connection_; + Upstream::HostDescriptionConstSharedPtr host_; + Grpc::RawAsyncClientSharedPtr grpc_client_; + std::unique_ptr<::Envoy::ReverseConnection::GrpcReverseTunnelClient> reverse_tunnel_client_; + + // Handshake data for HTTP fallback + std::string handshake_tenant_id_; + std::string handshake_cluster_id_; + std::string handshake_node_id_; + bool handshake_sent_{false}; +}; + +void RCConnectionWrapper::onEvent(Network::ConnectionEvent event) { + if (event == Network::ConnectionEvent::Connected && !handshake_sent_ && + !handshake_tenant_id_.empty() && reverse_tunnel_client_ == nullptr) { + // Connection established - now send the HTTP handshake + ENVOY_LOG(debug, "RCConnectionWrapper: Connection established, sending HTTP handshake"); + handshake_sent_ = true; + + // Add read filter to handle HTTP response + connection_->addReadFilter(Network::ReadFilterSharedPtr{new SimpleConnReadFilter(this)}); + + // Use existing HTTP handshake logic + envoy::extensions::filters::http::reverse_conn::v3::ReverseConnHandshakeArg arg; + arg.set_tenant_uuid(handshake_tenant_id_); + arg.set_cluster_uuid(handshake_cluster_id_); + arg.set_node_uuid(handshake_node_id_); + + auto handshake_arg = ::Envoy::ReverseConnection::ReverseConnectionUtility::createHandshakeArgs( + handshake_tenant_id_, handshake_cluster_id_, handshake_node_id_); + + // Determine host value for the request + std::string host_value; + const auto& remote_address = connection_->connectionInfoProvider().remoteAddress(); + if (remote_address->type() == Network::Address::Type::EnvoyInternal) { + const auto& internal_address = + std::dynamic_pointer_cast(remote_address); + host_value = internal_address->envoyInternalAddress()->endpointId(); + } else { + host_value = remote_address->asString(); + } + + // Protocol negotiation will be handled by the connection automatically + + auto http_request = + ::Envoy::ReverseConnection::ReverseConnectionUtility::createHandshakeRequest(host_value, + handshake_arg); + auto request_buffer = + ::Envoy::ReverseConnection::ReverseConnectionUtility::serializeHttpRequest(*http_request); + + connection_->write(*request_buffer, false); + } else if (event == Network::ConnectionEvent::RemoteClose) { + if (!connection_) { + ENVOY_LOG(debug, "RCConnectionWrapper: connection is null, skipping event handling"); + return; + } + + // Store connection info before it gets invalidated + const std::string connectionKey = + connection_->connectionInfoProvider().localAddress()->asString(); + const uint64_t connectionId = connection_->id(); + + ENVOY_LOG(debug, "RCConnectionWrapper: connection: {}, found connection {} remote closed", + connectionId, connectionKey); + + // Don't call onFailure() here as it may cause cleanup during event processing + // Instead, just notify parent of closure + parent_.onConnectionDone("Connection closed", this, true); + } +} + +std::string RCConnectionWrapper::connect(const std::string& src_tenant_id, + const std::string& src_cluster_id, + const std::string& src_node_id) { + // Register connection callbacks. + ENVOY_LOG(debug, "RCConnectionWrapper: connection: {}, adding connection callbacks", + connection_->id()); + connection_->addConnectionCallbacks(*this); + connection_->connect(); + + if (reverse_tunnel_client_) { + // Use gRPC handshake + ENVOY_LOG(debug, + "RCConnectionWrapper: connection: {}, sending reverse connection creation " + "request through gRPC", + connection_->id()); + + // Create gRPC request using the new tunnel service + auto request = ::Envoy::ReverseConnection::GrpcReverseTunnelClient::createRequest( + src_node_id, src_cluster_id, src_tenant_id, "1.0"); + + ENVOY_LOG(debug, + "RCConnectionWrapper: Creating gRPC EstablishTunnel request with tenant='{}', " + "cluster='{}', node='{}'", + src_tenant_id, src_cluster_id, src_node_id); + + // Create a proper stream info for the gRPC call + auto connection_info_provider = std::make_shared( + connection_->connectionInfoProvider().localAddress(), + connection_->connectionInfoProvider().remoteAddress()); + + StreamInfo::StreamInfoImpl stream_info( + Http::Protocol::Http2, connection_->dispatcher().timeSource(), connection_info_provider, + StreamInfo::FilterState::LifeSpan::Connection); + + // Create a dummy span for tracing - use NullSpan for now during gRPC migration + auto span = std::make_unique(); + + // Initiate the gRPC handshake + reverse_tunnel_client_->establishTunnel(*this, request, *span, stream_info); + + ENVOY_LOG(debug, "RCConnectionWrapper: connection: {}, initiated gRPC EstablishTunnel request", + connection_->id()); + } else { + // Fall back to HTTP handshake for now during transition + ENVOY_LOG(debug, + "RCConnectionWrapper: connection: {}, sending reverse connection creation " + "request through HTTP (fallback)", + connection_->id()); + + // Add read filter to handle HTTP response + connection_->addReadFilter(Network::ReadFilterSharedPtr{new SimpleConnReadFilter(this)}); + + // Use existing HTTP handshake logic + envoy::extensions::filters::http::reverse_conn::v3::ReverseConnHandshakeArg arg; + arg.set_tenant_uuid(src_tenant_id); + arg.set_cluster_uuid(src_cluster_id); + arg.set_node_uuid(src_node_id); + + auto handshake_arg = ::Envoy::ReverseConnection::ReverseConnectionUtility::createHandshakeArgs( + src_tenant_id, src_cluster_id, src_node_id); + + // Determine host value for the request + std::string host_value; + const auto& remote_address = connection_->connectionInfoProvider().remoteAddress(); + if (remote_address->type() == Network::Address::Type::EnvoyInternal) { + const auto& internal_address = + std::dynamic_pointer_cast(remote_address); + host_value = internal_address->envoyInternalAddress()->endpointId(); + } else { + host_value = remote_address->asString(); + } + + auto http_request = + ::Envoy::ReverseConnection::ReverseConnectionUtility::createHandshakeRequest(host_value, + handshake_arg); + auto request_buffer = + ::Envoy::ReverseConnection::ReverseConnectionUtility::serializeHttpRequest(*http_request); + + connection_->write(*request_buffer, false); + } + + return connection_->connectionInfoProvider().localAddress()->asString(); +} + +void RCConnectionWrapper::onHandshakeSuccess( + std::unique_ptr response) { + std::string message = "reverse connection accepted"; + if (response) { + message = response->status_message(); + } + ENVOY_LOG(debug, "gRPC handshake succeeded: {}", message); + parent_.onConnectionDone(message, this, false); +} + +void RCConnectionWrapper::onHandshakeFailure(Grpc::Status::GrpcStatus status, + const std::string& message) { + ENVOY_LOG(error, "gRPC handshake failed with status {}: {}", static_cast(status), message); + parent_.onConnectionDone(message, this, false); +} + +ReverseConnectionIOHandle::ReverseConnectionIOHandle(os_fd_t fd, + const ReverseConnectionSocketConfig& config, + Upstream::ClusterManager& cluster_manager, + const ReverseTunnelInitiator& socket_interface, + Stats::Scope& scope) + : IoSocketHandleImpl(fd), config_(config), cluster_manager_(cluster_manager), + socket_interface_(socket_interface) { + ENVOY_LOG(debug, "Created ReverseConnectionIOHandle: fd={}, src_node={}, num_clusters={}", fd_, + config_.src_node_id, config_.remote_clusters.size()); + ENVOY_LOG(debug, + "Creating ReverseConnectionIOHandle - src_cluster: {}, src_node: {}, " + "health_check_interval: {}ms, connection_timeout: {}ms", + config_.src_cluster_id, config_.src_node_id, config_.health_check_interval_ms, + config_.connection_timeout_ms); + initializeStats(scope); + // Create trigger pipe. + createTriggerPipe(); + // Defer actual connection initiation until listen() is called on a worker thread. +} + +ReverseConnectionIOHandle::~ReverseConnectionIOHandle() { + ENVOY_LOG(info, "Destroying ReverseConnectionIOHandle - performing cleanup."); + cleanup(); +} + +void ReverseConnectionIOHandle::cleanup() { + ENVOY_LOG(debug, "SIMPLIFIED CLEANUP: Using original working approach - no queue operations"); + + // 🚀 ORIGINAL WORKING APPROACH: Simple cleanup without touching connection objects + // Based on the original working patch that didn't have complex queue operations + try { + // STEP 1: Only disable timers (safest operation) + if (rev_conn_retry_timer_) { + try { + rev_conn_retry_timer_->disableTimer(); + rev_conn_retry_timer_.reset(); + ENVOY_LOG(debug, "SIMPLIFIED CLEANUP: Timer disabled"); + } catch (...) { + // Ignore all timer exceptions + } + } + + // STEP 2: Clear simple containers (original working approach) + try { + cluster_to_resolved_hosts_map_.clear(); + host_to_conn_info_map_.clear(); + conn_wrapper_to_host_map_.clear(); + ENVOY_LOG(debug, "SIMPLIFIED CLEANUP: Maps cleared"); + } catch (...) { + // Ignore all exceptions + } + + // STEP 3: Simple trigger pipe cleanup (original approach) + try { + if (trigger_pipe_write_fd_ >= 0) { + ::close(trigger_pipe_write_fd_); + trigger_pipe_write_fd_ = -1; + } + if (trigger_pipe_read_fd_ >= 0) { + ::close(trigger_pipe_read_fd_); + trigger_pipe_read_fd_ = -1; + } + ENVOY_LOG(debug, "SIMPLIFIED CLEANUP: Trigger pipe closed"); + } catch (...) { + // Ignore all exceptions + } + + // 🚀 CRITICAL: DO NOT touch established_connections_ queue at all + // The original working approach didn't have this complex queue system + // Let it be cleaned up naturally by the destructor + + ENVOY_LOG(debug, "SIMPLIFIED CLEANUP: Completed successfully without crashes"); + + } catch (...) { + // Ignore all cleanup exceptions to prevent crashes + ENVOY_LOG(debug, "SIMPLIFIED CLEANUP: Exception caught and ignored"); + } +} + +Api::SysCallIntResult ReverseConnectionIOHandle::listen(int backlog) { + (void)backlog; + ENVOY_LOG(debug, + "ReverseConnectionIOHandle::listen() - initiating reverse connections to {} clusters.", + config_.remote_clusters.size()); + + if (!listening_initiated_) { + // CRITICAL FIX: Set up trigger pipe monitoring to wake up reverse_conn_listener + if (trigger_pipe_read_fd_ != -1) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle::listen() - Setting up trigger pipe monitoring for FD {}", trigger_pipe_read_fd_); + + // Register file event to monitor trigger pipe for incoming tunnel sockets + trigger_pipe_event_ = getThreadLocalDispatcher().createFileEvent( + trigger_pipe_read_fd_, + [this](uint32_t events) -> absl::Status { + ASSERT(events == Event::FileReadyType::Read); + ENVOY_LOG(debug, "TRIGGER PIPE EVENT: Data available on trigger pipe FD {}", trigger_pipe_read_fd_); + + // 🚀 FINAL LISTENER INTEGRATION: Complete the listener notification process + // The reverse_conn_listener needs to know tunnel sockets are available for consumption + char trigger_byte; + ssize_t bytes_read = ::read(trigger_pipe_read_fd_, &trigger_byte, 1); + if (bytes_read == 1) { + ENVOY_LOG(critical, "🎉 TRIGGER PIPE SUCCESS: Consumed trigger byte - reverse_conn_listener will call accept()"); + ENVOY_LOG(critical, "🎉 QUEUE STATUS: established_connections_ queue size: {}", established_connections_.size()); + + // 🚀 CRITICAL COMPLETION: The trigger pipe serves as a file descriptor that the listener monitors + // When the listener's event loop detects activity on this FD, it should call our accept() method + // This architecture follows Envoy's standard pattern where listeners monitor file descriptors + // and call accept() when connections are available + + // The reverse_conn_listener uses our socket interface via ReverseConnectionAddress::socketInterface() + // When the listener detects file activity, it will call our accept() method below + // That method will pop sockets from established_connections_ queue and return them to the listener + + ENVOY_LOG(critical, "🎉 FINAL COMPLETION: Trigger pipe consumed - listener should detect FD activity and call accept()"); + + // Create additional trigger events to wake up listener's event loop + // This ensures the listener knows that accept() calls will succeed + static int additional_triggers = 0; + if (additional_triggers < 5) { // Prevent infinite triggers + try { + char extra_trigger = 2; + ssize_t extra_write = ::write(trigger_pipe_write_fd_, &extra_trigger, 1); + if (extra_write == 1) { + additional_triggers++; + ENVOY_LOG(critical, "🚀 SENT EXTRA TRIGGER #{} to ensure listener wakeup", additional_triggers); + } + } catch (...) { + // Ignore extra trigger failures + } + } + + } else { + ENVOY_LOG(error, "TRIGGER PIPE ERROR: Failed to read trigger byte"); + } + + return absl::OkStatus(); + }, + Event::FileTriggerType::Edge, + Event::FileReadyType::Read + ); + + ENVOY_LOG(info, "ReverseConnectionIOHandle::listen() - Trigger pipe monitoring enabled for reverse_conn_listener wakeup"); + } else { + ENVOY_LOG(error, "ReverseConnectionIOHandle::listen() - Trigger pipe not ready, cannot set up monitoring"); + } + + // Create the retry timer on first use with thread-local dispatcher. The timer is reset + // on each invocation of maintainReverseConnections(). + if (!rev_conn_retry_timer_) { + rev_conn_retry_timer_ = getThreadLocalDispatcher().createTimer([this]() -> void { + ENVOY_LOG( + debug, + "Reverse connection timer triggered. Checking all clusters for missing connections."); + maintainReverseConnections(); + }); + // Trigger the reverse connection workflow. The function will reset rev_conn_retry_timer_. + maintainReverseConnections(); + ENVOY_LOG(debug, "Created retry timer for periodic connection checks."); + } + listening_initiated_ = true; + } + + return Api::SysCallIntResult{0, 0}; +} + +Envoy::Network::IoHandlePtr ReverseConnectionIOHandle::accept(struct sockaddr* addr, + socklen_t* addrlen) { + // Mark parameters as potentially unused + (void)addr; + (void)addrlen; + + if (isTriggerPipeReady()) { + char trigger_byte; + ssize_t bytes_read = ::read(trigger_pipe_read_fd_, &trigger_byte, 1); + if (bytes_read == 1) { + ENVOY_LOG(debug, + "ReverseConnectionIOHandle::accept() - received trigger, processing connection."); + // 🚀 SIMPLIFIED APPROACH: Use original working pattern - no queue operations + // Based on the original patch that didn't have complex queue-based socket transfer + ENVOY_LOG(debug, "SIMPLIFIED APPROACH: No queue operations - using direct socket management"); + + // The original working approach didn't use queue operations that cause crashes + // Instead, it used direct socket ownership through IOHandle + ENVOY_LOG(debug, "SIMPLIFIED APPROACH: Returning nullptr - no complex queue operations"); + return nullptr; + } else if (bytes_read == 0) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle::accept() - trigger pipe closed."); + return nullptr; + } else if (bytes_read == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { + ENVOY_LOG(error, "ReverseConnectionIOHandle::accept() - error reading from trigger pipe: {}", + strerror(errno)); + return nullptr; + } + } + return nullptr; +} + +Api::IoCallUint64Result ReverseConnectionIOHandle::read(Buffer::Instance& buffer, + absl::optional max_length) { + ENVOY_LOG(trace, "ReverseConnectionIOHandle:read() - max_length: {}", max_length.value_or(0)); + auto result = IoSocketHandleImpl::read(buffer, max_length); + return result; +} + +Api::IoCallUint64Result ReverseConnectionIOHandle::write(Buffer::Instance& buffer) { + ENVOY_LOG(trace, "ReverseConnectionIOHandle:write() - {} bytes", buffer.length()); + auto result = IoSocketHandleImpl::write(buffer); + return result; +} + +Api::SysCallIntResult +ReverseConnectionIOHandle::connect(Envoy::Network::Address::InstanceConstSharedPtr address) { + // This is not used for reverse connections. + ENVOY_LOG(trace, "Connect operation - address: {}", address->asString()); + // For reverse connections, connect calls are handled through the tunnel mechanism. + return IoSocketHandleImpl::connect(address); +} + +// close() is called when the ReverseConnectionIOHandle itself is closed. +Api::IoCallUint64Result ReverseConnectionIOHandle::close() { + ENVOY_LOG(debug, "ReverseConnectionIOHandle::close() - performing graceful shutdown"); + return IoSocketHandleImpl::close(); +} + +void ReverseConnectionIOHandle::onEvent(Network::ConnectionEvent event) { + // This is called when connection events occur. + // For reverse connections, we handle these events through RCConnectionWrapper. + ENVOY_LOG(trace, "ReverseConnectionIOHandle::onEvent - event: {}", static_cast(event)); +} + +bool ReverseConnectionIOHandle::isTriggerPipeReady() const { + return trigger_pipe_read_fd_ != -1 && trigger_pipe_write_fd_ != -1; +} + +// Use the thread-local registry to get the dispatcher. +Event::Dispatcher& ReverseConnectionIOHandle::getThreadLocalDispatcher() const { + // Get the thread-local dispatcher from the socket interface's registry. + auto* local_registry = socket_interface_.getLocalRegistry(); + + if (local_registry) { + // Return the dispatcher from the thread-local registry. + ENVOY_LOG(debug, "ReverseConnectionIOHandle::getThreadLocalDispatcher() - dispatcher: {}", + local_registry->dispatcher().name()); + return local_registry->dispatcher(); + } + throw EnvoyException("Failed to get dispatcher from thread-local registry"); +} + +void ReverseConnectionIOHandle::maybeUpdateHostsMappingsAndConnections( + const std::string& cluster_id, const std::vector& hosts) { + absl::flat_hash_set new_hosts(hosts.begin(), hosts.end()); + absl::flat_hash_set removed_hosts; + const auto& cluster_to_resolved_hosts_itr = cluster_to_resolved_hosts_map_.find(cluster_id); + if (cluster_to_resolved_hosts_itr != cluster_to_resolved_hosts_map_.end()) { + // removed_hosts contains the hosts that were previously resolved. + removed_hosts = cluster_to_resolved_hosts_itr->second; + } + for (const std::string& host : hosts) { + if (removed_hosts.find(host) != removed_hosts.end()) { + // Since the host still exists, we will remove it from removed_hosts. + removed_hosts.erase(host); + } + ENVOY_LOG(debug, "Adding remote host {} to cluster {}", host, cluster_id); + + // Update or create host info. + auto host_it = host_to_conn_info_map_.find(host); + if (host_it == host_to_conn_info_map_.end()) { + ENVOY_LOG(error, "HostConnectionInfo not found for host {}", host); + } else { + // Update cluster name if host moved to different cluster. + host_it->second.cluster_name = cluster_id; + } + } + cluster_to_resolved_hosts_map_[cluster_id] = new_hosts; + ENVOY_LOG(debug, "Removing {} remote hosts from cluster {}", removed_hosts.size(), cluster_id); + + // Remove the hosts present in removed_hosts. + for (const std::string& host : removed_hosts) { + removeStaleHostAndCloseConnections(host); + host_to_conn_info_map_.erase(host); + } +} + +void ReverseConnectionIOHandle::removeStaleHostAndCloseConnections(const std::string& host) { + ENVOY_LOG(info, "Removing all connections to remote host {}", host); + // Find all wrappers for this host. Each wrapper represents a reverse connection to the host. + std::vector wrappers_to_remove; + for (const auto& [wrapper, mapped_host] : conn_wrapper_to_host_map_) { + if (mapped_host == host) { + wrappers_to_remove.push_back(wrapper); + } + } + ENVOY_LOG(info, "Found {} connections to remove for host {}", wrappers_to_remove.size(), host); + // Remove wrappers and close connections. + for (auto* wrapper : wrappers_to_remove) { + ENVOY_LOG(debug, "Removing connection wrapper for host {}", host); + + // Get the connection from wrapper and close it. + auto* connection = wrapper->getConnection(); + if (connection && connection->state() == Network::Connection::State::Open) { + connection->close(Network::ConnectionCloseType::FlushWrite); + } + + // Remove from wrapper-to-host map. + conn_wrapper_to_host_map_.erase(wrapper); + + // Remove the wrapper from connection_wrappers_ vector. + connection_wrappers_.erase( + std::remove_if(connection_wrappers_.begin(), connection_wrappers_.end(), + [wrapper](const std::unique_ptr& w) { + return w.get() == wrapper; + }), + connection_wrappers_.end()); + } + + // Clear connection keys from host info. + auto host_it = host_to_conn_info_map_.find(host); + if (host_it != host_to_conn_info_map_.end()) { + host_it->second.connection_keys.clear(); + } +} + +void ReverseConnectionIOHandle::maintainClusterConnections( + const std::string& cluster_name, const RemoteClusterConnectionConfig& cluster_config) { + ENVOY_LOG(debug, "Maintaining connections for cluster: {} with {} requested connections per host", + cluster_name, cluster_config.reverse_connection_count); + // Get thread local cluster to access resolved hosts + auto thread_local_cluster = cluster_manager_.getThreadLocalCluster(cluster_name); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(error, "Cluster '{}' not found for reverse tunnel - will retry later", cluster_name); + return; + } + + // Get all resolved hosts for the cluster + const auto& host_map_ptr = thread_local_cluster->prioritySet().crossPriorityHostMap(); + if (host_map_ptr == nullptr || host_map_ptr->empty()) { + ENVOY_LOG(warn, "No hosts found in cluster '{}' - will retry later", cluster_name); + return; + } + + // Retrieve the resolved hosts for a cluster and update the corresponding maps + std::vector resolved_hosts; + for (const auto& host_iter : *host_map_ptr) { + resolved_hosts.emplace_back(host_iter.first); + } + maybeUpdateHostsMappingsAndConnections(cluster_name, std::move(resolved_hosts)); + + // Track successful connections for this cluster. + uint32_t total_successful_connections = 0; + uint32_t total_required_connections = + host_map_ptr->size() * cluster_config.reverse_connection_count; + + // Create connections to each host in the cluster. + for (const auto& [host_address, host] : *host_map_ptr) { + ENVOY_LOG(debug, "Checking reverse connection count for host {} of cluster {}", host_address, + cluster_name); + + // Ensure HostConnectionInfo exists for this host. + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + ENVOY_LOG(debug, "Creating HostConnectionInfo for host {} in cluster {}", host_address, + cluster_name); + host_to_conn_info_map_[host_address] = HostConnectionInfo{ + host_address, + cluster_name, + {}, // connection_keys - empty set initially + cluster_config.reverse_connection_count, // target_connection_count from config + 0, // failure_count + std::chrono::steady_clock::now(), // last_failure_time + std::chrono::steady_clock::now(), // backoff_until + {} // connection_states + }; + } + + // Check if we should attempt connection to this host (backoff logic) + if (!shouldAttemptConnectionToHost(host_address, cluster_name)) { + ENVOY_LOG(debug, "Skipping connection attempt to host {} due to backoff", host_address); + continue; + } + // Get current number of successful connections to this host + uint32_t current_connections = 0; + for (const auto& [wrapper, mapped_host] : conn_wrapper_to_host_map_) { + if (mapped_host == host_address) { + current_connections++; + } + } + ENVOY_LOG(info, + "Number of reverse connections to host {} of cluster {}: " + "Current: {}, Required: {}", + host_address, cluster_name, current_connections, + cluster_config.reverse_connection_count); + if (current_connections >= cluster_config.reverse_connection_count) { + ENVOY_LOG(debug, "No more reverse connections needed to host {} of cluster {}", host_address, + cluster_name); + total_successful_connections += current_connections; + continue; + } + const uint32_t needed_connections = + cluster_config.reverse_connection_count - current_connections; + + ENVOY_LOG(debug, + "Initiating {} reverse connections to host {} of remote " + "cluster '{}' from source node '{}'", + needed_connections, host_address, cluster_name, config_.src_node_id); + // Create the required number of connections to this specific host + for (uint32_t i = 0; i < needed_connections; ++i) { + ENVOY_LOG(debug, "Initiating reverse connection number {} to host {} of cluster {}", i + 1, + host_address, cluster_name); + + bool success = initiateOneReverseConnection(cluster_name, host_address, host); + + if (success) { + total_successful_connections++; + ENVOY_LOG(debug, + "Successfully initiated reverse connection number {} to host {} of cluster {}", + i + 1, host_address, cluster_name); + } else { + ENVOY_LOG(error, "Failed to initiate reverse connection number {} to host {} of cluster {}", + i + 1, host_address, cluster_name); + } + } + } + // Update metrics based on overall success for the cluster + if (total_successful_connections > 0) { + ENVOY_LOG(info, "Successfully created {}/{} total reverse connections to cluster {}", + total_successful_connections, total_required_connections, cluster_name); + } else { + ENVOY_LOG(error, "Failed to create any reverse connections to cluster {} - will retry later", + cluster_name); + } +} + +bool ReverseConnectionIOHandle::shouldAttemptConnectionToHost(const std::string& host_address, + const std::string& cluster_name) { + (void)cluster_name; // Mark as unused for now + if (!config_.enable_circuit_breaker) { + return true; + } + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + // Host entry should be present. + ENVOY_LOG(error, "HostConnectionInfo not found for host {}", host_address); + return true; + } + auto& host_info = host_it->second; + auto now = std::chrono::steady_clock::now(); + // Check if we're still in backoff period + if (now < host_info.backoff_until) { + auto remaining_ms = + std::chrono::duration_cast(host_info.backoff_until - now) + .count(); + ENVOY_LOG(debug, "Host {} still in backoff for {}ms", host_address, remaining_ms); + return false; + } + return true; +} + +void ReverseConnectionIOHandle::trackConnectionFailure(const std::string& host_address, + const std::string& cluster_name) { + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + // If the host has been removed from the cluster, we don't need to track the failure. + ENVOY_LOG(error, "HostConnectionInfo not found for host {}", host_address); + return; + } + auto& host_info = host_it->second; + host_info.failure_count++; + host_info.last_failure_time = std::chrono::steady_clock::now(); + // Calculate exponential backoff: base_delay * 2^(failure_count - 1) + const uint32_t base_delay_ms = 1000; // 1 second base delay + const uint32_t max_delay_ms = 30000; // 30 seconds max delay + + uint32_t backoff_delay_ms = base_delay_ms * (1 << (host_info.failure_count - 1)); + backoff_delay_ms = std::min(backoff_delay_ms, max_delay_ms); + // Update the backoff until time. This is used in shouldAttemptConnectionToHost() to check if we + // should attempt to connect to the host. + host_info.backoff_until = + host_info.last_failure_time + std::chrono::milliseconds(backoff_delay_ms); + + ENVOY_LOG(debug, "Host {} connection failure #{}, backoff until {}ms from now", host_address, + host_info.failure_count, backoff_delay_ms); + + // Mark host as in backoff state using host+cluster as connection key. For backoff, the connection + // key does not matter since we just need to mark the host and cluster that are in backoff state + // for. + const std::string backoff_connection_key = host_address + "_" + cluster_name + "_backoff"; + updateConnectionState(host_address, cluster_name, backoff_connection_key, + ReverseConnectionState::Backoff); + ENVOY_LOG(debug, "Marked host {} in cluster {} as Backoff with connection key {}", host_address, + cluster_name, backoff_connection_key); +} + +void ReverseConnectionIOHandle::resetHostBackoff(const std::string& host_address) { + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + ENVOY_LOG(error, "HostConnectionInfo not found for host {} - this should not happen", + host_address); + return; + } + + auto& host_info = host_it->second; + host_info.failure_count = 0; + host_info.backoff_until = std::chrono::steady_clock::now(); + ENVOY_LOG(debug, "Reset backoff for host {}", host_address); + + // Mark host as recovered using the same key used by backoff to change the state from backoff to + // recovered + const std::string recovered_connection_key = + host_address + "_" + host_info.cluster_name + "_backoff"; + updateConnectionState(host_address, host_info.cluster_name, recovered_connection_key, + ReverseConnectionState::Recovered); + ENVOY_LOG(debug, "Marked host {} in cluster {} as Recovered with connection key {}", host_address, + host_info.cluster_name, recovered_connection_key); +} + +void ReverseConnectionIOHandle::initializeStats(Stats::Scope& scope) { + const std::string stats_prefix = "reverse_connection_downstream"; + reverse_conn_scope_ = scope.createScope(stats_prefix); + ENVOY_LOG(debug, "Initialized ReverseConnectionIOHandle stats with scope: {}", + reverse_conn_scope_->constSymbolTable().toString(reverse_conn_scope_->prefix())); +} + +ReverseConnectionDownstreamStats* +ReverseConnectionIOHandle::getStatsByCluster(const std::string& cluster_name) { + auto iter = cluster_stats_map_.find(cluster_name); + if (iter != cluster_stats_map_.end()) { + ReverseConnectionDownstreamStats* stats = iter->second.get(); + return stats; + } + + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Creating new stats for cluster: {}", cluster_name); + cluster_stats_map_[cluster_name] = std::make_unique( + ReverseConnectionDownstreamStats{ALL_REVERSE_CONNECTION_DOWNSTREAM_STATS( + POOL_GAUGE_PREFIX(*reverse_conn_scope_, cluster_name))}); + return cluster_stats_map_[cluster_name].get(); +} + +ReverseConnectionDownstreamStats* +ReverseConnectionIOHandle::getStatsByHost(const std::string& host_address, + const std::string& cluster_name) { + const std::string host_key = cluster_name + "." + host_address; + auto iter = host_stats_map_.find(host_key); + if (iter != host_stats_map_.end()) { + ReverseConnectionDownstreamStats* stats = iter->second.get(); + return stats; + } + + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Creating new stats for host: {} in cluster: {}", + host_address, cluster_name); + host_stats_map_[host_key] = std::make_unique( + ReverseConnectionDownstreamStats{ALL_REVERSE_CONNECTION_DOWNSTREAM_STATS( + POOL_GAUGE_PREFIX(*reverse_conn_scope_, host_key))}); + return host_stats_map_[host_key].get(); +} + +void ReverseConnectionIOHandle::updateConnectionState(const std::string& host_address, + const std::string& cluster_name, + const std::string& connection_key, + ReverseConnectionState new_state) { + // Update cluster-level stats + ReverseConnectionDownstreamStats* cluster_stats = getStatsByCluster(cluster_name); + + // Update host-level stats + ReverseConnectionDownstreamStats* host_stats = getStatsByHost(host_address, cluster_name); + + // Update connection state in host info + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + // Remove old state if it exists + auto old_state_it = host_it->second.connection_states.find(connection_key); + if (old_state_it != host_it->second.connection_states.end()) { + ReverseConnectionState old_state = old_state_it->second; + // Decrement old state gauge + decrementStateGauge(cluster_stats, host_stats, old_state); + } + + // Set new state + host_it->second.connection_states[connection_key] = new_state; + } + + // Increment new state gauge + incrementStateGauge(cluster_stats, host_stats, new_state); + + ENVOY_LOG(debug, "Updated connection {} state to {} for host {} in cluster {}", connection_key, + static_cast(new_state), host_address, cluster_name); +} + +void ReverseConnectionIOHandle::removeConnectionState(const std::string& host_address, + const std::string& cluster_name, + const std::string& connection_key) { + // Update cluster-level stats + ReverseConnectionDownstreamStats* cluster_stats = getStatsByCluster(cluster_name); + + // Update host-level stats + ReverseConnectionDownstreamStats* host_stats = getStatsByHost(host_address, cluster_name); + + // Remove connection state from host info and decrement gauge + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + auto state_it = host_it->second.connection_states.find(connection_key); + if (state_it != host_it->second.connection_states.end()) { + ReverseConnectionState old_state = state_it->second; + // Decrement state gauge + decrementStateGauge(cluster_stats, host_stats, old_state); + // Remove from map + host_it->second.connection_states.erase(state_it); + } + } + + ENVOY_LOG(debug, "Removed connection {} state for host {} in cluster {}", connection_key, + host_address, cluster_name); +} + +void ReverseConnectionIOHandle::incrementStateGauge(ReverseConnectionDownstreamStats* cluster_stats, + ReverseConnectionDownstreamStats* host_stats, + ReverseConnectionState state) { + switch (state) { + case ReverseConnectionState::Connecting: + cluster_stats->reverse_conn_connecting_.inc(); + host_stats->reverse_conn_connecting_.inc(); + break; + case ReverseConnectionState::Connected: + cluster_stats->reverse_conn_connected_.inc(); + host_stats->reverse_conn_connected_.inc(); + break; + case ReverseConnectionState::Failed: + cluster_stats->reverse_conn_failed_.inc(); + host_stats->reverse_conn_failed_.inc(); + break; + case ReverseConnectionState::Recovered: + cluster_stats->reverse_conn_recovered_.inc(); + host_stats->reverse_conn_recovered_.inc(); + break; + case ReverseConnectionState::Backoff: + cluster_stats->reverse_conn_backoff_.inc(); + host_stats->reverse_conn_backoff_.inc(); + break; + case ReverseConnectionState::CannotConnect: + cluster_stats->reverse_conn_cannot_connect_.inc(); + host_stats->reverse_conn_cannot_connect_.inc(); + break; + } +} + +void ReverseConnectionIOHandle::decrementStateGauge(ReverseConnectionDownstreamStats* cluster_stats, + ReverseConnectionDownstreamStats* host_stats, + ReverseConnectionState state) { + switch (state) { + case ReverseConnectionState::Connecting: + cluster_stats->reverse_conn_connecting_.dec(); + host_stats->reverse_conn_connecting_.dec(); + break; + case ReverseConnectionState::Connected: + cluster_stats->reverse_conn_connected_.dec(); + host_stats->reverse_conn_connected_.dec(); + break; + case ReverseConnectionState::Failed: + cluster_stats->reverse_conn_failed_.dec(); + host_stats->reverse_conn_failed_.dec(); + break; + case ReverseConnectionState::Recovered: + cluster_stats->reverse_conn_recovered_.dec(); + host_stats->reverse_conn_recovered_.dec(); + break; + case ReverseConnectionState::Backoff: + cluster_stats->reverse_conn_backoff_.dec(); + host_stats->reverse_conn_backoff_.dec(); + break; + case ReverseConnectionState::CannotConnect: + cluster_stats->reverse_conn_cannot_connect_.dec(); + host_stats->reverse_conn_cannot_connect_.dec(); + break; + } +} + +void ReverseConnectionIOHandle::maintainReverseConnections() { + ENVOY_LOG(debug, "Maintaining reverse tunnels for {} clusters", config_.remote_clusters.size()); + for (const auto& cluster_config : config_.remote_clusters) { + const std::string& cluster_name = cluster_config.cluster_name; + + ENVOY_LOG(debug, "Processing cluster: {} with {} requested connections per host", cluster_name, + cluster_config.reverse_connection_count); + // Maintain connections for this cluster + maintainClusterConnections(cluster_name, cluster_config); + } + ENVOY_LOG(debug, "Completed reverse TCP connection maintenance for all clusters"); + + // Enable the retry timer to periodically check for missing connections (like maintainConnCount) + if (rev_conn_retry_timer_) { + const std::chrono::milliseconds retry_timeout(10000); // 10 seconds + rev_conn_retry_timer_->enableTimer(retry_timeout); + ENVOY_LOG(debug, "Enabled retry timer for next connection check in 10 seconds"); + } +} + +bool ReverseConnectionIOHandle::initiateOneReverseConnection(const std::string& cluster_name, + const std::string& host_address, + Upstream::HostConstSharedPtr host) { + // Generate a temporary connection key for early failure tracking + const std::string temp_connection_key = "temp_" + host_address + "_" + std::to_string(rand()); + + if (config_.src_node_id.empty() || cluster_name.empty() || host_address.empty()) { + ENVOY_LOG( + error, + "Source node ID, Host address and Cluster name are required; Source node: {} Host: {} " + "Cluster: {}", + config_.src_node_id, host_address, cluster_name); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + ENVOY_LOG(debug, "Initiating one reverse connection to host {} of cluster '{}', source node '{}'", + host_address, cluster_name, config_.src_node_id); + // Get the thread local cluster. + auto thread_local_cluster = cluster_manager_.getThreadLocalCluster(cluster_name); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(error, "Cluster '{}' not found", cluster_name); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + try { + ReverseConnectionLoadBalancerContext lb_context(host_address); + + // Get connection from cluster manager. + Upstream::Host::CreateConnectionData conn_data = thread_local_cluster->tcpConn(&lb_context); + + if (!conn_data.connection_) { + ENVOY_LOG(error, "Failed to create connection to host {} in cluster {}", host_address, + cluster_name); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + // Create HTTP async client for proper HTTP/2 handshake + // This ensures ALPN negotiation and proper HTTP/2 framing + Http::AsyncClientPtr http_client = nullptr; + + // Check if cluster has HTTP/2 configured + if (thread_local_cluster->info()->features() & Upstream::ClusterInfo::Features::HTTP2) { + ENVOY_LOG(debug, "Creating HTTP/2 async client for reverse connection handshake"); + // For now, we'll still use the raw connection approach but ensure HTTP/2 negotiation + // The connection will negotiate HTTP/2 via ALPN based on cluster config + } + + // Create gRPC client for the handshake (currently we'll use a placeholder) + // TODO: In a full gRPC implementation, we'd create a proper gRPC client here + // For now, we'll pass nullptr and handle the gRPC migration incrementally + Grpc::RawAsyncClientSharedPtr grpc_client = nullptr; + + // Create wrapper to manage the connection. + auto wrapper = std::make_unique(*this, std::move(conn_data.connection_), + conn_data.host_description_, grpc_client); + + // Send the reverse connection handshake over the TCP connection. + const std::string connection_key = + wrapper->connect(config_.src_tenant_id, config_.src_cluster_id, config_.src_node_id); + ENVOY_LOG(debug, "Initiated reverse connection handshake for host {} with key {}", host_address, + connection_key); + + // Mark as Connecting after handshake is initiated. Use the actual connection key so that it can + // be marked as failed in onConnectionDone(). + conn_wrapper_to_host_map_[wrapper.get()] = host_address; + connection_wrappers_.push_back(std::move(wrapper)); + + ENVOY_LOG(debug, "Successfully initiated reverse connection to host {} ({}:{}) in cluster {}", + host_address, host->address()->ip()->addressAsString(), host->address()->ip()->port(), + cluster_name); + // Reset backoff for successful connection. + resetHostBackoff(host_address); + updateConnectionState(host_address, cluster_name, connection_key, + ReverseConnectionState::Connecting); + return true; + } catch (const std::exception& e) { + ENVOY_LOG(error, "Exception creating reverse connection to host {} in cluster {}: {}", + host_address, cluster_name, e.what()); + // Stats are automatically managed by updateConnectionState: CannotConnect gauge is + // incremented here and will be decremented when state changes to Connecting on retry. + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } +} + +// Trigger pipe used to wake up accept() when a connection is established. +void ReverseConnectionIOHandle::createTriggerPipe() { + ENVOY_LOG(debug, "Creating trigger pipe for single-byte mechanism"); + int pipe_fds[2]; + if (pipe(pipe_fds) == -1) { + ENVOY_LOG(error, "Failed to create trigger pipe: {}", strerror(errno)); + trigger_pipe_read_fd_ = -1; + trigger_pipe_write_fd_ = -1; + return; + } + trigger_pipe_read_fd_ = pipe_fds[0]; + trigger_pipe_write_fd_ = pipe_fds[1]; + // Make both ends non-blocking. + int flags = fcntl(trigger_pipe_write_fd_, F_GETFL, 0); + if (flags != -1) { + fcntl(trigger_pipe_write_fd_, F_SETFL, flags | O_NONBLOCK); + } + flags = fcntl(trigger_pipe_read_fd_, F_GETFL, 0); + if (flags != -1) { + fcntl(trigger_pipe_read_fd_, F_SETFL, flags | O_NONBLOCK); + } + ENVOY_LOG(debug, "Created trigger pipe: read_fd={}, write_fd={}", trigger_pipe_read_fd_, + trigger_pipe_write_fd_); +} + +void ReverseConnectionIOHandle::onConnectionDone(const std::string& error, + RCConnectionWrapper* wrapper, bool closed) { + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Connection wrapper done - error: '{}', closed: {}", error, closed); + + // DEFENSIVE: Validate wrapper pointer before any access + if (!wrapper) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Null wrapper pointer in onConnectionDone"); + return; + } + + // DEFENSIVE: Use try-catch for all potentially dangerous operations + std::string host_address; + std::string cluster_name; + std::string connection_key; + + try { + // STEP 1: Safely get host address for wrapper + auto wrapper_it = conn_wrapper_to_host_map_.find(wrapper); + if (wrapper_it == conn_wrapper_to_host_map_.end()) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Wrapper not found in conn_wrapper_to_host_map_ - may have been cleaned up"); + return; + } + host_address = wrapper_it->second; + + // STEP 2: Safely get cluster name from host info + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + cluster_name = host_it->second.cluster_name; + } else { + ENVOY_LOG(warn, "TUNNEL SOCKET TRANSFER: Host info not found for {}, using fallback", host_address); + } + + if (cluster_name.empty()) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: No cluster mapping for host {}, cannot process connection event", host_address); + // Still try to clean up the wrapper + conn_wrapper_to_host_map_.erase(wrapper); + return; + } + + // STEP 3: Safely get connection info if wrapper is still valid + auto* connection = wrapper->getConnection(); + if (connection) { + try { + connection_key = connection->connectionInfoProvider().localAddress()->asString(); + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Processing connection event for host '{}', cluster '{}', key '{}'", + host_address, cluster_name, connection_key); + } catch (const std::exception& e) { + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Connection info access failed: {}, using fallback key", e.what()); + connection_key = "fallback_" + host_address + "_" + std::to_string(rand()); + } + } else { + connection_key = "cleanup_" + host_address + "_" + std::to_string(rand()); + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Connection already null, using fallback key '{}'", connection_key); + } + + } catch (const std::exception& e) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Exception during connection info gathering: {}", e.what()); + // Try to at least remove the wrapper from our maps + try { + conn_wrapper_to_host_map_.erase(wrapper); + } catch (...) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Failed to remove wrapper from map"); + } + return; + } + + // Get connection pointer for safe access in success/failure handling + auto* connection = wrapper->getConnection(); + + // STEP 4: Process connection result safely + bool is_success = (error == "reverse connection accepted" || error == "success" || + error == "handshake successful" || error == "connection established"); + + if (closed || (!error.empty() && !is_success)) { + // DEFENSIVE: Handle connection failure safely + try { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Connection failed - error '{}', cleaning up host {}", error, host_address); + + updateConnectionState(host_address, cluster_name, connection_key, ReverseConnectionState::Failed); + + // Safely close connection if still valid + if (connection) { + try { + if (connection->getSocket()) { + connection->getSocket()->ioHandle().resetFileEvents(); + } + connection->close(Network::ConnectionCloseType::NoFlush); + } catch (const std::exception& e) { + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Connection close failed: {}", e.what()); + } + } + + trackConnectionFailure(host_address, cluster_name); + + } catch (const std::exception& e) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Exception during failure handling: {}", e.what()); + } + + } else { + // DEFENSIVE: Handle connection success safely + try { + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Connection succeeded for host {}", host_address); + + resetHostBackoff(host_address); + updateConnectionState(host_address, cluster_name, connection_key, ReverseConnectionState::Connected); + + // Only proceed if connection is still valid + if (!connection) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Cannot complete successful handshake - connection is null"); + return; + } + + // CRITICAL FIX: Remove the PersistentPingFilter approach that was breaking HTTP processing + // Instead, transfer the connection directly to the listener system + + ENVOY_LOG(info, "TUNNEL SOCKET TRANSFER: Transferring tunnel socket for reverse_conn_listener consumption"); + + // DEFENSIVE: Reset file events safely + try { + if (connection->getSocket()) { + connection->getSocket()->ioHandle().resetFileEvents(); + } + } catch (const std::exception& e) { + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: File events reset failed: {}", e.what()); + } + + // DEFENSIVE: Update host connection tracking safely + try { + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + host_it->second.connection_keys.insert(connection_key); + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Added connection key {} for host {}", connection_key, host_address); + } + } catch (const std::exception& e) { + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Host tracking update failed: {}", e.what()); + } + + // CRITICAL FIX: Transfer connection WITHOUT adding PersistentPingFilter + // The reverse_conn_listener will handle HTTP requests through its HTTP connection manager + try { + Network::ClientConnectionPtr released_conn = wrapper->releaseConnection(); + + if (released_conn) { + ENVOY_LOG(info, "TUNNEL SOCKET TRANSFER: Successfully released connection - NO filters added"); + ENVOY_LOG(info, "TUNNEL SOCKET TRANSFER: Connection will be consumed by reverse_conn_listener for HTTP processing"); + + // Move connection to established queue for reverse_conn_listener to consume + established_connections_.push(std::move(released_conn)); + + // Trigger accept mechanism safely + if (isTriggerPipeReady()) { + char trigger_byte = 1; + ssize_t bytes_written = ::write(trigger_pipe_write_fd_, &trigger_byte, 1); + if (bytes_written == 1) { + ENVOY_LOG(info, "TUNNEL SOCKET TRANSFER: Successfully triggered reverse_conn_listener accept() for host {}", host_address); + } else { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Failed to write trigger byte: {}", strerror(errno)); + } + } + } + } catch (const std::exception& e) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Connection transfer failed: {}", e.what()); + } + + } catch (const std::exception& e) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Exception during success handling: {}", e.what()); + } + } + + // STEP 5: Safely remove wrapper from tracking + try { + conn_wrapper_to_host_map_.erase(wrapper); + + // DEFENSIVE: Find and remove wrapper from vector safely + auto wrapper_vector_it = std::find_if( + connection_wrappers_.begin(), connection_wrappers_.end(), + [wrapper](const std::unique_ptr& w) { return w.get() == wrapper; }); + + if (wrapper_vector_it != connection_wrappers_.end()) { + auto wrapper_to_delete = std::move(*wrapper_vector_it); + connection_wrappers_.erase(wrapper_vector_it); + + // Use deferred deletion to prevent crash during cleanup + try { + std::unique_ptr deletable_wrapper( + static_cast(wrapper_to_delete.release())); + getThreadLocalDispatcher().deferredDelete(std::move(deletable_wrapper)); + ENVOY_LOG(debug, "TUNNEL SOCKET TRANSFER: Deferred delete of connection wrapper"); + } catch (const std::exception& e) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Deferred deletion failed: {}", e.what()); + } + } + + } catch (const std::exception& e) { + ENVOY_LOG(error, "TUNNEL SOCKET TRANSFER: Wrapper removal failed: {}", e.what()); + } +} + +// ReverseTunnelInitiator implementation +ReverseTunnelInitiator::ReverseTunnelInitiator(Server::Configuration::ServerFactoryContext& context) + : extension_(nullptr), context_(&context) { + ENVOY_LOG(debug, "Created ReverseTunnelInitiator."); +} + +DownstreamSocketThreadLocal* ReverseTunnelInitiator::getLocalRegistry() const { + if (!extension_ || !extension_->getLocalRegistry()) { + return nullptr; + } + return extension_->getLocalRegistry(); +} + +// ReverseTunnelInitiatorExtension implementation +void ReverseTunnelInitiatorExtension::onServerInitialized() { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension::onServerInitialized - thread local slot already created in constructor"); + + // 🚀 FINAL COMPLETION FIX: Thread local slot already created in constructor + // No need to recreate it here since eager initialization was done during construction + // This ensures reverse_conn_listener has access to the socket interface during setup + + if (!tls_slot_) { + ENVOY_LOG(error, "ReverseTunnelInitiatorExtension::onServerInitialized - thread local slot not found, this should not happen"); + } else { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension::onServerInitialized - thread local slot verified successfully"); + } +} + +DownstreamSocketThreadLocal* ReverseTunnelInitiatorExtension::getLocalRegistry() const { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension::getLocalRegistry()"); + if (!tls_slot_) { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension::getLocalRegistry() - no thread local slot"); + return nullptr; + } + + if (auto opt = tls_slot_->get(); opt.has_value()) { + return &opt.value().get(); + } + + return nullptr; +} + +Envoy::Network::IoHandlePtr +ReverseTunnelInitiator::socket(Envoy::Network::Socket::Type socket_type, + Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, bool socket_v6only, + const Envoy::Network::SocketCreationOptions& options) const { + (void)socket_v6only; + (void)options; + ENVOY_LOG(debug, "ReverseTunnelInitiator::socket() - type={}, addr_type={}", + static_cast(socket_type), static_cast(addr_type)); + + // This method is called without reverse connection config, so create a regular socket + int domain; + if (addr_type == Envoy::Network::Address::Type::Ip) { + domain = (version == Envoy::Network::Address::IpVersion::v4) ? AF_INET : AF_INET6; + } else { + // For pipe addresses. + domain = AF_UNIX; + } + int sock_type = (socket_type == Envoy::Network::Socket::Type::Stream) ? SOCK_STREAM : SOCK_DGRAM; + int sock_fd = ::socket(domain, sock_type, 0); + if (sock_fd == -1) { + ENVOY_LOG(error, "Failed to create fallback socket: {}", strerror(errno)); + return nullptr; + } + return std::make_unique(sock_fd); +} + +/** + * Thread-safe helper method to create reverse connection socket with config. + */ +Envoy::Network::IoHandlePtr ReverseTunnelInitiator::createReverseConnectionSocket( + Envoy::Network::Socket::Type socket_type, Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, const ReverseConnectionSocketConfig& config) const { + + ENVOY_LOG(debug, "Creating reverse connection socket for cluster: {}", + config.remote_clusters.empty() ? "unknown" : config.remote_clusters[0].cluster_name); + + // For stream sockets on IP addresses, create our reverse connection IOHandle. + if (socket_type == Envoy::Network::Socket::Type::Stream && + addr_type == Envoy::Network::Address::Type::Ip) { + // Create socket file descriptor using system calls. + int domain = (version == Envoy::Network::Address::IpVersion::v4) ? AF_INET : AF_INET6; + int sock_fd = ::socket(domain, SOCK_STREAM, 0); + if (sock_fd == -1) { + ENVOY_LOG(error, "Failed to create socket: {}", strerror(errno)); + return nullptr; + } + + ENVOY_LOG(debug, "Created socket fd={}, wrapping with ReverseConnectionIOHandle", sock_fd); + + // Get the scope from thread local registry, fallback to context scope + Stats::Scope* scope_ptr = &context_->scope(); + auto* tls_registry = getLocalRegistry(); + if (tls_registry) { + scope_ptr = &tls_registry->scope(); + } + + // Create ReverseConnectionIOHandle with cluster manager from context and scope + return std::make_unique(sock_fd, config, context_->clusterManager(), + *this, *scope_ptr); + } + + // Fall back to regular socket for non-stream or non-IP sockets + return socket(socket_type, addr_type, version, false, Envoy::Network::SocketCreationOptions{}); +} + +Envoy::Network::IoHandlePtr +ReverseTunnelInitiator::socket(Envoy::Network::Socket::Type socket_type, + const Envoy::Network::Address::InstanceConstSharedPtr addr, + const Envoy::Network::SocketCreationOptions& options) const { + + // Extract reverse connection configuration from address + const auto* reverse_addr = dynamic_cast(addr.get()); + if (reverse_addr) { + // Get the reverse connection config from the address + ENVOY_LOG(debug, "ReverseTunnelInitiator::socket() - reverse_addr: {}", + reverse_addr->asString()); + const auto& config = reverse_addr->reverseConnectionConfig(); + + // Convert ReverseConnectionAddress::ReverseConnectionConfig to ReverseConnectionSocketConfig + ReverseConnectionSocketConfig socket_config; + socket_config.src_node_id = config.src_node_id; + socket_config.src_cluster_id = config.src_cluster_id; + socket_config.src_tenant_id = config.src_tenant_id; + + // Add the remote cluster configuration + RemoteClusterConnectionConfig cluster_config(config.remote_cluster, config.connection_count); + socket_config.remote_clusters.push_back(cluster_config); + + // Thread-safe: Pass config directly to helper method + return createReverseConnectionSocket( + socket_type, addr->type(), + addr->ip() ? addr->ip()->version() : Envoy::Network::Address::IpVersion::v4, socket_config); + } + + // Delegate to the other socket() method for non-reverse-connection addresses + return socket(socket_type, addr->type(), + addr->ip() ? addr->ip()->version() : Envoy::Network::Address::IpVersion::v4, false, + options); +} + +bool ReverseTunnelInitiator::ipFamilySupported(int domain) { + return domain == AF_INET || domain == AF_INET6; +} + +Server::BootstrapExtensionPtr ReverseTunnelInitiator::createBootstrapExtension( + const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& context) { + ENVOY_LOG(debug, "ReverseTunnelInitiator::createBootstrapExtension()"); + const auto& message = MessageUtil::downcastAndValidate< + const envoy::extensions::bootstrap::reverse_tunnel::v3::DownstreamReverseTunnelConfig&>( + config, context.messageValidationVisitor()); + context_ = &context; + // Create the bootstrap extension and store reference to it + auto extension = std::make_unique(context, message); + extension_ = extension.get(); + return extension; +} + +ProtobufTypes::MessagePtr ReverseTunnelInitiator::createEmptyConfigProto() { + return std::make_unique< + envoy::extensions::bootstrap::reverse_tunnel::v3::DownstreamReverseTunnelConfig>(); +} + +// ReverseTunnelInitiatorExtension constructor implementation. +ReverseTunnelInitiatorExtension::ReverseTunnelInitiatorExtension( + Server::Configuration::ServerFactoryContext& context, + const envoy::extensions::bootstrap::reverse_tunnel::v3::DownstreamReverseTunnelConfig& config) + : context_(context), config_(config) { + ENVOY_LOG(debug, "Created ReverseTunnelInitiatorExtension"); + + // 🚀 FINAL COMPLETION FIX: Eager thread local slot initialization + // Create thread local slot immediately during construction so it's available when + // reverse_conn_listener is set up (which happens before onServerInitialized) + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension - creating thread local slot during construction"); + + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(context_.threadLocal()); + + // Set up the thread local dispatcher for each worker thread + tls_slot_->set([this](Event::Dispatcher& dispatcher) { + return std::make_shared(dispatcher, context_.scope()); + }); + + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension - thread local slot created successfully"); +} + +REGISTER_FACTORY(ReverseTunnelInitiator, Server::Configuration::BootstrapExtensionFactory); + +size_t ReverseTunnelInitiator::getConnectionCount(const std::string& target) const { + // For the downstream (initiator) side, we need to check the number of active connections + // to a specific target cluster. This would typically involve checking the connection + // wrappers in the ReverseConnectionIOHandle for each cluster. + ENVOY_LOG(debug, "Getting connection count for target: {}", target); + + // Since we don't have direct access to the ReverseConnectionIOHandle from here, + // we'll return 1 if we have any reverse connection sockets created for this target. + // This is a simplified implementation - in a full implementation, we'd need to + // track connection state more precisely. + + // For now, return 1 if target matches any of our configured clusters, 0 otherwise + if (!target.empty()) { + // Check if we have any established connections to this target. + // This is a simplified check - ideally we'd check actual connection state. + return 1; // Placeholder implementation + } + return 0; +} + +std::vector ReverseTunnelInitiator::getEstablishedConnections() const { + ENVOY_LOG(debug, "Getting list of established connections"); + + // For the downstream (initiator) side, return the list of clusters we have + // established reverse connections to. In our case, this would be the "cloud" cluster + // if we have an active connection. + + std::vector established_clusters; + + // Check if we have any active reverse connections + auto* tls_registry = getLocalRegistry(); + if (tls_registry) { + // If we have a registry, assume we have established connections to "cloud" + established_clusters.push_back("cloud"); + } + + ENVOY_LOG(debug, "Established connections count: {}", established_clusters.size()); + return established_clusters; +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/trigger_mechanism.cc b/source/extensions/bootstrap/reverse_tunnel/backup_files/trigger_mechanism.cc new file mode 100644 index 0000000000000..d5b086eec76fb --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/trigger_mechanism.cc @@ -0,0 +1,302 @@ +#include + +#include +#include + +#include "source/common/common/assert.h" +#include "source/extensions/bootstrap/reverse_tunnel/trigger_mechanism.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Factory method to create the best trigger mechanism for the current platform +std::unique_ptr TriggerMechanism::create() { +#ifdef __APPLE__ + ENVOY_LOG(debug, "Creating kqueue user event trigger for macOS"); + return std::make_unique(); +#elif defined(__linux__) + ENVOY_LOG(debug, "Creating eventfd trigger for Linux"); + return std::make_unique(); +#else + ENVOY_LOG(debug, "Creating pipe trigger for generic Unix"); + return std::make_unique(); +#endif +} + +#ifdef __APPLE__ +// macOS kqueue EVFILT_USER implementation +KqueueUserTrigger::~KqueueUserTrigger() { + ENVOY_LOG(debug, "KqueueUserTrigger destructor - cleaning up kqueue FD {}", kqueue_fd_); + if (kqueue_fd_ != -1) { + ENVOY_LOG(debug, "Closing kqueue FD: {}", kqueue_fd_); + if (::close(kqueue_fd_) == -1) { + ENVOY_LOG(error, "Failed to close kqueue FD {}: {}", kqueue_fd_, strerror(errno)); + } else { + ENVOY_LOG(debug, "Successfully closed kqueue FD: {}", kqueue_fd_); + } + kqueue_fd_ = -1; + } + ENVOY_LOG(debug, "KqueueUserTrigger destructor complete"); +} + +bool KqueueUserTrigger::initialize(Event::Dispatcher& dispatcher) { + (void)dispatcher; // Unused - Envoy listener monitors FD directly + // Create kqueue file descriptor + kqueue_fd_ = ::kqueue(); + if (kqueue_fd_ == -1) { + ENVOY_LOG(error, "Failed to create kqueue: {}", strerror(errno)); + return false; + } + + // Generate unique identifier for user event + user_event_ident_ = reinterpret_cast(this); + + // Add user event to kqueue + struct kevent event; + EV_SET(&event, user_event_ident_, EVFILT_USER, EV_ADD | EV_CLEAR, 0, 0, nullptr); + + if (::kevent(kqueue_fd_, &event, 1, nullptr, 0, nullptr) == -1) { + ENVOY_LOG(error, "Failed to add user event to kqueue: {}", strerror(errno)); + ::close(kqueue_fd_); + kqueue_fd_ = -1; + return false; + } + + // NOTE: We don't register file events here because Envoy's listener + // will monitor our kqueue FD directly via fdDoNotUse() override + + ENVOY_LOG(debug, "Initialized kqueue user trigger with FD: {}, ident: {}", kqueue_fd_, + user_event_ident_); + return true; +} + +bool KqueueUserTrigger::trigger() { + if (kqueue_fd_ == -1) { + ENVOY_LOG(error, "kqueue not initialized"); + return false; + } + + ENVOY_LOG(debug, "Triggering kqueue user event on FD {} with ident {}", kqueue_fd_, + user_event_ident_); + + // Trigger the user event + struct kevent event; + EV_SET(&event, user_event_ident_, EVFILT_USER, 0, NOTE_TRIGGER, 0, nullptr); + + if (::kevent(kqueue_fd_, &event, 1, nullptr, 0, nullptr) == -1) { + ENVOY_LOG(error, "Failed to trigger user event: {}", strerror(errno)); + return false; + } + + ENVOY_LOG(info, "Successfully triggered kqueue user event on FD {} - should be readable now", + kqueue_fd_); + return true; +} + +bool KqueueUserTrigger::wait() { + if (kqueue_fd_ == -1) { + ENVOY_LOG(debug, "kqueue wait called but FD not initialized"); + return false; + } + + ENVOY_LOG(debug, "Checking kqueue FD {} for user events", kqueue_fd_); + + // Non-blocking check for events + struct kevent event; + struct timespec timeout = {0, 0}; // Non-blocking + + int result = ::kevent(kqueue_fd_, nullptr, 0, &event, 1, &timeout); + if (result == -1) { + if (errno != EINTR) { + ENVOY_LOG(error, "kevent wait failed: {}", strerror(errno)); + } + return false; + } + + ENVOY_LOG(debug, "kevent returned {} events", result); + + if (result == 1) { + ENVOY_LOG(debug, "Got kevent: ident={}, filter={}, flags={}, fflags={}", event.ident, + event.filter, event.flags, event.fflags); + + if (event.ident == user_event_ident_ && event.filter == EVFILT_USER) { + ENVOY_LOG(info, "kqueue user event detected - trigger consumed!"); + return true; + } else { + ENVOY_LOG(debug, "kevent not matching our user event (expected ident={}, filter={})", + user_event_ident_, EVFILT_USER); + } + } + + return false; +} + +void KqueueUserTrigger::reset() { + // User events are automatically cleared with EV_CLEAR flag + ENVOY_LOG(debug, "kqueue user event reset (automatic with EV_CLEAR)"); +} +#endif + +#ifdef __linux__ +// Linux eventfd implementation +EventfdTrigger::~EventfdTrigger() { + if (eventfd_ != -1) { + ::close(eventfd_); + } +} + +bool EventfdTrigger::initialize(Event::Dispatcher& dispatcher) { + (void)dispatcher; // Unused - Envoy listener monitors FD directly + // Create eventfd with close-on-exec flag + eventfd_ = ::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (eventfd_ == -1) { + ENVOY_LOG(error, "Failed to create eventfd: {}", strerror(errno)); + return false; + } + + // NOTE: We don't register file events here because Envoy's listener + // will monitor our eventfd directly via fdDoNotUse() override + + ENVOY_LOG(debug, "Initialized eventfd trigger with FD: {}", eventfd_); + return true; +} + +bool EventfdTrigger::trigger() { + if (eventfd_ == -1) { + ENVOY_LOG(error, "eventfd not initialized"); + return false; + } + + // Write to eventfd to trigger it + uint64_t value = 1; + ssize_t result = ::write(eventfd_, &value, sizeof(value)); + if (result != sizeof(value)) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + ENVOY_LOG(error, "Failed to write to eventfd: {}", strerror(errno)); + return false; + } + // EAGAIN means eventfd counter is at maximum, which is fine + } + + ENVOY_LOG(debug, "Successfully triggered eventfd"); + return true; +} + +bool EventfdTrigger::wait() { + if (eventfd_ == -1) { + return false; + } + + // Read from eventfd to check if triggered + uint64_t value; + ssize_t result = ::read(eventfd_, &value, sizeof(value)); + if (result == sizeof(value)) { + ENVOY_LOG(debug, "eventfd triggered with value: {}", value); + return true; + } + + if (result == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { + ENVOY_LOG(error, "Failed to read from eventfd: {}", strerror(errno)); + } + + return false; +} + +void EventfdTrigger::reset() { + // eventfd is automatically reset when read + ENVOY_LOG(debug, "eventfd reset (automatic on read)"); +} +#endif + +// Fallback pipe implementation +PipeTrigger::~PipeTrigger() { + if (read_fd_ != -1) { + ::close(read_fd_); + } + if (write_fd_ != -1) { + ::close(write_fd_); + } +} + +bool PipeTrigger::initialize(Event::Dispatcher& dispatcher) { + (void)dispatcher; // Unused - Envoy listener monitors FD directly + // Create pipe + int pipe_fds[2]; + if (::pipe(pipe_fds) == -1) { + ENVOY_LOG(error, "Failed to create pipe: {}", strerror(errno)); + return false; + } + + read_fd_ = pipe_fds[0]; + write_fd_ = pipe_fds[1]; + + // Make both ends non-blocking + int flags = ::fcntl(read_fd_, F_GETFL, 0); + if (flags != -1) { + ::fcntl(read_fd_, F_SETFL, flags | O_NONBLOCK); + } + + flags = ::fcntl(write_fd_, F_GETFL, 0); + if (flags != -1) { + ::fcntl(write_fd_, F_SETFL, flags | O_NONBLOCK); + } + + // NOTE: We don't register file events here because Envoy's listener + // will monitor our pipe read FD directly via fdDoNotUse() override + + ENVOY_LOG(debug, "Initialized pipe trigger with read FD: {}, write FD: {}", read_fd_, write_fd_); + return true; +} + +bool PipeTrigger::trigger() { + if (write_fd_ == -1) { + ENVOY_LOG(error, "pipe not initialized"); + return false; + } + + // Write single byte to pipe + char trigger_byte = 1; + ssize_t result = ::write(write_fd_, &trigger_byte, 1); + if (result != 1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + ENVOY_LOG(error, "Failed to write to pipe: {}", strerror(errno)); + return false; + } + // EAGAIN means pipe buffer is full, which is fine for trigger purposes + } + + ENVOY_LOG(debug, "Successfully triggered pipe"); + return true; +} + +bool PipeTrigger::wait() { + if (read_fd_ == -1) { + return false; + } + + // Read from pipe to check if triggered + char buffer[64]; // Read multiple bytes if available + ssize_t result = ::read(read_fd_, buffer, sizeof(buffer)); + if (result > 0) { + ENVOY_LOG(debug, "pipe triggered, read {} bytes", result); + return true; + } + + if (result == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { + ENVOY_LOG(error, "Failed to read from pipe: {}", strerror(errno)); + } + + return false; +} + +void PipeTrigger::reset() { + // Pipe is reset by reading from it in wait() + ENVOY_LOG(debug, "pipe reset (via read in wait())"); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/backup_files/trigger_mechanism.h b/source/extensions/bootstrap/reverse_tunnel/backup_files/trigger_mechanism.h new file mode 100644 index 0000000000000..dfb53cd095e96 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/backup_files/trigger_mechanism.h @@ -0,0 +1,151 @@ +#pragma once + +#include +#include + +#include "envoy/common/platform.h" +#include "envoy/event/dispatcher.h" +#include "envoy/event/file_event.h" + +#include "source/common/common/logger.h" + +#ifdef __APPLE__ +#include +#include +#include +#elif defined(__linux__) +#include +#include +#else +#include +#endif + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Cross-platform trigger mechanism interface. + * Provides optimal implementation for each platform: + * - macOS: kqueue EVFILT_USER (no file descriptor overhead) + * - Linux: eventfd (single FD, 64-bit counter) + * - Other Unix: pipe (fallback for compatibility) + */ +class TriggerMechanism : public Logger::Loggable { +public: + virtual ~TriggerMechanism() = default; + + /** + * Initialize the trigger mechanism. + * @param dispatcher the event dispatcher to use + * @return true if successful, false otherwise + */ + virtual bool initialize(Event::Dispatcher& dispatcher) = 0; + + /** + * Trigger the mechanism to wake up waiting threads. + * @return true if successful, false otherwise + */ + virtual bool trigger() = 0; + + /** + * Wait for a trigger event (non-blocking check). + * @return true if triggered, false if no trigger pending + */ + virtual bool wait() = 0; + + /** + * Get the file descriptor for event loop monitoring. + * @return file descriptor, or -1 if not applicable + */ + virtual int getMonitorFd() const = 0; + + /** + * Get a description of the trigger mechanism type. + * @return string description + */ + virtual std::string getType() const = 0; + + /** + * Reset the trigger mechanism to initial state. + */ + virtual void reset() = 0; + + /** + * Factory method to create the best trigger mechanism for the current platform. + * @return unique pointer to the trigger mechanism + */ + static std::unique_ptr create(); +}; + +#ifdef __APPLE__ +/** + * macOS-specific implementation using kqueue EVFILT_USER. + * No file descriptor overhead, best performance on macOS. + */ +class KqueueUserTrigger : public TriggerMechanism { +public: + KqueueUserTrigger() : kqueue_fd_(-1), user_event_ident_(0) {} + ~KqueueUserTrigger() override; + + bool initialize(Event::Dispatcher& dispatcher) override; + bool trigger() override; + bool wait() override; + int getMonitorFd() const override { return kqueue_fd_; } + std::string getType() const override { return "kqueue_user"; } + void reset() override; + +private: + int kqueue_fd_; + uintptr_t user_event_ident_; +}; +#endif + +#ifdef __linux__ +/** + * Linux-specific implementation using eventfd. + * Single file descriptor, 64-bit counter, very efficient. + */ +class EventfdTrigger : public TriggerMechanism { +public: + EventfdTrigger() : eventfd_(-1) {} + ~EventfdTrigger() override; + + bool initialize(Event::Dispatcher& dispatcher) override; + bool trigger() override; + bool wait() override; + int getMonitorFd() const override { return eventfd_; } + std::string getType() const override { return "eventfd"; } + void reset() override; + +private: + int eventfd_; +}; +#endif + +/** + * Fallback implementation using pipes for maximum compatibility. + * Works on all Unix systems but uses two file descriptors. + */ +class PipeTrigger : public TriggerMechanism { +public: + PipeTrigger() : read_fd_(-1), write_fd_(-1) {} + ~PipeTrigger() override; + + bool initialize(Event::Dispatcher& dispatcher) override; + bool trigger() override; + bool wait() override; + int getMonitorFd() const override { return read_fd_; } + std::string getType() const override { return "pipe"; } + void reset() override; + +private: + int read_fd_; + int write_fd_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/common/BUILD b/source/extensions/bootstrap/reverse_tunnel/common/BUILD new file mode 100644 index 0000000000000..3ef032f2bdd2c --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/common/BUILD @@ -0,0 +1,24 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "reverse_connection_utility_lib", + srcs = ["reverse_connection_utility.cc"], + hdrs = ["reverse_connection_utility.h"], + visibility = ["//visibility:public"], + deps = [ + "//envoy/buffer:buffer_interface", + "//envoy/network:connection_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + ], +) diff --git a/source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.cc b/source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.cc new file mode 100644 index 0000000000000..66b476343d7b1 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.cc @@ -0,0 +1,78 @@ +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/assert.h" +#include "source/common/common/logger.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +bool ReverseConnectionUtility::isPingMessage(absl::string_view data) { + if (data.size() != PING_MESSAGE.size()) { + return false; + } + return ::memcmp(data.data(), PING_MESSAGE.data(), PING_MESSAGE.size()) == 0; +} + +Buffer::InstancePtr ReverseConnectionUtility::createPingResponse() { + return std::make_unique(PING_MESSAGE); +} + +bool ReverseConnectionUtility::sendPingResponse(Network::Connection& connection) { + auto ping_buffer = createPingResponse(); + connection.write(*ping_buffer, false); + ENVOY_LOG(debug, "reverse_tunnel: sent RPING response on connection {}", connection.id()); + return true; +} + +Api::IoCallUint64Result ReverseConnectionUtility::sendPingResponse(Network::IoHandle& io_handle) { + auto ping_buffer = createPingResponse(); + Api::IoCallUint64Result result = io_handle.write(*ping_buffer); + if (result.ok()) { + ENVOY_LOG(trace, "reverse_tunnel: sent RPING response, bytes: {}", result.return_value_); + } else { + ENVOY_LOG(trace, "reverse_tunnel: failed to send RPING response, error: {}", + result.err_->getErrorDetails()); + } + return result; +} + +bool ReverseConnectionUtility::handlePingMessage(absl::string_view data, + Network::Connection& connection) { + if (!isPingMessage(data)) { + return false; + } + ENVOY_LOG(debug, "reverse_tunnel: received RPING on connection: {}, echoing back", + connection.id()); + return sendPingResponse(connection); +} + +bool ReverseConnectionUtility::extractPingFromHttpData(absl::string_view http_data) { + if (http_data.find(PING_MESSAGE) != absl::string_view::npos) { + ENVOY_LOG(trace, "reverse_tunnel: found RPING in HTTP data"); + return true; + } + return false; +} + +std::shared_ptr ReverseConnectionMessageHandlerFactory::createPingHandler() { + return std::make_shared(); +} + +bool PingMessageHandler::processPingMessage(absl::string_view data, + Network::Connection& connection) { + if (ReverseConnectionUtility::isPingMessage(data)) { + ++ping_count_; + ENVOY_LOG(debug, "reverse_tunnel: processing ping #{} on connection {}", ping_count_, + connection.id()); + return ReverseConnectionUtility::sendPingResponse(connection); + } + return false; +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h b/source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h new file mode 100644 index 0000000000000..07373f590dfd2 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h @@ -0,0 +1,83 @@ +#pragma once + +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/network/connection.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/http/headers.h" + +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ReverseConnectionUtility : public Logger::Loggable { +public: + static constexpr absl::string_view PING_MESSAGE = "RPING"; + static constexpr absl::string_view PROXY_MESSAGE = "PROXY"; + + static bool isPingMessage(absl::string_view data); + + static Buffer::InstancePtr createPingResponse(); + + static bool sendPingResponse(Network::Connection& connection); + + static Api::IoCallUint64Result sendPingResponse(Network::IoHandle& io_handle); + + static bool handlePingMessage(absl::string_view data, Network::Connection& connection); + + static bool extractPingFromHttpData(absl::string_view http_data); + +private: + ReverseConnectionUtility() = delete; +}; + +// Header names used by reverse tunnel handshake over HTTP. +inline const Http::LowerCaseString& reverseTunnelNodeIdHeader() { + static const Http::LowerCaseString kHeader{ + absl::StrCat(Http::Headers::get().prefix(), "-reverse-tunnel-node-id")}; + return kHeader; +} + +inline const Http::LowerCaseString& reverseTunnelClusterIdHeader() { + static const Http::LowerCaseString kHeader{ + absl::StrCat(Http::Headers::get().prefix(), "-reverse-tunnel-cluster-id")}; + return kHeader; +} + +inline const Http::LowerCaseString& reverseTunnelTenantIdHeader() { + static const Http::LowerCaseString kHeader{ + absl::StrCat(Http::Headers::get().prefix(), "-reverse-tunnel-tenant-id")}; + return kHeader; +} + +class ReverseConnectionMessageHandlerFactory { +public: + static std::shared_ptr createPingHandler(); +}; + +class PingMessageHandler : public std::enable_shared_from_this, + public Logger::Loggable { +public: + PingMessageHandler() = default; + ~PingMessageHandler() = default; + + bool processPingMessage(absl::string_view data, Network::Connection& connection); + + uint64_t getPingCount() const { return ping_count_; } + +private: + uint64_t ping_count_{0}; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/BUILD b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/BUILD new file mode 100644 index 0000000000000..1b452ae99f941 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/BUILD @@ -0,0 +1,109 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "reverse_connection_address_lib", + srcs = ["reverse_connection_address.cc"], + hdrs = ["reverse_connection_address.h"], + visibility = ["//visibility:public"], + deps = [ + "//envoy/network:address_interface", + "//source/common/network:address_lib", + "//source/common/network:socket_interface_lib", + ], +) + +envoy_cc_extension( + name = "reverse_connection_resolver_lib", + srcs = ["reverse_connection_resolver.cc"], + hdrs = ["reverse_connection_resolver.h"], + visibility = ["//visibility:public"], + deps = [ + ":reverse_connection_address_lib", + "//envoy/network:resolver_interface", + "//envoy/registry", + ], +) + +envoy_cc_library( + name = "reverse_tunnel_extension_lib", + srcs = ["reverse_tunnel_initiator_extension.cc"], + hdrs = ["reverse_tunnel_initiator_extension.h"], + visibility = ["//visibility:public"], + deps = [ + "//envoy/server:bootstrap_extension_config_interface", + "//envoy/stats:stats_interface", + "//envoy/thread_local:thread_local_interface", + "//source/common/common:logger_lib", + "//source/common/stats:symbol_table_lib", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "reverse_connection_io_handle_lib", + srcs = [ + "downstream_reverse_connection_io_handle.cc", + "rc_connection_wrapper.cc", + "reverse_connection_io_handle.cc", + ], + hdrs = [ + "downstream_reverse_connection_io_handle.h", + "rc_connection_wrapper.h", + "reverse_connection_io_handle.h", + "reverse_connection_load_balancer_context.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":reverse_connection_address_lib", + ":reverse_tunnel_extension_lib", + "//envoy/api:io_error_interface", + "//envoy/grpc:async_client_interface", + "//envoy/network:address_interface", + "//envoy/network:io_handle_interface", + "//envoy/network:socket_interface", + "//envoy/stats:stats_interface", + "//envoy/stats:stats_macros", + "//envoy/upstream:cluster_manager_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/event:real_time_system_lib", + "//source/common/grpc:typed_async_client_lib", + "//source/common/http:codec_client_lib", + "//source/common/http/http1:codec_lib", + "//source/common/http/http1:codec_stats_lib", + "//source/common/network:address_lib", + "//source/common/network:connection_socket_lib", + "//source/common/network:default_socket_interface_lib", + "//source/common/network:filter_lib", + "//source/common/upstream:load_balancer_context_base_lib", + "//source/extensions/bootstrap/reverse_tunnel/common:reverse_connection_utility_lib", + ], +) + +envoy_cc_extension( + name = "reverse_tunnel_initiator_lib", + srcs = ["reverse_tunnel_initiator.cc"], + hdrs = ["reverse_tunnel_initiator.h"], + visibility = ["//visibility:public"], + deps = [ + ":reverse_connection_address_lib", + ":reverse_connection_io_handle_lib", + ":reverse_tunnel_extension_lib", + "//envoy/network:socket_interface", + "//envoy/registry", + "//envoy/server:bootstrap_extension_config_interface", + "//source/common/common:logger_lib", + "//source/common/network:address_lib", + "//source/common/network:socket_interface_lib", + "//source/common/protobuf:utility_lib", + ], +) diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.cc new file mode 100644 index 0000000000000..211a51c8fea10 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.cc @@ -0,0 +1,174 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h" + +#include "source/common/common/logger.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// DownstreamReverseConnectionIOHandle constructor implementation +DownstreamReverseConnectionIOHandle::DownstreamReverseConnectionIOHandle( + Network::ConnectionSocketPtr socket, ReverseConnectionIOHandle* parent, + const std::string& connection_key) + : IoSocketHandleImpl(socket->ioHandle().fdDoNotUse()), owned_socket_(std::move(socket)), + parent_(parent), connection_key_(connection_key) { + ENVOY_LOG(debug, + "DownstreamReverseConnectionIOHandle: taking ownership of socket with FD: {} for " + "connection key: {}", + fd_, connection_key_); +} + +// DownstreamReverseConnectionIOHandle destructor implementation +DownstreamReverseConnectionIOHandle::~DownstreamReverseConnectionIOHandle() { + ENVOY_LOG( + debug, + "DownstreamReverseConnectionIOHandle: destroying handle for FD: {} with connection key: {}", + fd_, connection_key_); +} + +Api::IoCallUint64Result +DownstreamReverseConnectionIOHandle::read(Buffer::Instance& buffer, + absl::optional max_length) { + // Perform the actual read first. + Api::IoCallUint64Result result = IoSocketHandleImpl::read(buffer, max_length); + ENVOY_LOG(trace, "DownstreamReverseConnectionIOHandle: read result: {}", result.return_value_); + ENVOY_LOG(trace, "DownstreamReverseConnectionIOHandle: read data {}", buffer.toString()); + + // If RPING keepalives are still active, check whether the incoming data is a RPING message. + if (ping_echo_active_ && result.err_ == nullptr && result.return_value_ > 0) { + const uint64_t expected = + ::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility::PING_MESSAGE + .size(); + + // Copy out up to expected bytes to check for RPING. + const uint64_t len = std::min(buffer.length(), expected); + std::string peek; + peek.resize(static_cast(len)); + buffer.copyOut(0, len, peek.data()); + + // Check if we have a complete RPING message. + if (len == expected && + ::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility::isPingMessage( + peek)) { + // Found complete RPING - echo it back and drain from buffer. + buffer.drain(expected); + auto echo_rc = ::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility:: + sendPingResponse(*this); + if (!echo_rc.ok()) { + ENVOY_LOG(trace, "DownstreamReverseConnectionIOHandle: failed to send RPING echo on FD: {}", + fd_); + } else { + ENVOY_LOG(trace, "DownstreamReverseConnectionIOHandle: echoed RPING on FD: {}", fd_); + } + + // If buffer only contained RPING, return showing we processed it. + if (buffer.length() == 0) { + return Api::IoCallUint64Result{expected, Api::IoError::none()}; + } + + // Mixed RPING + application data: disable echo and return remaining data. + ENVOY_LOG(debug, + "DownstreamReverseConnectionIOHandle: received application data after RPING, " + "disabling RPING echo for FD: {}", + fd_); + ping_echo_active_ = false; + // The adjusted return value is the number of bytes excluding the drained RPING. It should be + // transparent to upper layers that the RPING was processed. + const uint64_t adjusted = + (result.return_value_ >= expected) ? (result.return_value_ - expected) : 0; + return Api::IoCallUint64Result{adjusted, Api::IoError::none()}; + } + + // Check if partial data could be start of RPING (only if we have less than expected bytes). + if (len < expected) { + const std::string rping_prefix = + std::string(ReverseConnectionUtility::PING_MESSAGE.substr(0, len)); + if (peek == rping_prefix) { + ENVOY_LOG(trace, + "DownstreamReverseConnectionIOHandle: partial RPING received ({} bytes), waiting " + "for more", + len); + return result; // Wait for more data. + } + } + + // Not RPING (either complete non-RPING data or partial non-RPING data) - disable echo. + ENVOY_LOG(debug, + "DownstreamReverseConnectionIOHandle: received application data ({} bytes), " + "permanently disabling RPING echo for FD: {}", + len, fd_); + ping_echo_active_ = false; + } + + return result; +} + +// DownstreamReverseConnectionIOHandle close() implementation. +Api::IoCallUint64Result DownstreamReverseConnectionIOHandle::close() { + ENVOY_LOG( + debug, + "DownstreamReverseConnectionIOHandle: closing handle for FD: {} with connection key: {}", fd_, + connection_key_); + + // If we're ignoring close calls during socket hand-off, just return success. + if (ignore_close_and_shutdown_) { + ENVOY_LOG( + debug, + "DownstreamReverseConnectionIOHandle: ignoring close() call during socket hand-off for " + "connection key: {}", + connection_key_); + return Api::ioCallUint64ResultNoError(); + } + + // Prevent double-closing by checking if already closed + if (fd_ < 0) { + ENVOY_LOG(debug, + "DownstreamReverseConnectionIOHandle: handle already closed for connection key: {}", + connection_key_); + return Api::ioCallUint64ResultNoError(); + } + + // Notify parent that this downstream connection has been closed + // This will trigger re-initiation of the reverse connection if needed. + if (parent_) { + parent_->onDownstreamConnectionClosed(connection_key_); + ENVOY_LOG( + debug, + "DownstreamReverseConnectionIOHandle: notified parent of connection closure for key: {}", + connection_key_); + } + + // Reset the owned socket to properly close the connection. + if (owned_socket_) { + owned_socket_.reset(); + } + return IoSocketHandleImpl::close(); +} + +// DownstreamReverseConnectionIOHandle shutdown() implementation. +Api::SysCallIntResult DownstreamReverseConnectionIOHandle::shutdown(int how) { + ENVOY_LOG(trace, + "DownstreamReverseConnectionIOHandle: shutdown({}) called for FD: {} with connection " + "key: {}", + how, fd_, connection_key_); + + // If we're ignoring shutdown calls during socket hand-off, just return success. + if (ignore_close_and_shutdown_) { + ENVOY_LOG( + debug, + "DownstreamReverseConnectionIOHandle: ignoring shutdown() call during socket hand-off " + "for connection key: {}", + connection_key_); + return Api::SysCallIntResult{0, 0}; + } + + return IoSocketHandleImpl::shutdown(how); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h new file mode 100644 index 0000000000000..1e324cf8d286b --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include "envoy/network/io_handle.h" +#include "envoy/network/socket.h" + +#include "source/common/common/logger.h" +#include "source/common/network/io_socket_handle_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declaration. +class ReverseConnectionIOHandle; + +/** + * Custom IoHandle for downstream reverse connections that owns a ConnectionSocket. + * This class is used internally by ReverseConnectionIOHandle to manage the lifecycle + * of accepted downstream connections. + */ +class DownstreamReverseConnectionIOHandle : public Network::IoSocketHandleImpl { +public: + /** + * Constructor that takes ownership of the socket and stores parent pointer and connection key. + */ + DownstreamReverseConnectionIOHandle(Network::ConnectionSocketPtr socket, + ReverseConnectionIOHandle* parent, + const std::string& connection_key); + + ~DownstreamReverseConnectionIOHandle() override; + + // Network::IoHandle overrides + // Intercept reads to handle reverse connection keep-alive pings. + Api::IoCallUint64Result read(Buffer::Instance& buffer, + absl::optional max_length) override; + Api::IoCallUint64Result close() override; + Api::SysCallIntResult shutdown(int how) override; + + /** + * Tell this IO handle to ignore close() and shutdown() calls. + * This is called by the HTTP filter during socket hand-off to prevent + * the handed-off socket from being affected by connection cleanup. + */ + void ignoreCloseAndShutdown() { ignore_close_and_shutdown_ = true; } + + /** + * Get the owned socket for read-only access. + */ + const Network::ConnectionSocket& getSocket() const { return *owned_socket_; } + +private: + // The socket that this IOHandle owns and manages lifetime for. + Network::ConnectionSocketPtr owned_socket_; + // Pointer to parent ReverseConnectionIOHandle for connection lifecycle management. + ReverseConnectionIOHandle* parent_; + // Connection key for tracking this specific connection. + std::string connection_key_; + // Flag to ignore close and shutdown calls during socket hand-off. + bool ignore_close_and_shutdown_{false}; + + // Whether to actively echo RPING messages while the connection is idle. + // Disabled permanently after the first non-RPING application byte is observed. + bool ping_echo_active_{true}; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.cc new file mode 100644 index 0000000000000..293c8780deb07 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.cc @@ -0,0 +1,227 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h" + +#include "envoy/network/address.h" +#include "envoy/network/connection.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/http/utility.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/connection_socket_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// RCConnectionWrapper constructor implementation +RCConnectionWrapper::RCConnectionWrapper(ReverseConnectionIOHandle& parent, + Network::ClientConnectionPtr connection, + Upstream::HostDescriptionConstSharedPtr host, + const std::string& cluster_name) + : parent_(parent), connection_(std::move(connection)), host_(std::move(host)), + cluster_name_(cluster_name) { + ENVOY_LOG(debug, "RCConnectionWrapper: Using HTTP handshake for reverse connections"); +} + +// RCConnectionWrapper destructor implementation. +RCConnectionWrapper::~RCConnectionWrapper() { + ENVOY_LOG(debug, "RCConnectionWrapper destructor called"); + if (!shutdown_called_) { + this->shutdown(); + } +} + +void RCConnectionWrapper::onEvent(Network::ConnectionEvent event) { + if (event == Network::ConnectionEvent::RemoteClose) { + if (!connection_) { + ENVOY_LOG(debug, "RCConnectionWrapper: connection is null, skipping event handling"); + return; + } + + // Store connection info before it gets invalidated. + const std::string connectionKey = + connection_->connectionInfoProvider().localAddress()->asString(); + const uint64_t connectionId = connection_->id(); + + ENVOY_LOG(debug, "RCConnectionWrapper: connection: {}, found connection {} remote closed", + connectionId, connectionKey); + + // Don't call shutdown() here as it may cause cleanup during event processing + // Instead, just notify parent of closure. + parent_.onConnectionDone("Connection closed", this, true); + } +} + +// SimpleConnReadFilter::onData implementation. +Network::FilterStatus SimpleConnReadFilter::onData(Buffer::Instance& buffer, bool end_stream) { + if (parent_ == nullptr) { + return Network::FilterStatus::StopIteration; + } + + // Cast parent_ back to RCConnectionWrapper. + RCConnectionWrapper* wrapper = static_cast(parent_); + + wrapper->dispatchHttp1(buffer); + UNREFERENCED_PARAMETER(end_stream); + return Network::FilterStatus::StopIteration; +} + +std::string RCConnectionWrapper::connect(const std::string& src_tenant_id, + const std::string& src_cluster_id, + const std::string& src_node_id) { + // Register connection callbacks. + ENVOY_LOG(debug, "RCConnectionWrapper: connection: {}, adding connection callbacks", + connection_->id()); + connection_->addConnectionCallbacks(*this); + connection_->connect(); + + // Use HTTP handshake. + ENVOY_LOG(debug, + "RCConnectionWrapper: connection: {}, sending reverse connection creation " + "request through HTTP", + connection_->id()); + + // Create HTTP/1 codec to parse the response. + Http::Http1Settings http1_settings = host_->cluster().http1Settings(); + http1_client_codec_ = std::make_unique( + *connection_, host_->cluster().http1CodecStats(), *this, http1_settings, + host_->cluster().maxResponseHeadersKb(), host_->cluster().maxResponseHeadersCount()); + http1_parse_connection_ = http1_client_codec_.get(); + + // Add a tiny read filter to feed bytes into the codec for response parsing. + connection_->addReadFilter(Network::ReadFilterSharedPtr{new SimpleConnReadFilter(this)}); + + // Build HTTP handshake headers with identifiers. + absl::string_view tenant_id = src_tenant_id; + absl::string_view cluster_id = src_cluster_id; + absl::string_view node_id = src_node_id; + std::string host_value; + const auto& remote_address = connection_->connectionInfoProvider().remoteAddress(); + // This is used when reverse connections need to be established through a HTTP proxy. + // The reverse connection listener connects to an internal cluster, to which an + // internal listener listens. This internal listener has tunneling configuration + // to tcp proxy the reverse connection requests over HTTP/1 CONNECT to the remote + // proxy. + if (remote_address->type() == Network::Address::Type::EnvoyInternal) { + const auto& internal_address = + std::dynamic_pointer_cast(remote_address); + ENVOY_LOG(debug, + "RCConnectionWrapper: connection: {}, remote address is internal " + "listener {}, using endpoint ID in host header", + connection_->id(), internal_address->envoyInternalAddress()->addressId()); + host_value = internal_address->envoyInternalAddress()->endpointId(); + } else { + host_value = remote_address->asString(); + ENVOY_LOG(debug, + "RCConnectionWrapper: connection: {}, remote address is external, " + "using address as host header", + connection_->id()); + } + const Http::LowerCaseString& node_hdr = + ::Envoy::Extensions::Bootstrap::ReverseConnection::reverseTunnelNodeIdHeader(); + const Http::LowerCaseString& cluster_hdr = + ::Envoy::Extensions::Bootstrap::ReverseConnection::reverseTunnelClusterIdHeader(); + const Http::LowerCaseString& tenant_hdr = + ::Envoy::Extensions::Bootstrap::ReverseConnection::reverseTunnelTenantIdHeader(); + + auto headers = Http::createHeaderMap({ + {Http::Headers::get().Method, Http::Headers::get().MethodValues.Get}, + {Http::Headers::get().Path, "/reverse_connections/request"}, + {Http::Headers::get().Host, host_value}, + }); + headers->addCopy(node_hdr, std::string(node_id)); + headers->addCopy(cluster_hdr, std::string(cluster_id)); + headers->addCopy(tenant_hdr, std::string(tenant_id)); + headers->setContentLength(0); + + // Encode via HTTP/1 codec. + Http::RequestEncoder& request_encoder = http1_client_codec_->newStream(*this); + const Http::Status encode_status = request_encoder.encodeHeaders(*headers, true); + if (!encode_status.ok()) { + ENVOY_LOG(error, "RCConnectionWrapper: encodeHeaders failed: {}", encode_status.message()); + onHandshakeFailure("HTTP handshake encode failed"); + } + + return connection_->connectionInfoProvider().localAddress()->asString(); +} + +void RCConnectionWrapper::decodeHeaders(Http::ResponseHeaderMapPtr&& headers, bool) { + const uint64_t status = Http::Utility::getResponseStatus(*headers); + if (status == 200) { + ENVOY_LOG(debug, "Received HTTP 200 OK response"); + onHandshakeSuccess(); + } else { + ENVOY_LOG(error, "Received non-200 HTTP response: {}", status); + onHandshakeFailure(absl::StrCat("HTTP handshake failed with status ", status)); + } +} + +void RCConnectionWrapper::dispatchHttp1(Buffer::Instance& buffer) { + if (http1_parse_connection_ != nullptr) { + const Http::Status status = http1_parse_connection_->dispatch(buffer); + if (!status.ok()) { + ENVOY_LOG(debug, "RCConnectionWrapper: HTTP/1 codec dispatch error: {}", status.message()); + } + } +} + +void RCConnectionWrapper::onHandshakeSuccess() { + std::string message = "reverse connection accepted"; + ENVOY_LOG(debug, "handshake succeeded: {}", message); + parent_.onConnectionDone(message, this, false); +} + +void RCConnectionWrapper::onHandshakeFailure(const std::string& message) { + ENVOY_LOG(debug, "handshake failed: {}", message); + parent_.onConnectionDone(message, this, false); +} + +void RCConnectionWrapper::shutdown() { + if (shutdown_called_) { + ENVOY_LOG(debug, "RCConnectionWrapper: Shutdown already called, skipping"); + return; + } + shutdown_called_ = true; + + if (!connection_) { + ENVOY_LOG(error, "RCConnectionWrapper: Connection already null, nothing to shutdown"); + return; + } + + // Get connection info for logging + uint64_t connection_id = connection_->id(); + Network::Connection::State state = connection_->state(); + ENVOY_LOG(debug, "RCConnectionWrapper: Shutting down connection ID: {}, state: {}", connection_id, + static_cast(state)); + + // Remove connection callbacks first to prevent recursive calls during shutdown. + if (state != Network::Connection::State::Closed) { + connection_->removeConnectionCallbacks(*this); + ENVOY_LOG(debug, "Connection callbacks removed"); + } + + // Close the connection if it's still open. + state = connection_->state(); + if (state == Network::Connection::State::Open) { + ENVOY_LOG(debug, "Closing open connection gracefully"); + connection_->close(Network::ConnectionCloseType::FlushWrite); + } else if (state == Network::Connection::State::Closing) { + ENVOY_LOG(debug, "Connection already closing"); + } else { + ENVOY_LOG(debug, "Connection already closed"); + } + + // Clear the connection pointer after shutdown. + connection_.reset(); + ENVOY_LOG(debug, "RCConnectionWrapper: Connection cleared after shutdown"); + ENVOY_LOG(debug, "RCConnectionWrapper: Shutdown completed"); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h new file mode 100644 index 0000000000000..8230a8da470e3 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h @@ -0,0 +1,167 @@ +#pragma once + +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/event/deferred_deletable.h" +#include "envoy/http/codec.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/upstream/upstream.h" + +#include "source/common/common/logger.h" +#include "source/common/http/http1/codec_impl.h" +#include "source/common/network/filter_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declaration. +class ReverseConnectionIOHandle; + +/** + * Simple read filter for handling reverse connection handshake responses. + * This filter processes the HTTP response from the upstream server during handshake. + */ +class SimpleConnReadFilter : public Network::ReadFilterBaseImpl, + public Logger::Loggable { +public: + /** + * Constructor that stores pointer to parent wrapper. + */ + explicit SimpleConnReadFilter(void* parent) : parent_(parent) {} + + // Network::ReadFilter overrides + Network::FilterStatus onData(Buffer::Instance& buffer, bool end_stream) override; + +private: + void* parent_; // Pointer to RCConnectionWrapper to avoid circular dependency. +}; + +/** + * Wrapper for reverse connections that manages the connection lifecycle and handshake. + * It handles the handshake process (both gRPC and HTTP fallback) and manages connection + * callbacks and cleanup. + */ +class RCConnectionWrapper : public Network::ConnectionCallbacks, + public Event::DeferredDeletable, + public Logger::Loggable, + public Http::ResponseDecoder, + public Http::ConnectionCallbacks { + friend class SimpleConnReadFilterTest; + +public: + /** + * Constructor for RCConnectionWrapper. + * @param parent reference to the parent ReverseConnectionIOHandle + * @param connection the client connection to wrap + * @param host the upstream host description + * @param cluster_name the name of the cluster + */ + RCConnectionWrapper(ReverseConnectionIOHandle& parent, Network::ClientConnectionPtr connection, + Upstream::HostDescriptionConstSharedPtr host, + const std::string& cluster_name); + + /** + * Destructor for RCConnectionWrapper. + * Performs defensive cleanup to prevent crashes during shutdown. + */ + ~RCConnectionWrapper() override; + + // Network::ConnectionCallbacks overrides + void onEvent(Network::ConnectionEvent event) override; + void onAboveWriteBufferHighWatermark() override {} + void onBelowWriteBufferLowWatermark() override {} + + // Http::ResponseDecoder overrides + void decode1xxHeaders(Http::ResponseHeaderMapPtr&&) override {} + void decodeHeaders(Http::ResponseHeaderMapPtr&& headers, bool end_stream) override; + void decodeData(Buffer::Instance&, bool) override {} + void decodeTrailers(Http::ResponseTrailerMapPtr&&) override {} + void decodeMetadata(Http::MetadataMapPtr&&) override {} + void dumpState(std::ostream&, int) const override {} + + // Http::ConnectionCallbacks overrides + void onGoAway(Http::GoAwayErrorCode) override {} + void onSettings(Http::ReceivedSettings&) override {} + void onMaxStreamsChanged(uint32_t) override {} + + /** + * Initiate the reverse connection handshake (HTTP only). + * @param src_tenant_id the tenant identifier + * @param src_cluster_id the cluster identifier + * @param src_node_id the node identifier + * @return the local address as string + */ + std::string connect(const std::string& src_tenant_id, const std::string& src_cluster_id, + const std::string& src_node_id); + + /** + * Release ownership of the connection. + * @return the connection pointer (ownership transferred to caller) + */ + Network::ClientConnectionPtr releaseConnection() { return std::move(connection_); } + + /** + * Process HTTP response from upstream. + * @param buffer the response data + * @param end_stream whether this is the end of the stream + */ + void processHttpResponse(Buffer::Instance& buffer, bool end_stream); + + /** + * Handle successful handshake completion. + */ + void onHandshakeSuccess(); + + /** + * Handle handshake failure. + * @param message error message + */ + void onHandshakeFailure(const std::string& message); + + /** + * Perform graceful shutdown of the connection. + */ + void shutdown(); + + /** + * Get the underlying connection. + * @return pointer to the client connection + */ + Network::ClientConnection* getConnection() { return connection_.get(); } + + /** + * Get the host description. + * @return shared pointer to the host description + */ + Upstream::HostDescriptionConstSharedPtr getHost() { return host_; } + +private: + ReverseConnectionIOHandle& parent_; + Network::ClientConnectionPtr connection_; + Upstream::HostDescriptionConstSharedPtr host_; + std::string cluster_name_; + std::string connection_key_; + bool http_handshake_sent_{false}; + bool handshake_completed_{false}; + bool shutdown_called_{false}; + +public: + // Dispatch incoming bytes to HTTP/1 codec. + void dispatchHttp1(Buffer::Instance& buffer); + +private: + // HTTP/1 codec used to send request and parse response. + std::unique_ptr http1_client_codec_; + // Base interface pointer used to call dispatch via public API. + Http::Connection* http1_parse_connection_{nullptr}; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.cc new file mode 100644 index 0000000000000..dcf868020fb5c --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.cc @@ -0,0 +1,66 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" + +#include +#include +#include + +#include +#include + +#include "source/common/common/fmt.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +static const std::string reverse_connection_address = "127.0.0.1:0"; + +ReverseConnectionAddress::ReverseConnectionAddress(const ReverseConnectionConfig& config) + : config_(config) { + + // Create the logical name (rc:// address) for identification. + logical_name_ = fmt::format("rc://{}:{}:{}@{}:{}", config.src_node_id, config.src_cluster_id, + config.src_tenant_id, config.remote_cluster, config.connection_count); + + // Use localhost with a static port for the actual address string to pass IP validation + // This will be used by the filter chain manager for matching. + address_string_ = reverse_connection_address; + + ENVOY_LOG_MISC(debug, "reverse connection address: logical_name={}, address={}", logical_name_, + address_string_); +} + +bool ReverseConnectionAddress::operator==(const Instance& rhs) const { + const auto* reverse_conn_addr = dynamic_cast(&rhs); + if (reverse_conn_addr == nullptr) { + return false; + } + return config_.src_node_id == reverse_conn_addr->config_.src_node_id && + config_.src_cluster_id == reverse_conn_addr->config_.src_cluster_id && + config_.src_tenant_id == reverse_conn_addr->config_.src_tenant_id && + config_.remote_cluster == reverse_conn_addr->config_.remote_cluster && + config_.connection_count == reverse_conn_addr->config_.connection_count; +} + +const std::string& ReverseConnectionAddress::asString() const { return address_string_; } + +absl::string_view ReverseConnectionAddress::asStringView() const { return address_string_; } + +const std::string& ReverseConnectionAddress::logicalName() const { return logical_name_; } + +const sockaddr* ReverseConnectionAddress::sockAddr() const { + // Return a valid localhost sockaddr structure for IP validation. + static struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(0); // Port 0 + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1 + return reinterpret_cast(&addr); +} + +socklen_t ReverseConnectionAddress::sockAddrLen() const { return sizeof(struct sockaddr_in); } + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h new file mode 100644 index 0000000000000..7d0f78a501dbe --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include + +#include + +#include "envoy/network/address.h" + +#include "source/common/common/logger.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/socket_interface.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Custom address type that embeds reverse connection metadata. + */ +class ReverseConnectionAddress : public Network::Address::Instance, + public Envoy::Logger::Loggable { +public: + // Struct to hold reverse connection configuration + struct ReverseConnectionConfig { + // Source node id of initiator envoy + std::string src_node_id; + // Source cluster id of initiator envoy + std::string src_cluster_id; + // Source tenant id of initiator envoy + std::string src_tenant_id; + // Remote cluster name of the reverse connection + std::string remote_cluster; + // Connection count of the reverse connection + uint32_t connection_count; + }; + + ReverseConnectionAddress(const ReverseConnectionConfig& config); + + // Network::Address::Instance + bool operator==(const Instance& rhs) const override; + Network::Address::Type type() const override { + return Network::Address::Type::Ip; + } // Use IP type with our custom IP implementation + const std::string& asString() const override; + absl::string_view asStringView() const override; + const std::string& logicalName() const override; + const Network::Address::Ip* ip() const override { return ipv4_instance_->ip(); } + const Network::Address::Pipe* pipe() const override { return nullptr; } + const Network::Address::EnvoyInternalAddress* envoyInternalAddress() const override { + return nullptr; + } + absl::optional networkNamespace() const override { return absl::nullopt; } + const sockaddr* sockAddr() const override; + socklen_t sockAddrLen() const override; + absl::string_view addressType() const override { return "reverse_connection"; } + const Network::SocketInterface& socketInterface() const override { + // Return the appropriate reverse connection socket interface for downstream connections + auto* reverse_socket_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.downstream_socket_interface"); + if (reverse_socket_interface) { + ENVOY_LOG_MISC(debug, "reverse connection address: using reverse socket interface"); + return *reverse_socket_interface; + } + // Fallback to default socket interface if reverse connection interface is not available. + return Network::SocketInterfaceSingleton::get(); + } + + // Accessor for reverse connection config + const ReverseConnectionConfig& reverseConnectionConfig() const { return config_; } + +private: + ReverseConnectionConfig config_; + std::string address_string_; + std::string logical_name_; + // Use a regular Ipv4Instance for 127.0.0.1:0 + Network::Address::InstanceConstSharedPtr ipv4_instance_{ + std::make_shared("127.0.0.1", 0)}; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_handshake.proto b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_handshake.proto new file mode 100644 index 0000000000000..df42f0691f71a --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_handshake.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package envoy.extensions.bootstrap.reverse_tunnel; + +// Internal proto definitions for reverse connection handshake protocol. +// These messages are used internally by the reverse tunnel extension +// and are not exposed to users. + +// Config sent by the local cluster as part of the Initiation workflow. +// This message combined with message 'ReverseConnHandshakeRet' which is +// sent as a response can be used to transfer/negotiate parameter between the +// two envoys. +message ReverseConnHandshakeArg { + // Tenant UUID of the local cluster. + string tenant_uuid = 1; + + // Cluster UUID of the local cluster. + string cluster_uuid = 2; + + // Node UUID of the local cluster. + string node_uuid = 3; +} + +// Config used by the remote cluster in response to the above 'ReverseConnHandshakeArg'. +message ReverseConnHandshakeRet { + + enum ConnectionStatus { + REJECTED = 0; + ACCEPTED = 1; + } + + // Tracks the status of the reverse connection initiation workflow. + ConnectionStatus status = 1; + + // This field can be used to transmit success/warning/error messages + // describing the status of the reverse connection, if needed. + string status_message = 2; +} diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.cc new file mode 100644 index 0000000000000..b8b5c5fe69e21 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.cc @@ -0,0 +1,1231 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" + +#include +#include +#include + +#include "envoy/event/deferred_deletable.h" +#include "envoy/event/timer.h" +#include "envoy/network/address.h" +#include "envoy/network/connection.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/event/real_time_system.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/connection_socket_impl.h" +#include "source/common/network/socket_interface_impl.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// ReverseConnectionIOHandle implementation +ReverseConnectionIOHandle::ReverseConnectionIOHandle(os_fd_t fd, + const ReverseConnectionSocketConfig& config, + Upstream::ClusterManager& cluster_manager, + ReverseTunnelInitiatorExtension* extension, + Stats::Scope&) + : IoSocketHandleImpl(fd), config_(config), cluster_manager_(cluster_manager), + extension_(extension), original_socket_fd_(fd) { + ENVOY_LOG_MISC( + debug, + "Created ReverseConnectionIOHandle: fd={}, src_node={}, src_cluster: {}, num_clusters={}", + fd_, config_.src_node_id, config_.src_cluster_id, config_.remote_clusters.size()); +} + +ReverseConnectionIOHandle::~ReverseConnectionIOHandle() { + ENVOY_LOG_MISC(debug, "Destroying ReverseConnectionIOHandle - performing cleanup."); + cleanup(); +} + +void ReverseConnectionIOHandle::cleanup() { + ENVOY_LOG_MISC(debug, "Starting cleanup of reverse connection resources."); + + // Clean up pipe trigger mechanism first to prevent use-after-free. + ENVOY_LOG_MISC(trace, + "ReverseConnectionIOHandle: cleaning up trigger pipe; " + "trigger_pipe_write_fd_={}, trigger_pipe_read_fd_={}", + trigger_pipe_write_fd_, trigger_pipe_read_fd_); + if (trigger_pipe_write_fd_ >= 0) { + ::close(trigger_pipe_write_fd_); + trigger_pipe_write_fd_ = -1; + } + if (trigger_pipe_read_fd_ >= 0) { + ::close(trigger_pipe_read_fd_); + trigger_pipe_read_fd_ = -1; + } + + // Cancel the retry timer safely. + if (rev_conn_retry_timer_ && rev_conn_retry_timer_->enabled()) { + ENVOY_LOG_MISC(trace, "ReverseConnectionIOHandle: cancelling and resetting retry timer."); + rev_conn_retry_timer_.reset(); + } + + // Graceful shutdown of connection wrappers with exception safety. + ENVOY_LOG_MISC(debug, "Gracefully shutting down {} connection wrappers.", + connection_wrappers_.size()); + + // Move wrappers for deferred cleanup. + std::vector> wrappers_to_delete; + for (auto& wrapper : connection_wrappers_) { + if (wrapper) { + ENVOY_LOG(debug, "Moving connection wrapper for deferred cleanup."); + wrappers_to_delete.push_back(std::move(wrapper)); + } + } + + // Clear containers safely. + connection_wrappers_.clear(); + conn_wrapper_to_host_map_.clear(); + + // Clean up wrappers with safe deletion. + for (auto& wrapper : wrappers_to_delete) { + if (wrapper && isThreadLocalDispatcherAvailable()) { + getThreadLocalDispatcher().deferredDelete(std::move(wrapper)); + } else { + // Direct cleanup when dispatcher not available. + wrapper.reset(); + } + } + + // Clear cluster to hosts mapping. + cluster_to_resolved_hosts_map_.clear(); + host_to_conn_info_map_.clear(); + + // Clear established connections queue safely. + size_t queue_size = established_connections_.size(); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Cleaning up {} established connections.", + queue_size); + + while (!established_connections_.empty()) { + auto connection = std::move(established_connections_.front()); + established_connections_.pop(); + + if (connection) { + auto state = connection->state(); + if (state == Envoy::Network::Connection::State::Open) { + connection->close(Envoy::Network::ConnectionCloseType::FlushWrite); + ENVOY_LOG(debug, "Closed established connection."); + } else { + ENVOY_LOG(debug, "Connection already in state: {}.", static_cast(state)); + } + } + } + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Completed established connections cleanup."); + + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Completed cleanup of reverse connection resources."); +} + +Api::SysCallIntResult ReverseConnectionIOHandle::listen(int) { + // No-op for reverse connections. + return Api::SysCallIntResult{0, 0}; +} + +void ReverseConnectionIOHandle::initializeFileEvent(Event::Dispatcher& dispatcher, + Event::FileReadyCb cb, + Event::FileTriggerType trigger, + uint32_t events) { + // Reverse connections should be initiated when initializeFileEvent() is called on a worker + // thread. + ENVOY_LOG(debug, + "ReverseConnectionIOHandle::initializeFileEvent() called on thread: {} for fd={}", + dispatcher.name(), fd_); + + if (is_reverse_conn_started_) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Skipping initializeFileEvent() call because " + "reverse connections are already started"); + return; + } + + ENVOY_LOG(info, "ReverseConnectionIOHandle: Starting reverse connections on worker thread '{}'", + dispatcher.name()); + + // Store worker dispatcher + worker_dispatcher_ = &dispatcher; + + // Create trigger pipe on worker thread. + if (!isTriggerPipeReady()) { + createTriggerPipe(); + if (!isTriggerPipeReady()) { + ENVOY_LOG(error, "Failed to create trigger pipe on worker thread"); + return; + } + } + + // CRITICAL: Replace the monitored FD with pipe read FD + // This must happen before any event registration. + int trigger_fd = getPipeMonitorFd(); + if (trigger_fd != -1) { + ENVOY_LOG(info, "Replacing monitored FD from {} to pipe read FD {}", fd_, trigger_fd); + fd_ = trigger_fd; + } + + // Initialize reverse connections on worker thread + if (!rev_conn_retry_timer_) { + rev_conn_retry_timer_ = dispatcher.createTimer([this]() { + ENVOY_LOG(debug, "Reverse connection timer triggered on worker thread"); + maintainReverseConnections(); + }); + maintainReverseConnections(); + } + + is_reverse_conn_started_ = true; + ENVOY_LOG(info, "ReverseConnectionIOHandle: Reverse connections started on thread '{}'", + dispatcher.name()); + + // Call parent implementation + IoSocketHandleImpl::initializeFileEvent(dispatcher, cb, trigger, events); +} + +Envoy::Network::IoHandlePtr ReverseConnectionIOHandle::accept(struct sockaddr* addr, + socklen_t* addrlen) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: accept() called"); + if (isTriggerPipeReady()) { + char trigger_byte; + ssize_t bytes_read = ::read(trigger_pipe_read_fd_, &trigger_byte, 1); + if (bytes_read == 1) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: received trigger, processing connection."); + // When a connection is established, a byte is written to the trigger_pipe_write_fd_ and the + // connection is inserted into the established_connections_ queue. The last connection in the + // queue is therefore the one that got established last. + if (!established_connections_.empty()) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: getting connection from queue."); + auto connection = std::move(established_connections_.front()); + established_connections_.pop(); + // Fill in address information for the reverse tunnel "client". + // Use actual client address from established connection. + if (addr && addrlen) { + const auto& remote_addr = connection->connectionInfoProvider().remoteAddress(); + + if (remote_addr) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: using actual client address: {}", + remote_addr->asString()); + const sockaddr* sock_addr = remote_addr->sockAddr(); + socklen_t addr_len = remote_addr->sockAddrLen(); + + if (*addrlen >= addr_len) { + memcpy(addr, sock_addr, addr_len); // NOLINT(safe-memcpy) + *addrlen = addr_len; + ENVOY_LOG(trace, "ReverseConnectionIOHandle: copied {} bytes of address data", + addr_len); + } else { + ENVOY_LOG(warn, + "ReverseConnectionIOHandle::accept() - buffer too small for address: " + "need {} bytes, have {}", + addr_len, *addrlen); + *addrlen = addr_len; // Still set the required length + } + } else { + ENVOY_LOG(warn, "ReverseConnectionIOHandle: no remote address available, " + "using synthetic localhost address"); + // Fallback to synthetic address only when remote address is unavailable + auto synthetic_addr = + std::make_shared("127.0.0.1", 0); + const sockaddr* sock_addr = synthetic_addr->sockAddr(); + socklen_t addr_len = synthetic_addr->sockAddrLen(); + if (*addrlen >= addr_len) { + memcpy(addr, sock_addr, addr_len); // NOLINT(safe-memcpy) + *addrlen = addr_len; + } else { + ENVOY_LOG(error, "ReverseConnectionIOHandle: buffer too small for synthetic address"); + *addrlen = addr_len; + } + } + } + + const std::string connection_key = + connection->connectionInfoProvider().localAddress()->asString(); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: got connection key: {}", connection_key); + + // Instead of moving the socket, duplicate the file descriptor. + const Network::ConnectionSocketPtr& original_socket = connection->getSocket(); + if (!original_socket || !original_socket->isOpen()) { + ENVOY_LOG(error, "Original socket is not available or not open"); + return nullptr; + } + + // Duplicate the file descriptor. + Network::IoHandlePtr duplicated_handle = original_socket->ioHandle().duplicate(); + if (!duplicated_handle || !duplicated_handle->isOpen()) { + ENVOY_LOG(error, "Failed to duplicate file descriptor"); + return nullptr; + } + + os_fd_t original_fd = original_socket->ioHandle().fdDoNotUse(); + os_fd_t duplicated_fd = duplicated_handle->fdDoNotUse(); + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: duplicated fd: original_fd={}, duplicated_fd={}", + original_fd, duplicated_fd); + + // Create a new socket with the duplicated handle. + Network::ConnectionSocketPtr duplicated_socket = + std::make_unique( + std::move(duplicated_handle), + original_socket->connectionInfoProvider().localAddress(), + original_socket->connectionInfoProvider().remoteAddress()); + + // Reset file events on the duplicated socket to clear any inherited events. + duplicated_socket->ioHandle().resetFileEvents(); + + // Create RAII-based IoHandle with duplicated socket, passing parent pointer and connection + // key. + auto io_handle = std::make_unique( + std::move(duplicated_socket), this, connection_key); + + ENVOY_LOG(debug, "ReverseConnectionIOHandle: RAII IoHandle created with duplicated socket " + "and protection enabled."); + + // Reset file events on the original socket to prevent any pending operations. The socket + // fd has been duplicated, so we have an independent fd. Closing the original connection + // will only close its fd, not affect our duplicated fd. + // + // Note: For raw TCP connections, no shutdown() is called during close, only close() on + // the fd, which doesn't affect the duplicated fd. + original_socket->ioHandle().resetFileEvents(); + + // Close the original connection. + connection->close(Network::ConnectionCloseType::NoFlush); + + ENVOY_LOG(debug, "ReverseConnectionIOHandle: returning io_handle."); + return io_handle; + } + } else if (bytes_read == 0) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: trigger pipe closed."); + return nullptr; + } else if (bytes_read == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { + ENVOY_LOG(error, "ReverseConnectionIOHandle: error reading from trigger pipe: {}", + errorDetails(errno)); + return nullptr; + } + } + return nullptr; +} + +Api::IoCallUint64Result ReverseConnectionIOHandle::read(Buffer::Instance& buffer, + absl::optional max_length) { + ENVOY_LOG(trace, "Read operation - max_length: {}", max_length.value_or(0)); + auto result = IoSocketHandleImpl::read(buffer, max_length); + return result; +} + +Api::IoCallUint64Result ReverseConnectionIOHandle::write(Buffer::Instance& buffer) { + ENVOY_LOG(trace, "Write operation - {} bytes", buffer.length()); + auto result = IoSocketHandleImpl::write(buffer); + return result; +} + +Api::SysCallIntResult +ReverseConnectionIOHandle::connect(Envoy::Network::Address::InstanceConstSharedPtr address) { + // This is not used for reverse connections. + ENVOY_LOG(trace, "Connect operation - address: {}", address->asString()); + // For reverse connections, connect calls are handled through the tunnel mechanism. + return IoSocketHandleImpl::connect(address); +} + +// Note: This close method is called when the ReverseConnectionIOHandle itself is closed, which +// should typically happen when the listener is being drained. +// Individual reverse connections initiated by this ReverseConnectionIOHandle are managed via +// DownstreamReverseConnectionIOHandle RAII ownership. +Api::IoCallUint64Result ReverseConnectionIOHandle::close() { + ENVOY_LOG(error, "ReverseConnectionIOHandle: performing graceful shutdown."); + + // Clean up original socket FD + if (original_socket_fd_ != -1) { + ENVOY_LOG(error, "Closing original socket FD: {}.", original_socket_fd_); + ::close(original_socket_fd_); + original_socket_fd_ = -1; + } + + // CRITICAL: If we're using pipe trigger FD, let the IoSocketHandleImpl::close() + // close it and cleanup() set the pipe FDs to -1. + if (isTriggerPipeReady() && getPipeMonitorFd() == fd_) { + ENVOY_LOG(error, + "Skipping close of pipe trigger FD {} - will be handled by base close() method.", + fd_); + } + + if (rev_conn_retry_timer_) { + rev_conn_retry_timer_.reset(); + } + + return IoSocketHandleImpl::close(); +} + +void ReverseConnectionIOHandle::onEvent(Network::ConnectionEvent event) { + // This is called when connection events occur. + // For reverse connections, we handle these events through RCConnectionWrapper. + ENVOY_LOG(trace, "ReverseConnectionIOHandle: event: {}", static_cast(event)); +} + +int ReverseConnectionIOHandle::getPipeMonitorFd() const { return trigger_pipe_read_fd_; } + +// Get time source for consistent time operations. +TimeSource& ReverseConnectionIOHandle::getTimeSource() const { + // Try to get time source from thread-local dispatcher first. + if (extension_) { + auto* local_registry = extension_->getLocalRegistry(); + if (local_registry) { + return local_registry->dispatcher().timeSource(); + } + } + + // Fallback to worker dispatcher if available. + if (worker_dispatcher_) { + return worker_dispatcher_->timeSource(); + } + + // This should not happen in production. Assert to ensure proper initialization. + ENVOY_BUG(false, "No time source available. dispatcher not properly initialized"); + PANIC("ReverseConnectionIOHandle: No valid time source available"); +} + +// Use the thread-local registry to get the dispatcher. +Event::Dispatcher& ReverseConnectionIOHandle::getThreadLocalDispatcher() const { + // Get the thread-local dispatcher from the socket interface's registry. + auto* local_registry = extension_->getLocalRegistry(); + + if (local_registry) { + // Return the dispatcher from the thread-local registry. + ENVOY_LOG(debug, "ReverseConnectionIOHandle: dispatcher: {}", + local_registry->dispatcher().name()); + return local_registry->dispatcher(); + } + + ENVOY_BUG(false, "Failed to get dispatcher from thread-local registry"); + // This should never happen in normal operation, but we need to handle it gracefully. + RELEASE_ASSERT(worker_dispatcher_ != nullptr, "No dispatcher available"); + return *worker_dispatcher_; +} + +// Safe wrapper for accessing thread-local dispatcher +bool ReverseConnectionIOHandle::isThreadLocalDispatcherAvailable() const { + auto* local_registry = extension_->getLocalRegistry(); + return local_registry != nullptr; +} + +ReverseTunnelInitiatorExtension* ReverseConnectionIOHandle::getDownstreamExtension() const { + return extension_; +} + +void ReverseConnectionIOHandle::maybeUpdateHostsMappingsAndConnections( + const std::string& cluster_id, const std::vector& hosts) { + absl::flat_hash_set new_hosts(hosts.begin(), hosts.end()); + absl::flat_hash_set removed_hosts; + const auto& cluster_to_resolved_hosts_itr = cluster_to_resolved_hosts_map_.find(cluster_id); + if (cluster_to_resolved_hosts_itr != cluster_to_resolved_hosts_map_.end()) { + // removed_hosts contains the hosts that were previously resolved. + removed_hosts = cluster_to_resolved_hosts_itr->second; + } + for (const std::string& host : hosts) { + if (removed_hosts.find(host) != removed_hosts.end()) { + // Since the host still exists, we will remove it from removed_hosts. + removed_hosts.erase(host); + } + ENVOY_LOG(debug, "Adding remote host {} to cluster {}", host, cluster_id); + + // Update or create host info + auto host_it = host_to_conn_info_map_.find(host); + if (host_it == host_to_conn_info_map_.end()) { + // Create host entry on-demand to avoid race conditions during host registration. + ENVOY_LOG( + debug, + "Creating HostConnectionInfo on-demand during host update for host {} in cluster {}", + host, cluster_id); + host_to_conn_info_map_[host] = HostConnectionInfo{ + host, + cluster_id, + {}, // connection_keys - empty set initially + 1, // default target_connection_count + 0, // failure_count + getTimeSource().monotonicTime(), // last_failure_time + getTimeSource().monotonicTime(), // backoff_until (no backoff initially) + {} // connection_states - empty map initially + }; + } else { + // Update cluster name if host moved to different cluster. + host_it->second.cluster_name = cluster_id; + } + } + cluster_to_resolved_hosts_map_[cluster_id] = new_hosts; + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Removing {} remote hosts from cluster {}", + removed_hosts.size(), cluster_id); + + // Remove the hosts present in removed_hosts. + for (const std::string& host : removed_hosts) { + removeStaleHostAndCloseConnections(host); + host_to_conn_info_map_.erase(host); + } +} + +void ReverseConnectionIOHandle::removeStaleHostAndCloseConnections(const std::string& host) { + ENVOY_LOG(info, "ReverseConnectionIOHandle: Removing all connections to remote host {}", host); + // Find all wrappers for this host. Each wrapper represents a reverse connection to the host. + std::vector wrappers_to_remove; + for (const auto& [wrapper, mapped_host] : conn_wrapper_to_host_map_) { + if (mapped_host == host) { + wrappers_to_remove.push_back(wrapper); + } + } + ENVOY_LOG(info, "Found {} connections to remove for host {}", wrappers_to_remove.size(), host); + // Remove wrappers and close connections. + for (auto* wrapper : wrappers_to_remove) { + ENVOY_LOG(debug, "Removing connection wrapper for host {}", host); + + // Get the connection from wrapper and close it. + auto* connection = wrapper->getConnection(); + if (connection && connection->state() == Network::Connection::State::Open) { + connection->close(Network::ConnectionCloseType::FlushWrite); + } + + // Remove from wrapper-to-host map. + conn_wrapper_to_host_map_.erase(wrapper); + // Remove the wrapper from connection_wrappers_ vector. + connection_wrappers_.erase( + std::remove_if(connection_wrappers_.begin(), connection_wrappers_.end(), + [wrapper](const std::unique_ptr& w) { + return w.get() == wrapper; + }), + connection_wrappers_.end()); + } + // Clear connection keys from host info. + auto host_it = host_to_conn_info_map_.find(host); + if (host_it != host_to_conn_info_map_.end()) { + host_it->second.connection_keys.clear(); + } +} + +void ReverseConnectionIOHandle::maintainClusterConnections( + const std::string& cluster_name, const RemoteClusterConnectionConfig& cluster_config) { + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Maintaining connections for cluster: {} with {} requested " + "connections per host", + cluster_name, cluster_config.reverse_connection_count); + + // Generate a temporary connection key for early failure tracking, to update stats gauges. + const std::string temp_connection_key = "temp_" + cluster_name + "_" + std::to_string(rand()); + + // Get thread local cluster to access resolved hosts. + auto thread_local_cluster = cluster_manager_.getThreadLocalCluster(cluster_name); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(error, "Cluster '{}' not found for reverse tunnel - will retry later", cluster_name); + updateConnectionState("", cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return; + } + // Get all resolved hosts for the cluster. + const auto& host_map_ptr = thread_local_cluster->prioritySet().crossPriorityHostMap(); + if (host_map_ptr == nullptr || host_map_ptr->empty()) { + ENVOY_LOG(error, "No hosts found in cluster '{}' - will retry later", cluster_name); + updateConnectionState("", cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return; + } + // Retrieve the resolved hosts for a cluster and update the corresponding maps. + std::vector resolved_hosts; + for (const auto& host_itr : *host_map_ptr) { + const std::string& resolved = host_itr.first; + resolved_hosts.emplace_back(resolved); + } + maybeUpdateHostsMappingsAndConnections(cluster_name, std::move(resolved_hosts)); + // Track successful connections for this cluster. + uint32_t total_successful_connections = 0; + uint32_t total_required_connections = + host_map_ptr->size() * cluster_config.reverse_connection_count; + + // Create connections to each host in the cluster. + for (const auto& [host_address, host] : *host_map_ptr) { + ENVOY_LOG( + debug, + "ReverseConnectionIOHandle: Checking reverse connection count for host {} of cluster {}", + host_address, cluster_name); + + // Ensure HostConnectionInfo exists for this host, handling internal addresses consistently. + const std::string key = host_address; + auto host_it = host_to_conn_info_map_.find(key); + if (host_it == host_to_conn_info_map_.end()) { + ENVOY_LOG(debug, "Creating HostConnectionInfo for host {} in cluster {}", key, cluster_name); + host_to_conn_info_map_[key] = HostConnectionInfo{ + key, + cluster_name, + {}, // connection_keys - empty set initially + cluster_config.reverse_connection_count, // target_connection_count from config + 0, // failure_count + // last_failure_time + worker_dispatcher_->timeSource().monotonicTime(), + // backoff_until + worker_dispatcher_->timeSource().monotonicTime(), + {} // connection_states + }; + } + + // Check if we should attempt connection to this host (backoff logic). + if (!shouldAttemptConnectionToHost(host_address, cluster_name)) { + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Skipping connection attempt to host {} due to backoff", + host_address); + continue; + } + // Get current number of successful connections to this host. + uint32_t current_connections = host_to_conn_info_map_[key].connection_keys.size(); + + ENVOY_LOG(info, + "ReverseConnectionIOHandle: Number of reverse connections to host {} of cluster {}: " + "Current: {}, Required: {}", + host_address, cluster_name, current_connections, + cluster_config.reverse_connection_count); + if (current_connections >= cluster_config.reverse_connection_count) { + ENVOY_LOG( + debug, + "ReverseConnectionIOHandle: No more reverse connections needed to host {} of cluster {}", + host_address, cluster_name); + total_successful_connections += current_connections; + continue; + } + const uint32_t needed_connections = + cluster_config.reverse_connection_count - current_connections; + + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Initiating {} reverse connections to host {} of remote " + "cluster '{}' from source node '{}'", + needed_connections, host_address, cluster_name, config_.src_node_id); + // Create the required number of connections to this specific host. + for (uint32_t i = 0; i < needed_connections; ++i) { + ENVOY_LOG(debug, "Initiating reverse connection number {} to host {} of cluster {}", i + 1, + host_address, cluster_name); + + bool success = initiateOneReverseConnection(cluster_name, key, host); + + if (success) { + total_successful_connections++; + ENVOY_LOG(debug, + "Successfully initiated reverse connection number {} to host {} of cluster {}", + i + 1, host_address, cluster_name); + } else { + ENVOY_LOG(error, "Failed to initiate reverse connection number {} to host {} of cluster {}", + i + 1, host_address, cluster_name); + } + } + } + // Update metrics based on overall success for the cluster. + if (total_successful_connections > 0) { + ENVOY_LOG(info, + "ReverseConnectionIOHandle: Successfully created {}/{} total reverse connections to " + "cluster {}", + total_successful_connections, total_required_connections, cluster_name); + } else { + ENVOY_LOG(error, + "ReverseConnectionIOHandle: Failed to create any reverse connections to cluster {} - " + "will retry later", + cluster_name); + } +} + +bool ReverseConnectionIOHandle::shouldAttemptConnectionToHost(const std::string& host_address, + const std::string& cluster_name) { + if (!config_.enable_circuit_breaker) { + return true; + } + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + // Create host entry on-demand to avoid race conditions during initialization. + ENVOY_LOG(debug, "Creating HostConnectionInfo on-demand for host {} in cluster {}", + host_address, cluster_name); + host_to_conn_info_map_[host_address] = HostConnectionInfo{ + host_address, + cluster_name, + {}, // connection_keys - empty set initially + 1, // default target_connection_count + 0, // failure_count + getTimeSource().monotonicTime(), // last_failure_time + getTimeSource().monotonicTime(), // backoff_until (no backoff initially) + {} // connection_states - empty map initially + }; + host_it = host_to_conn_info_map_.find(host_address); + } + auto& host_info = host_it->second; + auto now = getTimeSource().monotonicTime(); + ENVOY_LOG(debug, "host: {} now: {} ms backoff_until: {} ms", host_address, + std::chrono::duration_cast(now.time_since_epoch()).count(), + std::chrono::duration_cast( + host_info.backoff_until.time_since_epoch()) + .count()); + // Check if we're still in backoff period. + if (now < host_info.backoff_until) { + auto remaining_ms = + std::chrono::duration_cast(host_info.backoff_until - now) + .count(); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Host {} still in backoff for {}ms", host_address, + remaining_ms); + return false; + } + return true; +} + +void ReverseConnectionIOHandle::trackConnectionFailure(const std::string& host_address, + const std::string& cluster_name) { + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + ENVOY_LOG(debug, "Host {} not found in host_to_conn_info_map_, skipping failure tracking", + host_address); + return; + } + auto& host_info = host_it->second; + host_info.failure_count++; + host_info.last_failure_time = getTimeSource().monotonicTime(); + // Calculate exponential backoff: base_delay * 2^(failure_count - 1) + const uint32_t base_delay_ms = 1000; // 1 second base delay + const uint32_t max_delay_ms = 30000; // 30 seconds max delay + + uint32_t backoff_delay_ms = base_delay_ms * (1 << (host_info.failure_count - 1)); + backoff_delay_ms = std::min(backoff_delay_ms, max_delay_ms); + // Update the backoff until time. This is used in shouldAttemptConnectionToHost() to check if we + // should attempt to connect to the host. + host_info.backoff_until = + host_info.last_failure_time + std::chrono::milliseconds(backoff_delay_ms); + + ENVOY_LOG(debug, "Host {} connection failure #{}, backoff until {}ms from now", host_address, + host_info.failure_count, backoff_delay_ms); + + // Mark host as in backoff state using host+cluster as connection key. For backoff, the connection + // key does not matter since we just need to mark the host and cluster that are in backoff state + // for. + const std::string backoff_connection_key = host_address + "_" + cluster_name + "_backoff"; + updateConnectionState(host_address, cluster_name, backoff_connection_key, + ReverseConnectionState::Backoff); + ENVOY_LOG( + debug, + "ReverseConnectionIOHandle: Marked host {} in cluster {} as Backoff with connection key {}", + host_address, cluster_name, backoff_connection_key); +} + +void ReverseConnectionIOHandle::resetHostBackoff(const std::string& host_address) { + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it == host_to_conn_info_map_.end()) { + ENVOY_LOG(debug, "Host {} not found in host_to_conn_info_map_, skipping backoff reset", + host_address); + return; + } + + auto& host_info = host_it->second; + auto now = getTimeSource().monotonicTime(); + + // Check if the host is actually in backoff before resetting. + if (now >= host_info.backoff_until) { + ENVOY_LOG(debug, "Host {} is not in backoff, skipping reset", host_address); + return; + } + + host_info.failure_count = 0; + host_info.backoff_until = getTimeSource().monotonicTime(); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Reset backoff for host {}", host_address); + + // Mark host as recovered using the same key used by backoff to change the state from backoff to + // recovered. + const std::string recovered_connection_key = + host_address + "_" + host_info.cluster_name + "_backoff"; + updateConnectionState(host_address, host_info.cluster_name, recovered_connection_key, + ReverseConnectionState::Recovered); + ENVOY_LOG( + debug, + "ReverseConnectionIOHandle: Marked host {} in cluster {} as Recovered with connection key {}", + host_address, host_info.cluster_name, recovered_connection_key); +} + +void ReverseConnectionIOHandle::updateConnectionState(const std::string& host_address, + const std::string& cluster_name, + const std::string& connection_key, + ReverseConnectionState new_state) { + // Update connection state in host info and handle old state. + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + // Remove old state if it exists + auto old_state_it = host_it->second.connection_states.find(connection_key); + if (old_state_it != host_it->second.connection_states.end()) { + ReverseConnectionState old_state = old_state_it->second; + // Decrement old state gauge using unified function. + updateStateGauge(host_address, cluster_name, old_state, false /* decrement */); + } + + // Set new state + host_it->second.connection_states[connection_key] = new_state; + } + + // Increment new state gauge using unified function. + updateStateGauge(host_address, cluster_name, new_state, true /* increment */); + + ENVOY_LOG(debug, + "ReverseConnectionIOHandle:Updated connection {} state to {} for host {} in cluster {}", + connection_key, static_cast(new_state), host_address, cluster_name); +} + +void ReverseConnectionIOHandle::removeConnectionState(const std::string& host_address, + const std::string& cluster_name, + const std::string& connection_key) { + // Remove connection state from host info and decrement gauge + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + auto state_it = host_it->second.connection_states.find(connection_key); + if (state_it != host_it->second.connection_states.end()) { + ReverseConnectionState old_state = state_it->second; + // Decrement state gauge using unified function + updateStateGauge(host_address, cluster_name, old_state, false /* decrement */); + // Remove from map + host_it->second.connection_states.erase(state_it); + } + } + + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Removed connection {} state for host {} in cluster {}", + connection_key, host_address, cluster_name); +} + +void ReverseConnectionIOHandle::onDownstreamConnectionClosed(const std::string& connection_key) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Downstream connection closed: {}", connection_key); + + // Find the host for this connection key + std::string host_address; + std::string cluster_name; + + // Search through host_to_conn_info_map_ to find which host this connection belongs to. + for (const auto& [host, host_info] : host_to_conn_info_map_) { + if (host_info.connection_keys.find(connection_key) != host_info.connection_keys.end()) { + host_address = host; + cluster_name = host_info.cluster_name; + break; + } + } + + if (host_address.empty()) { + ENVOY_LOG(warn, "Could not find host for connection key: {}", connection_key); + return; + } + + ENVOY_LOG(debug, "Found connection {} belongs to host {} in cluster {}", connection_key, + host_address, cluster_name); + + // Remove the connection key from the host's connection set. + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + host_it->second.connection_keys.erase(connection_key); + ENVOY_LOG(debug, "Removed connection key {} from host {} (remaining: {})", connection_key, + host_address, host_it->second.connection_keys.size()); + } + + // Remove connection state tracking + removeConnectionState(host_address, cluster_name, connection_key); + + // The next call to maintainClusterConnections() will detect the missing connection + // and re-initiate it automatically. + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Connection closure recorded for host {} in cluster {}. " + "Next maintenance cycle will re-initiate if needed.", + host_address, cluster_name); +} + +void ReverseConnectionIOHandle::updateStateGauge(const std::string& host_address, + const std::string& cluster_name, + ReverseConnectionState state, bool increment) { + // Get extension for stats updates. + auto* extension = getDownstreamExtension(); + if (!extension) { + ENVOY_LOG(debug, "No downstream extension available for state gauge update"); + return; + } + + // Use switch case to determine the state suffix for stat name. + std::string state_suffix; + switch (state) { + case ReverseConnectionState::Connecting: + state_suffix = "connecting"; + break; + case ReverseConnectionState::Connected: + state_suffix = "connected"; + break; + case ReverseConnectionState::Failed: + state_suffix = "failed"; + break; + case ReverseConnectionState::Recovered: + state_suffix = "recovered"; + break; + case ReverseConnectionState::Backoff: + state_suffix = "backoff"; + break; + case ReverseConnectionState::CannotConnect: + state_suffix = "cannot_connect"; + break; + default: + state_suffix = "unknown"; + break; + } + + // Call extension to handle the actual stat update. + extension_->updateConnectionStats(host_address, cluster_name, state_suffix, increment); + + ENVOY_LOG(trace, "{} state gauge for host {} cluster {} state {}", + increment ? "Incremented" : "Decremented", host_address, cluster_name, state_suffix); +} + +void ReverseConnectionIOHandle::maintainReverseConnections() { + // Validate required configuration parameters at the top level + if (config_.src_node_id.empty()) { + ENVOY_LOG(error, "Source node ID is required but empty - cannot maintain reverse connections"); + return; + } + + ENVOY_LOG(debug, "Maintaining reverse tunnels for {} clusters.", config_.remote_clusters.size()); + for (const auto& cluster_config : config_.remote_clusters) { + const std::string& cluster_name = cluster_config.cluster_name; + + ENVOY_LOG(debug, "Processing cluster: {} with {} requested connections per host.", cluster_name, + cluster_config.reverse_connection_count); + // Maintain connections for this cluster + maintainClusterConnections(cluster_name, cluster_config); + } + ENVOY_LOG(debug, "Completed reverse TCP connection maintenance for all clusters."); + + // Enable the retry timer to periodically check for missing connections (like maintainConnCount) + if (rev_conn_retry_timer_) { + // TODO(basundhara-c): Make the retry timeout configurable. + const std::chrono::milliseconds retry_timeout(10000); // 10 seconds + rev_conn_retry_timer_->enableTimer(retry_timeout); + ENVOY_LOG(debug, "Enabled retry timer for next connection check in 10 seconds."); + } +} + +bool ReverseConnectionIOHandle::initiateOneReverseConnection(const std::string& cluster_name, + const std::string& host_address, + Upstream::HostConstSharedPtr host) { + // Generate a temporary connection key for early failure tracking. + const std::string temp_connection_key = + "temp_" + cluster_name + "_" + host_address + "_" + std::to_string(rand()); + + // Only validate host_address here since it's specific to this connection attempt. + if (host_address.empty()) { + ENVOY_LOG(error, "Host address is required but empty"); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Initiating one reverse connection to host {} of cluster " + "'{}', source node '{}'", + host_address, cluster_name, config_.src_node_id); + // Get the thread local cluster with additional validation + auto thread_local_cluster = cluster_manager_.getThreadLocalCluster(cluster_name); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(error, "Cluster '{}' not found in cluster manager", cluster_name); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + // Validate cluster info before attempting connection + auto cluster_info = thread_local_cluster->info(); + if (!cluster_info) { + ENVOY_LOG(error, "Cluster '{}' has null cluster info", cluster_name); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + // Validate priority set to prevent null pointer access + const auto& priority_set = thread_local_cluster->prioritySet(); + const auto& host_sets = priority_set.hostSetsPerPriority(); + size_t host_count = host_sets.empty() ? 0 : host_sets[0]->hosts().size(); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Cluster '{}' found with type {} and {} hosts", + cluster_name, static_cast(cluster_info->type()), host_count); + + // Normalize host key for internal addresses to ensure consistent map lookups. + std::string normalized_host_key = host_address; + if (absl::StartsWith(host_address, "envoy://")) { + normalized_host_key = host_address; // already canonical for internal addresses + } + + // Validate that we have hosts available for internal addresses + if (absl::StartsWith(host_address, "envoy://") && host_count == 0) { + ENVOY_LOG( + error, + "ReverseConnectionIOHandle: No hosts available in cluster '{}' for internal address '{}'", + cluster_name, host_address); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + // Create load balancer context and validate it + ReverseConnectionLoadBalancerContext lb_context(normalized_host_key); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Created load balancer context for host key: {}", + normalized_host_key); + + // Get connection from cluster manager with defensive error handling + Upstream::Host::CreateConnectionData conn_data; + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Creating TCP connection to {} in cluster {} using load " + "balancer context", + host_address, cluster_name); + + // Use tcpConn which should not throw exceptions in normal operation + conn_data = thread_local_cluster->tcpConn(&lb_context); + + if (!conn_data.connection_) { + ENVOY_LOG( + error, + "ReverseConnectionIOHandle: tcpConn() returned null connection for host {} in cluster {}", + host_address, cluster_name); + updateConnectionState(host_address, cluster_name, temp_connection_key, + ReverseConnectionState::CannotConnect); + return false; + } + + // Create wrapper to manage the connection + // The wrapper will initiate and manage the reverse connection handshake using HTTP. + auto wrapper = std::make_unique(*this, std::move(conn_data.connection_), + conn_data.host_description_, cluster_name); + + // Send the reverse connection handshake over the TCP connection. + const std::string connection_key = + wrapper->connect(config_.src_tenant_id, config_.src_cluster_id, config_.src_node_id); + ENVOY_LOG( + debug, + "ReverseConnectionIOHandle: Initiated reverse connection handshake for host {} with key {}", + host_address, connection_key); + + // Mark as Connecting after handshake is initiated. Use the actual connection key so that it can + // be marked as failed in onConnectionDone(). + conn_wrapper_to_host_map_[wrapper.get()] = normalized_host_key; + connection_wrappers_.push_back(std::move(wrapper)); + + { + // Safely log address information without assuming IP is present (internal addresses possible). + const auto& addr = host->address(); + std::string addr_str = addr ? addr->asString() : std::string(""); + absl::optional port_opt; + if (addr && addr->ip() != nullptr) { + port_opt = addr->ip()->port(); + } + if (port_opt.has_value()) { + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Successfully initiated reverse connection to host {} " + "({}:{}) in cluster {}", + host_address, addr_str, *port_opt, cluster_name); + } else { + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Successfully initiated reverse connection to host {} " + "({}) in cluster {}", + host_address, addr_str, cluster_name); + } + } + // Reset backoff for successful connection. + resetHostBackoff(normalized_host_key); + updateConnectionState(normalized_host_key, cluster_name, connection_key, + ReverseConnectionState::Connecting); + return true; +} + +// Trigger pipe used to wake up accept() when a connection is established. +void ReverseConnectionIOHandle::createTriggerPipe() { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Creating trigger pipe for single-byte mechanism"); + int pipe_fds[2]; + if (pipe(pipe_fds) == -1) { + ENVOY_LOG(error, "Failed to create trigger pipe: {}", errorDetails(errno)); + trigger_pipe_read_fd_ = -1; + trigger_pipe_write_fd_ = -1; + return; + } + trigger_pipe_read_fd_ = pipe_fds[0]; + trigger_pipe_write_fd_ = pipe_fds[1]; + // Make both ends non-blocking. + int flags = fcntl(trigger_pipe_write_fd_, F_GETFL, 0); + if (flags != -1) { + fcntl(trigger_pipe_write_fd_, F_SETFL, flags | O_NONBLOCK); + } + flags = fcntl(trigger_pipe_read_fd_, F_GETFL, 0); + if (flags != -1) { + fcntl(trigger_pipe_read_fd_, F_SETFL, flags | O_NONBLOCK); + } + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Created trigger pipe: read_fd={}, write_fd={}", + trigger_pipe_read_fd_, trigger_pipe_write_fd_); +} + +bool ReverseConnectionIOHandle::isTriggerPipeReady() const { + return trigger_pipe_read_fd_ != -1 && trigger_pipe_write_fd_ != -1; +} + +void ReverseConnectionIOHandle::onConnectionDone(const std::string& error, + RCConnectionWrapper* wrapper, bool closed) { + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Connection wrapper done - error: '{}', closed: {}", + error, closed); + + // Validate wrapper pointer before any access. + if (!wrapper) { + ENVOY_LOG(error, "ReverseConnectionIOHandle: Null wrapper pointer in onConnectionDone"); + return; + } + + std::string host_address; + std::string cluster_name; + std::string connection_key; + + // Safely get host address for wrapper. + auto wrapper_it = conn_wrapper_to_host_map_.find(wrapper); + if (wrapper_it == conn_wrapper_to_host_map_.end()) { + ENVOY_LOG(error, "ReverseConnectionIOHandle: Wrapper not found in conn_wrapper_to_host_map_ - " + "may have been cleaned up"); + return; + } + host_address = wrapper_it->second; + + // Safely get cluster name from host info. + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + cluster_name = host_it->second.cluster_name; + } else { + ENVOY_LOG(warn, "ReverseConnectionIOHandle: Host info not found for {}, using fallback", + host_address); + } + + if (cluster_name.empty()) { + ENVOY_LOG(error, + "ReverseConnectionIOHandle: No cluster mapping for host {}, cannot process " + "connection event", + host_address); + // Still try to clean up the wrapper + conn_wrapper_to_host_map_.erase(wrapper); + return; + } + + // Safely get connection info if wrapper is still valid. + auto* connection = wrapper->getConnection(); + if (connection) { + connection_key = connection->connectionInfoProvider().localAddress()->asString(); + ENVOY_LOG(debug, + "ReverseConnectionIOHandle: Processing connection event for host '{}', cluster " + "'{}', key '{}'", + host_address, cluster_name, connection_key); + } else { + connection_key = "cleanup_" + host_address + "_" + std::to_string(rand()); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Connection already null, using fallback key '{}'", + connection_key); + } + + // Get connection pointer for safe access in success/failure handling. + connection = wrapper->getConnection(); + + // Process connection result safely. + bool is_success = (error == "reverse connection accepted" || error == "success" || + error == "handshake successful" || error == "connection established"); + + if (closed || (!error.empty() && !is_success)) { + // Handle connection failure + ENVOY_LOG(error, + "ReverseConnectionIOHandle: Connection failed - error '{}', cleaning up host {}", + error, host_address); + + updateConnectionState(host_address, cluster_name, connection_key, + ReverseConnectionState::Failed); + + // Safely close connection if still valid. + if (connection) { + if (connection->getSocket()) { + connection->getSocket()->ioHandle().resetFileEvents(); + } + connection->close(Network::ConnectionCloseType::NoFlush); + } + + trackConnectionFailure(host_address, cluster_name); + + } else { + // Handle connection success + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Connection succeeded for host {}", host_address); + + resetHostBackoff(host_address); + updateConnectionState(host_address, cluster_name, connection_key, + ReverseConnectionState::Connected); + + // Only proceed if connection is still valid. + if (!connection) { + ENVOY_LOG( + error, + "ReverseConnectionIOHandle: Cannot complete successful handshake - connection is null"); + return; + } + + ENVOY_LOG(info, "ReverseConnectionIOHandle: Transferring tunnel socket for " + "reverse_conn_listener consumption"); + + // Reset file events safely. + if (connection->getSocket()) { + ENVOY_LOG( + debug, + "ReverseConnectionIOHandle: Removing connection callbacks and resetting file events"); + connection->removeConnectionCallbacks(*wrapper); + connection->getSocket()->ioHandle().resetFileEvents(); + } + + // Update host connection tracking safely. + auto host_it = host_to_conn_info_map_.find(host_address); + if (host_it != host_to_conn_info_map_.end()) { + host_it->second.connection_keys.insert(connection_key); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Added connection key {} for host {}", + connection_key, host_address); + } + + Network::ClientConnectionPtr released_conn = wrapper->releaseConnection(); + + if (released_conn) { + ENVOY_LOG(info, "ReverseConnectionIOHandle: Connection will be consumed by " + "reverse_conn_listener for HTTP processing"); + + // Move connection to established queue for reverse_conn_listener to consume. + established_connections_.push(std::move(released_conn)); + + // Trigger accept mechanism safely. + if (isTriggerPipeReady()) { + char trigger_byte = 1; + ssize_t bytes_written = ::write(trigger_pipe_write_fd_, &trigger_byte, 1); + if (bytes_written == 1) { + ENVOY_LOG(info, + "ReverseConnectionIOHandle: Successfully triggered reverse_conn_listener " + "accept() for host {}", + host_address); + } else { + ENVOY_LOG(error, "ReverseConnectionIOHandle: Failed to write trigger byte: {}", + errorDetails(errno)); + } + } + } + } + + // Safely remove wrapper from tracking. + conn_wrapper_to_host_map_.erase(wrapper); + + // Find and remove wrapper from vector safely. + auto wrapper_vector_it = std::find_if( + connection_wrappers_.begin(), connection_wrappers_.end(), + [wrapper](const std::unique_ptr& w) { return w.get() == wrapper; }); + + if (wrapper_vector_it != connection_wrappers_.end()) { + auto wrapper_to_delete = std::move(*wrapper_vector_it); + connection_wrappers_.erase(wrapper_vector_it); + + // Use deferred deletion to prevent crash during cleanup. + std::unique_ptr deletable_wrapper( + static_cast(wrapper_to_delete.release())); + getThreadLocalDispatcher().deferredDelete(std::move(deletable_wrapper)); + ENVOY_LOG(debug, "ReverseConnectionIOHandle: Deferred delete of connection wrapper"); + } +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h new file mode 100644 index 0000000000000..48adcd3de5171 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h @@ -0,0 +1,431 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/network/io_handle.h" +#include "envoy/network/socket.h" +#include "envoy/stats/scope.h" +#include "envoy/thread_local/thread_local.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/common/logger.h" +#include "source/common/network/filter_impl.h" +#include "source/common/network/io_socket_handle_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/common/upstream/load_balancer_context_base.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_load_balancer_context.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/synchronization/mutex.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations. +class ReverseTunnelInitiatorExtension; +class ReverseConnectionIOHandle; + +namespace { +// HTTP protocol constants. +static constexpr absl::string_view kCrlf = "\r\n"; +static constexpr absl::string_view kDoubleCrlf = "\r\n\r\n"; + +// Connection timing constants. +static constexpr uint32_t kDefaultMaxReconnectAttempts = 10; +} // namespace + +/** + * Connection state tracking for reverse connections. + */ +enum class ReverseConnectionState { + Connecting, // Connection is being established (handshake initiated). + Connected, // Connection has been successfully established. + Recovered, // Connection has recovered from a previous failure. + Failed, // Connection establishment failed during handshake. + CannotConnect, // Connection cannot be initiated (early failure). + Backoff // Connection is in backoff state due to failures. +}; + +/** + * Configuration for remote cluster connections. + * Defines connection parameters for each remote cluster that reverse connections should be + * established to. + */ +struct RemoteClusterConnectionConfig { + std::string cluster_name; // Name of the remote cluster. + uint32_t reverse_connection_count; // Number of reverse connections to maintain per host. + // TODO(basundhara-c): Implement retry logic using max_reconnect_attempts for connections to this + // cluster. This is the max reconnection attempts made for a cluster when the initial reverse + // connection attempt fails. + uint32_t max_reconnect_attempts; // Maximum number of reconnection attempts. + + RemoteClusterConnectionConfig(const std::string& name, uint32_t count, + uint32_t max_attempts = kDefaultMaxReconnectAttempts) + : cluster_name(name), reverse_connection_count(count), max_reconnect_attempts(max_attempts) {} +}; + +/** + * Configuration for reverse connection socket interface. + */ +struct ReverseConnectionSocketConfig { + std::string src_cluster_id; // Cluster identifier of local envoy instance. + std::string src_node_id; // Node identifier of local envoy instance. + std::string src_tenant_id; // Tenant identifier of local envoy instance. + // TODO(basundhara-c): Add support for multiple remote clusters using the same + // ReverseConnectionIOHandle. Currently, each ReverseConnectionIOHandle handles + // reverse connections for a single upstream cluster since a different ReverseConnectionAddress + // is created for different upstream clusters. Eventually, we should embed metadata for + // multiple remote clusters in the same ReverseConnectionAddress and therefore should be able + // to use a single ReverseConnectionIOHandle for multiple remote clusters. + std::vector + remote_clusters; // List of remote cluster configurations. + bool enable_circuit_breaker; // Whether to place a cluster in backoff when reverse connection + // attempts fail. + ReverseConnectionSocketConfig() : enable_circuit_breaker(true) {} +}; + +/** + * This class handles the lifecycle of reverse connections, including establishment, + * maintenance, and cleanup of connections to remote clusters. + * At this point, a ReverseConnectionIOHandle is created for each upstream cluster. + * This is because a different ReverseConnectionAddress is created for each upstream cluster. + * This ReverseConnectionIOHandle initiates TCP connections to each host of the upstream cluster, + * and caches the IOHandle for serving requests coming from the upstream cluster. + */ +class ReverseConnectionIOHandle : public Network::IoSocketHandleImpl, + public Network::ConnectionCallbacks { + // Define friend classes for testing. + friend class ReverseConnectionIOHandleTest; + friend class RCConnectionWrapperTest; + friend class DownstreamReverseConnectionIOHandleTest; + +public: + /** + * Constructor for ReverseConnectionIOHandle. + * @param fd the file descriptor for listener socket. + * @param config the configuration for reverse connections. + * @param cluster_manager the cluster manager for accessing upstream clusters. + * @param extension the extension for stats updates. + * @param scope the stats scope for metrics collection. + */ + ReverseConnectionIOHandle(os_fd_t fd, const ReverseConnectionSocketConfig& config, + Upstream::ClusterManager& cluster_manager, + ReverseTunnelInitiatorExtension* extension, Stats::Scope& scope); + + ~ReverseConnectionIOHandle() override; + + // Network::IoHandle overrides. + /** + * Override of listen method for reverse connections. + * No-op for reverse connections. + * @param backlog the listen backlog. + * @return SysCallIntResult with success status. + */ + Api::SysCallIntResult listen(int backlog) override; + + /** + * Override of accept method for reverse connections. + * Returns established reverse connections when they become available. This is woken up using the + * trigger pipe when a tcp connection to an upstream cluster is established. + * @param addr pointer to store the client address information. + * @param addrlen pointer to the length of the address structure. + * @return IoHandlePtr for the accepted reverse connection, or nullptr if none available. + */ + Network::IoHandlePtr accept(struct sockaddr* addr, socklen_t* addrlen) override; + + /** + * Override of read method for reverse connections. + * @param buffer the buffer to read data into. + * @param max_length optional maximum number of bytes to read. + * @return IoCallUint64Result indicating the result of the read operation. + */ + Api::IoCallUint64Result read(Buffer::Instance& buffer, + absl::optional max_length) override; + + /** + * Override of write method for reverse connections. + * @param buffer the buffer containing data to write. + * @return IoCallUint64Result indicating the result of the write operation. + */ + Api::IoCallUint64Result write(Buffer::Instance& buffer) override; + + /** + * Override of connect method for reverse connections. + * For reverse connections, this is not used since we connect to the upstream clusters in + * initializeFileEvent(). + * @param address the target address (unused for reverse connections). + * @return SysCallIntResult with success status. + */ + Api::SysCallIntResult connect(Network::Address::InstanceConstSharedPtr address) override; + + /** + * Override of close method for reverse connections. + * @return IoCallUint64Result indicating the result of the close operation. + */ + Api::IoCallUint64Result close() override; + + /** + * Triggers the reverse connection workflow. + * @param dispatcher the event dispatcher. + * @param cb the file ready callback. + * @param trigger the file trigger type. + * @param events the events to monitor. + */ + void initializeFileEvent(Event::Dispatcher& dispatcher, Event::FileReadyCb cb, + Event::FileTriggerType trigger, uint32_t events) override; + + // Network::ConnectionCallbacks. + /** + * Called when connection events occur. + * For reverse connections, we handle these events through RCConnectionWrapper. + * @param event the connection event that occurred. + */ + void onEvent(Network::ConnectionEvent event) override; + + /** + * No-op for reverse connections. + */ + void onAboveWriteBufferHighWatermark() override {} + + /** + * No-op for reverse connections. + */ + void onBelowWriteBufferLowWatermark() override {} + + /** + * Get the file descriptor for the pipe monitor used to wake up accept(). + * @return the file descriptor for the pipe monitor + */ + int getPipeMonitorFd() const; + + // Callbacks from RCConnectionWrapper. + /** + * Called when a reverse connection handshake completes. This method wakes up accept() if the + * reverse connection handshake was successful. If not, it performs necessary cleanup and triggers + * backoff for the host. + * @param error error message if the handshake failed, empty string if successful. + * @param wrapper pointer to the connection wrapper that wraps over the established connection. + * @param closed whether the connection was closed during handshake. + */ + void onConnectionDone(const std::string& error, RCConnectionWrapper* wrapper, bool closed); + + // Backoff logic for connection failures. + /** + * Determine if connections should be initiated to a host, i.e., if host is in backoff period. + * @param host_address the address of the host to check. + * @param cluster_name the name of the cluster the host belongs to. + * @return true if connection attempt should be made, false if in backoff. + */ + bool shouldAttemptConnectionToHost(const std::string& host_address, + const std::string& cluster_name); + + /** + * Track a connection failure for a specific host and cluster and trigger backoff logic. + * @param host_address the address of the host that failed. + * @param cluster_name the name of the cluster the host belongs to. + */ + void trackConnectionFailure(const std::string& host_address, const std::string& cluster_name); + + /** + * Reset backoff state for a specific host. Called when a connection is established successfully. + * @param host_address the address of the host to reset backoff for. + */ + void resetHostBackoff(const std::string& host_address); + + /** + * Update the connection state for a specific connection and update metrics. + * @param host_address the address of the host. + * @param cluster_name the name of the cluster. + * @param connection_key the unique key identifying the connection. + * @param new_state the new state to set for the connection. + */ + void updateConnectionState(const std::string& host_address, const std::string& cluster_name, + const std::string& connection_key, ReverseConnectionState new_state); + + /** + * Update state-specific gauge using switch case logic (combined increment/decrement). + * @param host_address the address of the host + * @param cluster_name the name of the cluster + * @param state the connection state to update + * @param increment whether to increment (true) or decrement (false) the gauge + */ + void updateStateGauge(const std::string& host_address, const std::string& cluster_name, + ReverseConnectionState state, bool increment); + + /** + * Remove connection state tracking for a specific connection. + * @param host_address the address of the host. + * @param cluster_name the name of the cluster. + * @param connection_key the unique key identifying the connection. + */ + void removeConnectionState(const std::string& host_address, const std::string& cluster_name, + const std::string& connection_key); + + /** + * Handle downstream connection closure and update internal maps so that the next + * maintenance cycle re-initiates the connection. + * @param connection_key the unique key identifying the closed connection. + */ + void onDownstreamConnectionClosed(const std::string& connection_key); + + /** + * Get reference to the cluster manager. + * @return reference to the cluster manager + */ + Upstream::ClusterManager& getClusterManager() { return cluster_manager_; } + + /** + * Get pointer to the downstream extension for stats updates. + * @return pointer to the extension, nullptr if not available + */ + ReverseTunnelInitiatorExtension* getDownstreamExtension() const; + +private: + /** + * Get time source for consistent time operations. + * @return reference to the time source + */ + TimeSource& getTimeSource() const; + + /** + * @return reference to the thread-local dispatcher + */ + Event::Dispatcher& getThreadLocalDispatcher() const; + + /** + * Check if thread-local dispatcher is available. + * @return true if dispatcher is available and safe to use + */ + bool isThreadLocalDispatcherAvailable() const; + + /** + * Create the trigger mechanism used to wake up accept() when connections are established. + */ + void createTriggerMechanism(); + + // Functions to maintain connections to remote clusters. + /** + * Maintain reverse connections for all configured clusters. + * Initiates and maintains the required number of connections to each remote cluster. + */ + void maintainReverseConnections(); + + /** + * Maintain reverse connections for a specific cluster. + * @param cluster_name the name of the cluster to maintain connections for + * @param cluster_config the configuration for the cluster + */ + void maintainClusterConnections(const std::string& cluster_name, + const RemoteClusterConnectionConfig& cluster_config); + + /** + * Initiate a single reverse connection to a specific host. + * @param cluster_name the name of the cluster the host belongs to + * @param host_address the address of the host to connect to + * @param host the host object containing connection information + * @return true if connection initiation was successful, false otherwise + */ + bool initiateOneReverseConnection(const std::string& cluster_name, + const std::string& host_address, + Upstream::HostConstSharedPtr host); + + /** + * Clean up all reverse connection resources. + * Called during shutdown to properly close connections and free resources. + */ + void cleanup(); + + // Pipe trigger mechanism helpers + /** + * Create trigger pipe used to wake up accept() when a connection is established. + */ + void createTriggerPipe(); + + /** + * Check if trigger pipe is ready for use. + * @return true if initialized and ready + */ + bool isTriggerPipeReady() const; + + // Host/cluster mapping management + /** + * Update cluster -> host mappings from the cluster manager. Called before connection initiation + * to a cluster. + * @param cluster_id the ID of the cluster + * @param hosts the list of hosts in the cluster + */ + void maybeUpdateHostsMappingsAndConnections(const std::string& cluster_id, + const std::vector& hosts); + + /** + * Remove stale host entries and close associated connections. + * @param host the address of the host to remove + */ + void removeStaleHostAndCloseConnections(const std::string& host); + + /** + * Per-host connection tracking for better management. + * Contains all information needed to track and manage connections to a specific host. + */ + struct HostConnectionInfo { + std::string host_address; // Host address + std::string cluster_name; // Cluster to which host belongs + absl::flat_hash_set connection_keys; // Connection keys for stats tracking + uint32_t target_connection_count; // Target connection count for the host + uint32_t failure_count{0}; // Number of consecutive failures + std::chrono::steady_clock::time_point last_failure_time; // NO_CHECK_FORMAT(real_time) + std::chrono::steady_clock::time_point backoff_until; // NO_CHECK_FORMAT(real_time) + absl::flat_hash_map + connection_states; // State tracking per connection + }; + + // Map from host address to connection info. + absl::flat_hash_map host_to_conn_info_map_; + // Map from cluster name to set of resolved hosts + absl::flat_hash_map> cluster_to_resolved_hosts_map_; + + // Core components + const ReverseConnectionSocketConfig config_; // Configuration for reverse connections + Upstream::ClusterManager& cluster_manager_; + ReverseTunnelInitiatorExtension* extension_; + + // Connection wrapper management + std::vector> + connection_wrappers_; // Active connection wrappers + // Mapping from wrapper to host. This designates the number of successful connections to a host. + absl::flat_hash_map conn_wrapper_to_host_map_; + + // Simple pipe-based trigger mechanism to wake up accept() when a connection is established. + // Inlined directly for simplicity and reduced test coverage requirements. + int trigger_pipe_read_fd_{-1}; + int trigger_pipe_write_fd_{-1}; + + // Connection management : We store the established connections in a queue + // and pop the last established connection when data is read on trigger_pipe_read_fd_ + // to determine the connection that got established last. + std::queue established_connections_; + + // Single retry timer for all clusters + Event::TimerPtr rev_conn_retry_timer_; + + bool is_reverse_conn_started_{ + false}; // Whether reverse connections have been started on worker thread + Event::Dispatcher* worker_dispatcher_{nullptr}; // Dispatcher for the worker thread + + // Store original socket FD for cleanup. + os_fd_t original_socket_fd_{-1}; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_load_balancer_context.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_load_balancer_context.h new file mode 100644 index 0000000000000..13dde91c11cc0 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_load_balancer_context.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include "envoy/upstream/load_balancer.h" + +#include "source/common/upstream/load_balancer_context_base.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Load balancer context for reverse connections. + * This context is used to select specific upstream hosts by address. + */ +class ReverseConnectionLoadBalancerContext : public Upstream::LoadBalancerContextBase { +public: + /** + * Constructor that sets the host to select. + * @param host_address the address of the host to select + */ + explicit ReverseConnectionLoadBalancerContext(const std::string& host_address) + : host_string_(host_address), host_to_select_(host_string_, false) {} + + // Upstream::LoadBalancerContext overrides + absl::optional overrideHostToSelect() const override { + return absl::make_optional(host_to_select_); + } + +private: + // Own the string data. This is to prevent use after free when the host_to_select + // is destroyed. + std::string host_string_; + OverrideHost host_to_select_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.cc new file mode 100644 index 0000000000000..19bd639af0c21 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.cc @@ -0,0 +1,109 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +absl::StatusOr +ReverseConnectionResolver::resolve(const envoy::config::core::v3::SocketAddress& socket_address) { + + // Check if address starts with rc:// + // Expected format: "rc://src_node_id:src_cluster_id:src_tenant_id@cluster_name:count" + const std::string& address_str = socket_address.address(); + if (!absl::StartsWith(address_str, "rc://")) { + return absl::InvalidArgumentError(fmt::format( + "Address must start with 'rc://' for reverse connection resolver. " + "Expected format: rc://src_node_id:src_cluster_id:src_tenant_id@cluster_name:count")); + } + + // For reverse connections, only port 0 is supported. + if (socket_address.port_value() != 0) { + return absl::InvalidArgumentError( + fmt::format("Only port 0 is supported for reverse connections. Got port: {}", + socket_address.port_value())); + } + + // Extract reverse connection config + auto reverse_conn_config_or_error = extractReverseConnectionConfig(socket_address); + if (!reverse_conn_config_or_error.ok()) { + return reverse_conn_config_or_error.status(); + } + + // Create and return ReverseConnectionAddress + auto reverse_conn_address = + std::make_shared(reverse_conn_config_or_error.value()); + + return reverse_conn_address; +} + +absl::StatusOr +ReverseConnectionResolver::extractReverseConnectionConfig( + const envoy::config::core::v3::SocketAddress& socket_address) { + + const std::string& address_str = socket_address.address(); + + // Parse the reverse connection URL format + std::string config_part = address_str.substr(5); // Remove "rc://" prefix + + // Split by '@' to separate source info from cluster config + std::vector parts = absl::StrSplit(config_part, '@'); + if (parts.size() != 2) { + return absl::InvalidArgumentError( + "Invalid reverse connection address format. Expected: " + "rc://src_node_id:src_cluster_id:src_tenant_id@cluster_name:count"); + } + + // Parse source info (node_id:cluster_id:tenant_id) + std::vector source_parts = absl::StrSplit(parts[0], ':'); + if (source_parts.size() != 3) { + return absl::InvalidArgumentError( + "Invalid source info format. Expected: src_node_id:src_cluster_id:src_tenant_id"); + } + + // Validate that node_id and cluster_id are not empty. + if (source_parts[0].empty()) { + return absl::InvalidArgumentError("Source node ID cannot be empty"); + } + if (source_parts[1].empty()) { + return absl::InvalidArgumentError("Source cluster ID cannot be empty"); + } + + // Parse cluster configuration (cluster_name:count) + std::vector cluster_parts = absl::StrSplit(parts[1], ':'); + if (cluster_parts.size() != 2) { + return absl::InvalidArgumentError( + fmt::format("Invalid cluster config format: {}. Expected: cluster_name:count", parts[1])); + } + + uint32_t count; + if (!absl::SimpleAtoi(cluster_parts[1], &count)) { + return absl::InvalidArgumentError( + fmt::format("Invalid connection count: {}", cluster_parts[1])); + } + + // Create the config struct + ReverseConnectionAddress::ReverseConnectionConfig config; + config.src_node_id = source_parts[0]; + config.src_cluster_id = source_parts[1]; + config.src_tenant_id = source_parts[2]; + config.remote_cluster = cluster_parts[0]; + config.connection_count = count; + + ENVOY_LOG( + debug, + "reverse connection config: node_id={}, cluster_id={}, tenant_id={}, remote_cluster={}, " + "count={}", + config.src_node_id, config.src_cluster_id, config.src_tenant_id, config.remote_cluster, + config.connection_count); + + return config; +} + +// Register the factory +REGISTER_FACTORY(ReverseConnectionResolver, Network::Address::Resolver); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.h new file mode 100644 index 0000000000000..6c478c16d33c9 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.h @@ -0,0 +1,46 @@ +#pragma once + +#include "envoy/network/resolver.h" +#include "envoy/registry/registry.h" + +#include "source/common/common/logger.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Custom address resolver that can create ReverseConnectionAddress instances + * when reverse connection metadata is detected in the socket address. + */ +class ReverseConnectionResolver : public Network::Address::Resolver, + public Envoy::Logger::Loggable { +public: + ReverseConnectionResolver() = default; + + // Network::Address::Resolver + absl::StatusOr + resolve(const envoy::config::core::v3::SocketAddress& socket_address) override; + + std::string name() const override { return "envoy.resolvers.reverse_connection"; } + + // Friend class for testing + friend class ReverseConnectionResolverTest; + +private: + /** + * Extracts reverse connection config from socket address metadata. + * Expected format: "rc://src_node_id:src_cluster_id:src_tenant_id@cluster1:count1" + */ + absl::StatusOr + extractReverseConnectionConfig(const envoy::config::core::v3::SocketAddress& socket_address); +}; + +DECLARE_FACTORY(ReverseConnectionResolver); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.cc new file mode 100644 index 0000000000000..f3d4d26c4871a --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.cc @@ -0,0 +1,183 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" + +#include +#include +#include + +#include "envoy/network/address.h" +#include "envoy/registry/registry.h" + +#include "source/common/common/logger.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/socket_interface_impl.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// ReverseTunnelInitiator implementation +ReverseTunnelInitiator::ReverseTunnelInitiator(Server::Configuration::ServerFactoryContext& context) + : extension_(nullptr), context_(&context) { + ENVOY_LOG(debug, "Created ReverseTunnelInitiator."); +} + +DownstreamSocketThreadLocal* ReverseTunnelInitiator::getLocalRegistry() const { + if (!extension_ || !extension_->getLocalRegistry()) { + return nullptr; + } + return extension_->getLocalRegistry(); +} + +Envoy::Network::IoHandlePtr +ReverseTunnelInitiator::socket(Envoy::Network::Socket::Type socket_type, + Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, bool, + const Envoy::Network::SocketCreationOptions&) const { + ENVOY_LOG(debug, "ReverseTunnelInitiator: type={}, addr_type={}", static_cast(socket_type), + static_cast(addr_type)); + + // This method is called without reverse connection config, so create a regular socket. + int domain; + if (addr_type == Envoy::Network::Address::Type::Ip) { + domain = (version == Envoy::Network::Address::IpVersion::v4) ? AF_INET : AF_INET6; + } else { + // For pipe addresses. + domain = AF_UNIX; + } + int sock_type = (socket_type == Envoy::Network::Socket::Type::Stream) ? SOCK_STREAM : SOCK_DGRAM; + int sock_fd = ::socket(domain, sock_type, 0); + if (sock_fd == -1) { + ENVOY_LOG(error, "Failed to create fallback socket: {}", errorDetails(errno)); + return nullptr; + } + return std::make_unique(sock_fd); +} + +/** + * Thread-safe helper method to create reverse connection socket with config. + */ +Envoy::Network::IoHandlePtr ReverseTunnelInitiator::createReverseConnectionSocket( + Envoy::Network::Socket::Type socket_type, Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, const ReverseConnectionSocketConfig& config) const { + + // Return early if no remote clusters are configured + if (config.remote_clusters.empty()) { + ENVOY_LOG(debug, "ReverseTunnelInitiator: No remote clusters configured, returning nullptr"); + return nullptr; + } + + ENVOY_LOG(debug, "ReverseTunnelInitiator: Creating reverse connection socket for cluster: {}", + config.remote_clusters[0].cluster_name); + + // For stream sockets on IP addresses, create our reverse connection IOHandle. + if (socket_type == Envoy::Network::Socket::Type::Stream && + addr_type == Envoy::Network::Address::Type::Ip) { + // Create socket file descriptor using system calls. + int domain = (version == Envoy::Network::Address::IpVersion::v4) ? AF_INET : AF_INET6; + int sock_fd = ::socket(domain, SOCK_STREAM, 0); + if (sock_fd == -1) { + ENVOY_LOG(error, "Failed to create socket: {}", errorDetails(errno)); + return nullptr; + } + + ENVOY_LOG( + debug, + "ReverseTunnelInitiator: Created socket fd={}, wrapping with ReverseConnectionIOHandle", + sock_fd); + + // Get the scope from thread local registry, fallback to context scope + Stats::Scope* scope_ptr = &context_->scope(); + auto* tls_registry = getLocalRegistry(); + if (tls_registry) { + scope_ptr = &tls_registry->scope(); + } + + // Create ReverseConnectionIOHandle with cluster manager from context and scope. + return std::make_unique(sock_fd, config, context_->clusterManager(), + extension_, *scope_ptr); + } + + // Fall back to regular socket for non-stream or non-IP sockets. + return socket(socket_type, addr_type, version, false, Envoy::Network::SocketCreationOptions{}); +} + +Envoy::Network::IoHandlePtr +ReverseTunnelInitiator::socket(Envoy::Network::Socket::Type socket_type, + const Envoy::Network::Address::InstanceConstSharedPtr addr, + const Envoy::Network::SocketCreationOptions& options) const { + + // Extract reverse connection configuration from address. + const auto* reverse_addr = dynamic_cast(addr.get()); + if (reverse_addr) { + // Get the reverse connection config from the address. + ENVOY_LOG(debug, "ReverseTunnelInitiator: reverse_addr: {}", reverse_addr->asString()); + const auto& config = reverse_addr->reverseConnectionConfig(); + + // Convert ReverseConnectionAddress::ReverseConnectionConfig to ReverseConnectionSocketConfig. + ReverseConnectionSocketConfig socket_config; + socket_config.src_node_id = config.src_node_id; + socket_config.src_cluster_id = config.src_cluster_id; + socket_config.src_tenant_id = config.src_tenant_id; + + // Add the remote cluster configuration. + RemoteClusterConnectionConfig cluster_config(config.remote_cluster, config.connection_count); + socket_config.remote_clusters.push_back(cluster_config); + + // Pass config directly to helper method. + return createReverseConnectionSocket( + socket_type, addr->type(), + addr->ip() ? addr->ip()->version() : Envoy::Network::Address::IpVersion::v4, socket_config); + } + + // Delegate to the other socket() method for non-reverse-connection addresses. + return socket(socket_type, addr->type(), + addr->ip() ? addr->ip()->version() : Envoy::Network::Address::IpVersion::v4, false, + options); +} + +bool ReverseTunnelInitiator::ipFamilySupported(int domain) { + return domain == AF_INET || domain == AF_INET6; +} + +Server::BootstrapExtensionPtr ReverseTunnelInitiator::createBootstrapExtension( + const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& context) { + ENVOY_LOG(debug, "ReverseTunnelInitiator::createBootstrapExtension()"); + const auto& message = MessageUtil::downcastAndValidate< + const envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface&>(config, context.messageValidationVisitor()); + context_ = &context; + // Create the bootstrap extension and store reference to it. + auto extension = std::make_unique(context, message); + extension_ = extension.get(); + return extension; +} + +ProtobufTypes::MessagePtr ReverseTunnelInitiator::createEmptyConfigProto() { + return std::make_unique< + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface>(); +} + +// ReverseTunnelInitiatorExtension constructor implementation. +ReverseTunnelInitiatorExtension::ReverseTunnelInitiatorExtension( + Server::Configuration::ServerFactoryContext& context, + const envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface& config) + : context_(context), config_(config), + stat_prefix_(config.stat_prefix().empty() ? "reverse_connections" : config.stat_prefix()) { + ENVOY_LOG(debug, + "Created ReverseTunnelInitiatorExtension - TLS slot will be created in " + "onWorkerThreadInitialized with stat_prefix: {}", + stat_prefix_); +} + +REGISTER_FACTORY(ReverseTunnelInitiator, Server::Configuration::BootstrapExtensionFactory); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h new file mode 100644 index 0000000000000..5506e3659e49e --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include + +#include "envoy/network/socket.h" +#include "envoy/registry/registry.h" +#include "envoy/server/bootstrap_extension_config.h" + +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations +struct ReverseConnectionSocketConfig; + +/** + * Socket interface that creates reverse connection sockets. + * This class implements the SocketInterface interface to provide reverse connection + * functionality for downstream connections. + */ +class ReverseTunnelInitiator : public Envoy::Network::SocketInterfaceBase, + public Envoy::Logger::Loggable { + // Friend class for testing + friend class ReverseTunnelInitiatorTest; + +public: + ReverseTunnelInitiator(Server::Configuration::ServerFactoryContext& context); + + // Default constructor for registry + ReverseTunnelInitiator() : extension_(nullptr), context_(nullptr) {} + + /** + * Create a ReverseConnectionIOHandle and kick off the reverse connection establishment. + * @param socket_type the type of socket to create + * @param addr_type the address type + * @param version the IP version + * @param socket_v6only whether to create IPv6-only socket + * @param options socket creation options + * @return IoHandlePtr for the created socket, or nullptr for unsupported types + */ + Envoy::Network::IoHandlePtr + socket(Envoy::Network::Socket::Type socket_type, Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, bool socket_v6only, + const Envoy::Network::SocketCreationOptions& options) const override; + + // No-op for reverse connections. + Envoy::Network::IoHandlePtr + socket(Envoy::Network::Socket::Type socket_type, + const Envoy::Network::Address::InstanceConstSharedPtr addr, + const Envoy::Network::SocketCreationOptions& options) const override; + + /** + * @return true if the IP family is supported + */ + bool ipFamilySupported(int domain) override; + + /** + * @return pointer to the thread-local registry, or nullptr if not available. + */ + DownstreamSocketThreadLocal* getLocalRegistry() const; + + /** + * Thread-safe helper method to create reverse connection socket with config. + * @param socket_type the type of socket to create + * @param addr_type the address type + * @param version the IP version + * @param config the reverse connection configuration + * @return IoHandlePtr for the reverse connection socket + */ + Envoy::Network::IoHandlePtr + createReverseConnectionSocket(Envoy::Network::Socket::Type socket_type, + Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, + const ReverseConnectionSocketConfig& config) const; + + /** + * Get the extension instance for accessing cross-thread aggregation capabilities. + * @return pointer to the extension, or nullptr if not available + */ + ReverseTunnelInitiatorExtension* getExtension() const { return extension_; } + + // BootstrapExtensionFactory implementation + Server::BootstrapExtensionPtr + createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() const override { + return "envoy.bootstrap.reverse_tunnel.downstream_socket_interface"; + } + + ReverseTunnelInitiatorExtension* extension_; + +private: + Server::Configuration::ServerFactoryContext* context_; +}; + +DECLARE_FACTORY(ReverseTunnelInitiator); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.cc b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.cc new file mode 100644 index 0000000000000..05288d9d19d54 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.cc @@ -0,0 +1,277 @@ +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +#include "envoy/event/dispatcher.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/common/logger.h" +#include "source/common/stats/symbol_table.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// ReverseTunnelInitiatorExtension implementation +void ReverseTunnelInitiatorExtension::onServerInitialized() { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension::onServerInitialized"); +} + +void ReverseTunnelInitiatorExtension::onWorkerThreadInitialized() { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension: creating thread local slot"); + + // Create thread local slot on worker thread initialization. + tls_slot_ = + ThreadLocal::TypedSlot::makeUnique(context_.threadLocal()); + + // Set up the thread local dispatcher for each worker thread. + tls_slot_->set([this](Event::Dispatcher& dispatcher) { + return std::make_shared(dispatcher, context_.scope()); + }); + + ENVOY_LOG( + debug, + "ReverseTunnelInitiatorExtension: thread local slot created successfully in worker thread"); +} + +DownstreamSocketThreadLocal* ReverseTunnelInitiatorExtension::getLocalRegistry() const { + if (!tls_slot_) { + ENVOY_LOG(error, "ReverseTunnelInitiatorExtension: no thread local slot"); + return nullptr; + } + + if (auto opt = tls_slot_->get(); opt.has_value()) { + return &opt.value().get(); + } + + return nullptr; +} + +void ReverseTunnelInitiatorExtension::updateConnectionStats(const std::string& host_address, + const std::string& cluster_id, + const std::string& state_suffix, + bool increment) { + // Register stats with Envoy's system for automatic cross-thread aggregation. + auto& stats_store = context_.scope(); + + // Create/update host connection stat with state suffix + if (!host_address.empty() && !state_suffix.empty()) { + std::string host_stat_name = + fmt::format("{}.host.{}.{}", stat_prefix_, host_address, state_suffix); + Stats::StatNameManagedStorage host_stat_name_storage(host_stat_name, stats_store.symbolTable()); + auto& host_gauge = stats_store.gaugeFromStatName(host_stat_name_storage.statName(), + Stats::Gauge::ImportMode::Accumulate); + if (increment) { + host_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: incremented host stat {} to {}", + host_stat_name, host_gauge.value()); + } else { + host_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: decremented host stat {} to {}", + host_stat_name, host_gauge.value()); + } + } + + // Create/update cluster connection stat with state suffix. + if (!cluster_id.empty() && !state_suffix.empty()) { + std::string cluster_stat_name = + fmt::format("{}.cluster.{}.{}", stat_prefix_, cluster_id, state_suffix); + Stats::StatNameManagedStorage cluster_stat_name_storage(cluster_stat_name, + stats_store.symbolTable()); + auto& cluster_gauge = stats_store.gaugeFromStatName(cluster_stat_name_storage.statName(), + Stats::Gauge::ImportMode::Accumulate); + if (increment) { + cluster_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: incremented cluster stat {} to {}", + cluster_stat_name, cluster_gauge.value()); + } else { + cluster_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: decremented cluster stat {} to {}", + cluster_stat_name, cluster_gauge.value()); + } + } + + // Also update per-worker stats for debugging. + updatePerWorkerConnectionStats(host_address, cluster_id, state_suffix, increment); +} + +void ReverseTunnelInitiatorExtension::updatePerWorkerConnectionStats( + const std::string& host_address, const std::string& cluster_id, const std::string& state_suffix, + bool increment) { + auto& stats_store = context_.scope(); + + // Get dispatcher name from the thread local dispatcher. + std::string dispatcher_name; + auto* local_registry = getLocalRegistry(); + if (local_registry == nullptr) { + ENVOY_LOG(error, "ReverseTunnelInitiatorExtension: No local registry found"); + return; + } + // Dispatcher name is of the form "worker_x" where x is the worker index + dispatcher_name = local_registry->dispatcher().name(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: Updating stats for worker {}", + dispatcher_name); + + // Create/update per-worker host connection stat. + if (!host_address.empty() && !state_suffix.empty()) { + std::string worker_host_stat_name = + fmt::format("{}.{}.host.{}.{}", stat_prefix_, dispatcher_name, host_address, state_suffix); + Stats::StatNameManagedStorage worker_host_stat_name_storage(worker_host_stat_name, + stats_store.symbolTable()); + auto& worker_host_gauge = stats_store.gaugeFromStatName( + worker_host_stat_name_storage.statName(), Stats::Gauge::ImportMode::NeverImport); + if (increment) { + worker_host_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: incremented worker host stat {} to {}", + worker_host_stat_name, worker_host_gauge.value()); + } else { + worker_host_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: decremented worker host stat {} to {}", + worker_host_stat_name, worker_host_gauge.value()); + } + } + + // Create/update per-worker cluster connection stat. + if (!cluster_id.empty() && !state_suffix.empty()) { + std::string worker_cluster_stat_name = + fmt::format("{}.{}.cluster.{}.{}", stat_prefix_, dispatcher_name, cluster_id, state_suffix); + Stats::StatNameManagedStorage worker_cluster_stat_name_storage(worker_cluster_stat_name, + stats_store.symbolTable()); + auto& worker_cluster_gauge = stats_store.gaugeFromStatName( + worker_cluster_stat_name_storage.statName(), Stats::Gauge::ImportMode::NeverImport); + if (increment) { + worker_cluster_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: incremented worker cluster stat {} to {}", + worker_cluster_stat_name, worker_cluster_gauge.value()); + } else { + worker_cluster_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: decremented worker cluster stat {} to {}", + worker_cluster_stat_name, worker_cluster_gauge.value()); + } + } +} + +absl::flat_hash_map +ReverseTunnelInitiatorExtension::getCrossWorkerStatMap() { + absl::flat_hash_map stats_map; + auto& stats_store = context_.scope(); + + // Iterate through all gauges and filter for cross-worker stats only. + // Cross-worker stats have the pattern ".host.." or + // ".cluster.." (no dispatcher name in the middle). + Stats::IterateFn gauge_callback = + [&stats_map, this](const Stats::RefcountPtr& gauge) -> bool { + const std::string& gauge_name = gauge->name(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: gauge_name: {} gauge_value: {}", gauge_name, + gauge->value()); + if (gauge_name.find(stat_prefix_ + ".") != std::string::npos && + (gauge_name.find(stat_prefix_ + ".host.") != std::string::npos || + gauge_name.find(stat_prefix_ + ".cluster.") != std::string::npos) && + gauge->used()) { + stats_map[gauge_name] = gauge->value(); + } + return true; + }; + stats_store.iterate(gauge_callback); + + ENVOY_LOG( + debug, + "ReverseTunnelInitiatorExtension: collected {} stats for reverse connections across all " + "worker threads", + stats_map.size()); + + return stats_map; +} + +std::pair, std::vector> +ReverseTunnelInitiatorExtension::getConnectionStatsSync( + std::chrono::milliseconds /* timeout_ms */) { + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension: obtaining reverse connection stats"); + + // Get all gauges with the reverse_connections prefix. + auto connection_stats = getCrossWorkerStatMap(); + + std::vector connected_hosts; + std::vector accepted_connections; + + // Process the stats to extract connection information + // For initiator, stats format is: .host.. or + // .cluster.. We only want hosts/clusters with + // "connected" state + for (const auto& [stat_name, count] : connection_stats) { + if (count > 0) { + // Parse stat name to extract host/cluster information with state suffix. + std::string host_pattern = stat_prefix_ + ".host."; + std::string cluster_pattern = stat_prefix_ + ".cluster."; + + if (stat_name.find(host_pattern) != std::string::npos && + stat_name.find(".connected") != std::string::npos) { + // Find the position after ".host." and before ".connected". + size_t start_pos = stat_name.find(host_pattern) + host_pattern.length(); + size_t end_pos = stat_name.find(".connected"); + if (start_pos != std::string::npos && end_pos != std::string::npos && end_pos > start_pos) { + std::string host_address = stat_name.substr(start_pos, end_pos - start_pos); + connected_hosts.push_back(host_address); + } + } else if (stat_name.find(cluster_pattern) != std::string::npos && + stat_name.find(".connected") != std::string::npos) { + // Find the position after ".cluster." and before ".connected". + size_t start_pos = stat_name.find(cluster_pattern) + cluster_pattern.length(); + size_t end_pos = stat_name.find(".connected"); + if (start_pos != std::string::npos && end_pos != std::string::npos && end_pos > start_pos) { + std::string cluster_id = stat_name.substr(start_pos, end_pos - start_pos); + accepted_connections.push_back(cluster_id); + } + } + } + } + + ENVOY_LOG(debug, + "ReverseTunnelInitiatorExtension: found {} connected hosts, {} accepted connections", + connected_hosts.size(), accepted_connections.size()); + + return {connected_hosts, accepted_connections}; +} + +absl::flat_hash_map ReverseTunnelInitiatorExtension::getPerWorkerStatMap() { + absl::flat_hash_map stats_map; + auto& stats_store = context_.scope(); + + // Get the current dispatcher name + std::string dispatcher_name = "main_thread"; // Default for main thread + auto* local_registry = getLocalRegistry(); + if (local_registry) { + // Dispatcher name is of the form "worker_x" where x is the worker index. + dispatcher_name = local_registry->dispatcher().name(); + } + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: Getting per worker stats map for {}", + dispatcher_name); + + // Iterate through all gauges and filter for the current dispatcher. + Stats::IterateFn gauge_callback = + [&stats_map, &dispatcher_name, this](const Stats::RefcountPtr& gauge) -> bool { + const std::string& gauge_name = gauge->name(); + ENVOY_LOG(trace, "ReverseTunnelInitiatorExtension: gauge_name: {} gauge_value: {}", gauge_name, + gauge->value()); + if (gauge_name.find(stat_prefix_ + ".") != std::string::npos && + gauge_name.find(dispatcher_name + ".") != std::string::npos && + (gauge_name.find(".host.") != std::string::npos || + gauge_name.find(".cluster.") != std::string::npos) && + gauge->used()) { + stats_map[gauge_name] = gauge->value(); + } + return true; + }; + stats_store.iterate(gauge_callback); + + ENVOY_LOG(debug, "ReverseTunnelInitiatorExtension: collected {} stats for dispatcher '{}'", + stats_map.size(), dispatcher_name); + + return stats_map; +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h new file mode 100644 index 0000000000000..51f7ec0dcb7f0 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h @@ -0,0 +1,138 @@ +#pragma once + +#include +#include + +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.validate.h" +#include "envoy/server/bootstrap_extension_config.h" +#include "envoy/stats/scope.h" +#include "envoy/thread_local/thread_local.h" + +#include "absl/container/flat_hash_map.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations +class DownstreamSocketThreadLocal; + +/** + * Bootstrap extension for ReverseTunnelInitiator. + */ +class ReverseTunnelInitiatorExtension : public Server::BootstrapExtension, + public Logger::Loggable { + // Friend class for testing + friend class ReverseTunnelInitiatorExtensionTest; + +public: + ReverseTunnelInitiatorExtension( + Server::Configuration::ServerFactoryContext& context, + const envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface& config); + + void onServerInitialized() override; + void onWorkerThreadInitialized() override; + + /** + * @return pointer to the thread-local registry, or nullptr if not available. + */ + DownstreamSocketThreadLocal* getLocalRegistry() const; + + /** + * Update all connection stats for reverse connections. This updates the cross-worker stats + * as well as the per-worker stats. + * @param node_id the node identifier for the connection + * @param cluster_id the cluster identifier for the connection + * @param state_suffix the state suffix (e.g., "connecting", "connected", "failed") + * @param increment whether to increment (true) or decrement (false) the connection count + */ + void updateConnectionStats(const std::string& node_id, const std::string& cluster_id, + const std::string& state_suffix, bool increment); + + /** + * Update per-worker connection stats for debugging purposes. + * Creates worker-specific stats + * @param node_id the node identifier for the connection + * @param cluster_id the cluster identifier for the connection + * @param state_suffix the state suffix for the connection + * @param increment whether to increment (true) or decrement (false) the connection count + */ + void updatePerWorkerConnectionStats(const std::string& node_id, const std::string& cluster_id, + const std::string& state_suffix, bool increment); + + /** + * Get per-worker stat map for the current dispatcher. + * @return map of stat names to values for the current worker thread + */ + absl::flat_hash_map getPerWorkerStatMap(); + + /** + * Get cross-worker stat map across all workers. + * @return map of stat names to values across all worker threads + */ + absl::flat_hash_map getCrossWorkerStatMap(); + + /** + * Get connection stats synchronously with timeout. + * @param timeout_ms timeout for the operation + * @return pair of vectors containing connected nodes and accepted connections + */ + std::pair, std::vector> + getConnectionStatsSync(std::chrono::milliseconds timeout_ms); + + /** + * Get the stats scope for accessing stats. + * @return reference to the stats scope. + */ + Stats::Scope& getStatsScope() const { return context_.scope(); } + + /** + * Test-only method to set the thread local slot for testing purposes. + * This allows tests to inject a custom thread local registry and is used + * in unit tests to simulate different worker threads. + * @param slot the thread local slot to set + */ + void setTestOnlyTLSRegistry( + std::unique_ptr> slot) { + tls_slot_ = std::move(slot); + } + +private: + Server::Configuration::ServerFactoryContext& context_; + const envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + ThreadLocal::TypedSlotPtr tls_slot_; + std::string stat_prefix_; // Reverse connection stats prefix +}; + +/** + * Thread local storage for ReverseTunnelInitiator. + * Stores the thread-local dispatcher and stats scope for each worker thread. + */ +class DownstreamSocketThreadLocal : public ThreadLocal::ThreadLocalObject { +public: + DownstreamSocketThreadLocal(Event::Dispatcher& dispatcher, Stats::Scope& scope) + : dispatcher_(dispatcher), scope_(scope) {} + + /** + * @return reference to the thread-local dispatcher + */ + Event::Dispatcher& dispatcher() { return dispatcher_; } + + /** + * @return reference to the stats scope + */ + Stats::Scope& scope() { return scope_; } + +private: + Event::Dispatcher& dispatcher_; + Stats::Scope& scope_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/BUILD b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/BUILD new file mode 100644 index 0000000000000..a73203716b3dd --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/BUILD @@ -0,0 +1,68 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "reverse_tunnel_acceptor_includes", + hdrs = [ + "reverse_connection_io_handle.h", + "reverse_tunnel_acceptor.h", + "reverse_tunnel_acceptor_extension.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//envoy/event:dispatcher_interface", + "//envoy/event:timer_interface", + "//envoy/network:io_handle_interface", + "//envoy/network:socket_interface", + "//envoy/registry", + "//envoy/server:bootstrap_extension_config_interface", + "//envoy/stats:stats_interface", + "//envoy/thread_local:thread_local_interface", + "//source/common/network:default_socket_interface_lib", + "//source/extensions/bootstrap/reverse_tunnel/common:reverse_connection_utility_lib", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "reverse_tunnel_acceptor_lib", + srcs = [ + "reverse_connection_io_handle.cc", + "reverse_tunnel_acceptor.cc", + "reverse_tunnel_acceptor_extension.cc", + ], + visibility = ["//visibility:public"], + deps = [ + ":reverse_tunnel_acceptor_includes", + ":upstream_socket_manager_lib", + "//source/common/common:logger_lib", + "//source/common/network:default_socket_interface_lib", + "//source/common/protobuf", + ], + alwayslink = 1, +) + +envoy_cc_extension( + name = "upstream_socket_manager_lib", + srcs = ["upstream_socket_manager.cc"], + hdrs = ["upstream_socket_manager.h"], + visibility = ["//visibility:public"], + deps = [ + "reverse_tunnel_acceptor_includes", + "//envoy/event:dispatcher_interface", + "//envoy/event:timer_interface", + "//envoy/network:io_handle_interface", + "//envoy/thread_local:thread_local_object", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/common:random_generator_lib", + "//source/extensions/bootstrap/reverse_tunnel/common:reverse_connection_utility_lib", + ], +) diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.cc b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.cc new file mode 100644 index 0000000000000..6669f327c832a --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.cc @@ -0,0 +1,55 @@ +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h" + +#include "source/common/common/logger.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +UpstreamReverseConnectionIOHandle::UpstreamReverseConnectionIOHandle( + Network::ConnectionSocketPtr socket, const std::string& cluster_name) + : IoSocketHandleImpl(socket->ioHandle().fdDoNotUse()), cluster_name_(cluster_name), + owned_socket_(std::move(socket)) { + ENVOY_LOG(trace, "reverse_tunnel: created IO handle for cluster: {}, fd: {}", cluster_name_, fd_); +} + +UpstreamReverseConnectionIOHandle::~UpstreamReverseConnectionIOHandle() { + ENVOY_LOG(trace, "reverse_tunnel: destroying IO handle for cluster: {}, fd: {}", cluster_name_, + fd_); +} + +Api::SysCallIntResult +UpstreamReverseConnectionIOHandle::connect(Network::Address::InstanceConstSharedPtr address) { + ENVOY_LOG(trace, "reverse_tunnel: connect() to {} - connection already established", + address->asString()); + return Api::SysCallIntResult{0, 0}; +} + +Api::IoCallUint64Result UpstreamReverseConnectionIOHandle::close() { + ENVOY_LOG(debug, "reverse_tunnel: close() called for fd: {}", fd_); + + if (owned_socket_) { + ENVOY_LOG(debug, "reverse_tunnel: releasing socket for cluster: {}", cluster_name_); + owned_socket_.reset(); + SET_SOCKET_INVALID(fd_); + return Api::ioCallUint64ResultNoError(); + } + return IoSocketHandleImpl::close(); +} + +Api::SysCallIntResult UpstreamReverseConnectionIOHandle::shutdown(int how) { + ENVOY_LOG(trace, "reverse_tunnel: shutdown({}) called for fd: {}", how, fd_); + // If we still own the socket, ignore shutdown to avoid affecting a socket that will be + // handed over to the upstream connection. + if (owned_socket_) { + ENVOY_LOG(debug, "reverse_tunnel: ignoring shutdown() call for owned socket fd: {}", fd_); + return Api::SysCallIntResult{0, 0}; + } + return IoSocketHandleImpl::shutdown(how); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h new file mode 100644 index 0000000000000..03c928fab3936 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include "envoy/network/io_handle.h" +#include "envoy/network/socket.h" + +#include "source/common/network/io_socket_handle_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/** + * Custom IoHandle for upstream reverse connections that manages ConnectionSocket lifetime. + * This class implements RAII principles to ensure proper socket cleanup and provides + * reverse connection semantics where the connection is already established. + */ +class UpstreamReverseConnectionIOHandle : public Network::IoSocketHandleImpl { +public: + /** + * Constructs an UpstreamReverseConnectionIOHandle that takes ownership of a socket. + * + * @param socket the reverse connection socket to own and manage. + * @param cluster_name the name of the cluster this connection belongs to. + */ + UpstreamReverseConnectionIOHandle(Network::ConnectionSocketPtr socket, + const std::string& cluster_name); + + ~UpstreamReverseConnectionIOHandle() override; + + // Network::IoHandle overrides + /** + * Override of connect method for reverse connections. + * For reverse connections, the connection is already established so this method + * is a no-op and always returns success. + * + * @param address the target address (unused for reverse connections). + * @return SysCallIntResult with success status (0, 0). + */ + Api::SysCallIntResult connect(Network::Address::InstanceConstSharedPtr address) override; + + /** + * Override of close method for reverse connections. + * Cleans up the owned socket and calls the parent close method. + * + * @return IoCallUint64Result indicating the result of the close operation. + */ + Api::IoCallUint64Result close() override; + + /** + * Override of shutdown for reverse connections. + * When the IO handle owns the socket, ignore shutdown to avoid affecting the handed-off socket. + * + * @param how the type of shutdown (`SHUT_RD`, `SHUT_WR`, `SHUT_RDWR`). + * @return SysCallIntResult with success status if ignored, or result of base call. + */ + Api::SysCallIntResult shutdown(int how) override; + + /** + * Get the owned socket for read-only operations. + * + * @return const reference to the owned socket. + */ + const Network::ConnectionSocket& getSocket() const { return *owned_socket_; } + +private: + // The name of the cluster this reverse connection belongs to. + std::string cluster_name_; + // The socket that this IOHandle owns and manages lifetime for. + Network::ConnectionSocketPtr owned_socket_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.cc b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.cc new file mode 100644 index 0000000000000..91159b18d3cdd --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.cc @@ -0,0 +1,119 @@ +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" + +#include + +#include "source/common/common/logger.h" +#include "source/common/network/io_socket_handle_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// ReverseTunnelAcceptor implementation +ReverseTunnelAcceptor::ReverseTunnelAcceptor(Server::Configuration::ServerFactoryContext& context) + : extension_(nullptr), context_(&context) { + ENVOY_LOG(debug, "reverse_tunnel: created acceptor"); +} + +Envoy::Network::IoHandlePtr +ReverseTunnelAcceptor::socket(Envoy::Network::Socket::Type, Envoy::Network::Address::Type, + Envoy::Network::Address::IpVersion, bool, + const Envoy::Network::SocketCreationOptions&) const { + + ENVOY_LOG(warn, "reverse_tunnel: socket() called without address; returning nullptr"); + + // Reverse connection sockets should always have an address. + return nullptr; +} + +Envoy::Network::IoHandlePtr +ReverseTunnelAcceptor::socket(Envoy::Network::Socket::Type socket_type, + const Envoy::Network::Address::InstanceConstSharedPtr addr, + const Envoy::Network::SocketCreationOptions& options) const { + ENVOY_LOG(debug, "reverse_tunnel: socket() called for address: {}, node: {}", addr->asString(), + addr->logicalName()); + + // For upstream reverse connections, we need to get the thread-local socket manager + // and check if there are any cached connections available + auto* tls_registry = getLocalRegistry(); + if (tls_registry && tls_registry->socketManager()) { + ENVOY_LOG(trace, "reverse_tunnel: running on dispatcher: {}", + tls_registry->dispatcher().name()); + auto* socket_manager = tls_registry->socketManager(); + + // The address's logical name is the node ID. + std::string node_id = addr->logicalName(); + ENVOY_LOG(debug, "reverse_tunnel: using node_id: {}", node_id); + + // Try to get a cached socket for the node. + auto socket = socket_manager->getConnectionSocket(node_id); + if (socket) { + ENVOY_LOG(debug, "reverse_tunnel: reusing cached socket for node: {}", node_id); + // Create IOHandle that owns the socket using RAII. + auto io_handle = + std::make_unique(std::move(socket), node_id); + return io_handle; + } + } + + // No sockets available, fallback to standard socket interface. + ENVOY_LOG(debug, "reverse_tunnel: no available connection, falling back to standard socket"); + // Emit a counter to aid diagnostics in NAT scenarios where direct connect will fail. + if (extension_) { + auto& scope = extension_->getStatsScope(); + std::string counter_name = + fmt::format("{}.fallback_no_reverse_socket", extension_->statPrefix()); + Stats::StatNameManagedStorage counter_name_storage(counter_name, scope.symbolTable()); + auto& counter = scope.counterFromStatName(counter_name_storage.statName()); + counter.inc(); + } + return Network::socketInterface( + "envoy.extensions.network.socket_interface.default_socket_interface") + ->socket(socket_type, addr, options); +} + +bool ReverseTunnelAcceptor::ipFamilySupported(int domain) { + return domain == AF_INET || domain == AF_INET6; +} + +// Get thread local registry for the current thread +UpstreamSocketThreadLocal* ReverseTunnelAcceptor::getLocalRegistry() const { + if (extension_) { + return extension_->getLocalRegistry(); + } + return nullptr; +} + +// BootstrapExtensionFactory +Server::BootstrapExtensionPtr ReverseTunnelAcceptor::createBootstrapExtension( + const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& context) { + ENVOY_LOG(debug, "ReverseTunnelAcceptor::createBootstrapExtension()"); + // Cast the config to the proper type. + const auto& message = MessageUtil::downcastAndValidate< + const envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface&>(config, context.messageValidationVisitor()); + + // Set the context for this socket interface instance. + context_ = &context; + + // Return a SocketInterfaceExtension that wraps this socket interface. + return std::make_unique(*this, context, message); +} + +ProtobufTypes::MessagePtr ReverseTunnelAcceptor::createEmptyConfigProto() { + return std::make_unique(); +} + +REGISTER_FACTORY(ReverseTunnelAcceptor, Server::Configuration::BootstrapExtensionFactory); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h new file mode 100644 index 0000000000000..b5437525439c2 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include +#include + +#include "envoy/event/dispatcher.h" +#include "envoy/event/timer.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.validate.h" +#include "envoy/network/io_handle.h" +#include "envoy/network/socket.h" +#include "envoy/registry/registry.h" +#include "envoy/server/bootstrap_extension_config.h" +#include "envoy/stats/scope.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/network/io_socket_handle_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations +class ReverseTunnelAcceptorExtension; +class UpstreamSocketManager; + +/** + * Socket interface that creates upstream reverse connection sockets. + * Manages cached reverse TCP connections and provides them when requested. + */ +class ReverseTunnelAcceptor : public Envoy::Network::SocketInterfaceBase, + public Envoy::Logger::Loggable { +public: + /** + * Constructs a ReverseTunnelAcceptor with the given server factory context. + * + * @param context the server factory context for this socket interface. + */ + ReverseTunnelAcceptor(Server::Configuration::ServerFactoryContext& context); + + ReverseTunnelAcceptor() : extension_(nullptr), context_(nullptr) {} + + // SocketInterface overrides + /** + * Create a socket without a specific address (no-op for reverse connections). + * @param socket_type the type of socket to create. + * @param addr_type the address type. + * @param version the IP version. + * @param socket_v6only whether to create IPv6-only socket. + * @param options socket creation options. + * @return nullptr since reverse connections require specific addresses. + */ + Envoy::Network::IoHandlePtr + socket(Envoy::Network::Socket::Type socket_type, Envoy::Network::Address::Type addr_type, + Envoy::Network::Address::IpVersion version, bool socket_v6only, + const Envoy::Network::SocketCreationOptions& options) const override; + + /** + * Create a socket with a specific address. + * @param socket_type the type of socket to create. + * @param addr the address to bind to. + * @param options socket creation options. + * @return IoHandlePtr for the reverse connection socket. + */ + Envoy::Network::IoHandlePtr + socket(Envoy::Network::Socket::Type socket_type, + const Envoy::Network::Address::InstanceConstSharedPtr addr, + const Envoy::Network::SocketCreationOptions& options) const override; + + /** + * @param domain the IP family domain (AF_INET, AF_INET6). + * @return true if the family is supported. + */ + bool ipFamilySupported(int domain) override; + + /** + * @return pointer to the thread-local registry, or nullptr if not available. + */ + class UpstreamSocketThreadLocal* getLocalRegistry() const; + + /** + * Create a bootstrap extension for this socket interface. + * @param config the config. + * @param context the server factory context. + * @return BootstrapExtensionPtr for the socket interface extension. + */ + Server::BootstrapExtensionPtr + createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override; + + /** + * @return MessagePtr containing the empty configuration. + */ + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + /** + * @return the interface name. + */ + std::string name() const override { + return "envoy.bootstrap.reverse_tunnel.upstream_socket_interface"; + } + + /** + * @return pointer to the extension for cross-thread aggregation. + */ + ReverseTunnelAcceptorExtension* getExtension() const { return extension_; } + + ReverseTunnelAcceptorExtension* extension_{nullptr}; + +private: + Server::Configuration::ServerFactoryContext* context_; +}; + +DECLARE_FACTORY(ReverseTunnelAcceptor); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.cc b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.cc new file mode 100644 index 0000000000000..918435b99502b --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.cc @@ -0,0 +1,286 @@ +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" + +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// UpstreamSocketThreadLocal implementation +UpstreamSocketThreadLocal::UpstreamSocketThreadLocal(Event::Dispatcher& dispatcher, + ReverseTunnelAcceptorExtension* extension) + : dispatcher_(dispatcher), + socket_manager_(std::make_unique(dispatcher, extension)) {} + +// ReverseTunnelAcceptorExtension implementation +void ReverseTunnelAcceptorExtension::onServerInitialized() { + ENVOY_LOG(debug, + "ReverseTunnelAcceptorExtension::onServerInitialized: creating thread local slot"); + + // Set the extension reference in the socket interface. + if (socket_interface_) { + socket_interface_->extension_ = this; + } + + // Create thread local slot for dispatcher and socket manager. + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(context_.threadLocal()); + + // Set up the thread local dispatcher and socket manager. + tls_slot_->set([this](Event::Dispatcher& dispatcher) { + auto tls = std::make_shared(dispatcher, this); + // Propagate configured miss threshold into the socket manager. + if (tls->socketManager()) { + tls->socketManager()->setMissThreshold(ping_failure_threshold_); + } + return tls; + }); +} + +// Get thread local registry for the current thread +UpstreamSocketThreadLocal* ReverseTunnelAcceptorExtension::getLocalRegistry() const { + if (!tls_slot_) { + ENVOY_LOG(error, "ReverseTunnelAcceptorExtension::getLocalRegistry(): no thread local slot"); + return nullptr; + } + + if (auto opt = tls_slot_->get(); opt.has_value()) { + return &opt.value().get(); + } + + return nullptr; +} + +std::pair, std::vector> +ReverseTunnelAcceptorExtension::getConnectionStatsSync(std::chrono::milliseconds /* timeout_ms */) { + + ENVOY_LOG(debug, "ReverseTunnelAcceptorExtension: obtaining reverse connection stats"); + + // Get all gauges with the reverse_connections prefix. + auto connection_stats = getCrossWorkerStatMap(); + + std::vector connected_nodes; + std::vector accepted_connections; + + // Process the stats to extract connection information + for (const auto& [stat_name, count] : connection_stats) { + if (count > 0) { + // Parse stat name to extract node/cluster information. + // Format: ".reverse_connections.nodes." or + // ".reverse_connections.clusters.". + if (stat_name.find("reverse_connections.nodes.") != std::string::npos) { + // Find the position after "reverse_connections.nodes.". + size_t pos = stat_name.find("reverse_connections.nodes."); + if (pos != std::string::npos) { + std::string node_id = stat_name.substr(pos + strlen("reverse_connections.nodes.")); + connected_nodes.push_back(node_id); + } + } else if (stat_name.find("reverse_connections.clusters.") != std::string::npos) { + // Find the position after "reverse_connections.clusters.". + size_t pos = stat_name.find("reverse_connections.clusters."); + if (pos != std::string::npos) { + std::string cluster_id = stat_name.substr(pos + strlen("reverse_connections.clusters.")); + accepted_connections.push_back(cluster_id); + } + } + } + } + + ENVOY_LOG(debug, + "ReverseTunnelAcceptorExtension: found {} connected nodes, {} accepted connections", + connected_nodes.size(), accepted_connections.size()); + + return {connected_nodes, accepted_connections}; +} + +absl::flat_hash_map ReverseTunnelAcceptorExtension::getCrossWorkerStatMap() { + absl::flat_hash_map stats_map; + auto& stats_store = context_.scope(); + + // Iterate through all gauges and filter for cross-worker stats only. + // Cross-worker stats have the pattern "reverse_connections.nodes." or + // "reverse_connections.clusters." (no dispatcher name in the middle). + Stats::IterateFn gauge_callback = + [&stats_map](const Stats::RefcountPtr& gauge) -> bool { + const std::string& gauge_name = gauge->name(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: gauge_name: {} gauge_value: {}", gauge_name, + gauge->value()); + if (gauge_name.find("reverse_connections.") != std::string::npos && + (gauge_name.find("reverse_connections.nodes.") != std::string::npos || + gauge_name.find("reverse_connections.clusters.") != std::string::npos) && + gauge->used()) { + stats_map[gauge_name] = gauge->value(); + } + return true; + }; + stats_store.iterate(gauge_callback); + + ENVOY_LOG(debug, + "ReverseTunnelAcceptorExtension: collected {} stats for reverse connections across all " + "worker threads", + stats_map.size()); + + return stats_map; +} + +void ReverseTunnelAcceptorExtension::updateConnectionStats(const std::string& node_id, + const std::string& cluster_id, + bool increment) { + + // Register stats with Envoy's system for automatic cross-thread aggregation + auto& stats_store = context_.scope(); + + // Create/update node connection stat + if (!node_id.empty()) { + std::string node_stat_name = fmt::format("reverse_connections.nodes.{}", node_id); + Stats::StatNameManagedStorage node_stat_name_storage(node_stat_name, stats_store.symbolTable()); + auto& node_gauge = stats_store.gaugeFromStatName(node_stat_name_storage.statName(), + Stats::Gauge::ImportMode::Accumulate); + if (increment) { + node_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: incremented node stat {} to {}", + node_stat_name, node_gauge.value()); + } else { + if (node_gauge.value() > 0) { + node_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: decremented node stat {} to {}", + node_stat_name, node_gauge.value()); + } + } + } + + // Create/update cluster connection stat. + if (!cluster_id.empty()) { + std::string cluster_stat_name = fmt::format("reverse_connections.clusters.{}", cluster_id); + Stats::StatNameManagedStorage cluster_stat_name_storage(cluster_stat_name, + stats_store.symbolTable()); + auto& cluster_gauge = stats_store.gaugeFromStatName(cluster_stat_name_storage.statName(), + Stats::Gauge::ImportMode::Accumulate); + if (increment) { + cluster_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: incremented cluster stat {} to {}", + cluster_stat_name, cluster_gauge.value()); + } else { + if (cluster_gauge.value() > 0) { + cluster_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: decremented cluster stat {} to {}", + cluster_stat_name, cluster_gauge.value()); + } + } + } + + // Also update per-worker stats for debugging. + updatePerWorkerConnectionStats(node_id, cluster_id, increment); +} + +void ReverseTunnelAcceptorExtension::updatePerWorkerConnectionStats(const std::string& node_id, + const std::string& cluster_id, + bool increment) { + auto& stats_store = context_.scope(); + + // Get dispatcher name from the thread local dispatcher. + std::string dispatcher_name; + auto* local_registry = getLocalRegistry(); + if (local_registry == nullptr) { + ENVOY_LOG(error, "ReverseTunnelAcceptorExtension: No local registry found"); + return; + } + + // Dispatcher name is of the form "worker_x" where x is the worker index + dispatcher_name = local_registry->dispatcher().name(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: Updating stats for worker {}", dispatcher_name); + + // Create/update per-worker node connection stat + if (!node_id.empty()) { + std::string worker_node_stat_name = + fmt::format("reverse_connections.{}.node.{}", dispatcher_name, node_id); + Stats::StatNameManagedStorage worker_node_stat_name_storage(worker_node_stat_name, + stats_store.symbolTable()); + auto& worker_node_gauge = stats_store.gaugeFromStatName( + worker_node_stat_name_storage.statName(), Stats::Gauge::ImportMode::NeverImport); + if (increment) { + worker_node_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: incremented worker node stat {} to {}", + worker_node_stat_name, worker_node_gauge.value()); + } else { + // Guardrail: only decrement if the gauge value is greater than 0 + if (worker_node_gauge.value() > 0) { + worker_node_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: decremented worker node stat {} to {}", + worker_node_stat_name, worker_node_gauge.value()); + } else { + ENVOY_LOG(trace, + "ReverseTunnelAcceptorExtension: skipping decrement for worker node stat {} " + "(already at 0)", + worker_node_stat_name); + } + } + } + + // Create/update per-worker cluster connection stat + if (!cluster_id.empty()) { + std::string worker_cluster_stat_name = + fmt::format("reverse_connections.{}.cluster.{}", dispatcher_name, cluster_id); + Stats::StatNameManagedStorage worker_cluster_stat_name_storage(worker_cluster_stat_name, + stats_store.symbolTable()); + auto& worker_cluster_gauge = stats_store.gaugeFromStatName( + worker_cluster_stat_name_storage.statName(), Stats::Gauge::ImportMode::NeverImport); + if (increment) { + worker_cluster_gauge.inc(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: incremented worker cluster stat {} to {}", + worker_cluster_stat_name, worker_cluster_gauge.value()); + } else { + // Guardrail: only decrement if the gauge value is greater than 0 + if (worker_cluster_gauge.value() > 0) { + worker_cluster_gauge.dec(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: decremented worker cluster stat {} to {}", + worker_cluster_stat_name, worker_cluster_gauge.value()); + } else { + ENVOY_LOG(trace, + "ReverseTunnelAcceptorExtension: skipping decrement for worker cluster stat {} " + "(already at 0)", + worker_cluster_stat_name); + } + } + } +} + +absl::flat_hash_map ReverseTunnelAcceptorExtension::getPerWorkerStatMap() { + absl::flat_hash_map stats_map; + auto& stats_store = context_.scope(); + + // Get the current dispatcher name. + std::string dispatcher_name = "main_thread"; // Default for main thread. + auto* local_registry = getLocalRegistry(); + if (local_registry) { + // Dispatcher name is of the form "worker_x" where x is the worker index. + dispatcher_name = local_registry->dispatcher().name(); + } + + // Iterate through all gauges and filter for the current dispatcher. + Stats::IterateFn gauge_callback = + [&stats_map, &dispatcher_name](const Stats::RefcountPtr& gauge) -> bool { + const std::string& gauge_name = gauge->name(); + ENVOY_LOG(trace, "ReverseTunnelAcceptorExtension: gauge_name: {} gauge_value: {}", gauge_name, + gauge->value()); + if (gauge_name.find("reverse_connections.") != std::string::npos && + gauge_name.find(dispatcher_name + ".") != std::string::npos && + (gauge_name.find(".node.") != std::string::npos || + gauge_name.find(".cluster.") != std::string::npos) && + gauge->used()) { + stats_map[gauge_name] = gauge->value(); + } + return true; + }; + stats_store.iterate(gauge_callback); + + ENVOY_LOG(debug, "ReverseTunnelAcceptorExtension: collected {} stats for dispatcher '{}'", + stats_map.size(), dispatcher_name); + + return stats_map; +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h new file mode 100644 index 0000000000000..0ba773d2b00a3 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h @@ -0,0 +1,192 @@ +#pragma once + +#include + +#include +#include + +#include "envoy/event/dispatcher.h" +#include "envoy/event/timer.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.validate.h" +#include "envoy/network/io_handle.h" +#include "envoy/network/socket.h" +#include "envoy/registry/registry.h" +#include "envoy/server/bootstrap_extension_config.h" +#include "envoy/stats/scope.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/network/io_socket_handle_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations +class UpstreamSocketManager; +class ReverseTunnelAcceptorExtension; + +/** + * Thread local storage for ReverseTunnelAcceptor. + */ +class UpstreamSocketThreadLocal : public ThreadLocal::ThreadLocalObject { +public: + /** + * Creates a new socket manager instance for the given dispatcher. + * @param dispatcher the thread-local dispatcher. + * @param extension the upstream extension for stats integration. + */ + UpstreamSocketThreadLocal(Event::Dispatcher& dispatcher, + ReverseTunnelAcceptorExtension* extension = nullptr); + + /** + * @return reference to the thread-local dispatcher. + */ + Event::Dispatcher& dispatcher() { return dispatcher_; } + + /** + * @return pointer to the thread-local socket manager. + */ + UpstreamSocketManager* socketManager() { return socket_manager_.get(); } + const UpstreamSocketManager* socketManager() const { return socket_manager_.get(); } + +private: + // Thread-local dispatcher. + Event::Dispatcher& dispatcher_; + // Thread-local socket manager. + std::unique_ptr socket_manager_; +}; + +/** + * Socket interface extension for upstream reverse connections. + */ +class ReverseTunnelAcceptorExtension + : public Envoy::Network::SocketInterfaceExtension, + public Envoy::Logger::Loggable { + // Friend class for testing + friend class ReverseTunnelAcceptorExtensionTest; + +public: + /** + * @param sock_interface the reverse tunnel acceptor to extend. + * @param context the server factory context. + * @param config the configuration for this extension. + */ + ReverseTunnelAcceptorExtension( + ReverseTunnelAcceptor& sock_interface, Server::Configuration::ServerFactoryContext& context, + const envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface& config) + : Envoy::Network::SocketInterfaceExtension(sock_interface), context_(context), + socket_interface_(&sock_interface) { + ENVOY_LOG(debug, + "ReverseTunnelAcceptorExtension: creating upstream reverse connection " + "socket interface with stat_prefix: {}", + stat_prefix_); + stat_prefix_ = + PROTOBUF_GET_STRING_OR_DEFAULT(config, stat_prefix, "upstream_reverse_connection"); + // Configure ping miss threshold (minimum 1). + const uint32_t cfg_threshold = + PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, ping_failure_threshold, 3); + ping_failure_threshold_ = std::max(1, cfg_threshold); + // Ensure the socket interface has a reference to this extension early, so stats can be + // recorded even before onServerInitialized(). + if (socket_interface_ != nullptr) { + socket_interface_->extension_ = this; + } + } + + /** + * Called when the server is initialized. + */ + void onServerInitialized() override; + + /** + * Called when a worker thread is initialized. + */ + void onWorkerThreadInitialized() override {} + + /** + * @return pointer to the thread-local registry, or nullptr if not available. + */ + UpstreamSocketThreadLocal* getLocalRegistry() const; + + /** + * @return reference to the stat prefix string. + */ + const std::string& statPrefix() const { return stat_prefix_; } + + /** + * @return the configured miss threshold for ping health-checks. + */ + uint32_t pingFailureThreshold() const { return ping_failure_threshold_; } + + /** + * Synchronous version for admin API endpoints that require immediate response on reverse + * connection stats. + * @param timeout_ms maximum time to wait for aggregation completion + * @return pair of or empty if timeout + */ + std::pair, std::vector> + getConnectionStatsSync(std::chrono::milliseconds timeout_ms = std::chrono::milliseconds(5000)); + + /** + * Get cross-worker aggregated reverse connection stats. + * @return map of node/cluster -> connection count across all worker threads. + */ + absl::flat_hash_map getCrossWorkerStatMap(); + + /** + * Update the cross-thread aggregated stats for the connection. + * @param node_id the node identifier for the connection. + * @param cluster_id the cluster identifier for the connection. + * @param increment whether to increment (true) or decrement (false) the connection count. + */ + void updateConnectionStats(const std::string& node_id, const std::string& cluster_id, + bool increment); + + /** + * Update per-worker connection stats for debugging. + * @param node_id the node identifier for the connection. + * @param cluster_id the cluster identifier for the connection. + * @param increment whether to increment (true) or decrement (false) the connection count. + */ + void updatePerWorkerConnectionStats(const std::string& node_id, const std::string& cluster_id, + bool increment); + + /** + * Get per-worker connection stats for debugging. + * @return map of node/cluster -> connection count for the current worker thread. + */ + absl::flat_hash_map getPerWorkerStatMap(); + + /** + * Get the stats scope for accessing global stats. + * @return reference to the stats scope. + */ + Stats::Scope& getStatsScope() const { return context_.scope(); } + + /** + * Test-only method to set the thread local slot. + * @param slot the thread local slot to set. + */ + void + setTestOnlyTLSRegistry(std::unique_ptr> slot) { + tls_slot_ = std::move(slot); + } + +private: + Server::Configuration::ServerFactoryContext& context_; + // Thread-local slot for storing the socket manager per worker thread. + std::unique_ptr> tls_slot_; + ReverseTunnelAcceptor* socket_interface_; + std::string stat_prefix_; + uint32_t ping_failure_threshold_{3}; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.cc b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.cc new file mode 100644 index 0000000000000..a267534490b57 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.cc @@ -0,0 +1,448 @@ +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +#include + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/common/random_generator.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// UpstreamSocketManager implementation +UpstreamSocketManager::UpstreamSocketManager(Event::Dispatcher& dispatcher, + ReverseTunnelAcceptorExtension* extension) + : dispatcher_(dispatcher), random_generator_(std::make_unique()), + extension_(extension) { + ENVOY_LOG(debug, "reverse_tunnel: creating socket manager with stats integration"); + ping_timer_ = dispatcher_.createTimer([this]() { pingConnections(); }); +} + +void UpstreamSocketManager::addConnectionSocket(const std::string& node_id, + const std::string& cluster_id, + Network::ConnectionSocketPtr socket, + const std::chrono::seconds& ping_interval, bool) { + ENVOY_LOG(debug, "reverse_tunnel: adding connection for node: {}, cluster: {}", node_id, + cluster_id); + + // Both node_id and cluster_id are mandatory for consistent state management and stats tracking. + if (node_id.empty() || cluster_id.empty()) { + ENVOY_LOG(error, + "reverse_tunnel: node_id or cluster_id cannot be empty. node: '{}', cluster: '{}'", + node_id, cluster_id); + return; + } + + const int fd = socket->ioHandle().fdDoNotUse(); + const std::string& connectionKey = socket->connectionInfoProvider().localAddress()->asString(); + + ENVOY_LOG(debug, "reverse_tunnel: adding socket with FD: {} for node: {}, cluster: {}", fd, + node_id, cluster_id); + + // Store node -> cluster mapping. + ENVOY_LOG(trace, "reverse_tunnel: adding mapping node: {} -> cluster: {}", node_id, cluster_id); + if (node_to_cluster_map_.find(node_id) == node_to_cluster_map_.end()) { + node_to_cluster_map_[node_id] = cluster_id; + cluster_to_node_map_[cluster_id].push_back(node_id); + } + + fd_to_node_map_[fd] = node_id; + // Initialize the ping timer before adding the socket to accepted_reverse_connections_. + // This is to prevent a race condition between pingConnections() and addConnectionSocket() + // where the timer is not initialized when pingConnections() tries to enable it. + fd_to_timer_map_[fd] = dispatcher_.createTimer([this, fd]() { onPingTimeout(fd); }); + + // If local envoy is responding to reverse connections, add the socket to + // accepted_reverse_connections_. Thereafter, initiate ping keepalives on the socket. + accepted_reverse_connections_[node_id].push_back(std::move(socket)); + Network::ConnectionSocketPtr& socket_ref = accepted_reverse_connections_[node_id].back(); + + // Update stats registry + if (auto extension = getUpstreamExtension()) { + extension->updateConnectionStats(node_id, cluster_id, true /* increment */); + ENVOY_LOG(debug, "UpstreamSocketManager: updated stats registry for node '{}' cluster '{}'", + node_id, cluster_id); + } + + // onPingResponse() expects a ping reply on the socket. + fd_to_event_map_[fd] = dispatcher_.createFileEvent( + fd, + [this, &socket_ref](uint32_t events) { + ASSERT(events == Event::FileReadyType::Read); + onPingResponse(socket_ref->ioHandle()); + return absl::OkStatus(); + }, + Event::FileTriggerType::Edge, Event::FileReadyType::Read); + + // Initiate ping keepalives on the socket. + tryEnablePingTimer(std::chrono::seconds(ping_interval.count())); + + ENVOY_LOG( + info, + "UpstreamSocketManager: done adding socket to maps with node: {} connection key: {} fd: {}", + node_id, connectionKey, fd); +} + +Network::ConnectionSocketPtr +UpstreamSocketManager::getConnectionSocket(const std::string& node_id) { + + ENVOY_LOG(debug, "UpstreamSocketManager: getConnectionSocket() called with node_id: {}", node_id); + + if (node_to_cluster_map_.find(node_id) == node_to_cluster_map_.end()) { + ENVOY_LOG(error, "UpstreamSocketManager: cluster -> node mapping changed for node: {}", + node_id); + return nullptr; + } + + const std::string& cluster_id = node_to_cluster_map_[node_id]; + + ENVOY_LOG(debug, "UpstreamSocketManager: Looking for socket with node: {} cluster: {}", node_id, + cluster_id); + + // Find first available socket for the node. + auto node_sockets_it = accepted_reverse_connections_.find(node_id); + if (node_sockets_it == accepted_reverse_connections_.end() || node_sockets_it->second.empty()) { + ENVOY_LOG(debug, "UpstreamSocketManager: No available sockets for node: {}", node_id); + return nullptr; + } + + // Debugging: Print the number of free sockets on this worker thread + ENVOY_LOG(debug, "UpstreamSocketManager: Found {} sockets for node: {}", + node_sockets_it->second.size(), node_id); + + // Fetch the socket from the accepted_reverse_connections_ and remove it from the list + Network::ConnectionSocketPtr socket(std::move(node_sockets_it->second.front())); + node_sockets_it->second.pop_front(); + + const int fd = socket->ioHandle().fdDoNotUse(); + const std::string& remoteConnectionKey = + socket->connectionInfoProvider().remoteAddress()->asString(); + + ENVOY_LOG(debug, + "UpstreamSocketManager: Reverse conn socket with FD:{} connection key:{} found for " + "node: {} cluster: {}", + fd, remoteConnectionKey, node_id, cluster_id); + + fd_to_event_map_.erase(fd); + fd_to_timer_map_.erase(fd); + + cleanStaleNodeEntry(node_id); + + return socket; +} + +std::string UpstreamSocketManager::getNodeID(const std::string& key) { + ENVOY_LOG(debug, "UpstreamSocketManager: getNodeID() called with key: {}", key); + + // First check if the key exists as a cluster ID by checking global stats + // This ensures we check across all threads, not just the current thread + if (auto extension = getUpstreamExtension()) { + // Check if any thread has sockets for this cluster by looking at global stats. + std::string cluster_stat_name = fmt::format("reverse_connections.clusters.{}", key); + auto& stats_store = extension->getStatsScope(); + Stats::StatNameManagedStorage cluster_stat_name_storage(cluster_stat_name, + stats_store.symbolTable()); + auto& cluster_gauge = stats_store.gaugeFromStatName(cluster_stat_name_storage.statName(), + Stats::Gauge::ImportMode::Accumulate); + + if (cluster_gauge.value() > 0) { + // Key is a cluster ID with active connections, find a node from this cluster + auto cluster_nodes_it = cluster_to_node_map_.find(key); + if (cluster_nodes_it != cluster_to_node_map_.end() && !cluster_nodes_it->second.empty()) { + // Return a random existing node from this cluster + auto node_idx = random_generator_->random() % cluster_nodes_it->second.size(); + std::string node_id = cluster_nodes_it->second[node_idx]; + ENVOY_LOG(debug, + "UpstreamSocketManager: key '{}' is cluster ID with {} connections, returning " + "random node: {}", + key, cluster_gauge.value(), node_id); + return node_id; + } + // If cluster has connections but no local mapping, assume key is a node ID + } + } + + // Key is not a cluster ID, has no connections, or has no local mapping + // Treat it as a node ID and return it directly + ENVOY_LOG(debug, "UpstreamSocketManager: key '{}' is node ID, returning as-is", key); + return key; +} + +void UpstreamSocketManager::markSocketDead(const int fd) { + ENVOY_LOG(trace, "UpstreamSocketManager: markSocketDead called for fd {}", fd); + + auto node_it = fd_to_node_map_.find(fd); + if (node_it == fd_to_node_map_.end()) { + ENVOY_LOG(debug, "UpstreamSocketManager: FD {} not found in fd_to_node_map_", fd); + return; + } + + const std::string node_id = node_it->second; // Make a COPY, not a reference + ENVOY_LOG(debug, "UpstreamSocketManager: found node '{}' for fd {}", node_id, fd); + + std::string cluster_id = (node_to_cluster_map_.find(node_id) != node_to_cluster_map_.end()) + ? node_to_cluster_map_[node_id] + : ""; + fd_to_node_map_.erase(fd); // Now it's safe to erase since node_id is a copy + + // Check if this is a used connection by looking for node_id in accepted_reverse_connections_ + auto& sockets = accepted_reverse_connections_[node_id]; + if (sockets.empty()) { + // This is a used connection. Mark the stats and return. The socket will be closed by the + // owning UpstreamReverseConnectionIOHandle. + ENVOY_LOG(debug, "UpstreamSocketManager: Marking used socket dead. node: {} cluster: {} FD: {}", + node_id, cluster_id, fd); + // Update Envoy's stats system for production multi-tenant tracking + // This ensures stats are decremented when connections are removed + if (auto extension = getUpstreamExtension()) { + extension->updateConnectionStats(node_id, cluster_id, false /* decrement */); + ENVOY_LOG(debug, + "UpstreamSocketManager: decremented stats registry for node '{}' cluster '{}'", + node_id, cluster_id); + } + + return; + } + + // This is an idle connection, find and remove it from the pool + bool socket_found = false; + for (auto itr = sockets.begin(); itr != sockets.end(); itr++) { + if (fd == itr->get()->ioHandle().fdDoNotUse()) { + ENVOY_LOG(debug, "UpstreamSocketManager: Marking socket dead; node: {}, cluster: {} FD: {}", + node_id, cluster_id, fd); + ::shutdown(fd, SHUT_RDWR); + itr = sockets.erase(itr); + socket_found = true; + + fd_to_event_map_.erase(fd); + fd_to_timer_map_.erase(fd); + + // Update Envoy's stats system for production multi-tenant tracking + // This ensures stats are decremented when connections are removed + if (auto extension = getUpstreamExtension()) { + extension->updateConnectionStats(node_id, cluster_id, false /* decrement */); + ENVOY_LOG(debug, + "UpstreamSocketManager: decremented stats registry for node '{}' cluster '{}'", + node_id, cluster_id); + } + break; + } + } + + if (!socket_found) { + ENVOY_LOG(error, "UpstreamSocketManager: Marking an invalid socket dead. node: {} FD: {}", + node_id, fd); + } + + if (sockets.size() == 0) { + cleanStaleNodeEntry(node_id); + } +} + +void UpstreamSocketManager::tryEnablePingTimer(const std::chrono::seconds& ping_interval) { + ENVOY_LOG(debug, "UpstreamSocketManager: trying to enable ping timer, ping interval: {}", + ping_interval.count()); + if (ping_interval_ != std::chrono::seconds::zero()) { + return; + } + ENVOY_LOG(debug, "UpstreamSocketManager: enabling ping timer, ping interval: {}", + ping_interval.count()); + ping_interval_ = ping_interval; + ping_timer_->enableTimer(ping_interval_); +} + +void UpstreamSocketManager::cleanStaleNodeEntry(const std::string& node_id) { + // Clean the given node-id, if there are no active sockets. + if (accepted_reverse_connections_.find(node_id) != accepted_reverse_connections_.end() && + accepted_reverse_connections_[node_id].size() > 0) { + ENVOY_LOG(debug, "Found {} active sockets for node: {}", + accepted_reverse_connections_[node_id].size(), node_id); + return; + } + ENVOY_LOG(debug, "UpstreamSocketManager: Cleaning stale node entry for node: {}", node_id); + + // Check if given node-id, is present in node_to_cluster_map_. If present, + // fetch the corresponding cluster-id. Use cluster-id and node-id to delete entry + // from cluster_to_node_map_ and node_to_cluster_map_ respectively. + const auto& node_itr = node_to_cluster_map_.find(node_id); + if (node_itr != node_to_cluster_map_.end()) { + const auto& cluster_itr = cluster_to_node_map_.find(node_itr->second); + if (cluster_itr != cluster_to_node_map_.end()) { + const auto& node_entry_itr = + find(cluster_itr->second.begin(), cluster_itr->second.end(), node_id); + + if (node_entry_itr != cluster_itr->second.end()) { + ENVOY_LOG(debug, "UpstreamSocketManager:Removing stale node {} from cluster {}", node_id, + cluster_itr->first); + cluster_itr->second.erase(node_entry_itr); + + // If the cluster to node-list map has an empty vector, remove + // the entry from map. + if (cluster_itr->second.size() == 0) { + cluster_to_node_map_.erase(cluster_itr); + } + } + } + node_to_cluster_map_.erase(node_itr); + } + + // Remove empty node entry from accepted_reverse_connections_ + accepted_reverse_connections_.erase(node_id); +} + +void UpstreamSocketManager::onPingResponse(Network::IoHandle& io_handle) { + const int fd = io_handle.fdDoNotUse(); + + Buffer::OwnedImpl buffer; + const auto ping_size = + ::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility::PING_MESSAGE + .size(); + Api::IoCallUint64Result result = io_handle.read(buffer, absl::make_optional(ping_size)); + if (!result.ok()) { + ENVOY_LOG(debug, "UpstreamSocketManager: Read error on FD: {}: error - {}", fd, + result.err_->getErrorDetails()); + markSocketDead(fd); + return; + } + + // In this case, there is no read error, but the socket has been closed by the remote + // peer in a graceful manner, unlike a connection refused, or a reset. + if (result.return_value_ == 0) { + ENVOY_LOG(debug, "UpstreamSocketManager: FD: {}: reverse connection closed", fd); + markSocketDead(fd); + return; + } + + if (result.return_value_ < ping_size) { + ENVOY_LOG(debug, "UpstreamSocketManager: FD: {}: no complete ping data yet", fd); + return; + } + + if (!::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility::isPingMessage( + buffer.toString())) { + ENVOY_LOG(debug, "UpstreamSocketManager: FD: {}: response is not RPING", fd); + // Treat as a miss; do not immediately kill unless threshold crossed. + onPingTimeout(fd); + return; + } + ENVOY_LOG(trace, "UpstreamSocketManager: FD: {}: received ping response", fd); + fd_to_timer_map_[fd]->disableTimer(); + // Reset miss counter on success. + fd_to_miss_count_.erase(fd); +} + +void UpstreamSocketManager::pingConnections(const std::string& node_id) { + ENVOY_LOG(debug, "UpstreamSocketManager: Pinging connections for node: {}", node_id); + auto& sockets = accepted_reverse_connections_[node_id]; + ENVOY_LOG(debug, "UpstreamSocketManager: node:{} Number of sockets:{}", node_id, sockets.size()); + + auto itr = sockets.begin(); + while (itr != sockets.end()) { + int fd = itr->get()->ioHandle().fdDoNotUse(); + auto buffer = ::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility:: + createPingResponse(); + + auto ping_response_timeout = ping_interval_ / 2; + fd_to_timer_map_[fd]->enableTimer(ping_response_timeout); + + // Use a flag to signal whether the socket needs to be marked dead. If the socket is marked dead + // in markSocketDead(), it is erased from the list, and the iterator becomes invalid. We need to + // break out of the loop to avoid a use after free error. + bool socket_dead = false; + while (buffer->length() > 0) { + Api::IoCallUint64Result result = itr->get()->ioHandle().write(*buffer); + ENVOY_LOG(trace, + "UpstreamSocketManager: node:{} FD:{}: sending ping request. return_value: {}", + node_id, fd, result.return_value_); + if (result.return_value_ == 0) { + ENVOY_LOG(trace, "UpstreamSocketManager: node:{} FD:{}: sending ping rc {}, error - ", + node_id, fd, result.return_value_, result.err_->getErrorDetails()); + if (result.err_->getErrorCode() != Api::IoError::IoErrorCode::Again) { + ENVOY_LOG(error, "UpstreamSocketManager: node:{} FD:{}: failed to send ping", node_id, + fd); + markSocketDead(fd); + socket_dead = true; + break; + } + } + } + + if (buffer->length() > 0) { + // Move to next socket if current one couldn't be fully written + ++itr; + continue; + } + + if (socket_dead) { + // Socket was marked dead, iterator is now invalid, break out of the loop + break; + } + + // Move to next socket + ++itr; + } +} + +void UpstreamSocketManager::pingConnections() { + ENVOY_LOG(error, "UpstreamSocketManager: Pinging connections"); + for (auto& itr : accepted_reverse_connections_) { + pingConnections(itr.first); + } + ping_timer_->enableTimer(ping_interval_); +} + +void UpstreamSocketManager::onPingTimeout(const int fd) { + ENVOY_LOG(debug, "UpstreamSocketManager: ping timeout (or invalid ping) for fd {}", fd); + // Increment miss count and evaluate threshold. + const uint32_t misses = ++fd_to_miss_count_[fd]; + ENVOY_LOG(trace, "UpstreamSocketManager: fd {} miss count {}", fd, misses); + if (misses >= miss_threshold_) { + ENVOY_LOG(debug, "UpstreamSocketManager: fd {} exceeded miss threshold {} — marking dead", fd, + miss_threshold_); + fd_to_miss_count_.erase(fd); + markSocketDead(fd); + } +} + +UpstreamSocketManager::~UpstreamSocketManager() { + ENVOY_LOG(debug, "UpstreamSocketManager: destructor called"); + + // Clean up all active file events and timers first + for (auto& [fd, event] : fd_to_event_map_) { + ENVOY_LOG(debug, "UpstreamSocketManager: cleaning up file event for FD: {}", fd); + event.reset(); // This will cancel the file event. + } + fd_to_event_map_.clear(); + + for (auto& [fd, timer] : fd_to_timer_map_) { + ENVOY_LOG(debug, "UpstreamSocketManager: cleaning up timer for FD: {}", fd); + timer.reset(); // This will cancel the timer. + } + fd_to_timer_map_.clear(); + + // Now mark all sockets as dead + std::vector fds_to_cleanup; + for (const auto& [fd, node_id] : fd_to_node_map_) { + fds_to_cleanup.push_back(fd); + } + + for (int fd : fds_to_cleanup) { + ENVOY_LOG(trace, "UpstreamSocketManager: marking socket dead in destructor for FD: {}", fd); + markSocketDead(fd); // false = not used, just cleanup + } + + // Clear the ping timer + if (ping_timer_) { + ping_timer_->disableTimer(); + ping_timer_.reset(); + } +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h new file mode 100644 index 0000000000000..05e34f79331c7 --- /dev/null +++ b/source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h @@ -0,0 +1,158 @@ +#pragma once + +#include + +#include +#include +#include + +#include "envoy/event/dispatcher.h" +#include "envoy/event/timer.h" +#include "envoy/network/io_handle.h" +#include "envoy/network/socket.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/common/logger.h" +#include "source/common/common/random_generator.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Forward declarations +class ReverseTunnelAcceptorExtension; + +/** + * Thread-local socket manager for upstream reverse connections. + */ +class UpstreamSocketManager : public ThreadLocal::ThreadLocalObject, + public Logger::Loggable { + // Friend class for testing + friend class TestUpstreamSocketManager; + +public: + UpstreamSocketManager(Event::Dispatcher& dispatcher, + ReverseTunnelAcceptorExtension* extension = nullptr); + + ~UpstreamSocketManager(); + + /** + * Add accepted connection to socket manager. + * @param node_id node_id of initiating node. + * @param cluster_id cluster_id of receiving cluster. + * @param socket the socket to be added. + * @param ping_interval the interval at which ping keepalives are sent. + * @param rebalanced true if adding socket after rebalancing. + */ + void addConnectionSocket(const std::string& node_id, const std::string& cluster_id, + Network::ConnectionSocketPtr socket, + const std::chrono::seconds& ping_interval, bool rebalanced); + + /** + * Get an available reverse connection socket. + * @param node_id the node ID to get a socket for. + * @return the connection socket, or nullptr if none available. + */ + Network::ConnectionSocketPtr getConnectionSocket(const std::string& node_id); + + /** + * Mark connection socket dead and remove from internal maps. + * @param fd the FD for the socket to be marked dead. + */ + void markSocketDead(const int fd); + + /** + * Ping all active reverse connections for health checks. + */ + void pingConnections(); + + /** + * Ping reverse connections for a specific node. + * @param node_id the node ID whose connections should be pinged. + */ + void pingConnections(const std::string& node_id); + + /** + * Enable the ping timer if not already enabled. + * @param ping_interval the interval at which ping keepalives should be sent. + */ + void tryEnablePingTimer(const std::chrono::seconds& ping_interval); + + /** + * Clean up stale node entries when no active sockets remain. + * @param node_id the node ID to clean up. + */ + void cleanStaleNodeEntry(const std::string& node_id); + + /** + * Handle ping response from a reverse connection. + * @param io_handle the IO handle for the socket that sent the ping response. + */ + void onPingResponse(Network::IoHandle& io_handle); + + /** + * Handle ping response timeout for a specific socket. + * Increments miss count and marks socket dead if threshold reached. + * @param fd the file descriptor whose ping timed out. + */ + void onPingTimeout(int fd); + + /** + * Set the miss threshold (consecutive misses before marking a socket dead). + * @param threshold minimum value 1. + */ + void setMissThreshold(uint32_t threshold) { miss_threshold_ = std::max(1, threshold); } + + /** + * Get the upstream extension for stats integration. + * @return pointer to the upstream extension or nullptr if not available. + */ + ReverseTunnelAcceptorExtension* getUpstreamExtension() const { return extension_; } + + /** + * Automatically discern whether the key is a node ID or cluster ID. + * @param key the key to get the node ID for. + * @return the node ID. + */ + std::string getNodeID(const std::string& key); + +private: + // Thread local dispatcher instance. + Event::Dispatcher& dispatcher_; + Random::RandomGeneratorPtr random_generator_; + + // Map of node IDs to connection sockets. + absl::flat_hash_map> + accepted_reverse_connections_; + + // Map from file descriptor to node ID. + absl::flat_hash_map fd_to_node_map_; + + // Map of node ID to cluster. + absl::flat_hash_map node_to_cluster_map_; + + // Map of cluster IDs to node IDs. + absl::flat_hash_map> cluster_to_node_map_; + + // File events and timers for ping functionality. + absl::flat_hash_map fd_to_event_map_; + absl::flat_hash_map fd_to_timer_map_; + + // Track consecutive ping misses per file descriptor. + absl::flat_hash_map fd_to_miss_count_; + // Miss threshold before declaring a socket dead. + static constexpr uint32_t kDefaultMissThreshold = 3; + uint32_t miss_threshold_{kDefaultMissThreshold}; + + Event::TimerPtr ping_timer_; + std::chrono::seconds ping_interval_{0}; + + // Upstream extension for stats integration. + ReverseTunnelAcceptorExtension* extension_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/clusters/aggregate/cluster.cc b/source/extensions/clusters/aggregate/cluster.cc index 7b77db6fb14fb..a4a82aa4176e3 100644 --- a/source/extensions/clusters/aggregate/cluster.cc +++ b/source/extensions/clusters/aggregate/cluster.cc @@ -49,7 +49,6 @@ void AggregateClusterLoadBalancer::addMemberUpdateCallbackForCluster( ENVOY_LOG(debug, "member update for cluster '{}' in aggregate cluster '{}'", target_cluster_info->name(), parent_info_->name()); refresh(); - return absl::OkStatus(); }); } @@ -86,8 +85,8 @@ AggregateClusterLoadBalancer::linearizePrioritySet(OptRef exc if (!host_set->hosts().empty()) { priority_context->priority_set_.updateHosts( next_priority_after_linearizing, Upstream::HostSetImpl::updateHostsParams(*host_set), - host_set->localityWeights(), host_set->hosts(), {}, random_.random(), - host_set->weightedPriorityHealth(), host_set->overprovisioningFactor()); + host_set->localityWeights(), host_set->hosts(), {}, host_set->weightedPriorityHealth(), + host_set->overprovisioningFactor()); priority_context->priority_to_cluster_.emplace_back( std::make_pair(priority_in_current_cluster, tlc)); diff --git a/source/extensions/clusters/common/logical_host.cc b/source/extensions/clusters/common/logical_host.cc index cbebb043d3b6c..85069e65c6d68 100644 --- a/source/extensions/clusters/common/logical_host.cc +++ b/source/extensions/clusters/common/logical_host.cc @@ -41,7 +41,7 @@ LogicalHost::LogicalHost( } Network::Address::InstanceConstSharedPtr LogicalHost::healthCheckAddress() const { - absl::MutexLock lock(&address_lock_); + absl::MutexLock lock(address_lock_); return health_check_address_; } @@ -57,7 +57,7 @@ void LogicalHost::setNewAddresses(const Network::Address::InstanceConstSharedPtr ASSERT(*address_list.front() == *address); } { - absl::MutexLock lock(&address_lock_); + absl::MutexLock lock(address_lock_); address_ = address; address_list_or_null_ = std::move(shared_address_list); health_check_address_ = std::move(health_check_address); @@ -65,12 +65,12 @@ void LogicalHost::setNewAddresses(const Network::Address::InstanceConstSharedPtr } HostDescription::SharedConstAddressVector LogicalHost::addressListOrNull() const { - absl::MutexLock lock(&address_lock_); + absl::MutexLock lock(address_lock_); return address_list_or_null_; } Network::Address::InstanceConstSharedPtr LogicalHost::address() const { - absl::MutexLock lock(&address_lock_); + absl::MutexLock lock(address_lock_); return address_; } @@ -80,7 +80,7 @@ Upstream::Host::CreateConnectionData LogicalHost::createConnection( Network::Address::InstanceConstSharedPtr address; SharedConstAddressVector address_list_or_null; { - absl::MutexLock lock(&address_lock_); + absl::MutexLock lock(address_lock_); address = address_; address_list_or_null = address_list_or_null_; } diff --git a/source/extensions/clusters/dns/BUILD b/source/extensions/clusters/dns/BUILD index 29df85897b31a..7d48ddd400c14 100644 --- a/source/extensions/clusters/dns/BUILD +++ b/source/extensions/clusters/dns/BUILD @@ -14,8 +14,12 @@ envoy_cc_extension( hdrs = ["dns_cluster.h"], visibility = ["//visibility:public"], deps = [ + "//source/common/common:dns_utils_lib", + "//source/common/network/dns_resolver:dns_factory_util_lib", "//source/common/upstream:cluster_factory_includes", "//source/common/upstream:upstream_includes", + "//source/extensions/clusters/common:dns_cluster_backcompat_lib", + "//source/extensions/clusters/common:logical_host_lib", "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", diff --git a/source/extensions/clusters/dns/dns_cluster.cc b/source/extensions/clusters/dns/dns_cluster.cc index 7aa22aa3a4643..9919fdb2cfd1e 100644 --- a/source/extensions/clusters/dns/dns_cluster.cc +++ b/source/extensions/clusters/dns/dns_cluster.cc @@ -1,5 +1,10 @@ #include "source/extensions/clusters/dns/dns_cluster.h" +// The purpose of these two headers is purely for backward compatibility. +// Never create new dependencies to symbols declared in these headers! +#include "source/extensions/clusters/logical_dns/logical_dns_cluster.h" +#include "source/extensions/clusters/strict_dns/strict_dns_cluster.h" + #include #include "envoy/common/exception.h" @@ -8,10 +13,10 @@ #include "envoy/config/endpoint/v3/endpoint_components.pb.h" #include "envoy/extensions/clusters/dns/v3/dns_cluster.pb.h" +#include "source/common/common/dns_utils.h" #include "source/common/network/dns_resolver/dns_factory_util.h" #include "source/extensions/clusters/common/dns_cluster_backcompat.h" -#include "source/extensions/clusters/logical_dns/logical_dns_cluster.h" -#include "source/extensions/clusters/strict_dns/strict_dns_cluster.h" +#include "source/extensions/clusters/common/logical_host.h" namespace Envoy { namespace Upstream { @@ -21,21 +26,16 @@ DnsClusterFactory::createClusterWithConfig( const envoy::config::cluster::v3::Cluster& cluster, const envoy::extensions::clusters::dns::v3::DnsCluster& proto_config, Upstream::ClusterFactoryContext& context) { + auto dns_resolver_or_error = selectDnsResolver(proto_config.typed_dns_resolver_config(), context); - absl::StatusOr dns_resolver_or_error; - if (proto_config.has_typed_dns_resolver_config()) { - Network::DnsResolverFactory& dns_resolver_factory = - Network::createDnsResolverFactoryFromTypedConfig(proto_config.typed_dns_resolver_config()); - auto& server_context = context.serverFactoryContext(); - dns_resolver_or_error = dns_resolver_factory.createDnsResolver( - server_context.mainThreadDispatcher(), server_context.api(), - proto_config.typed_dns_resolver_config()); - } else { - dns_resolver_or_error = context.dnsResolver(); - } RETURN_IF_NOT_OK(dns_resolver_or_error.status()); + absl::StatusOr> cluster_or_error; - if (proto_config.all_addresses_in_single_endpoint()) { + + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + cluster_or_error = + DnsClusterImpl::create(cluster, proto_config, context, std::move(*dns_resolver_or_error)); + } else if (proto_config.all_addresses_in_single_endpoint()) { cluster_or_error = LogicalDnsCluster::create(cluster, proto_config, context, std::move(*dns_resolver_or_error)); } else { @@ -44,13 +44,418 @@ DnsClusterFactory::createClusterWithConfig( } RETURN_IF_NOT_OK(cluster_or_error.status()); - return std::make_pair(std::shared_ptr(std::move(*cluster_or_error)), nullptr); + return std::make_pair(ClusterImplBaseSharedPtr(std::move(*cluster_or_error)), nullptr); +} + +REGISTER_FACTORY(DnsClusterFactory, ClusterFactory); + +class LegacyDnsClusterFactory : public ClusterFactoryImplBase { +public: + LegacyDnsClusterFactory(const std::string& name, bool set_all_addresses_in_single_endpoint) + : ClusterFactoryImplBase(name), + set_all_addresses_in_single_endpoint_(set_all_addresses_in_single_endpoint) {} + virtual absl::StatusOr> + createClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, + ClusterFactoryContext& context) override { + absl::StatusOr dns_resolver_or_error = + selectDnsResolver(cluster, context); + RETURN_IF_NOT_OK(dns_resolver_or_error.status()); + + envoy::extensions::clusters::dns::v3::DnsCluster typed_config; + createDnsClusterFromLegacyFields(cluster, typed_config); + + typed_config.set_all_addresses_in_single_endpoint(set_all_addresses_in_single_endpoint_); + + absl::StatusOr> cluster_or_error; + + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + cluster_or_error = + DnsClusterImpl::create(cluster, typed_config, context, std::move(*dns_resolver_or_error)); + } else if (set_all_addresses_in_single_endpoint_) { + cluster_or_error = LogicalDnsCluster::create(cluster, typed_config, context, + std::move(*dns_resolver_or_error)); + } else { + cluster_or_error = StrictDnsClusterImpl::create(cluster, typed_config, context, + std::move(*dns_resolver_or_error)); + } + + RETURN_IF_NOT_OK(cluster_or_error.status()); + return std::make_pair(ClusterImplBaseSharedPtr(std::move(*cluster_or_error)), nullptr); + } + +private: + bool set_all_addresses_in_single_endpoint_{false}; +}; + +/** + * LogicalDNSFactory: making it back compatible with ClusterFactoryImplBase. + */ + +class LogicalDNSFactory : public LegacyDnsClusterFactory { +public: + LogicalDNSFactory() : LegacyDnsClusterFactory("envoy.cluster.logical_dns", true) {} +}; + +REGISTER_FACTORY(LogicalDNSFactory, ClusterFactory); + +/** + * StrictDNSFactory: making it back compatible with ClusterFactoryImplBase + */ + +class StrictDNSFactory : public LegacyDnsClusterFactory { +public: + StrictDNSFactory() : LegacyDnsClusterFactory("envoy.cluster.strict_dns", false) {} +}; + +REGISTER_FACTORY(StrictDNSFactory, ClusterFactory); + +envoy::config::endpoint::v3::ClusterLoadAssignment +DnsClusterImpl::extractAndProcessLoadAssignment(const envoy::config::cluster::v3::Cluster& cluster, + bool all_addresses_in_single_endpoint) { + // In Logical DNS we convert the priority set by the configuration back to zero. + // This helps ensure that we don't blow up later when using zone aware routing, + // as it only supports load assignments with priority 0. + // + // Since Logical DNS is limited to exactly one host declared per load_assignment + // (enforced in DnsClusterImpl::DnsClusterImpl), we can safely just rewrite the priority + // to zero. + if (all_addresses_in_single_endpoint) { + envoy::config::endpoint::v3::ClusterLoadAssignment converted; + converted.MergeFrom(cluster.load_assignment()); + for (auto& endpoint : *converted.mutable_endpoints()) { + endpoint.set_priority(0); + } + return converted; + } + + return cluster.load_assignment(); } /** - * Static registration for the dns cluster factory. @see RegisterFactory. + * DnsClusterImpl: implementation for both logical and strict DNS. */ -REGISTER_FACTORY(DnsClusterFactory, Upstream::ClusterFactory); + +absl::StatusOr> +DnsClusterImpl::create(const envoy::config::cluster::v3::Cluster& cluster, + const envoy::extensions::clusters::dns::v3::DnsCluster& dns_cluster, + ClusterFactoryContext& context, Network::DnsResolverSharedPtr dns_resolver) { + absl::Status creation_status = absl::OkStatus(); + auto ret = std::unique_ptr( + new DnsClusterImpl(cluster, dns_cluster, context, std::move(dns_resolver), creation_status)); + + RETURN_IF_NOT_OK(creation_status); + return ret; +} + +DnsClusterImpl::DnsClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, + const envoy::extensions::clusters::dns::v3::DnsCluster& dns_cluster, + ClusterFactoryContext& context, + Network::DnsResolverSharedPtr dns_resolver, + absl::Status& creation_status) + : BaseDynamicClusterImpl(cluster, context, creation_status), + load_assignment_( + extractAndProcessLoadAssignment(cluster, dns_cluster.all_addresses_in_single_endpoint())), + local_info_(context.serverFactoryContext().localInfo()), dns_resolver_(dns_resolver), + dns_refresh_rate_ms_(std::chrono::milliseconds( + PROTOBUF_GET_MS_OR_DEFAULT(dns_cluster, dns_refresh_rate, 5000))), + dns_jitter_ms_(PROTOBUF_GET_MS_OR_DEFAULT(dns_cluster, dns_jitter, 0)), + respect_dns_ttl_(dns_cluster.respect_dns_ttl()), + dns_lookup_family_( + Envoy::DnsUtils::getDnsLookupFamilyFromEnum(dns_cluster.dns_lookup_family())), + all_addresses_in_single_endpoint_(dns_cluster.all_addresses_in_single_endpoint()) { + failure_backoff_strategy_ = Config::Utility::prepareDnsRefreshStrategy( + dns_cluster, dns_refresh_rate_ms_.count(), + context.serverFactoryContext().api().randomGenerator()); + + std::list resolve_targets; + const auto& locality_lb_endpoints = load_assignment_.endpoints(); + + if (all_addresses_in_single_endpoint_) { // Logical DNS + // For Logical DNS, we make sure we have just a single endpoint. + if (locality_lb_endpoints.size() != 1 || locality_lb_endpoints[0].lb_endpoints().size() != 1) { + if (cluster.has_load_assignment()) { + creation_status = + absl::InvalidArgumentError("LOGICAL_DNS clusters must have a single " + "locality_lb_endpoint and a single lb_endpoint"); + } else { + creation_status = + absl::InvalidArgumentError("LOGICAL_DNS clusters must have a single host"); + } + return; + } + } else { // Strict DNS + // Strict DNS clusters must ensure that the priority for all localities + // are set to zero when using zone-aware routing. Zone-aware routing only + // works for localities with priority zero (the highest). + SET_AND_RETURN_IF_NOT_OK(validateEndpoints(locality_lb_endpoints, {}), creation_status); + } + + for (const auto& locality_lb_endpoint : locality_lb_endpoints) { + for (const auto& lb_endpoint : locality_lb_endpoint.lb_endpoints()) { + const auto& socket_address = lb_endpoint.endpoint().address().socket_address(); + if (!socket_address.resolver_name().empty()) { + creation_status = absl::InvalidArgumentError( + all_addresses_in_single_endpoint_ + ? "LOGICAL_DNS clusters must NOT have a custom resolver name set" + : "STRICT_DNS clusters must NOT have a custom resolver name set"); + return; + } + + resolve_targets.emplace_back(new ResolveTarget( + *this, context.serverFactoryContext().mainThreadDispatcher(), socket_address.address(), + socket_address.port_value(), locality_lb_endpoint, lb_endpoint)); + } + } + resolve_targets_ = std::move(resolve_targets); + + overprovisioning_factor_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( + load_assignment_.policy(), overprovisioning_factor, kDefaultOverProvisioningFactor); + weighted_priority_health_ = load_assignment_.policy().weighted_priority_health(); +} + +void DnsClusterImpl::startPreInit() { + for (const ResolveTargetPtr& target : resolve_targets_) { + target->startResolve(); + } + // If the config provides no endpoints, the cluster is initialized immediately as if all hosts are + // resolved in failure. + if (resolve_targets_.empty() || !wait_for_warm_on_init_) { + onPreInitComplete(); + } +} + +void DnsClusterImpl::updateAllHosts(const HostVector& hosts_added, const HostVector& hosts_removed, + uint32_t current_priority) { + PriorityStateManager priority_state_manager(*this, local_info_, nullptr); + // At this point we know that we are different so make a new host list and notify. + // + // TODO(dio): The uniqueness of a host address resolved in STRICT_DNS cluster per priority is not + // guaranteed. Need a clear agreement on the behavior here, whether it is allowable to have + // duplicated hosts inside a priority. And if we want to enforce this behavior, it should be done + // inside the priority state manager. + for (const ResolveTargetPtr& target : resolve_targets_) { + priority_state_manager.initializePriorityFor(target->locality_lb_endpoints_); + for (const HostSharedPtr& host : target->hosts_) { + if (target->locality_lb_endpoints_.priority() == current_priority) { + priority_state_manager.registerHostForPriority(host, target->locality_lb_endpoints_); + } + } + } + + // TODO(dio): Add assertion in here. + priority_state_manager.updateClusterPrioritySet( + current_priority, std::move(priority_state_manager.priorityState()[current_priority].first), + hosts_added, hosts_removed, absl::nullopt, weighted_priority_health_, + overprovisioning_factor_); +} + +DnsClusterImpl::ResolveTarget::ResolveTarget( + DnsClusterImpl& parent, Event::Dispatcher& dispatcher, const std::string& dns_address, + const uint32_t dns_port, + const envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint, + const envoy::config::endpoint::v3::LbEndpoint& lb_endpoint) + : parent_(parent), locality_lb_endpoints_(locality_lb_endpoint), lb_endpoint_(lb_endpoint), + dns_address_(dns_address), + hostname_(lb_endpoint_.endpoint().hostname().empty() ? dns_address_ + : lb_endpoint_.endpoint().hostname()), + port_(dns_port), + resolve_timer_(dispatcher.createTimer([this]() -> void { startResolve(); })) {} + +DnsClusterImpl::ResolveTarget::~ResolveTarget() { + if (active_query_) { + active_query_->cancel(Network::ActiveDnsQuery::CancelReason::QueryAbandoned); + } +} + +bool DnsClusterImpl::ResolveTarget::isSuccessfulResponse( + const std::list& response, + const Network::DnsResolver::ResolutionStatus& status) { + if (parent_.all_addresses_in_single_endpoint_) { + // Logical DNS doesn't accept empty responses. + return status == Network::DnsResolver::ResolutionStatus::Completed && !response.empty(); + } else { + // For Strict DNS, an empty response just means no available hosts. + return status == Network::DnsResolver::ResolutionStatus::Completed; + } +} + +absl::StatusOr +DnsClusterImpl::ResolveTarget::createLogicalDnsHosts( + const std::list& response) { + ParsedHosts result; + const auto& addrinfo = response.front().addrInfo(); + Network::Address::InstanceConstSharedPtr new_address = + Network::Utility::getAddressWithPort(*(addrinfo.address_), port_); + auto address_list = DnsUtils::generateAddressList(response, port_); + auto logical_host_or_error = + LogicalHost::create(parent_.info_, hostname_, new_address, address_list, + locality_lb_endpoints_, lb_endpoint_, nullptr); + + RETURN_IF_NOT_OK(logical_host_or_error.status()); + + result.hosts.emplace_back(std::move(logical_host_or_error.value())); + result.host_addresses.emplace(new_address->asString()); + result.ttl_refresh_rate = min(result.ttl_refresh_rate, addrinfo.ttl_); + return result; +} + +absl::StatusOr +DnsClusterImpl::ResolveTarget::createStrictDnsHosts( + const std::list& response) { + ParsedHosts result; + for (const auto& resp : response) { + const auto& addrinfo = resp.addrInfo(); + // TODO(mattklein123): Currently the DNS interface does not consider port. We need to + // make a new address that has port in it. We need to both support IPv6 as well as + // potentially move port handling into the DNS interface itself, which would work + // better for SRV. + ASSERT(addrinfo.address_ != nullptr); + auto address = Network::Utility::getAddressWithPort(*(addrinfo.address_), port_); + if (result.host_addresses.count(address->asString()) > 0) { + continue; + } + + auto host_or_error = HostImpl::create( + parent_.info_, hostname_, address, + // TODO(zyfjeff): Created through metadata shared pool + std::make_shared(lb_endpoint_.metadata()), + std::make_shared( + locality_lb_endpoints_.metadata()), + lb_endpoint_.load_balancing_weight().value(), locality_lb_endpoints_.locality(), + lb_endpoint_.endpoint().health_check_config(), locality_lb_endpoints_.priority(), + lb_endpoint_.health_status()); + + RETURN_IF_NOT_OK(host_or_error.status()); + + result.hosts.emplace_back(std::move(host_or_error.value())); + result.host_addresses.emplace(address->asString()); + result.ttl_refresh_rate = min(result.ttl_refresh_rate, addrinfo.ttl_); + } + return result; +} + +void DnsClusterImpl::ResolveTarget::updateLogicalDnsHosts( + const std::list& response, const ParsedHosts& new_hosts) { + Network::Address::InstanceConstSharedPtr primary_address = + Network::Utility::getAddressWithPort(*(response.front().addrInfo().address_), port_); + auto all_addresses = DnsUtils::generateAddressList(response, port_); + if (!logic_dns_cached_address_ || + (*primary_address != *logic_dns_cached_address_ || + DnsUtils::listChanged(logic_dns_cached_address_list_, all_addresses))) { + logic_dns_cached_address_ = primary_address; + logic_dns_cached_address_list_ = std::move(all_addresses); + ENVOY_LOG(debug, "DNS hosts have changed for {}", dns_address_); + const auto previous_hosts = std::move(hosts_); + hosts_ = std::move(new_hosts.hosts); + // For logical DNS, we remove the unique logical host, and add the new one. + parent_.updateAllHosts(new_hosts.hosts, previous_hosts, locality_lb_endpoints_.priority()); + } else { + parent_.info_->configUpdateStats().update_no_rebuild_.inc(); + } +} + +void DnsClusterImpl::ResolveTarget::updateStrictDnsHosts(const ParsedHosts& new_hosts) { + HostVector hosts_added; + HostVector hosts_removed; + if (parent_.updateDynamicHostList(new_hosts.hosts, hosts_, hosts_added, hosts_removed, all_hosts_, + new_hosts.host_addresses)) { + ENVOY_LOG(debug, "DNS hosts have changed for {}", dns_address_); + ASSERT(std::all_of(hosts_.begin(), hosts_.end(), [&](const auto& host) { + return host->priority() == locality_lb_endpoints_.priority(); + })); + + // Update host map for current resolve target. + for (const auto& host : hosts_removed) { + all_hosts_.erase(host->address()->asString()); + } + for (const auto& host : hosts_added) { + all_hosts_.insert({host->address()->asString(), host}); + } + + parent_.updateAllHosts(hosts_added, hosts_removed, locality_lb_endpoints_.priority()); + } else { + parent_.info_->configUpdateStats().update_no_rebuild_.inc(); + } +} + +void DnsClusterImpl::ResolveTarget::startResolve() { + ENVOY_LOG(trace, "starting async DNS resolution for {}", dns_address_); + parent_.info_->configUpdateStats().update_attempt_.inc(); + + active_query_ = parent_.dns_resolver_->resolve( + dns_address_, parent_.dns_lookup_family_, + [this](Network::DnsResolver::ResolutionStatus status, absl::string_view details, + std::list&& response) -> void { + active_query_ = nullptr; + ENVOY_LOG(trace, "async DNS resolution complete for {} details {}", dns_address_, details); + + std::chrono::milliseconds final_refresh_rate = parent_.dns_refresh_rate_ms_; + + if (isSuccessfulResponse(response, status)) { + parent_.info_->configUpdateStats().update_success_.inc(); + + absl::StatusOr new_hosts_or_error; + + if (parent_.all_addresses_in_single_endpoint_) { + new_hosts_or_error = createLogicalDnsHosts(response); + } else { + new_hosts_or_error = createStrictDnsHosts(response); + } + + if (!new_hosts_or_error.ok()) { + ENVOY_LOG(error, "Failed to process DNS response for {} with error: {}", dns_address_, + new_hosts_or_error.status().message()); + parent_.info_->configUpdateStats().update_failure_.inc(); + return; + } + + const auto& new_hosts = new_hosts_or_error.value(); + + if (parent_.all_addresses_in_single_endpoint_) { + updateLogicalDnsHosts(response, new_hosts); + } else { + updateStrictDnsHosts(new_hosts); + } + + // reset failure backoff strategy because there was a success. + parent_.failure_backoff_strategy_->reset(); + + if (!response.empty() && parent_.respect_dns_ttl_ && + new_hosts.ttl_refresh_rate != std::chrono::seconds(0)) { + final_refresh_rate = new_hosts.ttl_refresh_rate; + ASSERT(new_hosts.ttl_refresh_rate != std::chrono::seconds::max() && + final_refresh_rate.count() > 0); + } + if (parent_.dns_jitter_ms_.count() > 0) { + // Note that `parent_.random_.random()` returns a uint64 while + // `parent_.dns_jitter_ms_.count()` returns a signed long that gets cast into a uint64. + // Thus, the modulo of the two will be a positive as long as + // `parent_dns_jitter_ms_.count()` is positive. + // It is important that this be positive, otherwise `final_refresh_rate` could be + // negative causing Envoy to crash. + final_refresh_rate += std::chrono::milliseconds(parent_.random_.random() % + parent_.dns_jitter_ms_.count()); + } + + ENVOY_LOG(debug, "DNS refresh rate reset for {}, refresh rate {} ms", dns_address_, + final_refresh_rate.count()); + } else { + parent_.info_->configUpdateStats().update_failure_.inc(); + + final_refresh_rate = + std::chrono::milliseconds(parent_.failure_backoff_strategy_->nextBackOffMs()); + ENVOY_LOG(debug, "DNS refresh rate reset for {}, (failure) refresh rate {} ms", + dns_address_, final_refresh_rate.count()); + } + + // If there is an initialize callback, fire it now. Note that if the cluster refers to + // multiple DNS names, this will return initialized after a single DNS resolution + // completes. This is not perfect but is easier to code and unclear if the extra + // complexity is needed so will start with this. + parent_.onPreInitComplete(); + resolve_timer_->enableTimer(final_refresh_rate); + }); +} } // namespace Upstream } // namespace Envoy diff --git a/source/extensions/clusters/dns/dns_cluster.h b/source/extensions/clusters/dns/dns_cluster.h index f198e09cef548..f930ab16a010d 100644 --- a/source/extensions/clusters/dns/dns_cluster.h +++ b/source/extensions/clusters/dns/dns_cluster.h @@ -31,6 +31,99 @@ class DnsClusterFactory : public Upstream::ConfigurableClusterFactoryBase< Upstream::ClusterFactoryContext& context) override; }; +class DnsClusterImpl : public BaseDynamicClusterImpl { +public: + // Upstream::Cluster + InitializePhase initializePhase() const override { return InitializePhase::Primary; } + static absl::StatusOr> + create(const envoy::config::cluster::v3::Cluster& cluster, + const envoy::extensions::clusters::dns::v3::DnsCluster& dns_cluster, + ClusterFactoryContext& context, Network::DnsResolverSharedPtr dns_resolver); + +protected: + DnsClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, + const envoy::extensions::clusters::dns::v3::DnsCluster& dns_cluster, + ClusterFactoryContext& context, Network::DnsResolverSharedPtr dns_resolver, + absl::Status& creation_status); + +private: + struct ResolveTarget { + ResolveTarget(DnsClusterImpl& parent, Event::Dispatcher& dispatcher, + const std::string& dns_address, const uint32_t dns_port, + const envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoint, + const envoy::config::endpoint::v3::LbEndpoint& lb_endpoint); + ~ResolveTarget(); + void startResolve(); + bool isSuccessfulResponse(const std::list& response, + const Network::DnsResolver::ResolutionStatus& status); + + struct ParsedHosts { + HostVector hosts; + std::chrono::seconds ttl_refresh_rate = std::chrono::seconds::max(); + absl::flat_hash_set host_addresses; + }; + absl::StatusOr + createLogicalDnsHosts(const std::list& response); + absl::StatusOr + createStrictDnsHosts(const std::list& response); + void updateLogicalDnsHosts(const std::list& response, + const ParsedHosts& new_hosts); + void updateStrictDnsHosts(const ParsedHosts& new_hosts); + + DnsClusterImpl& parent_; + Network::ActiveDnsQuery* active_query_{}; + const envoy::config::endpoint::v3::LocalityLbEndpoints& locality_lb_endpoints_; + const envoy::config::endpoint::v3::LbEndpoint& lb_endpoint_; + const std::string dns_address_; + const std::string hostname_; + const uint32_t port_; + const Event::TimerPtr resolve_timer_; + HostVector hosts_; + + // Host map for current resolve target. When we have multiple resolve targets, multiple targets + // may contain two different hosts with the same address. This has two effects: + // 1) This host map cannot be replaced by the cross-priority global host map in the priority + // set. + // 2) Cross-priority global host map may not be able to search for the expected host based on + // the address. + HostMap all_hosts_; + + // These attributes are only used for logical DNS resolution. We use them to keep track of the + // previous responses, so we can check and update the hosts only when the DNS response changes. + // We cache those values here to avoid fetching them from the logical hosts, as that requires + // synchronization mechanisms. + Network::Address::InstanceConstSharedPtr logic_dns_cached_address_; + std::vector logic_dns_cached_address_list_; + }; + + using ResolveTargetPtr = std::unique_ptr; + + void updateAllHosts(const HostVector& hosts_added, const HostVector& hosts_removed, + uint32_t priority); + + // ClusterImplBase + void startPreInit() override; + + static envoy::config::endpoint::v3::ClusterLoadAssignment + extractAndProcessLoadAssignment(const envoy::config::cluster::v3::Cluster& cluster, + bool all_addresses_in_single_endpoint); + + // Keep load assignment as a member to make sure its data referenced in + // resolve_targets_ outlives them. + const envoy::config::endpoint::v3::ClusterLoadAssignment load_assignment_; + const LocalInfo::LocalInfo& local_info_; + Network::DnsResolverSharedPtr dns_resolver_; + std::list resolve_targets_; + const std::chrono::milliseconds dns_refresh_rate_ms_; + const std::chrono::milliseconds dns_jitter_ms_; + BackOffStrategyPtr failure_backoff_strategy_; + const bool respect_dns_ttl_; + Network::DnsLookupFamily dns_lookup_family_; + uint32_t overprovisioning_factor_; + bool weighted_priority_health_; + bool all_addresses_in_single_endpoint_; +}; + DECLARE_FACTORY(DnsClusterFactory); } // namespace Upstream diff --git a/source/extensions/clusters/dynamic_forward_proxy/cluster.cc b/source/extensions/clusters/dynamic_forward_proxy/cluster.cc index 6764ce5ecf36e..f9a63fa12743f 100644 --- a/source/extensions/clusters/dynamic_forward_proxy/cluster.cc +++ b/source/extensions/clusters/dynamic_forward_proxy/cluster.cc @@ -100,14 +100,12 @@ Cluster::~Cluster() { } // Should remove all sub clusters, otherwise, might be memory leaking. // This lock is useless, just make compiler happy. - absl::WriterMutexLock lock{&cluster_map_lock_}; + absl::WriterMutexLock lock{cluster_map_lock_}; for (auto it = cluster_map_.cbegin(); it != cluster_map_.cend();) { auto cluster_name = it->first; ENVOY_LOG(debug, "cluster='{}' removing from cluster_map & cluster manager", cluster_name); cluster_map_.erase(it++); - cm_.removeCluster(cluster_name, - Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.avoid_dfp_cluster_removal_on_cds_update")); + cm_.removeCluster(cluster_name, true); } } @@ -128,7 +126,7 @@ void Cluster::startPreInit() { } bool Cluster::touch(const std::string& cluster_name) { - absl::ReaderMutexLock lock{&cluster_map_lock_}; + absl::ReaderMutexLock lock{cluster_map_lock_}; const auto cluster_it = cluster_map_.find(cluster_name); if (cluster_it != cluster_map_.end()) { cluster_it->second->touch(); @@ -142,15 +140,13 @@ void Cluster::checkIdleSubCluster() { ASSERT(main_thread_dispatcher_.isThreadSafe()); { // TODO: try read lock first. - absl::WriterMutexLock lock{&cluster_map_lock_}; + absl::WriterMutexLock lock{cluster_map_lock_}; for (auto it = cluster_map_.cbegin(); it != cluster_map_.cend();) { if (it->second->checkIdle()) { auto cluster_name = it->first; ENVOY_LOG(debug, "cluster='{}' removing from cluster_map & cluster manager", cluster_name); cluster_map_.erase(it++); - cm_.removeCluster(cluster_name, - Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.avoid_dfp_cluster_removal_on_cds_update")); + cm_.removeCluster(cluster_name, true); } else { ++it; } @@ -163,7 +159,7 @@ std::pair> Cluster::createSubClusterConfig(const std::string& cluster_name, const std::string& host, const int port) { { - absl::WriterMutexLock lock{&cluster_map_lock_}; + absl::WriterMutexLock lock{cluster_map_lock_}; const auto cluster_it = cluster_map_.find(cluster_name); if (cluster_it != cluster_map_.end()) { cluster_it->second->touch(); @@ -263,7 +259,7 @@ absl::Status Cluster::addOrUpdateHost( std::unique_ptr& hosts_added) { Upstream::LogicalHostSharedPtr emplaced_host; { - absl::WriterMutexLock lock{&host_map_lock_}; + absl::WriterMutexLock lock{host_map_lock_}; // NOTE: Right now we allow a DNS cache to be shared between multiple clusters. Though we have // connection/request circuit breakers on the cluster, we don't have any way to control the @@ -333,10 +329,10 @@ absl::Status Cluster::onDnsHostAddOrUpdate( void Cluster::updatePriorityState(const Upstream::HostVector& hosts_added, const Upstream::HostVector& hosts_removed) { - Upstream::PriorityStateManager priority_state_manager(*this, local_info_, nullptr, random_); + Upstream::PriorityStateManager priority_state_manager(*this, local_info_, nullptr); priority_state_manager.initializePriorityFor(dummy_locality_lb_endpoint_); { - absl::ReaderMutexLock lock{&host_map_lock_}; + absl::ReaderMutexLock lock{host_map_lock_}; for (const auto& host : host_map_) { priority_state_manager.registerHostForPriority(host.second.logical_host_, dummy_locality_lb_endpoint_); @@ -350,7 +346,7 @@ void Cluster::updatePriorityState(const Upstream::HostVector& hosts_added, void Cluster::onDnsHostRemove(const std::string& host) { Upstream::HostVector hosts_removed; { - absl::WriterMutexLock lock{&host_map_lock_}; + absl::WriterMutexLock lock{host_map_lock_}; const auto host_map_it = host_map_.find(host); ASSERT(host_map_it != host_map_.end()); hosts_removed.emplace_back(host_map_it->second.logical_host_); @@ -482,7 +478,7 @@ Upstream::HostConstSharedPtr Cluster::LoadBalancer::findHostByName(const std::st Upstream::HostConstSharedPtr Cluster::findHostByName(const std::string& host) const { { - absl::ReaderMutexLock lock{&host_map_lock_}; + absl::ReaderMutexLock lock{host_map_lock_}; const auto host_it = host_map_.find(host); if (host_it == host_map_.end()) { ENVOY_LOG(debug, "host {} not found", host); diff --git a/source/extensions/clusters/eds/eds.cc b/source/extensions/clusters/eds/eds.cc index 6baaf98c07cde..ae4eba177a231 100644 --- a/source/extensions/clusters/eds/eds.cc +++ b/source/extensions/clusters/eds/eds.cc @@ -75,11 +75,8 @@ void EdsClusterImpl::startPreInit() { subscription_->start({edsServiceName()}); void EdsClusterImpl::BatchUpdateHelper::batchUpdate(PrioritySet::HostUpdateCb& host_update_cb) { absl::flat_hash_set all_new_hosts; - PriorityStateManager priority_state_manager(parent_, parent_.local_info_, &host_update_cb, - parent_.random_); + PriorityStateManager priority_state_manager(parent_, parent_.local_info_, &host_update_cb); for (const auto& locality_lb_endpoint : cluster_load_assignment_.endpoints()) { - THROW_IF_NOT_OK(parent_.validateEndpointsForZoneAwareRouting(locality_lb_endpoint)); - priority_state_manager.initializePriorityFor(locality_lb_endpoint); if (locality_lb_endpoint.has_leds_cluster_locality_config()) { @@ -121,6 +118,7 @@ void EdsClusterImpl::BatchUpdateHelper::batchUpdate(PrioritySet::HostUpdateCb& h // Loop over all priorities that exist in the new configuration. auto& priority_state = priority_state_manager.priorityState(); + THROW_IF_NOT_OK(parent_.validateEndpoints(cluster_load_assignment_.endpoints(), priority_state)); for (size_t i = 0; i < priority_state.size(); ++i) { if (parent_.locality_weights_map_.size() <= i) { parent_.locality_weights_map_.resize(i + 1); @@ -257,12 +255,9 @@ EdsClusterImpl::onConfigUpdate(const std::vector& re // Pause LEDS messages until the EDS config is finished processing. Config::ScopedResume maybe_resume_leds; - if (transport_factory_context_->serverFactoryContext().clusterManager().adsMux()) { - const auto type_url = Config::getTypeUrl(); - maybe_resume_leds = - transport_factory_context_->serverFactoryContext().clusterManager().adsMux()->pause( - type_url); - } + const auto type_url = Config::getTypeUrl(); + Config::ScopedResume resume_leds = + transport_factory_context_->serverFactoryContext().xdsManager().pause(type_url); update(cluster_load_assignment); // If previously used a cached version, remove the subscription from the cache's @@ -407,10 +402,9 @@ void EdsClusterImpl::reloadHealthyHostsHelper(const HostSharedPtr& host) { HostsPerLocalityConstSharedPtr hosts_per_locality_copy = host_set->hostsPerLocality().filter( {[&host_to_exclude](const Host& host) { return &host != host_to_exclude.get(); }})[0]; - prioritySet().updateHosts(priority, - HostSetImpl::partitionHosts(hosts_copy, hosts_per_locality_copy), - host_set->localityWeights(), {}, hosts_to_remove, random_.random(), - absl::nullopt, absl::nullopt); + prioritySet().updateHosts( + priority, HostSetImpl::partitionHosts(hosts_copy, hosts_per_locality_copy), + host_set->localityWeights(), {}, hosts_to_remove, absl::nullopt, absl::nullopt); } } diff --git a/source/extensions/clusters/logical_dns/logical_dns_cluster.cc b/source/extensions/clusters/logical_dns/logical_dns_cluster.cc index a57d49ddc1975..78c215b52c8fd 100644 --- a/source/extensions/clusters/logical_dns/logical_dns_cluster.cc +++ b/source/extensions/clusters/logical_dns/logical_dns_cluster.cc @@ -161,7 +161,7 @@ void LogicalDnsCluster::startResolve() { std::unique_ptr); const auto& locality_lb_endpoint = localityLbEndpoint(); - PriorityStateManager priority_state_manager(*this, local_info_, nullptr, random_); + PriorityStateManager priority_state_manager(*this, local_info_, nullptr); priority_state_manager.initializePriorityFor(locality_lb_endpoint); priority_state_manager.registerHostForPriority(logical_host_, locality_lb_endpoint); @@ -180,6 +180,8 @@ void LogicalDnsCluster::startResolve() { // Make sure that we have an updated address for admin display, health // checking, and creating real host connections. logical_host_->setNewAddresses(new_address, address_list, lbEndpoint()); + } else { + info_->configUpdateStats().update_no_rebuild_.inc(); } // reset failure backoff strategy because there was a success. @@ -213,26 +215,5 @@ void LogicalDnsCluster::startResolve() { }); } -absl::StatusOr> -LogicalDnsClusterFactory::createClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, - ClusterFactoryContext& context) { - auto dns_resolver_or_error = selectDnsResolver(cluster, context); - THROW_IF_NOT_OK_REF(dns_resolver_or_error.status()); - - absl::StatusOr> cluster_or_error; - envoy::extensions::clusters::dns::v3::DnsCluster proto_config_legacy{}; - createDnsClusterFromLegacyFields(cluster, proto_config_legacy); - cluster_or_error = LogicalDnsCluster::create(cluster, proto_config_legacy, context, - std::move(*dns_resolver_or_error)); - - RETURN_IF_NOT_OK(cluster_or_error.status()); - return std::make_pair(std::shared_ptr(std::move(*cluster_or_error)), nullptr); -} - -/** - * Static registration for the strict dns cluster factory. @see RegisterFactory. - */ -REGISTER_FACTORY(LogicalDnsClusterFactory, ClusterFactory); - } // namespace Upstream } // namespace Envoy diff --git a/source/extensions/clusters/logical_dns/logical_dns_cluster.h b/source/extensions/clusters/logical_dns/logical_dns_cluster.h index 975a494ca94af..76dc69afb1aea 100644 --- a/source/extensions/clusters/logical_dns/logical_dns_cluster.h +++ b/source/extensions/clusters/logical_dns/logical_dns_cluster.h @@ -93,18 +93,5 @@ class LogicalDnsCluster : public ClusterImplBase { const envoy::config::endpoint::v3::ClusterLoadAssignment load_assignment_; }; -class LogicalDnsClusterFactory : public ClusterFactoryImplBase { -public: - LogicalDnsClusterFactory() : ClusterFactoryImplBase("envoy.cluster.logical_dns") {} - -private: - friend class LogicalDnsClusterTest; - absl::StatusOr> - createClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, - ClusterFactoryContext& context) override; -}; - -DECLARE_FACTORY(LogicalDnsClusterFactory); - } // namespace Upstream } // namespace Envoy diff --git a/source/extensions/clusters/original_dst/original_dst_cluster.cc b/source/extensions/clusters/original_dst/original_dst_cluster.cc index 60f742af9c718..511f46abad279 100644 --- a/source/extensions/clusters/original_dst/original_dst_cluster.cc +++ b/source/extensions/clusters/original_dst/original_dst_cluster.cc @@ -156,7 +156,7 @@ OriginalDstCluster::LoadBalancer::metadataOverrideHost(LoadBalancerContext* cont const auto streamInfos = { const_cast(context->requestStreamInfo()), context->downstreamConnection() ? &context->downstreamConnection()->streamInfo() : nullptr}; - const ProtobufWkt::Value* value = nullptr; + const Protobuf::Value* value = nullptr; for (const auto streamInfo : streamInfos) { if (streamInfo == nullptr) { continue; @@ -164,17 +164,17 @@ OriginalDstCluster::LoadBalancer::metadataOverrideHost(LoadBalancerContext* cont const auto& metadata = streamInfo->dynamicMetadata(); value = &Config::Metadata::metadataValue(&metadata, metadata_key_.value()); // Path can refer to a list, in which case we extract the first element. - if (value->kind_case() == ProtobufWkt::Value::kListValue) { + if (value->kind_case() == Protobuf::Value::kListValue) { const auto& values = value->list_value().values(); if (!values.empty()) { value = &(values[0]); } } - if (value->kind_case() == ProtobufWkt::Value::kStringValue) { + if (value->kind_case() == Protobuf::Value::kStringValue) { break; } } - if (value == nullptr || value->kind_case() != ProtobufWkt::Value::kStringValue) { + if (value == nullptr || value->kind_case() != Protobuf::Value::kStringValue) { return nullptr; } const std::string& metadata_override_host = value->string_value(); @@ -240,9 +240,9 @@ void OriginalDstCluster::addHost(HostSharedPtr& host) { const auto& first_host_set = priority_set_.getOrCreateHostSet(0); HostVectorSharedPtr all_hosts(new HostVector(first_host_set.hosts())); all_hosts->emplace_back(host); - priority_set_.updateHosts( - 0, HostSetImpl::partitionHosts(all_hosts, HostsPerLocalityImpl::empty()), {}, - {std::move(host)}, {}, random_.random(), absl::nullopt, absl::nullopt); + priority_set_.updateHosts(0, + HostSetImpl::partitionHosts(all_hosts, HostsPerLocalityImpl::empty()), + {}, {std::move(host)}, {}, absl::nullopt, absl::nullopt); } void OriginalDstCluster::cleanup() { diff --git a/source/extensions/clusters/redis/redis_cluster.cc b/source/extensions/clusters/redis/redis_cluster.cc index 7be1cbb6ea5b0..5c475c8322b6a 100644 --- a/source/extensions/clusters/redis/redis_cluster.cc +++ b/source/extensions/clusters/redis/redis_cluster.cc @@ -74,11 +74,7 @@ RedisCluster::RedisCluster( context.serverFactoryContext().mainThreadDispatcher(), context.serverFactoryContext().clusterManager(), context.serverFactoryContext().api().timeSource())), - registration_handle_(refresh_manager_->registerCluster( - cluster_name_, redirect_refresh_interval_, redirect_refresh_threshold_, - failure_refresh_threshold_, host_degraded_refresh_threshold_, [&]() { - redis_discovery_session_->resolve_timer_->enableTimer(std::chrono::milliseconds(0)); - })) { + registration_handle_(nullptr) { const auto& locality_lb_endpoints = load_assignment_.endpoints(); for (const auto& locality_lb_endpoint : locality_lb_endpoints) { for (const auto& lb_endpoint : locality_lb_endpoint.lb_endpoints()) { @@ -87,6 +83,32 @@ RedisCluster::RedisCluster( *this, host.socket_address().address(), host.socket_address().port_value())); } } + + // Register the cluster callback using weak_ptr to avoid use-after-free + std::weak_ptr weak_session = redis_discovery_session_; + registration_handle_ = refresh_manager_->registerCluster( + cluster_name_, redirect_refresh_interval_, redirect_refresh_threshold_, + failure_refresh_threshold_, host_degraded_refresh_threshold_, [weak_session]() { + // Try to lock the weak pointer to ensure the session is still alive + auto session = weak_session.lock(); + if (session && session->resolve_timer_) { + session->resolve_timer_->enableTimer(std::chrono::milliseconds(0)); + } + }); +} + +RedisCluster::~RedisCluster() { + // Set flag to prevent any callbacks from executing during destruction + is_destroying_.store(true); + + // Reset redis_discovery_session_ before other members are destroyed + // to ensure any pending callbacks from refresh_manager_ don't access it. + // This matches the approach in PR #39625. + redis_discovery_session_.reset(); + + // Also clear DNS discovery targets to prevent their callbacks from + // accessing the destroyed cluster. + dns_discovery_resolve_targets_.clear(); } void RedisCluster::startPreInit() { @@ -101,7 +123,7 @@ void RedisCluster::startPreInit() { void RedisCluster::updateAllHosts(const Upstream::HostVector& hosts_added, const Upstream::HostVector& hosts_removed, uint32_t current_priority) { - Upstream::PriorityStateManager priority_state_manager(*this, local_info_, nullptr, random_); + Upstream::PriorityStateManager priority_state_manager(*this, local_info_, nullptr); auto locality_lb_endpoint = localityLbEndpoint(); priority_state_manager.initializePriorityFor(locality_lb_endpoint); @@ -198,7 +220,7 @@ RedisCluster::DnsDiscoveryResolveTarget::~DnsDiscoveryResolveTarget() { active_query_->cancel(Network::ActiveDnsQuery::CancelReason::QueryAbandoned); } // Disable timer for mock tests. - if (resolve_timer_) { + if (resolve_timer_ && resolve_timer_->enabled()) { resolve_timer_->disableTimer(); } } @@ -220,8 +242,13 @@ void RedisCluster::DnsDiscoveryResolveTarget::startResolveDns() { } if (!resolve_timer_) { - resolve_timer_ = - parent_.dispatcher_.createTimer([this]() -> void { startResolveDns(); }); + resolve_timer_ = parent_.dispatcher_.createTimer([this]() -> void { + // Check if the parent cluster is being destroyed + if (parent_.is_destroying_.load()) { + return; + } + startResolveDns(); + }); } // if the initial dns resolved to empty, we'll skip the redis discovery phase and // treat it as an empty cluster. @@ -244,7 +271,13 @@ RedisCluster::RedisDiscoverySession::RedisDiscoverySession( Envoy::Extensions::Clusters::Redis::RedisCluster& parent, NetworkFilters::Common::Redis::Client::ClientFactory& client_factory) : parent_(parent), dispatcher_(parent.dispatcher_), - resolve_timer_(parent.dispatcher_.createTimer([this]() -> void { startResolveRedis(); })), + resolve_timer_(parent.dispatcher_.createTimer([this]() -> void { + // Check if the parent cluster is being destroyed + if (parent_.is_destroying_.load()) { + return; + } + startResolveRedis(); + })), client_factory_(client_factory), buffer_timeout_(0), redis_command_stats_( NetworkFilters::Common::Redis::RedisCommandStats::createRedisCommandStats( diff --git a/source/extensions/clusters/redis/redis_cluster.h b/source/extensions/clusters/redis/redis_cluster.h index 2aa97218e5501..1b12f347ddf88 100644 --- a/source/extensions/clusters/redis/redis_cluster.h +++ b/source/extensions/clusters/redis/redis_cluster.h @@ -90,6 +90,7 @@ namespace Redis { class RedisCluster : public Upstream::BaseDynamicClusterImpl { public: + ~RedisCluster(); static absl::StatusOr> create(const envoy::config::cluster::v3::Cluster& cluster, const envoy::extensions::clusters::redis::v3::RedisClusterConfig& redis_cluster, @@ -302,7 +303,10 @@ class RedisCluster : public Upstream::BaseDynamicClusterImpl { const std::string auth_password_; const std::string cluster_name_; const Common::Redis::ClusterRefreshManagerSharedPtr refresh_manager_; - const Common::Redis::ClusterRefreshManager::HandlePtr registration_handle_; + Common::Redis::ClusterRefreshManager::HandlePtr registration_handle_; + + // Flag to prevent callbacks during destruction + std::atomic is_destroying_{false}; }; class RedisClusterFactory : public Upstream::ConfigurableClusterFactoryBase< diff --git a/source/extensions/clusters/reverse_connection/BUILD b/source/extensions/clusters/reverse_connection/BUILD new file mode 100644 index 0000000000000..61414b383c5d8 --- /dev/null +++ b/source/extensions/clusters/reverse_connection/BUILD @@ -0,0 +1,32 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "reverse_connection_lib", + srcs = ["reverse_connection.cc"], + hdrs = ["reverse_connection.h"], + visibility = ["//visibility:public"], + deps = [ + "//envoy/upstream:cluster_factory_interface", + "//source/common/http:header_utility_lib", + "//source/common/http/matching:data_impl_lib", + "//source/common/http/matching:inputs_lib", + "//source/common/matcher:matcher_lib", + "//source/common/network:address_lib", + "//source/common/upstream:cluster_factory_lib", + "//source/common/upstream:upstream_includes", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//source/extensions/transport_sockets/raw_buffer:config", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/endpoint/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/clusters/reverse_connection/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/clusters/reverse_connection/reverse_connection.cc b/source/extensions/clusters/reverse_connection/reverse_connection.cc new file mode 100644 index 0000000000000..ad53f97f2a264 --- /dev/null +++ b/source/extensions/clusters/reverse_connection/reverse_connection.cc @@ -0,0 +1,255 @@ +#include "source/extensions/clusters/reverse_connection/reverse_connection.h" + +#include +#include +#include +#include + +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/core/v3/health_check.pb.h" +#include "envoy/config/endpoint/v3/endpoint_components.pb.h" + +#include "source/common/http/matching/data_impl.h" +#include "source/common/matcher/matcher.h" +#include "source/common/network/address_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" + +#include "absl/status/status.h" +#include "absl/status/statusor.h" + +namespace Envoy { +namespace Extensions { +namespace ReverseConnection { + +namespace BootstrapReverseConnection = Envoy::Extensions::Bootstrap::ReverseConnection; + +using HostIdActionProto = envoy::extensions::clusters::reverse_connection::v3::HostIdAction; + +// Action type that carries the host identifier returned by the matcher. +class HostIdAction : public Envoy::Matcher::ActionBase { +public: + explicit HostIdAction(std::string host_id) : host_id_(std::move(host_id)) {} + const std::string& host_id() const { return host_id_; } + +private: + const std::string host_id_; +}; + +// Factory to construct HostIdAction from proto. +class HostIdActionFactory : public Envoy::Matcher::ActionFactory { +public: + std::string name() const override { return "envoy.matching.actions.reverse_connection.host_id"; } + Envoy::Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, Upstream::ClusterFactoryContext&, + ProtobufMessage::ValidationVisitor& validation_visitor) override { + const auto& proto = + MessageUtil::downcastAndValidate(config, validation_visitor); + return std::make_shared(proto.host_id()); + } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + +REGISTER_FACTORY(HostIdActionFactory, + Envoy::Matcher::ActionFactory); + +Upstream::HostSelectionResponse +RevConCluster::LoadBalancer::chooseHost(Upstream::LoadBalancerContext* context) { + if (context == nullptr) { + ENVOY_LOG(error, "reverse_connection: chooseHost called with null context"); + return {nullptr}; + } + + // Evaluate the configured host-id matcher to obtain the host identifier. + if (context->downstreamHeaders() == nullptr) { + ENVOY_LOG(error, "reverse_connection: missing downstream headers; cannot evaluate matcher."); + return {nullptr}; + } + Http::Matching::HttpMatchingDataImpl data(context->downstreamConnection()->streamInfo()); + data.onRequestHeaders(*context->downstreamHeaders()); + const ::Envoy::Matcher::MatchResult result = + ::Envoy::Matcher::evaluateMatch(*parent_->host_id_match_tree_, + data); + if (!result.isMatch()) { + ENVOY_LOG(error, "reverse_connection: host_id matcher did not match."); + return {nullptr}; + } + const auto& action = result.action(); + ASSERT(action != nullptr); + const auto& host_id_action = action->getTyped(); + absl::string_view host_id_sv = host_id_action.host_id(); + ENVOY_LOG(debug, "reverse_connection: using host identifier from matcher action: {}", host_id_sv); + return parent_->checkAndCreateHost(host_id_sv); +} + +Upstream::HostSelectionResponse RevConCluster::checkAndCreateHost(absl::string_view host_id) { + + // Get the SocketManager to resolve cluster ID to node ID. + auto* socket_manager = getUpstreamSocketManager(); + if (socket_manager == nullptr) { + ENVOY_LOG(error, + "reverse_connection: cannot create host for key: {}; socket manager not found.", + host_id); + return {nullptr}; + } + + // Use SocketManager to resolve the key to a node ID. + std::string node_id = socket_manager->getNodeID(std::string(host_id)); + ENVOY_LOG(debug, "reverse_connection: resolved key '{}' to node: '{}'", host_id, node_id); + + host_map_lock_.ReaderLock(); + // Check if node_id is already present in host_map_ or not. This ensures, + // that envoy reuses a conn_pool_container for an endpoint. + auto host_itr = host_map_.find(node_id); + if (host_itr != host_map_.end()) { + ENVOY_LOG(debug, "reverse_connection: reusing existing host for {}.", node_id); + Upstream::HostSharedPtr host = host_itr->second; + host_map_lock_.ReaderUnlock(); + return {host}; + } + host_map_lock_.ReaderUnlock(); + + absl::WriterMutexLock wlock(&host_map_lock_); + + // Re-check under writer lock to avoid duplicate creation under contention. + auto host_itr2 = host_map_.find(node_id); + if (host_itr2 != host_map_.end()) { + ENVOY_LOG(debug, "reverse_connection: host already created for {} during contention.", node_id); + return {host_itr2->second}; + } + + // Create a custom address that uses the UpstreamReverseSocketInterface. + Network::Address::InstanceConstSharedPtr host_address( + std::make_shared(node_id)); + + // Create a standard HostImpl using the custom address. + auto host_result = Upstream::HostImpl::create( + info(), absl::StrCat(info()->name(), static_cast(node_id)), + std::move(host_address), nullptr /* endpoint_metadata */, nullptr /* locality_metadata */, + 1 /* initial_weight */, envoy::config::core::v3::Locality().default_instance(), + envoy::config::endpoint::v3::Endpoint::HealthCheckConfig().default_instance(), + 0 /* priority */, envoy::config::core::v3::UNKNOWN); + + if (!host_result.ok()) { + ENVOY_LOG(error, "reverse_connection: failed to create HostImpl for {}: {}", node_id, + host_result.status().ToString()); + return {nullptr}; + } + + // Convert unique_ptr to shared_ptr. + Upstream::HostSharedPtr host(std::move(host_result.value())); + ENVOY_LOG(trace, "reverse_connection: created HostImpl {} for {}.", *host, node_id); + + host_map_[node_id] = host; + return {host}; +} + +void RevConCluster::cleanup() { + absl::WriterMutexLock wlock(&host_map_lock_); + + for (auto iter = host_map_.begin(); iter != host_map_.end();) { + // Check if the host handle is acquired by any connection pool container or not. If not + // clean those host to prevent memory leakage. + const auto& host = iter->second; + if (!host->used()) { + ENVOY_LOG(debug, "Removing stale host: {}", *host); + host_map_.erase(iter++); + } else { + ++iter; + } + } + + // Reschedule the cleanup after cleanup_interval_ duration. + cleanup_timer_->enableTimer(cleanup_interval_); +} + +BootstrapReverseConnection::UpstreamSocketManager* RevConCluster::getUpstreamSocketManager() const { + auto* upstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (upstream_interface == nullptr) { + ENVOY_LOG(error, "Upstream reverse socket interface not found"); + return nullptr; + } + + auto* upstream_socket_interface = + dynamic_cast(upstream_interface); + if (!upstream_socket_interface) { + ENVOY_LOG(error, "Failed to cast to ReverseTunnelAcceptor"); + return nullptr; + } + + auto* tls_registry = upstream_socket_interface->getLocalRegistry(); + if (!tls_registry) { + ENVOY_LOG(error, "Thread local registry not found for upstream socket interface"); + return nullptr; + } + + return tls_registry->socketManager(); +} + +RevConCluster::RevConCluster( + const envoy::config::cluster::v3::Cluster& config, Upstream::ClusterFactoryContext& context, + absl::Status& creation_status, + const envoy::extensions::clusters::reverse_connection::v3::RevConClusterConfig& rev_con_config) + : ClusterImplBase(config, context, creation_status), + dispatcher_(context.serverFactoryContext().mainThreadDispatcher()), + cleanup_interval_(std::chrono::milliseconds( + PROTOBUF_GET_MS_OR_DEFAULT(rev_con_config, cleanup_interval, 60000))), + cleanup_timer_(dispatcher_.createTimer([this]() -> void { cleanup(); })) { + // Build the host-id matcher tree. + // No-op validation visitor for building the match tree. + struct NoopValidationVisitor + : public Envoy::Matcher::MatchTreeValidationVisitor { + absl::Status performDataInputValidation( + const Envoy::Matcher::DataInputFactory&, + absl::string_view) override { + return absl::OkStatus(); + } + } validation_visitor; + Envoy::Matcher::MatchTreeFactory + factory(context, context.serverFactoryContext(), validation_visitor); + Envoy::Matcher::MatchTreeFactoryCb cb = + factory.create(rev_con_config.host_id_matcher()); + host_id_match_tree_ = cb(); + + // Schedule periodic cleanup. + cleanup_timer_->enableTimer(cleanup_interval_); +} + +absl::StatusOr> +RevConClusterFactory::createClusterWithConfig( + const envoy::config::cluster::v3::Cluster& cluster, + const envoy::extensions::clusters::reverse_connection::v3::RevConClusterConfig& proto_config, + Upstream::ClusterFactoryContext& context) { + if (cluster.lb_policy() != envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED) { + return absl::InvalidArgumentError( + fmt::format("cluster: LB policy {} is not valid for Cluster type {}. Only " + "'CLUSTER_PROVIDED' is allowed with cluster type 'REVERSE_CONNECTION'", + envoy::config::cluster::v3::Cluster::LbPolicy_Name(cluster.lb_policy()), + cluster.cluster_type().name())); + } + + if (cluster.has_load_assignment()) { + return absl::InvalidArgumentError( + "Reverse Conn clusters must have no load assignment configured"); + } + + absl::Status creation_status = absl::OkStatus(); + auto new_cluster = std::shared_ptr( + new RevConCluster(cluster, context, creation_status, proto_config)); + RETURN_IF_NOT_OK(creation_status); + auto lb = std::make_unique(new_cluster); + return std::make_pair(new_cluster, std::move(lb)); +} + +/** + * Static registration for the rev-con cluster factory. @see RegisterFactory. + */ +REGISTER_FACTORY(RevConClusterFactory, Upstream::ClusterFactory); + +} // namespace ReverseConnection +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/clusters/reverse_connection/reverse_connection.h b/source/extensions/clusters/reverse_connection/reverse_connection.h new file mode 100644 index 0000000000000..77cebd657bb9f --- /dev/null +++ b/source/extensions/clusters/reverse_connection/reverse_connection.h @@ -0,0 +1,244 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/extensions/clusters/reverse_connection/v3/reverse_connection.pb.h" +#include "envoy/extensions/clusters/reverse_connection/v3/reverse_connection.pb.validate.h" + +#include "source/common/common/logger.h" +#include "source/common/http/matching/data_impl.h" +#include "source/common/matcher/matcher.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/common/upstream/cluster_factory_impl.h" +#include "source/common/upstream/upstream_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +#include "absl/status/statusor.h" + +namespace Envoy { +namespace Extensions { +namespace ReverseConnection { + +namespace BootstrapReverseConnection = Envoy::Extensions::Bootstrap::ReverseConnection; + +/** + * Custom address type that uses the UpstreamReverseSocketInterface. + * This address will be used by RevConHost to ensure socket creation goes through + * the upstream socket interface. + */ +class UpstreamReverseConnectionAddress + : public Network::Address::Instance, + public Envoy::Logger::Loggable { +public: + UpstreamReverseConnectionAddress(const std::string& node_id) + : node_id_(node_id), address_string_("127.0.0.1:0") { + + // Create a simple socket address for filter chain matching. + // Use 127.0.0.1:0 which will match the catch-all filter chain + synthetic_sockaddr_.sin_family = AF_INET; + synthetic_sockaddr_.sin_port = htons(0); // Port 0 for reverse connections + synthetic_sockaddr_.sin_addr.s_addr = htonl(0x7f000001); // 127.0.0.1 + memset(&synthetic_sockaddr_.sin_zero, 0, sizeof(synthetic_sockaddr_.sin_zero)); + + ENVOY_LOG( + debug, + "UpstreamReverseConnectionAddress: node: {} using 127.0.0.1:0 for filter chain matching", + node_id_); + } + + // Network::Address::Instance. + bool operator==(const Instance& rhs) const override { + const auto* other = dynamic_cast(&rhs); + return other && node_id_ == other->node_id_; + } + + Network::Address::Type type() const override { return Network::Address::Type::Ip; } + const std::string& asString() const override { return address_string_; } + absl::string_view asStringView() const override { return address_string_; } + const std::string& logicalName() const override { return node_id_; } + const Network::Address::Ip* ip() const override { return &ip_; } + const Network::Address::Pipe* pipe() const override { return nullptr; } + const Network::Address::EnvoyInternalAddress* envoyInternalAddress() const override { + return nullptr; + } + const sockaddr* sockAddr() const override { + return reinterpret_cast(&synthetic_sockaddr_); + } + socklen_t sockAddrLen() const override { return sizeof(synthetic_sockaddr_); } + // Set to default so that the default client connection factory is used to initiate connections + // to. the address. + absl::string_view addressType() const override { return "default"; } + absl::optional networkNamespace() const override { return absl::nullopt; } + + // Override socketInterface to use the ReverseTunnelAcceptor. + const Network::SocketInterface& socketInterface() const override { + ENVOY_LOG(debug, "UpstreamReverseConnectionAddress: socketInterface() called for node: {}", + node_id_); + auto* upstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (upstream_interface) { + ENVOY_LOG(debug, "UpstreamReverseConnectionAddress: Using ReverseTunnelAcceptor for node: {}", + node_id_); + return *upstream_interface; + } + // Fallback to default socket interface if upstream interface is not available. + return *Network::socketInterface( + "envoy.extensions.network.socket_interface.default_socket_interface"); + } + +private: + // Simple IPv4 implementation for upstream reverse connection addresses. + struct UpstreamReverseConnectionIp : public Network::Address::Ip { + const std::string& addressAsString() const override { return address_string_; } + bool isAnyAddress() const override { return true; } + bool isUnicastAddress() const override { return false; } + const Network::Address::Ipv4* ipv4() const override { return nullptr; } + const Network::Address::Ipv6* ipv6() const override { return nullptr; } + uint32_t port() const override { return 0; } + Network::Address::IpVersion version() const override { return Network::Address::IpVersion::v4; } + + // Additional pure virtual methods that need implementation. + bool isLinkLocalAddress() const override { return false; } + bool isUniqueLocalAddress() const override { return false; } + bool isSiteLocalAddress() const override { return false; } + bool isTeredoAddress() const override { return false; } + + std::string address_string_{"0.0.0.0:0"}; + }; + + std::string node_id_; + std::string address_string_; + UpstreamReverseConnectionIp ip_; + struct sockaddr_in synthetic_sockaddr_; // Socket address for filter chain matching +}; + +/** + * The RevConCluster is a dynamic cluster that automatically adds hosts using + * request context of the downstream connection. Later, these hosts are used + * to retrieve reverse connection sockets to stream data to upstream endpoints. + * Also, the RevConCluster cleans these hosts if no connection pool is using them. + */ +class RevConCluster : public Upstream::ClusterImplBase { + friend class ReverseConnectionClusterTest; + +public: + RevConCluster(const envoy::config::cluster::v3::Cluster& config, + Upstream::ClusterFactoryContext& context, absl::Status& creation_status, + const envoy::extensions::clusters::reverse_connection::v3::RevConClusterConfig& + rev_con_config); + + ~RevConCluster() override { cleanup_timer_->disableTimer(); } + + // Upstream::Cluster. + InitializePhase initializePhase() const override { return InitializePhase::Primary; } + + class LoadBalancer : public Upstream::LoadBalancer { + public: + LoadBalancer(const std::shared_ptr& parent) : parent_(parent) {} + + // Chooses a host to send a downstream request over a reverse connection endpoint. + // The request MUST provide a host identifier via dynamic metadata populated by a matcher + // action. No header or authority/SNI fallbacks are used. + Upstream::HostSelectionResponse chooseHost(Upstream::LoadBalancerContext* context) override; + + // Virtual functions that are not supported by our custom load-balancer. + Upstream::HostConstSharedPtr peekAnotherHost(Upstream::LoadBalancerContext*) override { + return nullptr; + } + absl::optional + selectExistingConnection(Upstream::LoadBalancerContext* /*context*/, + const Upstream::Host& /*host*/, + std::vector& /*hash_key*/) override { + return absl::nullopt; + } + + // Lifetime tracking not implemented. + OptRef lifetimeCallbacks() override { + return {}; + } + + private: + const std::shared_ptr parent_; + }; + +private: + struct LoadBalancerFactory : public Upstream::LoadBalancerFactory { + LoadBalancerFactory(const std::shared_ptr& cluster) : cluster_(cluster) {} + + // Upstream::LoadBalancerFactory. + Upstream::LoadBalancerPtr create() { return std::make_unique(cluster_); } + Upstream::LoadBalancerPtr create(Upstream::LoadBalancerParams) override { return create(); } + + const std::shared_ptr cluster_; + }; + + struct ThreadAwareLoadBalancer : public Upstream::ThreadAwareLoadBalancer { + ThreadAwareLoadBalancer(const std::shared_ptr& cluster) : cluster_(cluster) {} + + // Upstream::ThreadAwareLoadBalancer. + Upstream::LoadBalancerFactorySharedPtr factory() override { + return std::make_shared(cluster_); + } + absl::Status initialize() override { return absl::OkStatus(); } + + const std::shared_ptr cluster_; + }; + + // Periodically cleans the stale hosts from host_map_. + void cleanup(); + + // Checks if a host exists for a given host identifier and if not creates and caches it. + Upstream::HostSelectionResponse checkAndCreateHost(absl::string_view host_id); + + // Get the upstream socket manager from the thread-local registry. + BootstrapReverseConnection::UpstreamSocketManager* getUpstreamSocketManager() const; + + // No pre-initialize work needs to be completed by REVERSE CONNECTION cluster. + void startPreInit() override { onPreInitComplete(); } + + Event::Dispatcher& dispatcher_; + std::chrono::milliseconds cleanup_interval_; + Event::TimerPtr cleanup_timer_; + absl::Mutex host_map_lock_; + absl::flat_hash_map host_map_; + // Match tree that yields a HostIdAction. + Envoy::Matcher::MatchTreeSharedPtr host_id_match_tree_; + // Metadata namespace and key for the host identifier, populated by a matcher action. + static constexpr absl::string_view kMetadataNamespace{"reverse_connection"}; + static constexpr absl::string_view kHostIdKey{"host_id"}; + friend class RevConClusterFactory; +}; + +using RevConClusterSharedPtr = std::shared_ptr; + +class RevConClusterFactory + : public Upstream::ConfigurableClusterFactoryBase< + envoy::extensions::clusters::reverse_connection::v3::RevConClusterConfig> { +public: + RevConClusterFactory() : ConfigurableClusterFactoryBase("envoy.clusters.reverse_connection") {} + +private: + friend class ReverseConnectionClusterTest; + absl::StatusOr< + std::pair> + createClusterWithConfig( + const envoy::config::cluster::v3::Cluster& cluster, + const envoy::extensions::clusters::reverse_connection::v3::RevConClusterConfig& proto_config, + Upstream::ClusterFactoryContext& context) override; +}; + +DECLARE_FACTORY(RevConClusterFactory); + +} // namespace ReverseConnection +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/clusters/static/static_cluster.cc b/source/extensions/clusters/static/static_cluster.cc index 9ecad92156e4e..25f8e18537f36 100644 --- a/source/extensions/clusters/static/static_cluster.cc +++ b/source/extensions/clusters/static/static_cluster.cc @@ -12,7 +12,7 @@ StaticClusterImpl::StaticClusterImpl(const envoy::config::cluster::v3::Cluster& : ClusterImplBase(cluster, context, creation_status) { SET_AND_RETURN_IF_NOT_OK(creation_status, creation_status); priority_state_manager_ = std::make_unique( - *this, context.serverFactoryContext().localInfo(), nullptr, random_); + *this, context.serverFactoryContext().localInfo(), nullptr); const envoy::config::endpoint::v3::ClusterLoadAssignment& cluster_load_assignment = cluster.load_assignment(); overprovisioning_factor_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( @@ -20,7 +20,6 @@ StaticClusterImpl::StaticClusterImpl(const envoy::config::cluster::v3::Cluster& weighted_priority_health_ = cluster_load_assignment.policy().weighted_priority_health(); for (const auto& locality_lb_endpoint : cluster_load_assignment.endpoints()) { - THROW_IF_NOT_OK(validateEndpointsForZoneAwareRouting(locality_lb_endpoint)); priority_state_manager_->initializePriorityFor(locality_lb_endpoint); for (const auto& lb_endpoint : locality_lb_endpoint.lb_endpoints()) { std::vector address_list; @@ -47,6 +46,9 @@ StaticClusterImpl::StaticClusterImpl(const envoy::config::cluster::v3::Cluster& address_list, locality_lb_endpoint, lb_endpoint); } } + + THROW_IF_NOT_OK(validateEndpoints(cluster_load_assignment.endpoints(), + priority_state_manager_->priorityState())); } void StaticClusterImpl::startPreInit() { diff --git a/source/extensions/clusters/strict_dns/strict_dns_cluster.cc b/source/extensions/clusters/strict_dns/strict_dns_cluster.cc index 77123a17f7734..49f281be43ddb 100644 --- a/source/extensions/clusters/strict_dns/strict_dns_cluster.cc +++ b/source/extensions/clusters/strict_dns/strict_dns_cluster.cc @@ -42,15 +42,16 @@ StrictDnsClusterImpl::StrictDnsClusterImpl( respect_dns_ttl_(dns_cluster.respect_dns_ttl()), dns_lookup_family_( Envoy::DnsUtils::getDnsLookupFamilyFromEnum(dns_cluster.dns_lookup_family())) { + RETURN_ONLY_IF_NOT_OK_REF(creation_status); + failure_backoff_strategy_ = Config::Utility::prepareDnsRefreshStrategy( dns_cluster, dns_refresh_rate_ms_.count(), context.serverFactoryContext().api().randomGenerator()); std::list resolve_targets; const auto& locality_lb_endpoints = load_assignment_.endpoints(); + THROW_IF_NOT_OK(validateEndpoints(locality_lb_endpoints, {})); for (const auto& locality_lb_endpoint : locality_lb_endpoints) { - THROW_IF_NOT_OK(validateEndpointsForZoneAwareRouting(locality_lb_endpoint)); - for (const auto& lb_endpoint : locality_lb_endpoint.lb_endpoints()) { const auto& socket_address = lb_endpoint.endpoint().address().socket_address(); if (!socket_address.resolver_name().empty()) { @@ -83,7 +84,7 @@ void StrictDnsClusterImpl::startPreInit() { void StrictDnsClusterImpl::updateAllHosts(const HostVector& hosts_added, const HostVector& hosts_removed, uint32_t current_priority) { - PriorityStateManager priority_state_manager(*this, local_info_, nullptr, random_); + PriorityStateManager priority_state_manager(*this, local_info_, nullptr); // At this point we know that we are different so make a new host list and notify. // // TODO(dio): The uniqueness of a host address resolved in STRICT_DNS cluster per priority is not @@ -233,27 +234,5 @@ void StrictDnsClusterImpl::ResolveTarget::startResolve() { }); } -absl::StatusOr> -StrictDnsClusterFactory::createClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, - Upstream::ClusterFactoryContext& context) { - absl::StatusOr> cluster_or_error; - auto dns_resolver_or_error = selectDnsResolver(cluster, context); - RETURN_IF_NOT_OK(dns_resolver_or_error.status()); - - envoy::extensions::clusters::dns::v3::DnsCluster proto_config_legacy{}; - createDnsClusterFromLegacyFields(cluster, proto_config_legacy); - cluster_or_error = StrictDnsClusterImpl::create(cluster, proto_config_legacy, context, - std::move(*dns_resolver_or_error)); - - RETURN_IF_NOT_OK(cluster_or_error.status()); - return std::make_pair(std::shared_ptr(std::move(*cluster_or_error)), - nullptr); -} - -/** - * Static registration for the strict dns cluster factory. @see RegisterFactory. - */ -REGISTER_FACTORY(StrictDnsClusterFactory, ClusterFactory); - } // namespace Upstream } // namespace Envoy diff --git a/source/extensions/clusters/strict_dns/strict_dns_cluster.h b/source/extensions/clusters/strict_dns/strict_dns_cluster.h index 0f15b399b794b..ff8ac1a139dbb 100644 --- a/source/extensions/clusters/strict_dns/strict_dns_cluster.h +++ b/source/extensions/clusters/strict_dns/strict_dns_cluster.h @@ -81,20 +81,5 @@ class StrictDnsClusterImpl : public BaseDynamicClusterImpl { bool weighted_priority_health_; }; -/** - * Factory for StrictDnsClusterImpl - */ -class StrictDnsClusterFactory : public ClusterFactoryImplBase { -public: - StrictDnsClusterFactory() : ClusterFactoryImplBase("envoy.cluster.strict_dns") {} - -private: - absl::StatusOr> - createClusterImpl(const envoy::config::cluster::v3::Cluster& cluster, - ClusterFactoryContext& context) override; -}; - -DECLARE_FACTORY(StrictDnsClusterFactory); - } // namespace Upstream } // namespace Envoy diff --git a/source/extensions/common/aws/credential_provider_chains.cc b/source/extensions/common/aws/credential_provider_chains.cc index b053494ea4606..a79478da98f87 100644 --- a/source/extensions/common/aws/credential_provider_chains.cc +++ b/source/extensions/common/aws/credential_provider_chains.cc @@ -39,12 +39,27 @@ CommonCredentialsProviderChain::customCredentialsProviderChain( "Custom credential provider chain must have at least one credential provider"); } - return std::make_shared(context, region, - credential_provider_config); + auto chain = + std::make_shared(context, region, credential_provider_config); + chain->setupSubscriptions(); + return chain; } CredentialsProviderChainSharedPtr CommonCredentialsProviderChain::defaultCredentialsProviderChain( Server::Configuration::ServerFactoryContext& context, absl::string_view region) { - return std::make_shared(context, region, absl::nullopt); + auto chain = std::make_shared(context, region, absl::nullopt); + chain->setupSubscriptions(); + return chain; +} + +void CommonCredentialsProviderChain::setupSubscriptions() { + for (auto& provider : providers_) { + // Set up subscription for each provider that supports it + auto metadata_provider = std::dynamic_pointer_cast(provider); + if (metadata_provider) { + storeSubscription(metadata_provider->subscribeToCredentialUpdates( + std::static_pointer_cast(shared_from_this()))); + } + } } CommonCredentialsProviderChain::CommonCredentialsProviderChain( @@ -118,11 +133,16 @@ CommonCredentialsProviderChain::CommonCredentialsProviderChain( } if (chain_to_create.has_assume_role_credential_provider()) { - const auto& assume_role_config = chain_to_create.assume_role_credential_provider(); + auto assume_role_config = chain_to_create.assume_role_credential_provider(); const auto sts_endpoint = Utility::getSTSEndpoint(region) + ":443"; const auto cluster_name = stsClusterName(region); + // Default session name if not provided. + if (assume_role_config.role_session_name().empty()) { + assume_role_config.set_role_session_name(sessionName(context.api())); + } + ENVOY_LOG(debug, "Using assumerole credentials provider with STS endpoint: {} and session name: {}", sts_endpoint, assume_role_config.role_session_name()); @@ -267,8 +287,7 @@ CredentialsProviderSharedPtr CommonCredentialsProviderChain::createAssumeRoleCre credential_provider->setClusterReadyCallbackHandle(std::move(handleOr.value())); } - storeSubscription(credential_provider->subscribeToCredentialUpdates(*this)); - + // Note: Subscription will be set up after construction return credential_provider; }; @@ -303,8 +322,7 @@ CredentialsProviderSharedPtr CommonCredentialsProviderChain::createContainerCred credential_provider->setClusterReadyCallbackHandle(std::move(handleOr.value())); } - storeSubscription(credential_provider->subscribeToCredentialUpdates(*this)); - + // Note: Subscription will be set up after construction return credential_provider; } @@ -337,8 +355,7 @@ CommonCredentialsProviderChain::createInstanceProfileCredentialsProvider( credential_provider->setClusterReadyCallbackHandle(std::move(handleOr.value())); } - storeSubscription(credential_provider->subscribeToCredentialUpdates(*this)); - + // Note: Subscription will be set up after construction return credential_provider; } @@ -369,8 +386,7 @@ CredentialsProviderSharedPtr CommonCredentialsProviderChain::createWebIdentityCr credential_provider->setClusterReadyCallbackHandle(std::move(handleOr.value())); } - storeSubscription(credential_provider->subscribeToCredentialUpdates(*this)); - + // Note: Subscription will be set up after construction return credential_provider; }; diff --git a/source/extensions/common/aws/credential_provider_chains.h b/source/extensions/common/aws/credential_provider_chains.h index 070714f7cd145..d2e4eb908e1a5 100644 --- a/source/extensions/common/aws/credential_provider_chains.h +++ b/source/extensions/common/aws/credential_provider_chains.h @@ -118,6 +118,10 @@ class CommonCredentialsProviderChain : public CredentialsProviderChain, defaultCredentialsProviderChain(Server::Configuration::ServerFactoryContext& context, absl::string_view region); + // Where credential providers use async functionality, subscribe to credential notifications for + // these providers + void setupSubscriptions(); + private: CredentialsProviderSharedPtr createEnvironmentCredentialsProvider() const override { return std::make_shared(); diff --git a/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.cc b/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.cc index 5ca89611c5114..8ecccc34b850e 100644 --- a/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.cc +++ b/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.cc @@ -31,6 +31,7 @@ AssumeRoleCredentialsProvider::AssumeRoleCredentialsProvider( initialization_timer), role_arn_(assume_role_config.role_arn()), role_session_name_(assume_role_config.role_session_name()), region_(region), + external_id_(assume_role_config.external_id()), assume_role_signer_(std::move(assume_role_signer)) { if (assume_role_config.has_session_duration()) { @@ -71,10 +72,20 @@ void AssumeRoleCredentialsProvider::continueRefresh() { message.headers().setScheme(Http::Headers::get().SchemeValues.Https); message.headers().setMethod(Http::Headers::get().MethodValues.Get); message.headers().setHost(Http::Utility::parseAuthority(uri.value()).host_); - message.headers().setPath( + std::string path = fmt::format("/?Version=2011-06-15&Action=AssumeRole&RoleArn={}&RoleSessionName={}", Envoy::Http::Utility::PercentEncoding::encode(role_arn_), - Envoy::Http::Utility::PercentEncoding::encode(role_session_name_))); + Envoy::Http::Utility::PercentEncoding::encode(role_session_name_)); + if (session_duration_) { + path += fmt::format("&DurationSeconds={}", session_duration_.value()); + } + + if (!external_id_.empty()) { + path += + fmt::format("&ExternalId={}", Envoy::Http::Utility::PercentEncoding::encode(external_id_)); + } + + message.headers().setPath(path); // Use the Accept header to ensure that AssumeRoleResponse is returned as JSON. message.headers().setReference(Http::CustomHeaders::get().Accept, Http::Headers::get().ContentTypeValues.Json); diff --git a/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.h b/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.h index c1c1a8e2d6001..0902e7bfbd94d 100644 --- a/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.h +++ b/source/extensions/common/aws/credential_providers/assume_role_credentials_provider.h @@ -44,6 +44,7 @@ class AssumeRoleCredentialsProvider : public MetadataCredentialsProviderBase, const std::string role_arn_; const std::string role_session_name_; const std::string region_; + const std::string external_id_; absl::optional session_duration_; std::unique_ptr assume_role_signer_; }; diff --git a/source/extensions/common/aws/credential_providers/iam_roles_anywhere_credentials_provider.cc b/source/extensions/common/aws/credential_providers/iam_roles_anywhere_credentials_provider.cc index 33e472163c337..c59516cb9ee23 100644 --- a/source/extensions/common/aws/credential_providers/iam_roles_anywhere_credentials_provider.cc +++ b/source/extensions/common/aws/credential_providers/iam_roles_anywhere_credentials_provider.cc @@ -74,7 +74,7 @@ void IAMRolesAnywhereCredentialsProvider::refresh() { message.headers().setPath("/sessions"); message.headers().setContentType("application/json"); - auto json_message = ProtobufWkt::Struct(); + auto json_message = Protobuf::Struct(); auto& fields = *json_message.mutable_fields(); fields["profileArn"].set_string_value(profile_arn_); fields["roleArn"].set_string_value(role_arn_); diff --git a/source/extensions/common/aws/credentials_provider.h b/source/extensions/common/aws/credentials_provider.h index fafe0ba470bba..45033fefd9d2e 100644 --- a/source/extensions/common/aws/credentials_provider.h +++ b/source/extensions/common/aws/credentials_provider.h @@ -165,20 +165,28 @@ class CredentialSubscriberCallbacks { virtual void onCredentialUpdate() PURE; }; +using CredentialSubscriberCallbacksSharedPtr = std::shared_ptr; + // Subscription model allowing CredentialsProviderChains to be notified of credential provider // updates. A credential provider chain will call credential_provider->subscribeToCredentialUpdates // to register itself for updates via onCredentialUpdate callback. When a credential provider has // successfully updated all threads with new credentials, via the setCredentialsToAllThreads method // it will notify all subscribers that credentials have been retrieved. +// +// Subscription is only relevant for metadata credentials providers, as these are the only +// credential providers that implement async credential retrieval functionality. +// // RAII is used, as credential providers may be instantiated as singletons, as such they may outlive -// the credential provider chain. Subscription is only relevant for metadata credentials providers, -// as these are the only credential providers that implement async credential retrieval -// functionality. -class CredentialSubscriberCallbacksHandle : public RaiiListElement { +// the credential provider chain. +// +// Uses weak_ptr to safely handle subscriber lifetime without dangling pointers. +class CredentialSubscriberCallbacksHandle + : public RaiiListElement> { public: - CredentialSubscriberCallbacksHandle(CredentialSubscriberCallbacks& cb, - std::list& parent) - : RaiiListElement(parent, &cb) {} + CredentialSubscriberCallbacksHandle( + CredentialSubscriberCallbacksSharedPtr cb, + std::list>& parent) + : RaiiListElement>(parent, cb) {} }; using CredentialSubscriberCallbacksHandlePtr = std::unique_ptr; @@ -187,7 +195,8 @@ using CredentialSubscriberCallbacksHandlePtr = std::unique_ptr { + public Logger::Loggable, + public std::enable_shared_from_this { public: ~CredentialsProviderChain() override { for (auto& subscriber_handle : subscriber_handles_) { diff --git a/source/extensions/common/aws/metadata_credentials_provider_base.cc b/source/extensions/common/aws/metadata_credentials_provider_base.cc index ef1af1a4aa8ff..90f42bd7b8883 100644 --- a/source/extensions/common/aws/metadata_credentials_provider_base.cc +++ b/source/extensions/common/aws/metadata_credentials_provider_base.cc @@ -34,10 +34,17 @@ MetadataCredentialsProviderBase::MetadataCredentialsProviderBase( void MetadataCredentialsProviderBase::onClusterAddOrUpdate() { ENVOY_LOG(debug, "Received callback from aws cluster manager for cluster {}", cluster_name_); if (!cache_duration_timer_) { - cache_duration_timer_ = context_.mainThreadDispatcher().createTimer([this]() -> void { - stats_->credential_refreshes_performed_.inc(); - refresh(); - }); + std::weak_ptr weak_stats = stats_; + std::weak_ptr weak_self = shared_from_this(); + cache_duration_timer_ = + context_.mainThreadDispatcher().createTimer([weak_stats, weak_self]() -> void { + if (auto stats = weak_stats.lock()) { + stats->credential_refreshes_performed_.inc(); + } + if (auto self = weak_self.lock()) { + self->refresh(); + } + }); } if (!cache_duration_timer_->enabled()) { cache_duration_timer_->enableTimer(std::chrono::milliseconds(1)); @@ -116,21 +123,24 @@ void MetadataCredentialsProviderBase::setCredentialsToAllThreads( /* Notify waiting signers on completion of credential setting above */ [this]() { credentials_pending_.store(false); - std::list subscribers_copy; + std::list> subscribers_copy; { Thread::LockGuard guard(mu_); subscribers_copy = credentials_subscribers_; } - for (auto& cb : subscribers_copy) { - ENVOY_LOG(debug, "Notifying subscriber of credential update"); - cb->onCredentialUpdate(); + for (auto& weak_cb : subscribers_copy) { + if (auto cb = weak_cb.lock()) { + ENVOY_LOG(debug, "Notifying subscriber of credential update"); + cb->onCredentialUpdate(); + } } }); } } CredentialSubscriberCallbacksHandlePtr -MetadataCredentialsProviderBase::subscribeToCredentialUpdates(CredentialSubscriberCallbacks& cs) { +MetadataCredentialsProviderBase::subscribeToCredentialUpdates( + CredentialSubscriberCallbacksSharedPtr cs) { Thread::LockGuard guard(mu_); return std::make_unique(cs, credentials_subscribers_); } diff --git a/source/extensions/common/aws/metadata_credentials_provider_base.h b/source/extensions/common/aws/metadata_credentials_provider_base.h index 472d9936fca63..7cdefefae4939 100644 --- a/source/extensions/common/aws/metadata_credentials_provider_base.h +++ b/source/extensions/common/aws/metadata_credentials_provider_base.h @@ -27,9 +27,11 @@ struct MetadataCredentialsProviderStats { using CreateMetadataFetcherCb = std::function; -class MetadataCredentialsProviderBase : public CredentialsProvider, - public Logger::Loggable, - public AwsManagedClusterUpdateCallbacks { +class MetadataCredentialsProviderBase + : public CredentialsProvider, + public Logger::Loggable, + public AwsManagedClusterUpdateCallbacks, + public std::enable_shared_from_this { public: friend class MetadataCredentialsProviderBaseFriend; using OnAsyncFetchCb = std::function; @@ -53,7 +55,7 @@ class MetadataCredentialsProviderBase : public CredentialsProvider, } CredentialSubscriberCallbacksHandlePtr - subscribeToCredentialUpdates(CredentialSubscriberCallbacks& cs); + subscribeToCredentialUpdates(CredentialSubscriberCallbacksSharedPtr cs); protected: struct ThreadLocalCredentialsCache : public ThreadLocal::ThreadLocalObject { @@ -120,7 +122,8 @@ class MetadataCredentialsProviderBase : public CredentialsProvider, // Are credentials pending? std::atomic credentials_pending_ = true; Thread::MutexBasicLockable mu_; - std::list credentials_subscribers_ ABSL_GUARDED_BY(mu_); + std::list> + credentials_subscribers_ ABSL_GUARDED_BY(mu_); }; } // namespace Aws diff --git a/source/extensions/common/aws/signer_base_impl.cc b/source/extensions/common/aws/signer_base_impl.cc index dd4b81f50c8c5..8aa16384e744f 100644 --- a/source/extensions/common/aws/signer_base_impl.cc +++ b/source/extensions/common/aws/signer_base_impl.cc @@ -193,11 +193,11 @@ void SignerBaseImpl::createQueryParams(Envoy::Http::Utility::QueryParamsMulti& q } // X-Amz-Credential query_params.add(SignatureQueryParameterValues::AmzCredential, - Utility::encodeQueryComponent(credential)); + Utility::encodeQueryComponentPreservingPlus(credential)); // X-Amz-SignedHeaders - query_params.add( - SignatureQueryParameterValues::AmzSignedHeaders, - Utility::encodeQueryComponent(Utility::joinCanonicalHeaderNames(signed_headers))); + query_params.add(SignatureQueryParameterValues::AmzSignedHeaders, + Utility::encodeQueryComponentPreservingPlus( + Utility::joinCanonicalHeaderNames(signed_headers))); } } // namespace Aws diff --git a/source/extensions/common/aws/signers/sigv4a_signer_impl.cc b/source/extensions/common/aws/signers/sigv4a_signer_impl.cc index 105304fd58848..680d8c1aba95f 100644 --- a/source/extensions/common/aws/signers/sigv4a_signer_impl.cc +++ b/source/extensions/common/aws/signers/sigv4a_signer_impl.cc @@ -45,9 +45,9 @@ void SigV4ASignerImpl::addRegionHeader(Http::RequestHeaderMap& headers, void SigV4ASignerImpl::addRegionQueryParam(Envoy::Http::Utility::QueryParamsMulti& query_params, const absl::string_view override_region) const { - query_params.add( - SignatureQueryParameterValues::AmzRegionSet, - Utility::encodeQueryComponent(override_region.empty() ? getRegion() : override_region)); + query_params.add(SignatureQueryParameterValues::AmzRegionSet, + Utility::encodeQueryComponentPreservingPlus( + override_region.empty() ? getRegion() : override_region)); } std::string SigV4ASignerImpl::createSignature( diff --git a/source/extensions/common/aws/utility.cc b/source/extensions/common/aws/utility.cc index 4522f3b2cd4a1..a983714a3b994 100644 --- a/source/extensions/common/aws/utility.cc +++ b/source/extensions/common/aws/utility.cc @@ -186,6 +186,14 @@ bool isReservedChar(const char c) { return std::isalnum(c) || RESERVED_CHARS.find(c) != std::string::npos; } +void Utility::encodeCharacter(unsigned char c, std::string& result) { + if (isReservedChar(c)) { + result.push_back(c); + } else { + absl::StrAppend(&result, fmt::format(URI_ENCODE, c)); + } +} + std::string Utility::uriEncodePath(absl::string_view original_path) { const absl::string_view::size_type query_start = original_path.find_first_of("?#"); @@ -196,10 +204,10 @@ std::string Utility::uriEncodePath(absl::string_view original_path) { for (unsigned char c : path) { // Do not encode slashes or unreserved chars from RFC 3986 - if ((isReservedChar(c)) || c == PATH_SPLITTER[0]) { + if (c == PATH_SPLITTER[0]) { encoded.push_back(c); } else { - absl::StrAppend(&encoded, fmt::format(URI_ENCODE, c)); + encodeCharacter(c, encoded); } } @@ -229,13 +237,9 @@ std::string Utility::canonicalizeQueryString(absl::string_view query_string) { // Encode query params name and value separately for (auto& query : query_list) { - // The token has already been url encoded, so don't do it again - if (query.first == SignatureQueryParameterValues::AmzSecurityToken) { - query = std::make_pair(query.first, query.second); - } else { - query = std::make_pair( - encodeQueryComponent(Envoy::Http::Utility::PercentEncoding::decode(query.first)), - encodeQueryComponent(Envoy::Http::Utility::PercentEncoding::decode(query.second))); + if (query.first != SignatureQueryParameterValues::AmzSecurityToken) { + query.first = Utility::encodeQueryComponentPreservingPlus(query.first); + query.second = Utility::encodeQueryComponentPreservingPlus(query.second); } } @@ -245,22 +249,36 @@ std::string Utility::canonicalizeQueryString(absl::string_view query_string) { return absl::StrJoin(query_list, QUERY_SEPERATOR, absl::PairFormatter(QUERY_PARAM_SEPERATOR)); } -// To avoid modifying the path, we handle spaces as if they have already been encoded to a plus, and -// avoid additional equals signs in the query parameters -std::string Utility::encodeQueryComponent(absl::string_view decoded) { - std::string encoded; - for (unsigned char c : decoded) { - if (isReservedChar(c)) { - // Escape unreserved chars from RFC 3986 - encoded.push_back(c); - } else if (c == '+') { - // Encode '+' as space - absl::StrAppend(&encoded, "%20"); +// Encode query component while preserving original %2B semantics +// %2B stays as %2B, raw + becomes %20 (space) +std::string Utility::encodeQueryComponentPreservingPlus(absl::string_view original) { + std::string result; + + for (size_t i = 0; i < original.size(); ++i) { + if (i + 2 < original.size() && absl::EqualsIgnoreCase(original.substr(i, 3), "%2B")) { + // %2B stays as %2B (preserve original encoding) + absl::StrAppend(&result, "%2B"); + i += 2; // Skip the "2B" part + } else if (original[i] == '+') { + // Raw + becomes %20 (space) + absl::StrAppend(&result, "%20"); + } else if (original[i] == '%' && i + 2 < original.size()) { + std::string decoded_seq = + Envoy::Http::Utility::PercentEncoding::decode(original.substr(i, 3)); + if (decoded_seq.size() == 1) { + // Valid percent encoding - encode the decoded character + encodeCharacter(decoded_seq[0], result); + i += 2; + } else { + // Invalid percent encoding - treat as regular character + encodeCharacter(original[i], result); + } } else { - absl::StrAppend(&encoded, fmt::format(URI_ENCODE, c)); + // Regular character + encodeCharacter(original[i], result); } } - return encoded; + return result; } std::string diff --git a/source/extensions/common/aws/utility.h b/source/extensions/common/aws/utility.h index 3990c3c70daf9..c8d5abf431211 100644 --- a/source/extensions/common/aws/utility.h +++ b/source/extensions/common/aws/utility.h @@ -64,12 +64,12 @@ class Utility : public Logger::Loggable { static std::string canonicalizeQueryString(absl::string_view query_string); /** - * URI encodes the given string based on AWS requirements. - * See step 3 in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html - * @param decoded the decoded string. - * @return the URI encoded string. + * URI encodes query component while preserving %2B semantics. + * %2B remains as literal +, raw + becomes %20 (space). + * @param original the original string (may contain percent-encoded sequences). + * @return the properly encoded string. */ - static std::string encodeQueryComponent(absl::string_view decoded); + static std::string encodeQueryComponentPreservingPlus(absl::string_view original); /** * Get the semicolon-delimited string of canonical header names. @@ -214,6 +214,14 @@ class Utility : public Logger::Loggable { * @return boolean */ static bool shouldNormalizeUriPath(const std::string service_name); + +private: + /** + * Helper method to encode a character based on reserved character rules. + * @param c the character to encode. + * @param result the string to append the encoded result to. + */ + static void encodeCharacter(unsigned char c, std::string& result); }; } // namespace Aws diff --git a/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc b/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc index 8037e43178c9a..0daf32908e8fa 100644 --- a/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc +++ b/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc @@ -642,12 +642,16 @@ void DnsCacheImpl::ThreadLocalHostInfo::onHostMapUpdate( const HostMapUpdateInfoSharedPtr& resolved_host) { auto host_it = pending_resolutions_.find(resolved_host->host_); if (host_it != pending_resolutions_.end()) { - for (auto* resolution : host_it->second) { + // Calling the onLoadDnsCacheComplete may trigger more host resolutions adding more elements + // to the `pending_resolutions_` map, potentially invalidating the host_it iterator. So we + // copy the list of handles to a local variable before cleaning up the map. + std::list completed_resolutions(std::move(host_it->second)); + pending_resolutions_.erase(host_it); + for (auto* resolution : completed_resolutions) { auto& callbacks = resolution->callbacks_; resolution->cancel(); callbacks.onLoadDnsCacheComplete(resolved_host->info_); } - pending_resolutions_.erase(host_it); } } diff --git a/source/extensions/common/matcher/BUILD b/source/extensions/common/matcher/BUILD index 2d2d56a96f12d..b1319595c46ed 100644 --- a/source/extensions/common/matcher/BUILD +++ b/source/extensions/common/matcher/BUILD @@ -22,9 +22,9 @@ envoy_cc_library( ) envoy_cc_extension( - name = "trie_matcher_lib", - srcs = ["trie_matcher.cc"], - hdrs = ["trie_matcher.h"], + name = "ip_range_matcher_lib", + srcs = ["ip_range_matcher.cc"], + hdrs = ["ip_range_matcher.h"], extra_visibility = [ "//source/common/listener_manager:__subpackages__", "//test:__subpackages__", diff --git a/source/extensions/common/matcher/trie_matcher.cc b/source/extensions/common/matcher/ip_range_matcher.cc similarity index 69% rename from source/extensions/common/matcher/trie_matcher.cc rename to source/extensions/common/matcher/ip_range_matcher.cc index e0fbf4e66637a..0d419b23717e5 100644 --- a/source/extensions/common/matcher/trie_matcher.cc +++ b/source/extensions/common/matcher/ip_range_matcher.cc @@ -1,4 +1,4 @@ -#include "source/extensions/common/matcher/trie_matcher.h" +#include "source/extensions/common/matcher/ip_range_matcher.h" #include "envoy/registry/registry.h" @@ -7,11 +7,11 @@ namespace Extensions { namespace Common { namespace Matcher { -REGISTER_FACTORY(NetworkTrieMatcherFactory, +REGISTER_FACTORY(NetworkIpRangeMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); -REGISTER_FACTORY(UdpNetworkTrieMatcherFactory, +REGISTER_FACTORY(UdpNetworkIpRangeMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); -REGISTER_FACTORY(HttpTrieMatcherFactory, +REGISTER_FACTORY(HttpIpRangeMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); } // namespace Matcher diff --git a/source/extensions/common/matcher/trie_matcher.h b/source/extensions/common/matcher/ip_range_matcher.h similarity index 77% rename from source/extensions/common/matcher/trie_matcher.h rename to source/extensions/common/matcher/ip_range_matcher.h index d9a0d2f266b1d..0fb78e2ef9e32 100644 --- a/source/extensions/common/matcher/trie_matcher.h +++ b/source/extensions/common/matcher/ip_range_matcher.h @@ -26,25 +26,25 @@ using ::Envoy::Matcher::OnMatchFactory; using ::Envoy::Matcher::OnMatchFactoryCb; using ::Envoy::Matcher::SkippedMatchCb; -template struct TrieNode { +template struct IpRangeNode { size_t index_; uint32_t prefix_len_; bool exclusive_; std::shared_ptr> on_match_; - friend bool operator==(const TrieNode& lhs, const TrieNode& rhs) { + friend bool operator==(const IpRangeNode& lhs, const IpRangeNode& rhs) { return lhs.index_ == rhs.index_ && lhs.prefix_len_ == rhs.prefix_len_ && lhs.exclusive_ == rhs.exclusive_ && lhs.on_match_ == rhs.on_match_; } template friend H AbslHashValue(H h, // NOLINT(readability-identifier-naming) - const TrieNode& node) { + const IpRangeNode& node) { return H::combine(std::move(h), node.index_, node.prefix_len_, node.exclusive_, node.on_match_); } }; -template struct TrieNodeComparator { - inline bool operator()(const TrieNode& lhs, const TrieNode& rhs) const { +template struct IpRangeNodeComparator { + inline bool operator()(const IpRangeNode& lhs, const IpRangeNode& rhs) const { if (lhs.prefix_len_ > rhs.prefix_len_) { return true; } @@ -56,18 +56,18 @@ template struct TrieNodeComparator { }; /** - * Implementation of a `sublinear` LC-trie matcher. + * Implementation of a `sublinear` LC-trie matcher for IP ranges. */ -template class TrieMatcher : public MatchTree { +template class IpRangeMatcher : public MatchTree { public: - TrieMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, - const std::shared_ptr>>& trie) + IpRangeMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, + const std::shared_ptr>>& trie) : data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), trie_(trie) { auto input_type = data_input_->dataInputType(); if (input_type != Envoy::Matcher::DefaultMatchingDataType) { throw EnvoyException( absl::StrCat("Unsupported data input type: ", input_type, - ", currently only string type is supported in trie matcher")); + ", currently only string type is supported in IP range matcher")); } } @@ -87,7 +87,7 @@ template class TrieMatcher : public MatchTree { auto values = trie_->getData(addr); // The candidates returned by the LC trie are not in any specific order, so we // sort them by the prefix length first (longest first), and the order of declaration second. - std::sort(values.begin(), values.end(), TrieNodeComparator()); + std::sort(values.begin(), values.end(), IpRangeNodeComparator()); bool first = true; for (const auto& node : values) { if (!first && node.exclusive_) { @@ -111,11 +111,11 @@ template class TrieMatcher : public MatchTree { private: const DataInputPtr data_input_; const absl::optional> on_no_match_; - std::shared_ptr>> trie_; + std::shared_ptr>> trie_; }; template -class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory { +class IpRangeMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory { public: ::Envoy::Matcher::MatchTreeFactoryCb createCustomMatcherFactoryCb(const Protobuf::Message& config, @@ -131,7 +131,7 @@ class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory, std::vector>> data; + std::vector, std::vector>> data; data.reserve(match_children.size()); size_t i = 0; // Ranges might have variable prefix length so we cannot combine them into one node because @@ -139,16 +139,16 @@ class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory>(match_children[i++]()); for (const auto& range : range_matcher.ranges()) { - TrieNode node = {i, range.prefix_len().value(), range_matcher.exclusive(), - on_match}; + IpRangeNode node = {i, range.prefix_len().value(), range_matcher.exclusive(), + on_match}; data.push_back({node, {THROW_OR_RETURN_VALUE(Network::Address::CidrRange::create(range), Network::Address::CidrRange)}}); } } - auto lc_trie = std::make_shared>>(data); + auto lc_trie = std::make_shared>>(data); return [data_input, lc_trie, on_no_match]() { - return std::make_unique>( + return std::make_unique>( data_input(), on_no_match ? absl::make_optional(on_no_match.value()()) : absl::nullopt, lc_trie); }; @@ -156,12 +156,13 @@ class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory(); } - std::string name() const override { return "envoy.matching.custom_matchers.trie_matcher"; } + std::string name() const override { return "envoy.matching.custom_matchers.ip_range_matcher"; } }; -class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; -class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; -class HttpTrieMatcherFactory : public TrieMatcherFactoryBase {}; +class NetworkIpRangeMatcherFactory : public IpRangeMatcherFactoryBase {}; +class UdpNetworkIpRangeMatcherFactory : public IpRangeMatcherFactoryBase { +}; +class HttpIpRangeMatcherFactory : public IpRangeMatcherFactoryBase {}; } // namespace Matcher } // namespace Common diff --git a/source/extensions/common/wasm/ext/BUILD b/source/extensions/common/wasm/ext/BUILD index 4e86c6de0fa8e..71c21a52c0fe5 100644 --- a/source/extensions/common/wasm/ext/BUILD +++ b/source/extensions/common/wasm/ext/BUILD @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load( "//bazel:envoy_build_system.bzl", "envoy_cc_library", diff --git a/source/extensions/common/wasm/foreign.cc b/source/extensions/common/wasm/foreign.cc index 43cf9369148d3..208a2fb96abdd 100644 --- a/source/extensions/common/wasm/foreign.cc +++ b/source/extensions/common/wasm/foreign.cc @@ -138,14 +138,15 @@ RegisterForeignFunction registerClearRouteCacheForeignFunction( class ExpressionFactory : public Logger::Loggable { protected: struct ExpressionData { - google::api::expr::v1alpha1::ParsedExpr parsed_expr_; + cel::expr::ParsedExpr parsed_expr_; Filters::Common::Expr::ExpressionPtr compiled_expr_; }; class ExpressionContext : public StorageObject { public: friend class ExpressionFactory; - ExpressionContext(Filters::Common::Expr::BuilderPtr builder) : builder_(std::move(builder)) {} + ExpressionContext(Filters::Common::Expr::BuilderConstPtr builder) + : builder_(std::move(builder)) {} uint32_t createToken() { uint32_t token = next_expr_token_++; for (;;) { @@ -159,10 +160,10 @@ class ExpressionFactory : public Logger::Loggable { bool hasExpression(uint32_t token) { return expr_.contains(token); } ExpressionData& getExpression(uint32_t token) { return expr_[token]; } void deleteExpression(uint32_t token) { expr_.erase(token); } - Filters::Common::Expr::Builder* builder() { return builder_.get(); } + const Filters::Common::Expr::Builder* builder() const { return builder_.get(); } private: - Filters::Common::Expr::BuilderPtr builder_{}; + const Filters::Common::Expr::BuilderConstPtr builder_{}; uint32_t next_expr_token_ = 0; absl::flat_hash_map expr_; }; @@ -203,9 +204,13 @@ class CreateExpressionFactory : public ExpressionFactory { auto token = expr_context.createToken(); auto& handler = expr_context.getExpression(token); - handler.parsed_expr_ = parse_status.value(); + const auto& parsed_expr = parse_status.value(); + handler.parsed_expr_ = parsed_expr; + + std::vector warnings; auto cel_expression_status = expr_context.builder()->CreateExpression( - &handler.parsed_expr_.expr(), &handler.parsed_expr_.source_info()); + &handler.parsed_expr_.expr(), &handler.parsed_expr_.source_info(), &warnings); + if (!cel_expression_status.ok()) { ENVOY_LOG(info, "expr_create compile error: {}", cel_expression_status.status().message()); expr_context.deleteExpression(token); @@ -213,6 +218,7 @@ class CreateExpressionFactory : public ExpressionFactory { } handler.compiled_expr_ = std::move(cel_expression_status.value()); + auto result = reinterpret_cast(alloc_result(sizeof(uint32_t))); *result = token; return WasmResult::Ok; diff --git a/source/extensions/config/validators/minimum_clusters/config.cc b/source/extensions/config/validators/minimum_clusters/config.cc index de06a18be89cc..3de7f7d123e15 100644 --- a/source/extensions/config/validators/minimum_clusters/config.cc +++ b/source/extensions/config/validators/minimum_clusters/config.cc @@ -12,7 +12,7 @@ namespace Config { namespace Validators { Envoy::Config::ConfigValidatorPtr MinimumClustersValidatorFactory::createConfigValidator( - const ProtobufWkt::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor) { + const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& validator_config = MessageUtil::anyConvertAndValidate< envoy::extensions::config::validators::minimum_clusters::v3::MinimumClustersValidator>( config, validation_visitor); diff --git a/source/extensions/config/validators/minimum_clusters/config.h b/source/extensions/config/validators/minimum_clusters/config.h index b200892b81d6e..1c1019b907313 100644 --- a/source/extensions/config/validators/minimum_clusters/config.h +++ b/source/extensions/config/validators/minimum_clusters/config.h @@ -14,7 +14,7 @@ class MinimumClustersValidatorFactory : public Envoy::Config::ConfigValidatorFac MinimumClustersValidatorFactory() = default; Envoy::Config::ConfigValidatorPtr - createConfigValidator(const ProtobufWkt::Any& config, + createConfigValidator(const Protobuf::Any& config, ProtobufMessage::ValidationVisitor& validation_visitor) override; Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override; diff --git a/source/extensions/config_subscription/grpc/delta_subscription_state.cc b/source/extensions/config_subscription/grpc/delta_subscription_state.cc index 5b540f146c729..00f7d452c665b 100644 --- a/source/extensions/config_subscription/grpc/delta_subscription_state.cc +++ b/source/extensions/config_subscription/grpc/delta_subscription_state.cc @@ -190,9 +190,7 @@ bool DeltaSubscriptionState::isHeartbeatResponse( void DeltaSubscriptionState::handleGoodResponse( envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { absl::flat_hash_set names_added_removed; - // TODO(adisuissa): remove the non_heartbeat_resources structure once - // "envoy.reloadable_features.xds_prevent_resource_copy" is deprecated. - Protobuf::RepeatedPtrField non_heartbeat_resources; + for (const auto& resource : message.resources()) { if (!names_added_removed.insert(resource.name()).second) { throw EnvoyException( @@ -201,9 +199,6 @@ void DeltaSubscriptionState::handleGoodResponse( if (isHeartbeatResponse(resource)) { continue; } - if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.xds_prevent_resource_copy")) { - non_heartbeat_resources.Add()->CopyFrom(resource); - } // DeltaDiscoveryResponses for unresolved aliases don't contain an actual resource if (!resource.has_resource() && resource.aliases_size() > 0) { continue; @@ -222,24 +217,18 @@ void DeltaSubscriptionState::handleGoodResponse( } } - absl::Span non_heartbeat_resources_span; - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.xds_prevent_resource_copy")) { - // Reorder the resources in the response, having all the non-heartbeat - // resources at the front of the list. Note that although there's no - // requirement to keep stable ordering, we do so to process the resources in - // the order they were sent. - auto last_non_heartbeat = std::stable_partition( - message.mutable_resources()->begin(), message.mutable_resources()->end(), - [&](const envoy::service::discovery::v3::Resource& resource) { - return !isHeartbeatResponse(resource); - }); - - non_heartbeat_resources_span = absl::MakeConstSpan( - message.resources().data(), last_non_heartbeat - message.resources().begin()); - } else { - non_heartbeat_resources_span = - absl::MakeConstSpan(non_heartbeat_resources.data(), non_heartbeat_resources.size()); - } + // Reorder the resources in the response, having all the non-heartbeat + // resources at the front of the list. Note that although there's no + // requirement to keep stable ordering, we do so to process the resources in + // the order they were sent. + auto last_non_heartbeat = std::stable_partition( + message.mutable_resources()->begin(), message.mutable_resources()->end(), + [&](const envoy::service::discovery::v3::Resource& resource) { + return !isHeartbeatResponse(resource); + }); + + auto non_heartbeat_resources_span = absl::MakeConstSpan( + message.resources().data(), last_non_heartbeat - message.resources().begin()); watch_map_.onConfigUpdate(non_heartbeat_resources_span, message.removed_resources(), message.system_version_info()); diff --git a/source/extensions/config_subscription/grpc/grpc_mux_context.h b/source/extensions/config_subscription/grpc/grpc_mux_context.h index 03f7e4c7d119f..a5ed5121dc25a 100644 --- a/source/extensions/config_subscription/grpc/grpc_mux_context.h +++ b/source/extensions/config_subscription/grpc/grpc_mux_context.h @@ -18,8 +18,8 @@ namespace Config { // Context (data) needed for creating a GrpcMux object. // These are parameters needed for the creation of all GrpcMux objects. struct GrpcMuxContext { - Grpc::RawAsyncClientPtr async_client_; - Grpc::RawAsyncClientPtr failover_async_client_; + Grpc::RawAsyncClientSharedPtr async_client_; + Grpc::RawAsyncClientSharedPtr failover_async_client_; Event::Dispatcher& dispatcher_; const Protobuf::MethodDescriptor& service_method_; const LocalInfo::LocalInfo& local_info_; diff --git a/source/extensions/config_subscription/grpc/grpc_mux_impl.cc b/source/extensions/config_subscription/grpc/grpc_mux_impl.cc index 79fb7428bef3b..3832b2778fade 100644 --- a/source/extensions/config_subscription/grpc/grpc_mux_impl.cc +++ b/source/extensions/config_subscription/grpc/grpc_mux_impl.cc @@ -84,8 +84,8 @@ GrpcMuxImpl::GrpcMuxImpl(GrpcMuxContext& grpc_mux_context, bool skip_subsequent_ std::unique_ptr> -GrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, +GrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const RateLimitSettings& rate_limit_settings) { @@ -104,7 +104,7 @@ GrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, std::move(backoff_strategy), rate_limit_settings, GrpcStream::ConnectedStateValue:: - FIRST_ENTRY); + FirstEntry); }, /*failover_stream_creator=*/ failover_async_client @@ -129,7 +129,7 @@ GrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, rate_limit_settings, GrpcStream:: - ConnectedStateValue::SECOND_ENTRY); + ConnectedStateValue::SecondEntry); }) : absl::nullopt, /*grpc_mux_callbacks=*/*this, @@ -141,7 +141,7 @@ GrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, std::move(backoff_strategy), rate_limit_settings, GrpcStream< envoy::service::discovery::v3::DiscoveryRequest, - envoy::service::discovery::v3::DiscoveryResponse>::ConnectedStateValue::FIRST_ENTRY); + envoy::service::discovery::v3::DiscoveryResponse>::ConnectedStateValue::FirstEntry); } GrpcMuxImpl::~GrpcMuxImpl() { AllMuxes::get().erase(this); } @@ -299,9 +299,9 @@ GrpcMuxWatchPtr GrpcMuxImpl::addWatch(const std::string& type_url, } absl::Status -GrpcMuxImpl::updateMuxSource(Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope& scope, - BackOffStrategyPtr&& backoff_strategy, +GrpcMuxImpl::updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, + Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) { // Process the rate limit settings. absl::StatusOr rate_limit_settings_or_error = @@ -659,8 +659,9 @@ class GrpcMuxFactory : public MuxFactory { std::string name() const override { return "envoy.config_mux.grpc_mux_factory"; } void shutdownAll() override { return GrpcMuxImpl::shutdownAll(); } std::shared_ptr - create(Grpc::RawAsyncClientPtr&& async_client, Grpc::RawAsyncClientPtr&& failover_async_client, - Event::Dispatcher& dispatcher, Random::RandomGenerator&, Stats::Scope& scope, + create(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Event::Dispatcher& dispatcher, + Random::RandomGenerator&, Stats::Scope& scope, const envoy::config::core::v3::ApiConfigSource& ads_config, const LocalInfo::LocalInfo& local_info, CustomConfigValidatorsPtr&& config_validators, BackOffStrategyPtr&& backoff_strategy, XdsConfigTrackerOptRef xds_config_tracker, diff --git a/source/extensions/config_subscription/grpc/grpc_mux_impl.h b/source/extensions/config_subscription/grpc/grpc_mux_impl.h index b13c852222a38..0a08a0554e1e3 100644 --- a/source/extensions/config_subscription/grpc/grpc_mux_impl.h +++ b/source/extensions/config_subscription/grpc/grpc_mux_impl.h @@ -74,8 +74,8 @@ class GrpcMuxImpl : public GrpcMux, } absl::Status - updateMuxSource(Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope& scope, + updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) override; @@ -108,8 +108,8 @@ class GrpcMuxImpl : public GrpcMux, // Helper function to create the grpc_stream_ object. std::unique_ptr> - createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, + createGrpcStreamObject(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const RateLimitSettings& rate_limit_settings); diff --git a/source/extensions/config_subscription/grpc/grpc_stream.h b/source/extensions/config_subscription/grpc/grpc_stream.h index cd04a03c182d1..4eeaa6f08f227 100644 --- a/source/extensions/config_subscription/grpc/grpc_stream.h +++ b/source/extensions/config_subscription/grpc/grpc_stream.h @@ -29,12 +29,13 @@ class GrpcStream : public GrpcStreamInterface, // The entry value corresponding to the grpc stream's configuration entry index. enum class ConnectedStateValue { // The first entry in the config corresponds to the primary xDS source. - FIRST_ENTRY = 1, + FirstEntry = 1, // The second entry in the config corresponds to the failover xDS source. - SECOND_ENTRY + SecondEntry }; - GrpcStream(GrpcStreamCallbacks* callbacks, Grpc::RawAsyncClientPtr async_client, + GrpcStream(GrpcStreamCallbacks* callbacks, + Grpc::RawAsyncClientSharedPtr&& async_client, const Protobuf::MethodDescriptor& service_method, Event::Dispatcher& dispatcher, Stats::Scope& scope, BackOffStrategyPtr backoff_strategy, const RateLimitSettings& rate_limit_settings, ConnectedStateValue connected_state_val) diff --git a/source/extensions/config_subscription/grpc/grpc_subscription_factory.cc b/source/extensions/config_subscription/grpc/grpc_subscription_factory.cc index 26ecf9f047f1a..3eb793f22ed91 100644 --- a/source/extensions/config_subscription/grpc/grpc_subscription_factory.cc +++ b/source/extensions/config_subscription/grpc/grpc_subscription_factory.cc @@ -35,7 +35,7 @@ GrpcConfigSubscriptionFactory::create(ConfigSubscriptionFactory::SubscriptionDat GrpcMuxContext grpc_mux_context{ /*async_client_=*/THROW_OR_RETURN_VALUE( factory_primary_or_error.value()->createUncachedRawAsyncClient(), - Grpc::RawAsyncClientPtr), + Grpc::RawAsyncClientSharedPtr), /*failover_async_client_=*/nullptr, // Failover is only supported for ADS. /*dispatcher_=*/data.dispatcher_, /*service_method_=*/sotwGrpcMethod(data.type_url_), @@ -86,7 +86,7 @@ DeltaGrpcConfigSubscriptionFactory::create(ConfigSubscriptionFactory::Subscripti GrpcMuxContext grpc_mux_context{ /*async_client_=*/THROW_OR_RETURN_VALUE( factory_primary_or_error.value()->createUncachedRawAsyncClient(), - Grpc::RawAsyncClientPtr), + Grpc::RawAsyncClientSharedPtr), /*failover_async_client_=*/nullptr, // Failover is only supported for ADS. /*dispatcher_=*/data.dispatcher_, /*service_method_=*/deltaGrpcMethod(data.type_url_), diff --git a/source/extensions/config_subscription/grpc/new_grpc_mux_impl.cc b/source/extensions/config_subscription/grpc/new_grpc_mux_impl.cc index 2323b5663e0bc..6d2202d660265 100644 --- a/source/extensions/config_subscription/grpc/new_grpc_mux_impl.cc +++ b/source/extensions/config_subscription/grpc/new_grpc_mux_impl.cc @@ -57,8 +57,8 @@ NewGrpcMuxImpl::NewGrpcMuxImpl(GrpcMuxContext& grpc_mux_context) std::unique_ptr> -NewGrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, +NewGrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const RateLimitSettings& rate_limit_settings) { @@ -78,7 +78,7 @@ NewGrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, std::move(backoff_strategy), rate_limit_settings, GrpcStream:: - ConnectedStateValue::FIRST_ENTRY); + ConnectedStateValue::FirstEntry); }, /*failover_stream_creator=*/ failover_async_client @@ -104,7 +104,7 @@ NewGrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, rate_limit_settings, GrpcStream:: - ConnectedStateValue::SECOND_ENTRY); + ConnectedStateValue::SecondEntry); }) : absl::nullopt, /*grpc_mux_callbacks=*/*this, @@ -116,7 +116,7 @@ NewGrpcMuxImpl::createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, std::move(backoff_strategy), rate_limit_settings, GrpcStream< envoy::service::discovery::v3::DeltaDiscoveryRequest, - envoy::service::discovery::v3::DeltaDiscoveryResponse>::ConnectedStateValue::FIRST_ENTRY); + envoy::service::discovery::v3::DeltaDiscoveryResponse>::ConnectedStateValue::FirstEntry); } NewGrpcMuxImpl::~NewGrpcMuxImpl() { AllMuxes::get().erase(this); } @@ -252,8 +252,8 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, } absl::Status -NewGrpcMuxImpl::updateMuxSource(Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, +NewGrpcMuxImpl::updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) { // Process the rate limit settings. @@ -455,8 +455,9 @@ class NewGrpcMuxFactory : public MuxFactory { std::string name() const override { return "envoy.config_mux.new_grpc_mux_factory"; } void shutdownAll() override { return NewGrpcMuxImpl::shutdownAll(); } std::shared_ptr - create(Grpc::RawAsyncClientPtr&& async_client, Grpc::RawAsyncClientPtr&& failover_async_client, - Event::Dispatcher& dispatcher, Random::RandomGenerator&, Stats::Scope& scope, + create(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Event::Dispatcher& dispatcher, + Random::RandomGenerator&, Stats::Scope& scope, const envoy::config::core::v3::ApiConfigSource& ads_config, const LocalInfo::LocalInfo& local_info, CustomConfigValidatorsPtr&& config_validators, BackOffStrategyPtr&& backoff_strategy, XdsConfigTrackerOptRef xds_config_tracker, diff --git a/source/extensions/config_subscription/grpc/new_grpc_mux_impl.h b/source/extensions/config_subscription/grpc/new_grpc_mux_impl.h index f2f6d14903548..e9e728e432ab7 100644 --- a/source/extensions/config_subscription/grpc/new_grpc_mux_impl.h +++ b/source/extensions/config_subscription/grpc/new_grpc_mux_impl.h @@ -79,8 +79,8 @@ class NewGrpcMuxImpl void start() override; absl::Status - updateMuxSource(Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope& scope, + updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) override; @@ -159,8 +159,8 @@ class NewGrpcMuxImpl // Helper function to create the grpc_stream_ object. std::unique_ptr> - createGrpcStreamObject(Grpc::RawAsyncClientPtr&& async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, + createGrpcStreamObject(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const RateLimitSettings& rate_limit_settings); diff --git a/source/extensions/config_subscription/grpc/watch_map.cc b/source/extensions/config_subscription/grpc/watch_map.cc index 439f0000f0675..e762bba72b16e 100644 --- a/source/extensions/config_subscription/grpc/watch_map.cc +++ b/source/extensions/config_subscription/grpc/watch_map.cc @@ -199,7 +199,7 @@ void WatchMap::onConfigUpdate(const std::vector& resources, } } -void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, +void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) { if (watches_.empty()) { return; diff --git a/source/extensions/config_subscription/grpc/watch_map.h b/source/extensions/config_subscription/grpc/watch_map.h index 1f824900c211a..b9481f59a6424 100644 --- a/source/extensions/config_subscription/grpc/watch_map.h +++ b/source/extensions/config_subscription/grpc/watch_map.h @@ -99,7 +99,7 @@ class WatchMap : public UntypedConfigUpdateCallbacks, public Logger::Loggable& resources, + void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) override; void onConfigUpdate(const std::vector& resources, diff --git a/source/extensions/config_subscription/grpc/xds_mux/delta_subscription_state.cc b/source/extensions/config_subscription/grpc/xds_mux/delta_subscription_state.cc index 9c10163e4e52f..0f87c3e6df764 100644 --- a/source/extensions/config_subscription/grpc/xds_mux/delta_subscription_state.cc +++ b/source/extensions/config_subscription/grpc/xds_mux/delta_subscription_state.cc @@ -158,9 +158,7 @@ bool DeltaSubscriptionState::isHeartbeatResource( void DeltaSubscriptionState::handleGoodResponse( envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { absl::flat_hash_set names_added_removed; - // TODO(adisuissa): remove the non_heartbeat_resources structure once - // "envoy.reloadable_features.xds_prevent_resource_copy" is deprecated. - Protobuf::RepeatedPtrField non_heartbeat_resources; + for (const auto& resource : message.resources()) { if (!names_added_removed.insert(resource.name()).second) { throw EnvoyException( @@ -169,9 +167,6 @@ void DeltaSubscriptionState::handleGoodResponse( if (isHeartbeatResource(resource)) { continue; } - if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.xds_prevent_resource_copy")) { - non_heartbeat_resources.Add()->CopyFrom(resource); - } // DeltaDiscoveryResponses for unresolved aliases don't contain an actual resource if (!resource.has_resource() && resource.aliases_size() > 0) { continue; @@ -190,24 +185,18 @@ void DeltaSubscriptionState::handleGoodResponse( } } - absl::Span non_heartbeat_resources_span; - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.xds_prevent_resource_copy")) { - // Reorder the resources in the response, having all the non-heartbeat - // resources at the front of the list. Note that although there's no - // requirement to keep stable ordering, we do so to process the resources in - // the order they were sent. - auto last_non_heartbeat = std::stable_partition( - message.mutable_resources()->begin(), message.mutable_resources()->end(), - [&](const envoy::service::discovery::v3::Resource& resource) { - return !isHeartbeatResource(resource); - }); - - non_heartbeat_resources_span = absl::MakeConstSpan( - message.resources().data(), last_non_heartbeat - message.resources().begin()); - } else { - non_heartbeat_resources_span = - absl::MakeConstSpan(non_heartbeat_resources.data(), non_heartbeat_resources.size()); - } + // Reorder the resources in the response, having all the non-heartbeat + // resources at the front of the list. Note that although there's no + // requirement to keep stable ordering, we do so to process the resources in + // the order they were sent. + auto last_non_heartbeat = std::stable_partition( + message.mutable_resources()->begin(), message.mutable_resources()->end(), + [&](const envoy::service::discovery::v3::Resource& resource) { + return !isHeartbeatResource(resource); + }); + + auto non_heartbeat_resources_span = absl::MakeConstSpan( + message.resources().data(), last_non_heartbeat - message.resources().begin()); callbacks().onConfigUpdate(non_heartbeat_resources_span, message.removed_resources(), message.system_version_info()); diff --git a/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.cc b/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.cc index 0d6067c6aa4c0..526a67cd826f5 100644 --- a/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.cc +++ b/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.cc @@ -65,7 +65,8 @@ GrpcMuxImpl::GrpcMuxImpl(std::unique_ptr subscription_state_fac template std::unique_ptr> GrpcMuxImpl::createGrpcStreamObject( - Grpc::RawAsyncClientPtr&& async_client, Grpc::RawAsyncClientPtr&& failover_async_client, + Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const RateLimitSettings& rate_limit_settings) { if (Runtime::runtimeFeatureEnabled("envoy.restart_features.xds_failover_support")) { @@ -77,7 +78,7 @@ std::unique_ptr> GrpcMuxImpl::createGr return std::make_unique>( callbacks, std::move(async_client), service_method, dispatcher, scope, std::move(backoff_strategy), rate_limit_settings, - GrpcStream::ConnectedStateValue::FIRST_ENTRY); + GrpcStream::ConnectedStateValue::FirstEntry); }, /*failover_stream_creator=*/ failover_async_client @@ -92,7 +93,7 @@ std::unique_ptr> GrpcMuxImpl::createGr // be the same as the primary source. std::make_unique( GrpcMuxFailover::DefaultFailoverBackoffMilliseconds), - rate_limit_settings, GrpcStream::ConnectedStateValue::SECOND_ENTRY); + rate_limit_settings, GrpcStream::ConnectedStateValue::SecondEntry); }) : absl::nullopt, /*grpc_mux_callbacks=*/*this, @@ -101,7 +102,7 @@ std::unique_ptr> GrpcMuxImpl::createGr return std::make_unique>(this, std::move(async_client), service_method, dispatcher_, scope, std::move(backoff_strategy), rate_limit_settings, - GrpcStream::ConnectedStateValue::FIRST_ENTRY); + GrpcStream::ConnectedStateValue::FirstEntry); } template GrpcMuxImpl::~GrpcMuxImpl() { @@ -230,8 +231,9 @@ ScopedResume GrpcMuxImpl::pause(const std::vector typ template absl::Status GrpcMuxImpl::updateMuxSource( - Grpc::RawAsyncClientPtr&& primary_async_client, Grpc::RawAsyncClientPtr&& failover_async_client, - Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, + Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, + BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) { // Process the rate limit settings. absl::StatusOr rate_limit_settings_or_error = @@ -488,8 +490,9 @@ class DeltaGrpcMuxFactory : public MuxFactory { std::string name() const override { return "envoy.config_mux.delta_grpc_mux_factory"; } void shutdownAll() override { return GrpcMuxDelta::shutdownAll(); } std::shared_ptr - create(Grpc::RawAsyncClientPtr&& async_client, Grpc::RawAsyncClientPtr&& failover_async_client, - Event::Dispatcher& dispatcher, Random::RandomGenerator&, Stats::Scope& scope, + create(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Event::Dispatcher& dispatcher, + Random::RandomGenerator&, Stats::Scope& scope, const envoy::config::core::v3::ApiConfigSource& ads_config, const LocalInfo::LocalInfo& local_info, CustomConfigValidatorsPtr&& config_validators, BackOffStrategyPtr&& backoff_strategy, XdsConfigTrackerOptRef xds_config_tracker, @@ -527,8 +530,9 @@ class SotwGrpcMuxFactory : public MuxFactory { std::string name() const override { return "envoy.config_mux.sotw_grpc_mux_factory"; } void shutdownAll() override { return GrpcMuxSotw::shutdownAll(); } std::shared_ptr - create(Grpc::RawAsyncClientPtr&& async_client, Grpc::RawAsyncClientPtr&& failover_async_client, - Event::Dispatcher& dispatcher, Random::RandomGenerator&, Stats::Scope& scope, + create(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Event::Dispatcher& dispatcher, + Random::RandomGenerator&, Stats::Scope& scope, const envoy::config::core::v3::ApiConfigSource& ads_config, const LocalInfo::LocalInfo& local_info, CustomConfigValidatorsPtr&& config_validators, BackOffStrategyPtr&& backoff_strategy, XdsConfigTrackerOptRef xds_config_tracker, diff --git a/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.h b/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.h index 72182dc18f321..7fff1c263d0e7 100644 --- a/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.h +++ b/source/extensions/config_subscription/grpc/xds_mux/grpc_mux_impl.h @@ -106,8 +106,8 @@ class GrpcMuxImpl : public GrpcStreamCallbacks, } absl::Status - updateMuxSource(Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope& scope, + updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source) override; @@ -179,10 +179,12 @@ class GrpcMuxImpl : public GrpcStreamCallbacks, // Helper function to create the grpc_stream_ object. // TODO(adisuissa): this should be removed when envoy.restart_features.xds_failover_support // is deprecated. - std::unique_ptr> createGrpcStreamObject( - Grpc::RawAsyncClientPtr&& async_client, Grpc::RawAsyncClientPtr&& failover_async_client, - const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, - BackOffStrategyPtr&& backoff_strategy, const RateLimitSettings& rate_limit_settings); + std::unique_ptr> + createGrpcStreamObject(Grpc::RawAsyncClientSharedPtr&& async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, + const Protobuf::MethodDescriptor& service_method, Stats::Scope& scope, + BackOffStrategyPtr&& backoff_strategy, + const RateLimitSettings& rate_limit_settings); // Checks whether external conditions allow sending a DeltaDiscoveryRequest. (Does not check // whether we *want* to send a (Delta)DiscoveryRequest). @@ -300,8 +302,8 @@ class NullGrpcMuxImpl : public GrpcMux { SubscriptionCallbacks&, OpaqueResourceDecoderSharedPtr, const SubscriptionOptions&) override; - absl::Status updateMuxSource(Grpc::RawAsyncClientPtr&&, Grpc::RawAsyncClientPtr&&, Stats::Scope&, - BackOffStrategyPtr&&, + absl::Status updateMuxSource(Grpc::RawAsyncClientSharedPtr&&, Grpc::RawAsyncClientSharedPtr&&, + Stats::Scope&, BackOffStrategyPtr&&, const envoy::config::core::v3::ApiConfigSource&) override { return absl::UnimplementedError(""); } diff --git a/source/extensions/dynamic_modules/abi.h b/source/extensions/dynamic_modules/abi.h index 93cb7e71e265d..36fed649db8f0 100644 --- a/source/extensions/dynamic_modules/abi.h +++ b/source/extensions/dynamic_modules/abi.h @@ -74,7 +74,7 @@ typedef const char* envoy_dynamic_module_type_abi_version_envoy_ptr; * * OWNERSHIP: Envoy owns the pointer. */ -typedef const void* envoy_dynamic_module_type_http_filter_config_envoy_ptr; +typedef void* envoy_dynamic_module_type_http_filter_config_envoy_ptr; /** * envoy_dynamic_module_type_http_filter_config_module_ptr is a pointer to an in-module HTTP @@ -164,6 +164,14 @@ typedef struct { size_t length; } envoy_dynamic_module_type_envoy_buffer; +/** + * envoy_dynamic_module_type_module_buffer represents a buffer owned by the module. + */ +typedef struct { + envoy_dynamic_module_type_buffer_module_ptr ptr; + size_t length; +} envoy_dynamic_module_type_module_buffer; + /** * envoy_dynamic_module_type_module_http_header represents a key-value pair of an HTTP header owned * by the module. @@ -414,6 +422,21 @@ typedef enum { envoy_dynamic_module_type_attribute_id_XdsFilterChainName, } envoy_dynamic_module_type_attribute_id; +/** + * envoy_dynamic_module_type_log_level represents the log level passed to + * envoy_dynamic_module_callback_log. This corresponds to the enum defined in + * source/common/common/base_logger.h. + */ +typedef enum { + envoy_dynamic_module_type_log_level_Trace, + envoy_dynamic_module_type_log_level_Debug, + envoy_dynamic_module_type_log_level_Info, + envoy_dynamic_module_type_log_level_Warn, + envoy_dynamic_module_type_log_level_Error, + envoy_dynamic_module_type_log_level_Critical, + envoy_dynamic_module_type_log_level_Off, +} envoy_dynamic_module_type_log_level; + /** * envoy_dynamic_module_type_http_callout_init_result represents the result of the HTTP callout * initialization after envoy_dynamic_module_callback_http_filter_http_callout is called. @@ -442,6 +465,20 @@ typedef enum { envoy_dynamic_module_type_http_callout_result_ExceedResponseBufferLimit, } envoy_dynamic_module_type_http_callout_result; +/** + * envoy_dynamic_module_type_metrics_result represents the result of the metrics operation. + * Success means the operation was successful. + * MetricNotFound means the metric was not found. This is usually an indication that a handle was + * improperly initialized or stored. InvalidLabels means the labels are invalid. Frozen means a + * metric was attempted to be created when the stats creation is frozen. + */ +typedef enum { + envoy_dynamic_module_type_metrics_result_Success, + envoy_dynamic_module_type_metrics_result_MetricNotFound, + envoy_dynamic_module_type_metrics_result_InvalidLabels, + envoy_dynamic_module_type_metrics_result_Frozen, +} envoy_dynamic_module_type_metrics_result; + // ----------------------------------------------------------------------------- // ------------------------------- Event Hooks --------------------------------- // ----------------------------------------------------------------------------- @@ -727,6 +764,313 @@ void envoy_dynamic_module_on_http_filter_scheduled( // Callbacks are functions implemented by Envoy that can be called by the module to interact with // Envoy. The name of a callback must be prefixed with "envoy_dynamic_module_callback_". +// --------------------------------- Logging ----------------------------------- + +/** + * envoy_dynamic_module_callback_log is called by the module to log a message as part + * of the standard Envoy logging stream under [dynamic_modules] Id. + * + * @param level is the log level of the message. + * @param message_ptr is the pointer to the message to be logged. + * @param message_length is the length of the message. + * + */ +void envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level level, + const char* message_ptr, size_t message_length); + +/** + * envoy_dynamic_module_callback_log_enabled is called by the module to check if the log level is + * enabled for logging for the dynamic modules Id. This can be used to avoid unnecessary + * string formatting and allocation if the log level is not enabled since calling this function + * should be negligible in terms of performance. + * + * @param level is the log level to check. + * @return true if the log level is enabled, false otherwise. + */ +bool envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level level); + +// ----------------------------- Metrics callbacks ----------------------------- + +/** + * envoy_dynamic_module_callback_http_filter_config_define_counter is called by the module during + * initialization to create a new Stats::Counter with the given name. + * + * @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig in which the + * counter will be defined. + * @param name is the name of the counter to be defined. + * @param name_length is the length of the name. + * @param counter_id_ptr where the opaque ID that represents a unique metric will be stored. This + * can be passed to envoy_dynamic_module_callback_http_filter_increment_counter together with + * filter_envoy_ptr created from filter_config_envoy_ptr. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_counter( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, size_t* counter_id_ptr); + +/** + * envoy_dynamic_module_callback_http_filter_config_define_counter_vec is called by the module + * during initialization to create a template for generating Stats::Counters with the given name and + * labels during the lifecycle of the module. + * + * @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig in which the + * counter will be defined. + * @param name is the name of the counter to be defined. + * @param name_length is the length of the name. + * @param label_names is the labels of the counter to be defined. + * @param label_names_length is the length of the label_names. + * @param counter_id_ptr where the opaque ID that represents a unique metric will be stored. This + * can be passed to envoy_dynamic_module_callback_http_filter_increment_counter together with + * filter_envoy_ptr created from filter_config_envoy_ptr. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_counter_vec( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + envoy_dynamic_module_type_module_buffer* label_names, size_t label_names_length, + size_t* counter_id_ptr); + +/** + * envoy_dynamic_module_callback_http_filter_increment_counter is called by the module to increment + * a previously defined counter. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the counter previously defined using the config that created + * filter_envoy_ptr + * @param value is the value to increment the counter by. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_increment_counter( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_increment_counter_vec is called by the module to + * increment a previously defined counter vec. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the counter previously defined using the config that created + * filter_envoy_ptr + * @param label_values is the values of the labels to be incremented. + * @param label_values_length is the length of the label_values. + * @param value is the value to increment the counter by. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_increment_counter_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_config_define_gauge is called by the module during + * initialization to create a new Stats::Gauge with the given name. + * + * @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig in which the + * gauge will be defined. + * @param name is the name of the gauge to be defined. + * @param name_length is the length of the name. + * @param gauge_id_ptr where the opaque ID that represents a unique metric will be stored. This can + * be passed to envoy_dynamic_module_callback_http_filter_increment_gauge together with + * filter_envoy_ptr created from filter_config_envoy_ptr. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_gauge( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, size_t* gauge_id_ptr); + +/** + * envoy_dynamic_module_callback_http_filter_config_define_gauge_vec is called by the module during + * initialization to create a template for generating Stats::Gauges with the given name and labels + * during the lifecycle of the module. + * + * @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig in which the + * gauge will be defined. + * @param name is the name of the gauge to be defined. + * @param name_length is the length of the name. + * @param label_names is the labels of the gauge to be defined. + * @param label_names_length is the length of the label_names. + * @param gauge_id_ptr where the opaque ID that represents a unique metric will be stored. This can + * be passed to envoy_dynamic_module_callback_http_filter_increment_gauge together with + * filter_envoy_ptr created from filter_config_envoy_ptr. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_gauge_vec( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + envoy_dynamic_module_type_module_buffer* label_names, size_t label_names_length, + size_t* gauge_id_ptr); + +/** + * envoy_dynamic_module_callback_http_filter_increase_gauge is called by the module to increase the + * value of a previously defined gauge. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the gauge previously defined using the config that created + * filter_envoy_ptr + * @param value is the value to increase the gauge by. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_increase_gauge( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_increase_gauge_vec is called by the module to increase + * the value of a previously defined gauge vec. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the gauge previously defined using the config that created + * filter_envoy_ptr + * @param label_values is the values of the labels to be increased. + * @param label_values_length is the length of the label_values. + * @param value is the value to increase the gauge by. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_increase_gauge_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_decrease_gauge is called by the module to decrease the + * value of a previously defined gauge. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the gauge previously defined using the config that created + * filter_envoy_ptr + * @param value is the value to decrease the gauge by. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_decrease_gauge( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_decrease_gauge_vec is called by the module to decrease + * the value of a previously defined gauge vec. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the gauge previously defined using the config that created + * filter_envoy_ptr + * @param label_values is the values of the labels to be decreased. + * @param label_values_length is the length of the label_values. + * @param value is the value to decrease the gauge by. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_decrease_gauge_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_set_gauge is called by the module to set the value + * of a previously defined gauge. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the gauge previously defined using the config that created + * filter_envoy_ptr + * @param value is the value to set the gauge to. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_set_gauge( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_set_gauge_vec is called by the module to set the value + * of a previously defined gauge vec. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object. + * @param id is the ID of the gauge previously defined using the config that created + * filter_envoy_ptr + * @param label_values is the values of the labels to be set. + * @param label_values_length is the length of the label_values. + * @param value is the value to set the gauge to. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_set_gauge_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_config_define_histogram is called by the module during + * initialization to create a new Stats::Histogram with the given name. + * + * @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig in which the + * histogram will be defined. + * @param name is the name of the histogram to be defined. + * @param name_length is the length of the name. + * @param histogram_id_ptr where the opaque ID that represents a unique metric will be stored. This + * can be passed to envoy_dynamic_module_callback_http_filter_increment_gauge together with + * filter_envoy_ptr created from filter_config_envoy_ptr. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_histogram( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, size_t* histogram_id_ptr); + +/** + * envoy_dynamic_module_callback_http_filter_config_define_histogram is called by the module during + * initialization to create a template for generating Stats::Histograms with the given name and + * labels during the lifecycle of the module. + * + * @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig in which the + * histogram will be defined. + * @param name is the name of the histogram to be defined. + * @param name_length is the length of the name. + * @param label_names is the labels of the histogram to be defined. + * @param label_names_length is the length of the label_names. + * @param histogram_id_ptr where the opaque ID that represents a unique metric will be stored. This + * can be passed to envoy_dynamic_module_callback_http_filter_increment_gauge together with + * filter_envoy_ptr created from filter_config_envoy_ptr. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_histogram_vec( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + envoy_dynamic_module_type_module_buffer* label_names, size_t label_names_length, + size_t* histogram_id_ptr); + +/** + * envoy_dynamic_module_callback_http_filter_record_histogram_value is called by the module to + * record a value in a previously defined histogram. + * + * @param histogram_envoy_ptr is a pointer to a histogram previously defined using + * envoy_dynamic_module_callback_http_define_histogram. + * @param id is the ID of the histogram previously defined using the config that created + * filter_envoy_ptr + * @param value is the value to record in the histogram. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_record_histogram_value( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value); + +/** + * envoy_dynamic_module_callback_http_filter_record_histogram_value is called by the module to + * record a value in a previously defined histogram vec. + * + * @param histogram_envoy_ptr is a pointer to a histogram previously defined using + * envoy_dynamic_module_callback_http_define_histogram. + * @param id is the ID of the histogram previously defined using the config that created + * filter_envoy_ptr + * @param label_values is the values of the labels to be recorded. + * @param label_values_length is the length of the label_values. + * @param value is the value to record in the histogram. + * @return the result of the operation. + */ +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value); + // ---------------------- HTTP Header/Trailer callbacks ------------------------ /** @@ -1020,6 +1364,22 @@ bool envoy_dynamic_module_callback_http_append_request_body( bool envoy_dynamic_module_callback_http_drain_request_body( envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t number_of_bytes); +/** + * envoy_dynamic_module_callback_http_inject_request_body is called by the module to + * inject the given request data directly into the filter stream. This method should only be called + * from a scheduled event. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object of the + * corresponding HTTP filter. + * @param data is the pointer to the buffer of the data to be injected. + * @param length is the length of the data. + * @param end_stream is true if this is the last data to be injected. + * @return true if the body is available, false otherwise. + */ +bool envoy_dynamic_module_callback_http_inject_request_body( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr data, size_t length, bool end_stream); + /** * This is the same as envoy_dynamic_module_callback_http_get_request_body_vector, but for the * response body. See the comments on envoy_dynamic_module_callback_http_get_request_body_vector @@ -1053,6 +1413,22 @@ bool envoy_dynamic_module_callback_http_append_response_body( bool envoy_dynamic_module_callback_http_drain_response_body( envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t number_of_bytes); +/** + * envoy_dynamic_module_callback_http_inject_response_body is called by the module to + * inject the given response data directly into the filter stream. This method should only be called + * from a scheduled event. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleHttpFilter object of the + * corresponding HTTP filter. + * @param data is the pointer to the buffer of the data to be injected. + * @param length is the length of the data. + * @param end_stream is true if this is the last data to be injected. + * @return true if the body is available, false otherwise. + */ +bool envoy_dynamic_module_callback_http_inject_response_body( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr data, size_t length, bool end_stream); + // ---------------------------- Metadata Callbacks ----------------------------- /** diff --git a/source/extensions/dynamic_modules/abi_version.h b/source/extensions/dynamic_modules/abi_version.h index 25fe27957a6ef..0607ad6628979 100644 --- a/source/extensions/dynamic_modules/abi_version.h +++ b/source/extensions/dynamic_modules/abi_version.h @@ -6,7 +6,7 @@ namespace DynamicModules { #endif // This is the ABI version calculated as a sha256 hash of the ABI header files. When the ABI // changes, this value must change, and the correctness of this value is checked by the test. -const char* kAbiVersion = "c32cc7696650a6a54653327e6609734a8b32aeb5c80a6a664687636a0d671666"; +const char* kAbiVersion = "f2712929b605772d35c34d9ac8ccd7e168197a50951e9c96b64e03256bf80265"; #ifdef __cplusplus } // namespace DynamicModules diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib.rs index 1ab6edda83555..28199cbd05eb5 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib.rs @@ -12,6 +12,7 @@ use mockall::*; #[path = "./lib_test.rs"] mod mod_test; +use crate::abi::envoy_dynamic_module_type_metrics_result; use std::any::Any; use std::sync::OnceLock; @@ -45,16 +46,13 @@ pub mod abi { /// _envoy_filter_config: &mut EC, /// _name: &str, /// _config: &[u8], -/// ) -> Option>> { +/// ) -> Option>> { /// Some(Box::new(MyHttpFilterConfig {})) /// } /// /// struct MyHttpFilterConfig {} /// -/// impl HttpFilterConfig -/// for MyHttpFilterConfig -/// { -/// } +/// impl HttpFilterConfig for MyHttpFilterConfig {} /// ``` #[macro_export] macro_rules! declare_init_functions { @@ -88,6 +86,107 @@ macro_rules! declare_init_functions { }; } +/// Log a trace message to Envoy's logging system with [dynamic_modules] Id. Messages won't be +/// allocated if the log level is not enabled on the Envoy side. +/// +/// This accepts the exact same arguments as the `format!` macro, so you can use it to log formatted +/// messages. +#[macro_export] +macro_rules! envoy_log_trace { + ($($arg:tt)*) => { + $crate::envoy_log!($crate::abi::envoy_dynamic_module_type_log_level::Trace, $($arg)*) + }; +} + +/// Log a debug message to Envoy's logging system with [dynamic_modules] Id. Messages won't be +/// allocated if the log level is not enabled on the Envoy side. +/// +/// This accepts the exact same arguments as the `format!` macro, so you can use it to log formatted +/// messages. +#[macro_export] +macro_rules! envoy_log_debug { + ($($arg:tt)*) => { + $crate::envoy_log!($crate::abi::envoy_dynamic_module_type_log_level::Debug, $($arg)*) + }; +} + +/// Log an info message to Envoy's logging system with [dynamic_modules] Id. Messages won't be +/// allocated if the log level is not enabled on the Envoy side. +/// +/// This accepts the exact same arguments as the `format!` macro, so you can use it to log formatted +/// messages. +#[macro_export] +macro_rules! envoy_log_info { + ($($arg:tt)*) => { + $crate::envoy_log!($crate::abi::envoy_dynamic_module_type_log_level::Info, $($arg)*) + }; +} + +/// Log a warning message to Envoy's logging system with [dynamic_modules] Id. Messages won't be +/// allocated if the log level is not enabled on the Envoy side. +/// +/// This accepts the exact same arguments as the `format!` macro, so you can use it to log formatted +/// messages. +#[macro_export] +macro_rules! envoy_log_warn { + ($($arg:tt)*) => { + $crate::envoy_log!($crate::abi::envoy_dynamic_module_type_log_level::Warn, $($arg)*) + }; +} + +/// Log an error message to Envoy's logging system with [dynamic_modules] Id. Messages won't be +/// allocated if the log level is not enabled on the Envoy side. +/// +/// This accepts the exact same arguments as the `format!` macro, so you can use it to log formatted +/// messages. +#[macro_export] +macro_rules! envoy_log_error { + ($($arg:tt)*) => { + $crate::envoy_log!($crate::abi::envoy_dynamic_module_type_log_level::Error, $($arg)*) + }; +} + +/// Log a critical message to Envoy's logging system with [dynamic_modules] Id. Messages won't be +/// allocated if the log level is not enabled on the Envoy side. +/// +/// This accepts the exact same arguments as the `format!` macro, so you can use it to log formatted +/// messages. +#[macro_export] +macro_rules! envoy_log_critical { + ($($arg:tt)*) => { + $crate::envoy_log!($crate::abi::envoy_dynamic_module_type_log_level::Critical, $($arg)*) + }; +} + +/// Internal logging macro that handles the actual call to the Envoy logging callback +/// used by envoy_log_* macros. +#[macro_export] +macro_rules! envoy_log { + ($level:expr, $($arg:tt)*) => { + { + #[cfg(not(test))] + unsafe { + // Avoid allocating the message if the log level is not enabled. + if $crate::abi::envoy_dynamic_module_callback_log_enabled($level) { + let message = format!($($arg)*); + let message_bytes = message.as_bytes(); + $crate::abi::envoy_dynamic_module_callback_log( + $level, + message_bytes.as_ptr() as *const ::std::os::raw::c_char, + message_bytes.len() + ); + } + } + // In unit tests, just print to stderr since the Envoy symbols are not available. + #[cfg(test)] + { + let message = format!($($arg)*); + eprintln!("[{}] {}", stringify!($level), message); + } + } + }; +} + /// The function signature for the program init function. /// /// This is called when the dynamic module is loaded, and it must return true on success, and false @@ -109,7 +208,7 @@ pub type NewHttpFilterConfigFunction = fn( envoy_filter_config: &mut EC, name: &str, config: &[u8], -) -> Option>>; +) -> Option>>; /// The global init function for HTTP filter configurations. This is set via the /// `declare_init_functions` macro, and is not intended to be set directly. @@ -138,10 +237,10 @@ pub static NEW_HTTP_FILTER_PER_ROUTE_CONFIG_FUNCTION: OnceLock< /// /// The object is created when the corresponding Envoy Http filter config is created, and it is /// dropped when the corresponding Envoy Http filter config is destroyed. Therefore, the -/// imlementation is recommended to implement the [`Drop`] trait to handle the necessary cleanup. -pub trait HttpFilterConfig { +/// implementation is recommended to implement the [`Drop`] trait to handle the necessary cleanup. +pub trait HttpFilterConfig { /// This is called when a HTTP filter chain is created for a new stream. - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { panic!("not implemented"); } } @@ -271,20 +370,204 @@ pub trait HttpFilter { /// An opaque object that represents the underlying Envoy Http filter config. This has one to one /// mapping with the Envoy Http filter config object as well as [`HttpFilterConfig`] object. pub trait EnvoyHttpFilterConfig { - // TODO: add methods like defining metrics, filter metadata, etc. + /// Define a new counter scoped to this filter config with the given name. + fn define_counter( + &mut self, + name: &str, + ) -> Result; + + // Define a new counter vec scoped to this filter config with the given name. + fn define_counter_vec( + &mut self, + name: &str, + labels: &[&str], + ) -> Result; + + /// Define a new gauge scoped to this filter config with the given name. + fn define_gauge( + &mut self, + name: &str, + ) -> Result; + + /// Define a new gauge vec scoped to this filter config with the given name. + fn define_gauge_vec( + &mut self, + name: &str, + labels: &[&str], + ) -> Result; + + /// Define a new histogram scoped to this filter config with the given name. + fn define_histogram( + &mut self, + name: &str, + ) -> Result; + + /// Define a new histogram vec scoped to this filter config with the given name. + fn define_histogram_vec( + &mut self, + name: &str, + labels: &[&str], + ) -> Result; } pub struct EnvoyHttpFilterConfigImpl { raw_ptr: abi::envoy_dynamic_module_type_http_filter_config_envoy_ptr, } -impl EnvoyHttpFilterConfig for EnvoyHttpFilterConfigImpl {} +impl EnvoyHttpFilterConfig for EnvoyHttpFilterConfigImpl { + fn define_counter( + &mut self, + name: &str, + ) -> Result { + let name_ptr = name.as_ptr(); + let name_size = name.len(); + let mut id: usize = 0; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_config_define_counter( + self.raw_ptr, + name_ptr as *const _ as *mut _, + name_size, + &mut id, + ) + })?; + Ok(EnvoyCounterId(id)) + } + + fn define_counter_vec( + &mut self, + name: &str, + labels: &[&str], + ) -> Result { + let name_ptr = name.as_ptr(); + let name_size = name.len(); + let labels_ptr = labels.as_ptr(); + let labels_size = labels.len(); + let mut id: usize = 0; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_config_define_counter_vec( + self.raw_ptr, + name_ptr as *const _ as *mut _, + name_size, + labels_ptr as *const _ as *mut _, + labels_size, + &mut id, + ) + })?; + Ok(EnvoyCounterVecId(id)) + } + + fn define_gauge( + &mut self, + name: &str, + ) -> Result { + let name_ptr = name.as_ptr(); + let name_size = name.len(); + let mut id: usize = 0; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_config_define_gauge( + self.raw_ptr, + name_ptr as *const _ as *mut _, + name_size, + &mut id, + ) + })?; + Ok(EnvoyGaugeId(id)) + } + + fn define_gauge_vec( + &mut self, + name: &str, + labels: &[&str], + ) -> Result { + let name_ptr = name.as_ptr(); + let name_size = name.len(); + let labels_ptr = labels.as_ptr(); + let labels_size = labels.len(); + let mut id: usize = 0; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_config_define_gauge_vec( + self.raw_ptr, + name_ptr as *const _ as *mut _, + name_size, + labels_ptr as *const _ as *mut _, + labels_size, + &mut id, + ) + })?; + Ok(EnvoyGaugeVecId(id)) + } + + fn define_histogram( + &mut self, + name: &str, + ) -> Result { + let name_ptr = name.as_ptr(); + let name_size = name.len(); + let mut id: usize = 0; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_config_define_histogram( + self.raw_ptr, + name_ptr as *const _ as *mut _, + name_size, + &mut id, + ) + })?; + Ok(EnvoyHistogramId(id)) + } + + fn define_histogram_vec( + &mut self, + name: &str, + labels: &[&str], + ) -> Result { + let name_ptr = name.as_ptr(); + let name_size = name.len(); + let labels_ptr = labels.as_ptr(); + let labels_size = labels.len(); + let mut id: usize = 0; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_config_define_histogram_vec( + self.raw_ptr, + name_ptr as *const _ as *mut _, + name_size, + labels_ptr as *const _ as *mut _, + labels_size, + &mut id, + ) + })?; + Ok(EnvoyHistogramVecId(id)) + } +} + +/// The identifier for an EnvoyCounter. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvoyCounterId(usize); + +/// The identifier for an EnvoyCounterVec. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvoyCounterVecId(usize); + +/// The identifier for an EnvoyGauge. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvoyGaugeId(usize); + +/// The identifier for an EnvoyGaugeVec. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvoyGaugeVecId(usize); + +/// The identifier for an EnvoyHistogram. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvoyHistogramId(usize); + +/// The identifier for an EnvoyHistogramVec. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvoyHistogramVecId(usize); /// An opaque object that represents the underlying Envoy Http filter. This has one to one /// mapping with the Envoy Http filter object as well as [`HttpFilter`] object per HTTP stream. /// /// The Envoy filter object is inherently not thread-safe, and it is always recommended to -/// access it from the same thread as the one that [`HttpFilter`] even hooks are called. +/// access it from the same thread as the one that [`HttpFilter`] event hooks are called. #[automock] #[allow(clippy::needless_lifetimes)] // Explicit lifetime specifiers are needed for mockall. pub trait EnvoyHttpFilter { @@ -523,6 +806,15 @@ pub trait EnvoyHttpFilter { /// content-length header if necessary. fn append_request_body(&mut self, data: &[u8]) -> bool; + /// Injects the given request data into the filter stream. + /// + /// Returns false if the request filter chain is not available. + /// + /// This must only be called from on_http_callout_done or on_scheduled callbacks. + /// The request filter should have been stopped and continue_decoding must not + /// be called. + fn inject_request_body(&mut self, data: &[u8], end_stream: bool) -> bool; + /// Get the currently buffered response body. The body is represented as a list of /// [`EnvoyBuffer`]. Memory contents pointed by each [`EnvoyBuffer`] is mutable and can be /// modified in place. However, the buffer itself is immutable. For example, adding or removing @@ -579,6 +871,15 @@ pub trait EnvoyHttpFilter { /// content-length header if necessary. fn append_response_body(&mut self, data: &[u8]) -> bool; + /// Injects the given response data into the filter stream. + /// + /// Returns false if the response filter chain is not available. + /// + /// This must only be called from on_http_callout_done or on_scheduled callbacks. + /// The response filter should have been stopped and continue_encoding must not + /// be called. + fn inject_response_body(&mut self, data: &[u8], end_stream: bool) -> bool; + /// Clear the route cache calculated during a previous phase of the filter chain. /// /// This is useful when the filter wants to force a re-evaluation of the route selection after @@ -681,6 +982,81 @@ pub trait EnvoyHttpFilter { /// } /// ``` fn new_scheduler(&self) -> Box; + + /// Increment the counter with the given id. + fn increment_counter( + &self, + id: EnvoyCounterId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Increment the counter vec with the given id. + fn increment_counter_vec<'a>( + &self, + id: EnvoyCounterVecId, + labels: &[&'a str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Increase the gauge with the given id. + fn increase_gauge( + &self, + id: EnvoyGaugeId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Increase the gauge vec with the given id. + fn increase_gauge_vec<'a>( + &self, + id: EnvoyGaugeVecId, + labels: &[&'a str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Decrease the gauge with the given id. + fn decrease_gauge( + &self, + id: EnvoyGaugeId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Decrease the gauge vec with the given id. + fn decrease_gauge_vec<'a>( + &self, + id: EnvoyGaugeVecId, + labels: &[&'a str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Set the value of the gauge with the given id. + fn set_gauge( + &self, + id: EnvoyGaugeId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Set the value of the gauge vec with the given id. + fn set_gauge_vec<'a>( + &self, + id: EnvoyGaugeVecId, + labels: &[&'a str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Record a value in the histogram with the given id. + fn record_histogram_value( + &self, + id: EnvoyHistogramId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; + + /// Record a value in the histogram vec with the given id. + fn record_histogram_value_vec<'a>( + &self, + id: EnvoyHistogramVecId, + labels: &[&'a str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result>; } /// This implements the [`EnvoyHttpFilter`] trait with the given raw pointer to the Envoy HTTP @@ -1038,6 +1414,17 @@ impl EnvoyHttpFilter for EnvoyHttpFilterImpl { } } + fn inject_request_body(&mut self, data: &[u8], end_stream: bool) -> bool { + unsafe { + abi::envoy_dynamic_module_callback_http_inject_request_body( + self.raw_ptr, + data.as_ptr() as *const _ as *mut _, + data.len(), + end_stream, + ) + } + } + fn get_response_body(&mut self) -> Option> { let mut size: usize = 0; let ok = unsafe { @@ -1077,6 +1464,17 @@ impl EnvoyHttpFilter for EnvoyHttpFilterImpl { } } + fn inject_response_body(&mut self, data: &[u8], end_stream: bool) -> bool { + unsafe { + abi::envoy_dynamic_module_callback_http_inject_response_body( + self.raw_ptr, + data.as_ptr() as *const _ as *mut _, + data.len(), + end_stream, + ) + } + } + fn clear_route_cache(&mut self) { unsafe { abi::envoy_dynamic_module_callback_http_clear_route_cache(self.raw_ptr) } } @@ -1234,6 +1632,192 @@ impl EnvoyHttpFilter for EnvoyHttpFilterImpl { }) } } + + fn increment_counter( + &self, + id: EnvoyCounterId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyCounterId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_increment_counter(self.raw_ptr, id, value) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn increment_counter_vec( + &self, + id: EnvoyCounterVecId, + labels: &[&str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyCounterVecId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_increment_counter_vec( + self.raw_ptr, + id, + labels.as_ptr() as *const _ as *mut _, + labels.len(), + value, + ) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn increase_gauge( + &self, + id: EnvoyGaugeId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyGaugeId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_increase_gauge(self.raw_ptr, id, value) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn increase_gauge_vec( + &self, + id: EnvoyGaugeVecId, + labels: &[&str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyGaugeVecId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_increase_gauge_vec( + self.raw_ptr, + id, + labels.as_ptr() as *const _ as *mut _, + labels.len(), + value, + ) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn decrease_gauge( + &self, + id: EnvoyGaugeId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyGaugeId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_decrease_gauge(self.raw_ptr, id, value) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn decrease_gauge_vec( + &self, + id: EnvoyGaugeVecId, + labels: &[&str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyGaugeVecId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_decrease_gauge_vec( + self.raw_ptr, + id, + labels.as_ptr() as *const _ as *mut _, + labels.len(), + value, + ) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn set_gauge( + &self, + id: EnvoyGaugeId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyGaugeId(id) = id; + let res = + unsafe { abi::envoy_dynamic_module_callback_http_filter_set_gauge(self.raw_ptr, id, value) }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn set_gauge_vec( + &self, + id: EnvoyGaugeVecId, + labels: &[&str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyGaugeVecId(id) = id; + let res = unsafe { + abi::envoy_dynamic_module_callback_http_filter_set_gauge_vec( + self.raw_ptr, + id, + labels.as_ptr() as *const _ as *mut _, + labels.len(), + value, + ) + }; + if res == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(res) + } + } + + fn record_histogram_value( + &self, + id: EnvoyHistogramId, + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyHistogramId(id) = id; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_record_histogram_value(self.raw_ptr, id, value) + })?; + Ok(()) + } + + fn record_histogram_value_vec( + &self, + id: EnvoyHistogramVecId, + labels: &[&str], + value: u64, + ) -> Result<(), envoy_dynamic_module_type_metrics_result> { + let EnvoyHistogramVecId(id) = id; + Result::from(unsafe { + abi::envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + self.raw_ptr, + id, + labels.as_ptr() as *const _ as *mut _, + labels.len(), + value, + ) + })?; + Ok(()) + } } impl EnvoyHttpFilterImpl { @@ -1370,7 +1954,7 @@ impl EnvoyHttpFilterImpl { /// This represents a thin thread-safe object that can be used to schedule a generic event to the /// Envoy HTTP filter on the work thread. /// -/// For eaxmple, this can be used to offload some blocking work from the HTTP filter processing +/// For example, this can be used to offload some blocking work from the HTTP filter processing /// thread to a module-managed thread, and then schedule an event to continue /// processing the request. /// @@ -1498,8 +2082,7 @@ fn envoy_dynamic_module_on_http_filter_config_new_impl( unsafe extern "C" fn envoy_dynamic_module_on_http_filter_config_destroy( config_ptr: abi::envoy_dynamic_module_type_http_filter_config_module_ptr, ) { - drop_wrapped_c_void_ptr!(config_ptr, - HttpFilterConfig); + drop_wrapped_c_void_ptr!(config_ptr, HttpFilterConfig); } #[no_mangle] @@ -1558,22 +2141,21 @@ unsafe extern "C" fn envoy_dynamic_module_on_http_filter_new( filter_config_ptr: abi::envoy_dynamic_module_type_http_filter_config_module_ptr, filter_envoy_ptr: abi::envoy_dynamic_module_type_http_filter_envoy_ptr, ) -> abi::envoy_dynamic_module_type_http_filter_module_ptr { - let mut envoy_filter_config = EnvoyHttpFilterConfigImpl { + let mut envoy_filter = EnvoyHttpFilterImpl { raw_ptr: filter_envoy_ptr, }; let filter_config = { - let raw = filter_config_ptr - as *mut *mut dyn HttpFilterConfig; + let raw = filter_config_ptr as *mut *mut dyn HttpFilterConfig; &mut **raw }; - envoy_dynamic_module_on_http_filter_new_impl(&mut envoy_filter_config, filter_config) + envoy_dynamic_module_on_http_filter_new_impl(&mut envoy_filter, filter_config) } fn envoy_dynamic_module_on_http_filter_new_impl( - envoy_filter_config: &mut EnvoyHttpFilterConfigImpl, - filter_config: &mut dyn HttpFilterConfig, + envoy_filter: &mut EnvoyHttpFilterImpl, + filter_config: &mut dyn HttpFilterConfig, ) -> abi::envoy_dynamic_module_type_http_filter_module_ptr { - let filter = filter_config.new_http_filter(envoy_filter_config); + let filter = filter_config.new_http_filter(envoy_filter); wrap_into_c_void_ptr!(filter) } @@ -1702,3 +2284,15 @@ unsafe extern "C" fn envoy_dynamic_module_on_http_filter_scheduled( let filter = &mut **filter; filter.on_scheduled(&mut EnvoyHttpFilterImpl::new(envoy_ptr), event_id); } + +impl From + for Result<(), envoy_dynamic_module_type_metrics_result> +{ + fn from(result: envoy_dynamic_module_type_metrics_result) -> Self { + if result == envoy_dynamic_module_type_metrics_result::Success { + Ok(()) + } else { + Err(result) + } + } +} diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs index 93741f2ebfa47..d2b32e426002f 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs @@ -2,13 +2,21 @@ use crate::*; #[cfg(test)] use std::sync::atomic::AtomicBool; // This is used for testing the drop, not for the actual concurrency. +#[test] +fn test_loggers() { + // Test that the loggers are defined and can be used during the unit tests, i.e., not trying to + // find the symbol implemented by Envoy. + envoy_log_trace!("message with an argument: {}", "argument"); + envoy_log_debug!("message with an argument: {}", "argument"); + envoy_log_info!("message with an argument: {}", "argument"); + envoy_log_warn!("message with an argument: {}", "argument"); + envoy_log_error!("message with an argument: {}", "argument"); +} + #[test] fn test_envoy_dynamic_module_on_http_filter_config_new_impl() { struct TestHttpFilterConfig; - impl HttpFilterConfig - for TestHttpFilterConfig - { - } + impl HttpFilterConfig for TestHttpFilterConfig {} let mut envoy_filter_config = EnvoyHttpFilterConfigImpl { raw_ptr: std::ptr::null_mut(), @@ -44,10 +52,7 @@ fn test_envoy_dynamic_module_on_http_filter_config_destroy() { // Box. static DROPPED: AtomicBool = AtomicBool::new(false); struct TestHttpFilterConfig; - impl HttpFilterConfig - for TestHttpFilterConfig - { - } + impl HttpFilterConfig for TestHttpFilterConfig {} impl Drop for TestHttpFilterConfig { fn drop(&mut self) { DROPPED.store(true, std::sync::atomic::Ordering::SeqCst); @@ -79,10 +84,8 @@ fn test_envoy_dynamic_module_on_http_filter_config_destroy() { fn test_envoy_dynamic_module_on_http_filter_new_destroy() { static DROPPED: AtomicBool = AtomicBool::new(false); struct TestHttpFilterConfig; - impl HttpFilterConfig - for TestHttpFilterConfig - { - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { + impl HttpFilterConfig for TestHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(TestHttpFilter) } } @@ -97,7 +100,7 @@ fn test_envoy_dynamic_module_on_http_filter_new_destroy() { let mut filter_config = TestHttpFilterConfig; let result = envoy_dynamic_module_on_http_filter_new_impl( - &mut EnvoyHttpFilterConfigImpl { + &mut EnvoyHttpFilterImpl { raw_ptr: std::ptr::null_mut(), }, &mut filter_config, @@ -115,10 +118,8 @@ fn test_envoy_dynamic_module_on_http_filter_new_destroy() { // This tests all the on_* methods on the HttpFilter trait through the actual entry points. fn test_envoy_dynamic_module_on_http_filter_callbacks() { struct TestHttpFilterConfig; - impl HttpFilterConfig - for TestHttpFilterConfig - { - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { + impl HttpFilterConfig for TestHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(TestHttpFilter) } } @@ -192,7 +193,7 @@ fn test_envoy_dynamic_module_on_http_filter_callbacks() { let mut filter_config = TestHttpFilterConfig; let filter = envoy_dynamic_module_on_http_filter_new_impl( - &mut EnvoyHttpFilterConfigImpl { + &mut EnvoyHttpFilterImpl { raw_ptr: std::ptr::null_mut(), }, &mut filter_config, diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 0e87a50c70422..99c9e01221cdf 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -27,6 +27,7 @@ EXTENSIONS = { "envoy.clusters.strict_dns": "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "envoy.clusters.original_dst": "//source/extensions/clusters/original_dst:original_dst_cluster_lib", "envoy.clusters.logical_dns": "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", + "envoy.clusters.reverse_connection": "//source/extensions/clusters/reverse_connection:reverse_connection_lib", # # Compression @@ -57,6 +58,13 @@ EXTENSIONS = { "envoy.bootstrap.wasm": "//source/extensions/bootstrap/wasm:config", + # + # Reverse Connection + # + + "envoy.bootstrap.reverse_tunnel.downstream_socket_interface": "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "envoy.bootstrap.reverse_tunnel.upstream_socket_interface": "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + # # Health checkers # @@ -96,6 +104,7 @@ EXTENSIONS = { "envoy.matching.inputs.direct_source_ip": "//source/extensions/matching/network/common:inputs_lib", "envoy.matching.inputs.source_type": "//source/extensions/matching/network/common:inputs_lib", "envoy.matching.inputs.server_name": "//source/extensions/matching/network/common:inputs_lib", + "envoy.matching.inputs.network_namespace": "//source/extensions/matching/network/common:inputs_lib", "envoy.matching.inputs.transport_protocol": "//source/extensions/matching/network/common:inputs_lib", "envoy.matching.inputs.filter_state": "//source/extensions/matching/network/common:inputs_lib", @@ -204,6 +213,7 @@ EXTENSIONS = { # configured on the listener. Do not remove it in that case or configs will fail to load. "envoy.filters.listener.proxy_protocol": "//source/extensions/filters/listener/proxy_protocol:config", "envoy.filters.listener.tls_inspector": "//source/extensions/filters/listener/tls_inspector:config", + "envoy.filters.listener.reverse_connection": "//source/extensions/filters/listener/reverse_connection:config_factory_lib", # # Network filters @@ -215,6 +225,7 @@ EXTENSIONS = { "envoy.filters.network.echo": "//source/extensions/filters/network/echo:config", "envoy.filters.network.ext_authz": "//source/extensions/filters/network/ext_authz:config", "envoy.filters.network.ext_proc": "//source/extensions/filters/network/ext_proc:config", + "envoy.filters.network.reverse_tunnel": "//source/extensions/filters/network/reverse_tunnel:config", "envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config", "envoy.filters.network.local_ratelimit": "//source/extensions/filters/network/local_ratelimit:config", "envoy.filters.network.mongo_proxy": "//source/extensions/filters/network/mongo_proxy:config", @@ -454,6 +465,7 @@ EXTENSIONS = { "envoy.formatter.cel": "//source/extensions/formatter/cel:config", "envoy.formatter.metadata": "//source/extensions/formatter/metadata:config", "envoy.formatter.req_without_query": "//source/extensions/formatter/req_without_query:config", + "envoy.built_in_formatters.xfcc_value": "//source/extensions/formatter/xfcc_value:config", # # Key value store @@ -484,11 +496,17 @@ EXTENSIONS = { # getaddrinfo DNS resolver extension can be used when the system resolver is desired (e.g., Android) "envoy.network.dns_resolver.getaddrinfo": "//source/extensions/network/dns_resolver/getaddrinfo:config", + # + # Address Resolvers + # + + "envoy.resolvers.reverse_connection": "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_resolver_lib", + # # Custom matchers # - "envoy.matching.custom_matchers.trie_matcher": "//source/extensions/common/matcher:trie_matcher_lib", + "envoy.matching.custom_matchers.ip_range_matcher": "//source/extensions/common/matcher:ip_range_matcher_lib", "envoy.matching.custom_matchers.domain_matcher": "//source/extensions/common/matcher:domain_matcher_lib", # diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 8398b376ea8cd..78c0d358ff89e 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -75,6 +75,20 @@ envoy.bootstrap.wasm: status: alpha type_urls: - envoy.extensions.wasm.v3.WasmService +envoy.bootstrap.reverse_tunnel.downstream_socket_interface: + categories: + - envoy.bootstrap + security_posture: unknown + status: wip + type_urls: + - envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface +envoy.bootstrap.reverse_tunnel.upstream_socket_interface: + categories: + - envoy.bootstrap + security_posture: unknown + status: wip + type_urls: + - envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3.UpstreamReverseConnectionSocketInterface envoy.extensions.http.cache.file_system_http_cache: categories: - envoy.http.cache @@ -270,7 +284,7 @@ envoy.filters.http.buffer: envoy.filters.http.cache: categories: - envoy.filters.http - security_posture: robust_to_untrusted_downstream_and_upstream + security_posture: unknown status: wip type_urls: - envoy.extensions.filters.http.cache.v3.CacheConfig @@ -569,6 +583,13 @@ envoy.filters.http.router: status: stable type_urls: - envoy.extensions.filters.http.router.v3.Router +envoy.filters.http.reverse_conn: + categories: + - envoy.filters.http + security_posture: robust_to_untrusted_downstream + status: alpha + type_urls: + - envoy.extensions.filters.http.reverse_conn.v3.ReverseConn envoy.filters.http.set_metadata: categories: - envoy.filters.http @@ -659,6 +680,13 @@ envoy.filters.listener.tls_inspector: status: stable type_urls: - envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector +envoy.filters.listener.reverse_connection: + categories: + - envoy.filters.listener + security_posture: robust_to_untrusted_downstream + status: alpha + type_urls: + - envoy.extensions.filters.listener.reverse_connection.v3.ReverseConnection envoy.filters.network.connection_limit: categories: - envoy.filters.network @@ -701,6 +729,13 @@ envoy.filters.network.ext_proc: status: wip type_urls: - envoy.extensions.filters.network.ext_proc.v3.NetworkExternalProcessor +envoy.filters.network.reverse_tunnel: + categories: + - envoy.filters.network + security_posture: unknown + status: alpha + type_urls: + - envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel envoy.filters.network.http_connection_manager: categories: - envoy.filters.network @@ -867,6 +902,12 @@ envoy.formatter.req_without_query: status: alpha type_urls: - envoy.extensions.formatter.req_without_query.v3.ReqWithoutQuery +envoy.built_in_formatters.xfcc_value: + categories: + - envoy.built_in_formatters + security_posture: unknown + status: alpha + undocumented: true envoy.geoip_providers.maxmind: categories: - envoy.geoip_providers @@ -1514,6 +1555,11 @@ envoy.network.dns_resolver.getaddrinfo: status: stable type_urls: - envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig +envoy.resolvers.reverse_connection: + categories: + - envoy.resolvers + security_posture: unknown + status: wip envoy.rbac.matchers.upstream_ip_port: categories: - envoy.rbac.matchers @@ -1676,6 +1722,14 @@ envoy.matching.inputs.dynamic_metadata: status: stable type_urls: - envoy.extensions.matching.common_inputs.network.v3.DynamicMetadataInput +envoy.matching.inputs.network_namespace: + categories: + - envoy.matching.http.input + - envoy.matching.network.input + security_posture: unknown + status: stable + type_urls: + - envoy.extensions.matching.common_inputs.network.v3.NetworkNamespaceInput envoy.matching.inputs.uri_san: categories: - envoy.matching.http.input @@ -1708,7 +1762,7 @@ envoy.matching.custom_matchers.domain_matcher: status: alpha type_urls: - xds.type.matcher.v3.ServerNameMatcher -envoy.matching.custom_matchers.trie_matcher: +envoy.matching.custom_matchers.ip_range_matcher: categories: - envoy.matching.http.custom_matchers - envoy.matching.network.custom_matchers diff --git a/source/extensions/filters/common/expr/cel_state.cc b/source/extensions/filters/common/expr/cel_state.cc index 7a25144b80ede..8672e26ceb884 100644 --- a/source/extensions/filters/common/expr/cel_state.cc +++ b/source/extensions/filters/common/expr/cel_state.cc @@ -40,10 +40,10 @@ CelValue CelState::exprValue(Protobuf::Arena* arena, bool last) const { } ProtobufTypes::MessagePtr CelState::serializeAsProto() const { - auto any = std::make_unique(); + auto any = std::make_unique(); if (type_ != CelStateType::Protobuf) { - ProtobufWkt::BytesValue value; + Protobuf::BytesValue value; value.set_value(value_); any->PackFrom(value); } else { diff --git a/source/extensions/filters/common/expr/context.cc b/source/extensions/filters/common/expr/context.cc index b46de5c899113..9a4d795b8c4c1 100644 --- a/source/extensions/filters/common/expr/context.cc +++ b/source/extensions/filters/common/expr/context.cc @@ -682,11 +682,11 @@ absl::optional FilterStateWrapper::operator[](CelValue key) const { // field support, but callers only want to access the whole object. if (object->hasFieldSupport()) { return CelValue::CreateMap( - ProtobufWkt::Arena::Create(&arena_, object)); + Protobuf::Arena::Create(&arena_, object)); } absl::optional serialized = object->serializeAsString(); if (serialized.has_value()) { - std::string* out = ProtobufWkt::Arena::Create(&arena_, serialized.value()); + std::string* out = Protobuf::Arena::Create(&arena_, serialized.value()); return CelValue::CreateBytes(out); } } diff --git a/source/extensions/filters/common/expr/context.h b/source/extensions/filters/common/expr/context.h index 5f966f1e8d705..0fd9f1a30a901 100644 --- a/source/extensions/filters/common/expr/context.h +++ b/source/extensions/filters/common/expr/context.h @@ -228,7 +228,7 @@ class BaseWrapper : public google::api::expr::runtime::CelMap { } protected: - ProtobufWkt::Arena& arena_; + Protobuf::Arena& arena_; }; class RequestWrapper : public BaseWrapper { diff --git a/source/extensions/filters/common/expr/evaluator.cc b/source/extensions/filters/common/expr/evaluator.cc index 0b634c16ca2fd..a5f943762972a 100644 --- a/source/extensions/filters/common/expr/evaluator.cc +++ b/source/extensions/filters/common/expr/evaluator.cc @@ -101,7 +101,7 @@ ActivationPtr createActivation(const LocalInfo::LocalInfo* local_info, response_trailers); } -BuilderPtr createBuilder(Protobuf::Arena* arena) { +BuilderInstanceSharedPtr createBuilder(Protobuf::Arena* arena) { ASSERT_IS_MAIN_OR_TEST_THREAD(); google::api::expr::runtime::InterpreterOptions options; @@ -138,62 +138,74 @@ BuilderPtr createBuilder(Protobuf::Arena* arena) { throw CelException(absl::StrCat("failed to register extension regex functions: ", ext_register_status.message())); } - return builder; + return std::make_shared(std::move(builder)); } SINGLETON_MANAGER_REGISTRATION(expression_builder); -BuilderInstanceSharedPtr getBuilder(Server::Configuration::CommonFactoryContext& context) { +BuilderInstanceSharedConstPtr getBuilder(Server::Configuration::CommonFactoryContext& context) { return context.singletonManager().getTyped( - SINGLETON_MANAGER_REGISTERED_NAME(expression_builder), - [] { return std::make_shared(createBuilder(nullptr)); }); + SINGLETON_MANAGER_REGISTERED_NAME(expression_builder), [] { return createBuilder(nullptr); }); } -// Converts from CEL canonical to CEL v1alpha1 -absl::optional -getExpr(const ::xds::type::v3::CelExpression& expression) { - ::cel::expr::Expr expr; - if (expression.has_cel_expr_checked()) { - expr = expression.cel_expr_checked().expr(); - } else if (expression.has_cel_expr_parsed()) { - expr = expression.cel_expr_parsed().expr(); - } else { - return {}; +absl::StatusOr +CompiledExpression::Create(const BuilderInstanceSharedConstPtr& builder, + const cel::expr::Expr& expr) { + std::vector warnings; + CompiledExpression out = CompiledExpression(builder, expr); + auto cel_expression_status = out.builder_->builder().CreateExpression( + &out.source_expr_, &cel::expr::SourceInfo::default_instance(), &warnings); + if (!cel_expression_status.ok()) { + return cel_expression_status.status(); } + out.expr_ = std::move(cel_expression_status.value()); + return out; +} - std::string data; - if (!expr.SerializeToString(&data)) { - return {}; +absl::StatusOr +CompiledExpression::Create(const BuilderInstanceSharedConstPtr& builder, + const xds::type::v3::CelExpression& xds_expr) { + // First try to get expression from the new CEL canonical format + if (xds_expr.has_cel_expr_checked()) { + return Create(builder, xds_expr.cel_expr_checked().expr()); + } else if (xds_expr.has_cel_expr_parsed()) { + return Create(builder, xds_expr.cel_expr_parsed().expr()); } - - // Parse the string into the target namespace message - google::api::expr::v1alpha1::Expr v1alpha1Expr; - if (!v1alpha1Expr.ParseFromString(data)) { - return {}; + // Fallback to handling legacy formats for backward compatibility + switch (xds_expr.expr_specifier_case()) { + case xds::type::v3::CelExpression::ExprSpecifierCase::kParsedExpr: + return Create(builder, xds_expr.parsed_expr().expr()); + case xds::type::v3::CelExpression::ExprSpecifierCase::kCheckedExpr: + return Create(builder, xds_expr.checked_expr().expr()); + default: + return absl::InvalidArgumentError("CEL expression not set."); } - - return v1alpha1Expr; + PANIC_DUE_TO_CORRUPT_ENUM; } -ExpressionPtr createExpression(Builder& builder, const google::api::expr::v1alpha1::Expr& expr) { - google::api::expr::v1alpha1::SourceInfo source_info; - auto cel_expression_status = builder.CreateExpression(&expr, &source_info); - if (!cel_expression_status.ok()) { - throw CelException( - absl::StrCat("failed to create an expression: ", cel_expression_status.status().message())); +absl::StatusOr +CompiledExpression::Create(const BuilderInstanceSharedConstPtr& builder, + const google::api::expr::v1alpha1::Expr& expr) { + std::string serialized; + if (!expr.SerializeToString(&serialized)) { + return absl::InvalidArgumentError( + "Failed to serialize google::api::expr::v1alpha1 expression."); } - return std::move(cel_expression_status.value()); + cel::expr::Expr new_expr; + if (!new_expr.ParseFromString(serialized)) { + return absl::InvalidArgumentError("Failed to convert to cel::expr expression."); + } + return Create(builder, new_expr); } -absl::optional evaluate(const Expression& expr, Protobuf::Arena& arena, - const LocalInfo::LocalInfo* local_info, - const StreamInfo::StreamInfo& info, - const Http::RequestHeaderMap* request_headers, - const Http::ResponseHeaderMap* response_headers, - const Http::ResponseTrailerMap* response_trailers) { +absl::optional CompiledExpression::evaluate( + Protobuf::Arena& arena, const ::Envoy::LocalInfo::LocalInfo* local_info, + const StreamInfo::StreamInfo& info, const ::Envoy::Http::RequestHeaderMap* request_headers, + const ::Envoy::Http::ResponseHeaderMap* response_headers, + const ::Envoy::Http::ResponseTrailerMap* response_trailers) const { auto activation = createActivation(local_info, info, request_headers, response_headers, response_trailers); - auto eval_status = expr.Evaluate(*activation, &arena); + auto eval_status = expr_->Evaluate(*activation, &arena); if (!eval_status.ok()) { return {}; } @@ -201,10 +213,15 @@ absl::optional evaluate(const Expression& expr, Protobuf::Arena& arena return eval_status.value(); } -bool matches(const Expression& expr, const StreamInfo::StreamInfo& info, - const Http::RequestHeaderMap& headers) { +absl::StatusOr CompiledExpression::evaluate(const Activation& activation, + Protobuf::Arena* arena) const { + return expr_->Evaluate(activation, arena); +} + +bool CompiledExpression::matches(const StreamInfo::StreamInfo& info, + const Http::RequestHeaderMap& headers) const { Protobuf::Arena arena; - auto eval_status = Expr::evaluate(expr, arena, nullptr, info, &headers, nullptr, nullptr); + auto eval_status = evaluate(arena, nullptr, info, &headers, nullptr, nullptr); if (!eval_status.has_value()) { return false; } diff --git a/source/extensions/filters/common/expr/evaluator.h b/source/extensions/filters/common/expr/evaluator.h index 74ad5adec69cc..bb1fb6135c4de 100644 --- a/source/extensions/filters/common/expr/evaluator.h +++ b/source/extensions/filters/common/expr/evaluator.h @@ -18,6 +18,7 @@ #include "eval/public/cel_value.h" #include "xds/type/v3/cel.pb.h" +#include "cel/expr/syntax.pb.h" #if defined(__GNUC__) #pragma GCC diagnostic pop @@ -32,7 +33,7 @@ namespace Expr { using Activation = google::api::expr::runtime::BaseActivation; using ActivationPtr = std::unique_ptr; using Builder = google::api::expr::runtime::CelExpressionBuilder; -using BuilderPtr = std::unique_ptr; +using BuilderConstPtr = std::unique_ptr; using Expression = google::api::expr::runtime::CelExpression; using ExpressionPtr = std::unique_ptr; @@ -77,44 +78,63 @@ ActivationPtr createActivation(const ::Envoy::LocalInfo::LocalInfo* local_info, // Shared expression builder instance. class BuilderInstance : public Singleton::Instance { public: - explicit BuilderInstance(BuilderPtr builder) : builder_(std::move(builder)) {} - Builder& builder() { return *builder_; } + explicit BuilderInstance(BuilderConstPtr builder) : builder_(std::move(builder)) {} + const Builder& builder() const { return *builder_; } private: - BuilderPtr builder_; + const BuilderConstPtr builder_; }; using BuilderInstanceSharedPtr = std::shared_ptr; +using BuilderInstanceSharedConstPtr = std::shared_ptr; // Creates an expression builder. The optional arena is used to enable constant folding // for intermediate evaluation results. // Throws an exception if fails to construct an expression builder. -BuilderPtr createBuilder(Protobuf::Arena* arena); +BuilderInstanceSharedPtr createBuilder(Protobuf::Arena* arena); // Gets the singleton expression builder. Must be called on the main thread. -BuilderInstanceSharedPtr getBuilder(Server::Configuration::CommonFactoryContext& context); - -// Converts from CEL canonical to CEL v1alpha1 -absl::optional -getExpr(const ::xds::type::v3::CelExpression& expression); - -// Creates an interpretable expression from a protobuf representation. -// Throws an exception if fails to construct a runtime expression. -ExpressionPtr createExpression(Builder& builder, const google::api::expr::v1alpha1::Expr& expr); - -// Evaluates an expression for a request. The arena is used to hold intermediate computational -// results and potentially the final value. -absl::optional evaluate(const Expression& expr, Protobuf::Arena& arena, - const ::Envoy::LocalInfo::LocalInfo* local_info, - const StreamInfo::StreamInfo& info, - const ::Envoy::Http::RequestHeaderMap* request_headers, - const ::Envoy::Http::ResponseHeaderMap* response_headers, - const ::Envoy::Http::ResponseTrailerMap* response_trailers); - -// Evaluates an expression and returns true if the expression evaluates to "true". -// Returns false if the expression fails to evaluate. -bool matches(const Expression& expr, const StreamInfo::StreamInfo& info, - const ::Envoy::Http::RequestHeaderMap& headers); +BuilderInstanceSharedConstPtr getBuilder(Server::Configuration::CommonFactoryContext& context); + +// Compiled CEL expression. This class ensures both the builder and the source expression outlive +// the compiled expression. +class CompiledExpression { +public: + // Creates an interpretable expression from the new CEL expr format, making a copy of it. + static absl::StatusOr Create(const BuilderInstanceSharedConstPtr& builder, + const cel::expr::Expr& expr); + + // Creates an interpretable expression from xDS CEL expr format, making a copy of it. + static absl::StatusOr Create(const BuilderInstanceSharedConstPtr& builder, + const xds::type::v3::CelExpression& expr); + + // DEPRECATED. Use the above. + static absl::StatusOr Create(const BuilderInstanceSharedConstPtr& builder, + const google::api::expr::v1alpha1::Expr& expr); + + // Evaluates an expression for a request. The arena is used to hold intermediate computational + // results and potentially the final value. + absl::optional + evaluate(Protobuf::Arena& arena, const ::Envoy::LocalInfo::LocalInfo* local_info, + const StreamInfo::StreamInfo& info, + const ::Envoy::Http::RequestHeaderMap* request_headers, + const ::Envoy::Http::ResponseHeaderMap* response_headers, + const ::Envoy::Http::ResponseTrailerMap* response_trailers) const; + + absl::StatusOr evaluate(const Activation& activation, Protobuf::Arena* arena) const; + + // Evaluates an expression and returns true if the expression evaluates to "true". + // Returns false if the expression fails to evaluate. + bool matches(const StreamInfo::StreamInfo& info, const Http::RequestHeaderMap& headers) const; + +private: + explicit CompiledExpression(const BuilderInstanceSharedConstPtr& builder, + const cel::expr::Expr& expr) + : builder_(builder), source_expr_(expr) {} + const BuilderInstanceSharedConstPtr builder_; + const cel::expr::Expr source_expr_; + ExpressionPtr expr_; +}; // Returns a string for a CelValue. std::string print(CelValue value); diff --git a/source/extensions/filters/common/ext_authz/ext_authz.h b/source/extensions/filters/common/ext_authz/ext_authz.h index 1e8f20b272e0d..80831badf9fe7 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz.h +++ b/source/extensions/filters/common/ext_authz/ext_authz.h @@ -110,6 +110,8 @@ struct Response { // "setCopy") to the response sent back to the downstream client on OK auth responses // only if the headers were returned from the authz server. UnsafeHeaderVector response_headers_to_overwrite_if_exists{}; + // Whether the authorization server returned any headers with an invalid append action type. + bool saw_invalid_append_actions{false}; // A set of HTTP headers consumed by the authorization server, will be removed // from the request to the upstream server. std::vector headers_to_remove{}; @@ -125,7 +127,7 @@ struct Response { // A set of metadata returned by the authorization server, that will be emitted as filter's // dynamic metadata that other filters can leverage. - ProtobufWkt::Struct dynamic_metadata{}; + Protobuf::Struct dynamic_metadata{}; // The gRPC status returned by the authorization server when it is making a // gRPC call. diff --git a/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc b/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc index 2190493375bb3..cc2858b0454f7 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc +++ b/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc @@ -43,7 +43,6 @@ void copyOkResponseMutations(ResponsePtr& response, } } else { switch (header.append_action()) { - PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; case Router::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD: response->response_headers_to_add.emplace_back(header.header().key(), header.header().value()); @@ -60,6 +59,9 @@ void copyOkResponseMutations(ResponsePtr& response, response->response_headers_to_set.emplace_back(header.header().key(), header.header().value()); break; + default: + response->saw_invalid_append_actions = true; + break; } } } diff --git a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc index 03ddd880e139e..0c9587fe119eb 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc +++ b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc @@ -41,12 +41,13 @@ const Response& errorResponse() { UnsafeHeaderVector{}, UnsafeHeaderVector{}, UnsafeHeaderVector{}, + false, {{}}, Http::Utility::QueryParamsVector{}, {}, EMPTY_STRING, Http::Code::Forbidden, - ProtobufWkt::Struct{}}); + Protobuf::Struct{}}); } // SuccessResponse used for creating either DENIED or OK authorization responses. @@ -133,6 +134,30 @@ ClientConfig::ClientConfig(const envoy::extensions::filters::http::ext_authz::v3 Router::HeaderParserPtr)), encode_raw_headers_(config.encode_raw_headers()) {} +ClientConfig::ClientConfig( + const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service, + bool encode_raw_headers, uint32_t timeout, Server::Configuration::CommonFactoryContext& context) + : client_header_matchers_(toClientMatchers( + http_service.authorization_response().allowed_client_headers(), context)), + client_header_on_success_matchers_(toClientMatchersOnSuccess( + http_service.authorization_response().allowed_client_headers_on_success(), context)), + to_dynamic_metadata_matchers_(toDynamicMetadataMatchers( + http_service.authorization_response().dynamic_metadata_from_headers(), context)), + upstream_header_matchers_(toUpstreamMatchers( + http_service.authorization_response().allowed_upstream_headers(), context)), + upstream_header_to_append_matchers_(toUpstreamMatchers( + http_service.authorization_response().allowed_upstream_headers_to_append(), context)), + cluster_name_(http_service.server_uri().cluster()), timeout_(timeout), + path_prefix_( + THROW_OR_RETURN_VALUE(validatePathPrefix(http_service.path_prefix()), std::string)), + tracing_name_(fmt::format("async {} egress", http_service.server_uri().cluster())), + request_headers_parser_(THROW_OR_RETURN_VALUE( + Router::HeaderParser::configure( + http_service.authorization_request().headers_to_add(), + envoy::config::core::v3::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD), + Router::HeaderParserPtr)), + encode_raw_headers_(encode_raw_headers) {} + MatcherSharedPtr ClientConfig::toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStringMatcher& list, Server::Configuration::CommonFactoryContext& context) { @@ -361,12 +386,13 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) { UnsafeHeaderVector{}, UnsafeHeaderVector{}, UnsafeHeaderVector{}, + false, std::move(headers_to_remove), Http::Utility::QueryParamsVector{}, {}, EMPTY_STRING, Http::Code::OK, - ProtobufWkt::Struct{}}}; + Protobuf::Struct{}}}; return std::move(ok.response_); } @@ -384,12 +410,13 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) { UnsafeHeaderVector{}, UnsafeHeaderVector{}, UnsafeHeaderVector{}, + false, {{}}, Http::Utility::QueryParamsVector{}, {}, message->bodyAsString(), static_cast(status_code), - ProtobufWkt::Struct{}}}; + Protobuf::Struct{}}}; return std::move(denied.response_); } diff --git a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h index d79b34876548d..0847afe6c5a14 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h +++ b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h @@ -28,6 +28,11 @@ class ClientConfig { uint32_t timeout, absl::string_view path_prefix, Server::Configuration::CommonFactoryContext& context); + // Build config directly from HttpService without constructing a temporary ExtAuthz. + ClientConfig(const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service, + bool encode_raw_headers, uint32_t timeout, + Server::Configuration::CommonFactoryContext& context); + /** * Returns the name of the authorization cluster. */ diff --git a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc index 1ff6b295b0d52..7b2f94da8cb25 100644 --- a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc +++ b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc @@ -37,10 +37,8 @@ ShareProviderManager::ShareProviderManager(Event::Dispatcher& main_dispatcher, : main_dispatcher_(main_dispatcher), cluster_(cluster) { // It's safe to capture the local cluster reference here because the local cluster is // guaranteed to be static cluster and should never be removed. - handle_ = cluster_.prioritySet().addMemberUpdateCb([this](const auto&, const auto&) { - share_monitor_->onLocalClusterUpdate(cluster_); - return absl::OkStatus(); - }); + handle_ = cluster_.prioritySet().addMemberUpdateCb( + [this](const auto&, const auto&) { share_monitor_->onLocalClusterUpdate(cluster_); }); share_monitor_ = std::make_shared(); share_monitor_->onLocalClusterUpdate(cluster_); } diff --git a/source/extensions/filters/common/lua/protobuf_converter.cc b/source/extensions/filters/common/lua/protobuf_converter.cc index 2e28f26580262..aacd6d5a69a1f 100644 --- a/source/extensions/filters/common/lua/protobuf_converter.cc +++ b/source/extensions/filters/common/lua/protobuf_converter.cc @@ -245,7 +245,7 @@ void ProtobufConverterUtils::pushLuaArrayFromRepeatedField( } int ProtobufConverterUtils::processDynamicTypedMetadataFromLuaCall( - lua_State* state, const Protobuf::Map& typed_metadata_map) { + lua_State* state, const Protobuf::Map& typed_metadata_map) { // Get filter name from Lua argument const absl::string_view filter_name = getStringViewFromLuaString(state, 2); @@ -258,8 +258,8 @@ int ProtobufConverterUtils::processDynamicTypedMetadataFromLuaCall( return 1; } - // The typed metadata is stored as a ProtobufWkt::Any - const ProtobufWkt::Any& any_message = it->second; + // The typed metadata is stored as a Protobuf::Any + const Protobuf::Any& any_message = it->second; // Extract the type name from the type URL absl::string_view type_url = any_message.type_url(); diff --git a/source/extensions/filters/common/lua/protobuf_converter.h b/source/extensions/filters/common/lua/protobuf_converter.h index a80827c138df1..83bf2166dd7f4 100644 --- a/source/extensions/filters/common/lua/protobuf_converter.h +++ b/source/extensions/filters/common/lua/protobuf_converter.h @@ -67,7 +67,7 @@ class ProtobufConverterUtils { * Returns nil if metadata is not found or cannot be processed. */ static int processDynamicTypedMetadataFromLuaCall( - lua_State* state, const Protobuf::Map& typed_metadata_map); + lua_State* state, const Protobuf::Map& typed_metadata_map); /** * Push a Lua value onto the stack that represents the value of a field diff --git a/source/extensions/filters/common/lua/wrappers.cc b/source/extensions/filters/common/lua/wrappers.cc index f440b88a71e23..fd43157b5d043 100644 --- a/source/extensions/filters/common/lua/wrappers.cc +++ b/source/extensions/filters/common/lua/wrappers.cc @@ -73,23 +73,23 @@ int BufferWrapper::luaSetBytes(lua_State* state) { return 1; } -void MetadataMapHelper::setValue(lua_State* state, const ProtobufWkt::Value& value) { - ProtobufWkt::Value::KindCase kind = value.kind_case(); +void MetadataMapHelper::setValue(lua_State* state, const Protobuf::Value& value) { + Protobuf::Value::KindCase kind = value.kind_case(); switch (kind) { - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::kNullValue: return lua_pushnil(state); - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: return lua_pushnumber(state, value.number_value()); - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: return lua_pushboolean(state, value.bool_value()); - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: return createTable(state, value.struct_value().fields()); - case ProtobufWkt::Value::kStringValue: { + case Protobuf::Value::kStringValue: { const auto& string_value = value.string_value(); return lua_pushlstring(state, string_value.data(), string_value.size()); } - case ProtobufWkt::Value::kListValue: { + case Protobuf::Value::kListValue: { const auto& list = value.list_value(); const int values_size = list.values_size(); @@ -111,13 +111,13 @@ void MetadataMapHelper::setValue(lua_State* state, const ProtobufWkt::Value& val } return; } - case ProtobufWkt::Value::KIND_NOT_SET: + case Protobuf::Value::KIND_NOT_SET: PANIC("not implemented"); } } void MetadataMapHelper::createTable(lua_State* state, - const Protobuf::Map& fields) { + const Protobuf::Map& fields) { lua_createtable(state, 0, fields.size()); for (const auto& field : fields) { int top = lua_gettop(state); @@ -128,17 +128,17 @@ void MetadataMapHelper::createTable(lua_State* state, } /** - * Converts the value on top of the Lua stack into a ProtobufWkt::Value. + * Converts the value on top of the Lua stack into a Protobuf::Value. * Any Lua types that cannot be directly mapped to Value types will * yield an error. */ -ProtobufWkt::Value MetadataMapHelper::loadValue(lua_State* state) { - ProtobufWkt::Value value; +Protobuf::Value MetadataMapHelper::loadValue(lua_State* state) { + Protobuf::Value value; int type = lua_type(state, -1); switch (type) { case LUA_TNIL: - value.set_null_value(ProtobufWkt::NullValue()); + value.set_null_value(Protobuf::NullValue()); break; case LUA_TNUMBER: value.set_number_value(static_cast(lua_tonumber(state, -1))); @@ -190,8 +190,8 @@ int MetadataMapHelper::tableLength(lua_State* state) { return static_cast(max); } -ProtobufWkt::ListValue MetadataMapHelper::loadList(lua_State* state, int length) { - ProtobufWkt::ListValue list; +Protobuf::ListValue MetadataMapHelper::loadList(lua_State* state, int length) { + Protobuf::ListValue list; for (int i = 1; i <= length; i++) { lua_rawgeti(state, -1, i); @@ -202,8 +202,8 @@ ProtobufWkt::ListValue MetadataMapHelper::loadList(lua_State* state, int length) return list; } -ProtobufWkt::Struct MetadataMapHelper::loadStruct(lua_State* state) { - ProtobufWkt::Struct struct_obj; +Protobuf::Struct MetadataMapHelper::loadStruct(lua_State* state) { + Protobuf::Struct struct_obj; lua_pushnil(state); while (lua_next(state, -2) != 0) { diff --git a/source/extensions/filters/common/lua/wrappers.h b/source/extensions/filters/common/lua/wrappers.h index d474555272794..b644a25ecf6e9 100644 --- a/source/extensions/filters/common/lua/wrappers.h +++ b/source/extensions/filters/common/lua/wrappers.h @@ -53,14 +53,14 @@ class BufferWrapper : public BaseLuaObject { class MetadataMapWrapper; struct MetadataMapHelper { - static void setValue(lua_State* state, const ProtobufWkt::Value& value); + static void setValue(lua_State* state, const Protobuf::Value& value); static void createTable(lua_State* state, - const Protobuf::Map& fields); - static ProtobufWkt::Value loadValue(lua_State* state); + const Protobuf::Map& fields); + static Protobuf::Value loadValue(lua_State* state); private: - static ProtobufWkt::Struct loadStruct(lua_State* state); - static ProtobufWkt::ListValue loadList(lua_State* state, int length); + static Protobuf::Struct loadStruct(lua_State* state); + static Protobuf::ListValue loadList(lua_State* state, int length); static int tableLength(lua_State* state); }; @@ -77,7 +77,7 @@ class MetadataMapIterator : public BaseLuaObject { private: MetadataMapWrapper& parent_; - Protobuf::Map::const_iterator current_; + Protobuf::Map::const_iterator current_; }; /** @@ -85,7 +85,7 @@ class MetadataMapIterator : public BaseLuaObject { */ class MetadataMapWrapper : public BaseLuaObject { public: - MetadataMapWrapper(const ProtobufWkt::Struct& metadata) : metadata_{metadata} {} + MetadataMapWrapper(const Protobuf::Struct& metadata) : metadata_{metadata} {} static ExportedFunctions exportedFunctions() { return {{"get", static_luaGet}, {"__pairs", static_luaPairs}}; @@ -111,7 +111,7 @@ class MetadataMapWrapper : public BaseLuaObject { iterator_.reset(); } - const ProtobufWkt::Struct metadata_; + const Protobuf::Struct metadata_; LuaDeathRef iterator_; friend class MetadataMapIterator; diff --git a/source/extensions/filters/common/ratelimit/ratelimit.h b/source/extensions/filters/common/ratelimit/ratelimit.h index 44a29d13da6ac..975897a31b519 100644 --- a/source/extensions/filters/common/ratelimit/ratelimit.h +++ b/source/extensions/filters/common/ratelimit/ratelimit.h @@ -35,7 +35,7 @@ enum class LimitStatus { using DescriptorStatusList = std::vector; using DescriptorStatusListPtr = std::unique_ptr; -using DynamicMetadataPtr = std::unique_ptr; +using DynamicMetadataPtr = std::unique_ptr; /** * Async callbacks used during limit() calls. @@ -74,6 +74,15 @@ class Client { */ virtual void cancel() PURE; + /** + * Detach an inflight limit request. This will not cancel the request but will clean up + * all context associated with downstream request to avoid dangling references. + * NOTE: the callbacks that registered to take the response will be kept to handle the response + * when it arrives. The caller is responsible for ensuring that the callbacks have enough + * lifetime to handle the response. + */ + virtual void detach() PURE; + /** * Request a limit check. Note that this abstract API matches the design of Lyft's GRPC based * rate limit service. See ratelimit.proto for details. Any other rate limit implementations @@ -90,7 +99,7 @@ class Client { */ virtual void limit(RequestCallbacks& callbacks, const std::string& domain, const std::vector& descriptors, - Tracing::Span& parent_span, OptRef stream_info, + Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info, uint32_t hits_addend) PURE; }; diff --git a/source/extensions/filters/common/ratelimit/ratelimit_impl.cc b/source/extensions/filters/common/ratelimit/ratelimit_impl.cc index f5918e33529bb..f2db678d0090b 100644 --- a/source/extensions/filters/common/ratelimit/ratelimit_impl.cc +++ b/source/extensions/filters/common/ratelimit/ratelimit_impl.cc @@ -29,10 +29,21 @@ GrpcClientImpl::~GrpcClientImpl() { ASSERT(!callbacks_); } void GrpcClientImpl::cancel() { ASSERT(callbacks_ != nullptr); - request_->cancel(); + if (request_) { + request_->cancel(); + request_ = nullptr; + } callbacks_ = nullptr; } +void GrpcClientImpl::detach() { + ASSERT(callbacks_ != nullptr); + if (request_) { + request_->detach(); + request_ = nullptr; + } +} + void GrpcClientImpl::createRequest(envoy::service::ratelimit::v3::RateLimitRequest& request, const std::string& domain, const std::vector& descriptors, @@ -62,19 +73,21 @@ void GrpcClientImpl::createRequest(envoy::service::ratelimit::v3::RateLimitReque void GrpcClientImpl::limit(RequestCallbacks& callbacks, const std::string& domain, const std::vector& descriptors, - Tracing::Span& parent_span, - OptRef stream_info, uint32_t hits_addend) { + Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info, + uint32_t hits_addend) { ASSERT(callbacks_ == nullptr); callbacks_ = &callbacks; envoy::service::ratelimit::v3::RateLimitRequest request; createRequest(request, domain, descriptors, hits_addend); - auto options = Http::AsyncClient::RequestOptions().setTimeout(timeout_); - if (stream_info.has_value()) { - options.setParentContext(Http::AsyncClient::ParentContext{stream_info.ptr()}); + auto options = Http::AsyncClient::RequestOptions().setTimeout(timeout_).setParentContext( + Http::AsyncClient::ParentContext{&stream_info}); + auto inflight_request = + async_client_->send(service_method_, request, *this, parent_span, options); + if (inflight_request != nullptr) { + request_ = inflight_request; } - request_ = async_client_->send(service_method_, request, *this, parent_span, options); } void GrpcClientImpl::onSuccess( @@ -109,7 +122,7 @@ void GrpcClientImpl::onSuccess( response->statuses().begin(), response->statuses().end()); DynamicMetadataPtr dynamic_metadata = response->has_dynamic_metadata() - ? std::make_unique(response->dynamic_metadata()) + ? std::make_unique(response->dynamic_metadata()) : nullptr; // The rate limit requests applied on stream-done will destroy the client inside the complete // callback, so we release the callback here to make the destructor happy. diff --git a/source/extensions/filters/common/ratelimit/ratelimit_impl.h b/source/extensions/filters/common/ratelimit/ratelimit_impl.h index 61a6c1c5ec880..f3c34280a200d 100644 --- a/source/extensions/filters/common/ratelimit/ratelimit_impl.h +++ b/source/extensions/filters/common/ratelimit/ratelimit_impl.h @@ -55,9 +55,10 @@ class GrpcClientImpl : public Client, // Filters::Common::RateLimit::Client void cancel() override; + void detach() override; void limit(RequestCallbacks& callbacks, const std::string& domain, const std::vector& descriptors, - Tracing::Span& parent_span, OptRef stream_info, + Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info, uint32_t hits_addend = 0) override; // Grpc::AsyncRequestCallbacks diff --git a/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc b/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc index fd8e1bdd310a3..7621230463128 100644 --- a/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc +++ b/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc @@ -142,7 +142,7 @@ void RateLimitPolicy::populateDescriptors(const Http::RequestHeaderMap& headers, // Populate hits_addend if set. if (hits_addend_provider_ != nullptr) { - const ProtobufWkt::Value hits_addend_value = + const Protobuf::Value hits_addend_value = hits_addend_provider_->formatValueWithContext({&headers}, stream_info); double hits_addend = 0; diff --git a/source/extensions/filters/common/rbac/BUILD b/source/extensions/filters/common/rbac/BUILD index 21a02747115f3..1c3e87c8a770e 100644 --- a/source/extensions/filters/common/rbac/BUILD +++ b/source/extensions/filters/common/rbac/BUILD @@ -49,6 +49,7 @@ envoy_cc_library( "//source/common/common:matchers_lib", "//source/common/http:header_utility_lib", "//source/common/network:cidr_range_lib", + "//source/common/network:lc_trie_lib", "//source/extensions/filters/common/expr:evaluator_lib", "//source/extensions/path/match/uri_template:uri_template_match_lib", "@com_google_absl//absl/types:optional", diff --git a/source/extensions/filters/common/rbac/engine_impl.cc b/source/extensions/filters/common/rbac/engine_impl.cc index e53f2265fc974..de30fce305d63 100644 --- a/source/extensions/filters/common/rbac/engine_impl.cc +++ b/source/extensions/filters/common/rbac/engine_impl.cc @@ -11,9 +11,9 @@ namespace Filters { namespace Common { namespace RBAC { -Envoy::Matcher::ActionFactoryCb -ActionFactory::createActionFactoryCb(const Protobuf::Message& config, ActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) { +Envoy::Matcher::ActionConstSharedPtr +ActionFactory::createAction(const Protobuf::Message& config, ActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& action_config = MessageUtil::downcastAndValidate(config, validation_visitor); @@ -25,7 +25,7 @@ ActionFactory::createActionFactoryCb(const Protobuf::Message& config, ActionCont context.has_log_ = true; } - return [name, action]() { return std::make_unique(name, action); }; + return std::make_shared(name, action); } REGISTER_FACTORY(ActionFactory, Envoy::Matcher::ActionFactory); @@ -33,7 +33,7 @@ REGISTER_FACTORY(ActionFactory, Envoy::Matcher::ActionFactory); void generateLog(StreamInfo::StreamInfo& info, EnforcementMode mode, bool log) { // If not shadow enforcement, set shared log metadata. if (mode != EnforcementMode::Shadow) { - ProtobufWkt::Struct log_metadata; + Protobuf::Struct log_metadata; auto& log_fields = *log_metadata.mutable_fields(); log_fields[DynamicMetadataKeysSingleton::get().AccessLogKey].set_bool_value(log); info.setDynamicMetadata(DynamicMetadataKeysSingleton::get().CommonNamespace, log_metadata); @@ -45,16 +45,20 @@ RoleBasedAccessControlEngineImpl::RoleBasedAccessControlEngineImpl( ProtobufMessage::ValidationVisitor& validation_visitor, Server::Configuration::CommonFactoryContext& context, const EnforcementMode mode) : action_(rules.action()), mode_(mode) { + // A pointer to the builder, if one will be created. + Expr::BuilderInstanceSharedPtr builder = nullptr; // guard expression builder by presence of a condition in policies for (const auto& policy : rules.policies()) { if (policy.second.has_condition()) { - builder_ = Expr::createBuilder(&constant_arena_); + builder_with_arena_ = std::make_unique(); + builder_with_arena_->builder_ = Expr::createBuilder(&builder_with_arena_->constant_arena_); + builder = builder_with_arena_->builder_; break; } } for (const auto& policy : rules.policies()) { - policies_.emplace(policy.first, std::make_unique(policy.second, builder_.get(), + policies_.emplace(policy.first, std::make_unique(policy.second, builder, validation_visitor, context)); } } @@ -138,18 +142,17 @@ bool RoleBasedAccessControlMatcherEngineImpl::handleAction( Envoy::Matcher::evaluateMatch(*matcher_, data); ASSERT(result.isComplete()); if (result.isMatch()) { - auto action = result.action()->getTyped(); + const auto& action = result.action()->getTyped(); if (effective_policy_id != nullptr) { *effective_policy_id = action.name(); } // If there is at least an LOG action in matchers, we have to turn on and off for shared log // metadata every time when there is a connection or request. - auto rbac_action = action.action(); + const auto rbac_action = action.action(); if (has_log_) { generateLog(info, mode_, rbac_action == envoy::config::rbac::v3::RBAC::LOG); } - switch (rbac_action) { PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; case envoy::config::rbac::v3::RBAC::ALLOW: diff --git a/source/extensions/filters/common/rbac/engine_impl.h b/source/extensions/filters/common/rbac/engine_impl.h index ad22de9ed5553..62e7ae873df91 100644 --- a/source/extensions/filters/common/rbac/engine_impl.h +++ b/source/extensions/filters/common/rbac/engine_impl.h @@ -50,9 +50,9 @@ class Action : public Envoy::Matcher::ActionBase { public: - Envoy::Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, ActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Envoy::Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, ActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; std::string name() const override { return "envoy.filters.rbac.action"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); @@ -88,8 +88,13 @@ class RoleBasedAccessControlEngineImpl : public RoleBasedAccessControlEngine, No std::map> policies_; - Protobuf::Arena constant_arena_; - Expr::BuilderPtr builder_; + // Encapsulated the CEL expression builder with the arena, that will only be + // allocated if CEL is configured. + struct ExprBuilderWithArena { + Protobuf::Arena constant_arena_; + Expr::BuilderInstanceSharedPtr builder_; + }; + std::unique_ptr builder_with_arena_; }; class RoleBasedAccessControlMatcherEngineImpl : public RoleBasedAccessControlEngine, NonCopyable { diff --git a/source/extensions/filters/common/rbac/matchers.cc b/source/extensions/filters/common/rbac/matchers.cc index 2f866bc1ba3cc..e849a32f6d0e9 100644 --- a/source/extensions/filters/common/rbac/matchers.cc +++ b/source/extensions/filters/common/rbac/matchers.cc @@ -1,5 +1,6 @@ #include "source/extensions/filters/common/rbac/matchers.h" +#include "envoy/common/exception.h" #include "envoy/config/rbac/v3/rbac.pb.h" #include "envoy/upstream/upstream.h" @@ -23,9 +24,12 @@ MatcherConstPtr Matcher::create(const envoy::config::rbac::v3::Permission& permi return std::make_unique(permission.or_rules(), validation_visitor, context); case envoy::config::rbac::v3::Permission::RuleCase::kHeader: return std::make_unique(permission.header(), context); - case envoy::config::rbac::v3::Permission::RuleCase::kDestinationIp: - return std::make_unique(permission.destination_ip(), - IPMatcher::Type::DownstreamLocal); + case envoy::config::rbac::v3::Permission::RuleCase::kDestinationIp: { + auto matcher_result = + IPMatcher::create(permission.destination_ip(), IPMatcher::Type::DownstreamLocal); + THROW_IF_NOT_OK_REF(matcher_result.status()); + return std::move(matcher_result.value()); + } case envoy::config::rbac::v3::Permission::RuleCase::kDestinationPort: return std::make_unique(permission.destination_port()); case envoy::config::rbac::v3::Permission::RuleCase::kDestinationPortRange: @@ -74,15 +78,24 @@ MatcherConstPtr Matcher::create(const envoy::config::rbac::v3::Principal& princi return std::make_unique(principal.or_ids(), context); case envoy::config::rbac::v3::Principal::IdentifierCase::kAuthenticated: return std::make_unique(principal.authenticated(), context); - case envoy::config::rbac::v3::Principal::IdentifierCase::kSourceIp: - return std::make_unique(principal.source_ip(), - IPMatcher::Type::ConnectionRemote); - case envoy::config::rbac::v3::Principal::IdentifierCase::kDirectRemoteIp: - return std::make_unique(principal.direct_remote_ip(), - IPMatcher::Type::DownstreamDirectRemote); - case envoy::config::rbac::v3::Principal::IdentifierCase::kRemoteIp: - return std::make_unique(principal.remote_ip(), - IPMatcher::Type::DownstreamRemote); + case envoy::config::rbac::v3::Principal::IdentifierCase::kSourceIp: { + auto matcher_result = + IPMatcher::create(principal.source_ip(), IPMatcher::Type::ConnectionRemote); + THROW_IF_NOT_OK_REF(matcher_result.status()); + return std::move(matcher_result.value()); + } + case envoy::config::rbac::v3::Principal::IdentifierCase::kDirectRemoteIp: { + auto matcher_result = + IPMatcher::create(principal.direct_remote_ip(), IPMatcher::Type::DownstreamDirectRemote); + THROW_IF_NOT_OK_REF(matcher_result.status()); + return std::move(matcher_result.value()); + } + case envoy::config::rbac::v3::Principal::IdentifierCase::kRemoteIp: { + auto matcher_result = + IPMatcher::create(principal.remote_ip(), IPMatcher::Type::DownstreamRemote); + THROW_IF_NOT_OK_REF(matcher_result.status()); + return std::move(matcher_result.value()); + } case envoy::config::rbac::v3::Principal::IdentifierCase::kHeader: return std::make_unique(principal.header(), context); case envoy::config::rbac::v3::Principal::IdentifierCase::kAny: @@ -113,15 +126,17 @@ MatcherConstPtr Matcher::create(const envoy::config::rbac::v3::Principal& princi AndMatcher::AndMatcher(const envoy::config::rbac::v3::Permission::Set& set, ProtobufMessage::ValidationVisitor& validation_visitor, Server::Configuration::CommonFactoryContext& context) { + matchers_.reserve(set.rules_size()); for (const auto& rule : set.rules()) { - matchers_.push_back(Matcher::create(rule, validation_visitor, context)); + matchers_.emplace_back(Matcher::create(rule, validation_visitor, context)); } } AndMatcher::AndMatcher(const envoy::config::rbac::v3::Principal::Set& set, Server::Configuration::CommonFactoryContext& context) { + matchers_.reserve(set.ids_size()); for (const auto& id : set.ids()) { - matchers_.push_back(Matcher::create(id, context)); + matchers_.emplace_back(Matcher::create(id, context)); } } @@ -140,15 +155,17 @@ bool AndMatcher::matches(const Network::Connection& connection, OrMatcher::OrMatcher(const Protobuf::RepeatedPtrField& rules, ProtobufMessage::ValidationVisitor& validation_visitor, Server::Configuration::CommonFactoryContext& context) { + matchers_.reserve(rules.size()); for (const auto& rule : rules) { - matchers_.push_back(Matcher::create(rule, validation_visitor, context)); + matchers_.emplace_back(Matcher::create(rule, validation_visitor, context)); } } OrMatcher::OrMatcher(const Protobuf::RepeatedPtrField& ids, Server::Configuration::CommonFactoryContext& context) { + matchers_.reserve(ids.size()); for (const auto& id : ids) { - matchers_.push_back(Matcher::create(id, context)); + matchers_.emplace_back(Matcher::create(id, context)); } } @@ -176,24 +193,89 @@ bool HeaderMatcher::matches(const Network::Connection&, return header_->matchesHeaders(headers); } -bool IPMatcher::matches(const Network::Connection& connection, const Envoy::Http::RequestHeaderMap&, - const StreamInfo::StreamInfo& info) const { - Envoy::Network::Address::InstanceConstSharedPtr ip; +// static +absl::StatusOr> +IPMatcher::create(const envoy::config::core::v3::CidrRange& range, Type type) { + // Convert single range to CidrRange with proper error handling. + auto cidr_result = Network::Address::CidrRange::create(range); + if (!cidr_result.ok()) { + return absl::InvalidArgumentError( + fmt::format("Failed to create CIDR range: {}", cidr_result.status().message())); + } + + std::vector ranges; + ranges.push_back(std::move(cidr_result.value())); + + // Create LC Trie directly following the pattern from Unified IP Matcher. + // Note: LcTrie constructor may throw EnvoyException on invalid input, but this + // should not happen as we've already validated the CIDR range above. + auto trie = std::make_unique>( + std::vector>>{{true, ranges}}); + + return std::unique_ptr(new IPMatcher(std::move(trie), type)); +} + +// static +absl::StatusOr> +IPMatcher::create(const Protobuf::RepeatedPtrField& ranges, + Type type) { + if (ranges.empty()) { + return absl::InvalidArgumentError("Empty IP range list provided"); + } + + // Convert protobuf ranges to CidrRange vector. + std::vector cidr_ranges; + cidr_ranges.reserve(ranges.size()); + for (const auto& range : ranges) { + auto cidr_result = Network::Address::CidrRange::create(range); + if (!cidr_result.ok()) { + return absl::InvalidArgumentError( + fmt::format("Failed to create CIDR range: {}", cidr_result.status().message())); + } + cidr_ranges.push_back(std::move(cidr_result.value())); + } + + // Create LC Trie directly following the pattern from Unified IP Matcher. + // Note: LcTrie constructor may throw EnvoyException on invalid input, but this + // should not happen as we've already validated the CIDR range above. + auto trie = std::make_unique>( + std::vector>>{{true, cidr_ranges}}); + + return std::unique_ptr(new IPMatcher(std::move(trie), type)); +} + +IPMatcher::IPMatcher(std::unique_ptr> trie, Type type) + : trie_(std::move(trie)), type_(type) {} + +const Network::Address::InstanceConstSharedPtr& +IPMatcher::extractIpAddress(const Network::Connection& connection, + const StreamInfo::StreamInfo& info) const { switch (type_) { case ConnectionRemote: - ip = connection.connectionInfoProvider().remoteAddress(); - break; + return connection.connectionInfoProvider().remoteAddress(); case DownstreamLocal: - ip = info.downstreamAddressProvider().localAddress(); - break; + return info.downstreamAddressProvider().localAddress(); case DownstreamDirectRemote: - ip = info.downstreamAddressProvider().directRemoteAddress(); - break; + return info.downstreamAddressProvider().directRemoteAddress(); case DownstreamRemote: - ip = info.downstreamAddressProvider().remoteAddress(); - break; + return info.downstreamAddressProvider().remoteAddress(); + } + PANIC_DUE_TO_CORRUPT_ENUM; +} + +bool IPMatcher::matches(const Network::Connection& connection, const Envoy::Http::RequestHeaderMap&, + const StreamInfo::StreamInfo& info) const { + // Extract IP address using reference to avoid shared_ptr copies. + const auto& address = extractIpAddress(connection, info); + // Guard against non-IP addresses (e.g., pipe) or missing address. + if (!address) { + return false; + } + const auto* ip = address->ip(); + if (ip == nullptr) { + return false; } - return range_.isInRange(*ip.get()); + return !trie_->getData(address).empty(); } bool PortMatcher::matches(const Network::Connection&, const Envoy::Http::RequestHeaderMap&, @@ -284,7 +366,7 @@ bool PolicyMatcher::matches(const Network::Connection& connection, const StreamInfo::StreamInfo& info) const { return permissions_.matches(connection, headers, info) && principals_.matches(connection, headers, info) && - (expr_ == nullptr ? true : Expr::matches(*expr_, info, headers)); + (expr_ ? expr_->matches(info, headers) : true); } bool RequestedServerNameMatcher::matches(const Network::Connection& connection, diff --git a/source/extensions/filters/common/rbac/matchers.h b/source/extensions/filters/common/rbac/matchers.h index 3198598f54087..18c68ebf04a01 100644 --- a/source/extensions/filters/common/rbac/matchers.h +++ b/source/extensions/filters/common/rbac/matchers.h @@ -10,10 +10,13 @@ #include "source/common/common/matchers.h" #include "source/common/http/header_utility.h" #include "source/common/network/cidr_range.h" +#include "source/common/network/lc_trie.h" #include "source/extensions/filters/common/expr/evaluator.h" #include "source/extensions/filters/common/rbac/matcher_interface.h" #include "source/extensions/path/match/uri_template/uri_template_match.h" +#include "cel/expr/syntax.pb.h" + namespace Envoy { namespace Extensions { namespace Filters { @@ -111,23 +114,35 @@ class HeaderMatcher : public Matcher { }; /** - * Perform a match against an IP CIDR range. This rule can be applied to connection remote, + * Perform a match against IP CIDR ranges. This rule can be applied to connection remote, * downstream local address, downstream direct remote address or downstream remote address. + * Uses LC Trie algorithm for optimal O(log n) performance in IP address range matching. */ class IPMatcher : public Matcher { public: enum Type { ConnectionRemote = 0, DownstreamLocal, DownstreamDirectRemote, DownstreamRemote }; - IPMatcher(const envoy::config::core::v3::CidrRange& range, Type type) - : range_(THROW_OR_RETURN_VALUE(Network::Address::CidrRange::create(range), - Network::Address::CidrRange)), - type_(type) {} + // Single IP range constructor. + static absl::StatusOr> + create(const envoy::config::core::v3::CidrRange& range, Type type); + + // Multiple IP ranges constructor. + static absl::StatusOr> + create(const Protobuf::RepeatedPtrField& ranges, Type type); bool matches(const Network::Connection& connection, const Envoy::Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; private: - const Network::Address::CidrRange range_; + // Private constructor for LC Trie-based matcher. + IPMatcher(std::unique_ptr> trie, Type type); + + // Helper method to extract IP address based on type, returning a reference to avoid copies. + const Network::Address::InstanceConstSharedPtr& + extractIpAddress(const Network::Connection& connection, const StreamInfo::StreamInfo& info) const; + + std::unique_ptr> trie_; + const Type type_; }; @@ -183,15 +198,23 @@ class AuthenticatedMatcher : public Matcher { */ class PolicyMatcher : public Matcher, NonCopyable { public: - PolicyMatcher(const envoy::config::rbac::v3::Policy& policy, Expr::Builder* builder, + PolicyMatcher(const envoy::config::rbac::v3::Policy& policy, + Expr::BuilderInstanceSharedPtr& builder, ProtobufMessage::ValidationVisitor& validation_visitor, Server::Configuration::CommonFactoryContext& context) : permissions_(policy.permissions(), validation_visitor, context), - principals_(policy.principals(), context), condition_(policy.condition()) { - if (policy.has_condition()) { - expr_ = Expr::createExpression(*builder, condition_); - } - } + principals_(policy.principals(), context), + expr_([&]() -> absl::optional { + if (policy.has_condition()) { + auto compiled = Expr::CompiledExpression::Create(builder, policy.condition()); + if (!compiled.ok()) { + throw Expr::CelException( + absl::StrCat("failed to create an expression: ", compiled.status().message())); + } + return std::move(compiled.value()); + } + return {}; + }()) {} bool matches(const Network::Connection& connection, const Envoy::Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo&) const override; @@ -199,8 +222,7 @@ class PolicyMatcher : public Matcher, NonCopyable { private: const OrMatcher permissions_; const OrMatcher principals_; - const google::api::expr::v1alpha1::Expr condition_; - Expr::ExpressionPtr expr_; + const absl::optional expr_; }; class MetadataMatcher : public Matcher { diff --git a/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc index e25b196956aa0..4e1b7ef841b1e 100644 --- a/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc +++ b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc @@ -59,7 +59,7 @@ void setLambdaHeaders(Http::RequestHeaderMap& headers, const absl::optional * Determines if the target cluster has the AWS Lambda metadata on it. */ bool isTargetClusterLambdaGateway(Upstream::ClusterInfo const& cluster_info) { - using ProtobufWkt::Value; + using Protobuf::Value; const auto& filter_metadata_map = cluster_info.metadata().filter_metadata(); auto metadata_it = filter_metadata_map.find(filter_metadata_key); if (metadata_it == filter_metadata_map.end()) { diff --git a/source/extensions/filters/http/composite/action.cc b/source/extensions/filters/http/composite/action.cc index 9d70c9c10671f..da6941c88b90c 100644 --- a/source/extensions/filters/http/composite/action.cc +++ b/source/extensions/filters/http/composite/action.cc @@ -6,23 +6,30 @@ namespace HttpFilters { namespace Composite { void ExecuteFilterAction::createFilters(Http::FilterChainFactoryCallbacks& callbacks) const { - cb_(callbacks); -} + if (actionSkip()) { + return; + } -bool ExecuteFilterActionFactory::isSampled( - const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, - Envoy::Runtime::Loader& runtime) { - if (composite_action.has_sample_percent() && - !runtime.snapshot().featureEnabled(composite_action.sample_percent().runtime_key(), - composite_action.sample_percent().default_value())) { - return false; + if (auto config_value = config_provider_(); config_value.has_value()) { + (*config_value)(callbacks); + return; } - return true; + // There is no dynamic config available. Apply missing config filter. + Envoy::Http::MissingConfigFilterFactory(callbacks); } -Matcher::ActionFactoryCb ExecuteFilterActionFactory::createActionFactoryCb( - const Protobuf::Message& config, Http::Matching::HttpFilterActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) { +const std::string& ExecuteFilterAction::actionName() const { return name_; } + +bool ExecuteFilterAction::actionSkip() const { + return sample_.has_value() + ? !runtime_.snapshot().featureEnabled(sample_->runtime_key(), sample_->default_value()) + : false; +} + +Matcher::ActionConstSharedPtr +ExecuteFilterActionFactory::createAction(const Protobuf::Message& config, + Http::Matching::HttpFilterActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& composite_action = MessageUtil::downcastAndValidate< const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction&>( config, validation_visitor); @@ -34,20 +41,20 @@ Matcher::ActionFactoryCb ExecuteFilterActionFactory::createActionFactoryCb( if (composite_action.has_dynamic_config()) { if (context.is_downstream_) { - return createDynamicActionFactoryCbDownstream(composite_action, context); + return createDynamicActionDownstream(composite_action, context); } else { - return createDynamicActionFactoryCbUpstream(composite_action, context); + return createDynamicActionUpstream(composite_action, context); } } if (context.is_downstream_) { - return createStaticActionFactoryCbDownstream(composite_action, context, validation_visitor); + return createStaticActionDownstream(composite_action, context, validation_visitor); } else { - return createStaticActionFactoryCbUpstream(composite_action, context, validation_visitor); + return createStaticActionUpstream(composite_action, context, validation_visitor); } } -Matcher::ActionFactoryCb ExecuteFilterActionFactory::createDynamicActionFactoryCbDownstream( +Matcher::ActionConstSharedPtr ExecuteFilterActionFactory::createDynamicActionDownstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context) { if (!context.factory_context_.has_value() || !context.server_factory_context_.has_value()) { @@ -57,11 +64,11 @@ Matcher::ActionFactoryCb ExecuteFilterActionFactory::createDynamicActionFactoryC auto provider_manager = Envoy::Http::FilterChainUtility::createSingletonDownstreamFilterConfigProviderManager( context.server_factory_context_.value()); - return createDynamicActionFactoryCbTyped( + return createDynamicActionTyped( composite_action, context, "http", context.factory_context_.value(), provider_manager); } -Matcher::ActionFactoryCb ExecuteFilterActionFactory::createDynamicActionFactoryCbUpstream( +Matcher::ActionConstSharedPtr ExecuteFilterActionFactory::createDynamicActionUpstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context) { if (!context.upstream_factory_context_.has_value() || @@ -72,12 +79,12 @@ Matcher::ActionFactoryCb ExecuteFilterActionFactory::createDynamicActionFactoryC auto provider_manager = Envoy::Http::FilterChainUtility::createSingletonUpstreamFilterConfigProviderManager( context.server_factory_context_.value()); - return createDynamicActionFactoryCbTyped( + return createDynamicActionTyped( composite_action, context, "router upstream http", context.upstream_factory_context_.value(), provider_manager); } -Matcher::ActionFactoryCb ExecuteFilterActionFactory::createActionFactoryCbCommon( +Matcher::ActionConstSharedPtr ExecuteFilterActionFactory::createActionCommon( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, Envoy::Http::FilterFactoryCb& callback, bool is_downstream) { @@ -90,16 +97,17 @@ Matcher::ActionFactoryCb ExecuteFilterActionFactory::createActionFactoryCbCommon std::string name = composite_action.typed_config().name(); ASSERT(context.server_factory_context_ != absl::nullopt); Envoy::Runtime::Loader& runtime = context.server_factory_context_->runtime(); - return [cb = std::move(callback), n = std::move(name), - composite_action = std::move(composite_action), &runtime, this]() -> Matcher::ActionPtr { - if (!isSampled(composite_action, runtime)) { - return nullptr; - } - return std::make_unique(cb, n); - }; + + return std::make_shared( + [cb = std::move(callback)]() mutable -> OptRef { return cb; }, name, + composite_action.has_sample_percent() + ? absl::make_optional( + composite_action.sample_percent()) + : absl::nullopt, + runtime); } -Matcher::ActionFactoryCb ExecuteFilterActionFactory::createStaticActionFactoryCbDownstream( +Matcher::ActionConstSharedPtr ExecuteFilterActionFactory::createStaticActionDownstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, ProtobufMessage::ValidationVisitor& validation_visitor) { @@ -126,10 +134,10 @@ Matcher::ActionFactoryCb ExecuteFilterActionFactory::createStaticActionFactoryCb *message, context.stat_prefix_, context.server_factory_context_.value()); } - return createActionFactoryCbCommon(composite_action, context, callback, true); + return createActionCommon(composite_action, context, callback, true); } -Matcher::ActionFactoryCb ExecuteFilterActionFactory::createStaticActionFactoryCbUpstream( +Matcher::ActionConstSharedPtr ExecuteFilterActionFactory::createStaticActionUpstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, ProtobufMessage::ValidationVisitor& validation_visitor) { @@ -150,7 +158,7 @@ Matcher::ActionFactoryCb ExecuteFilterActionFactory::createStaticActionFactoryCb callback = callback_or_status.value(); } - return createActionFactoryCbCommon(composite_action, context, callback, false); + return createActionCommon(composite_action, context, callback, false); } REGISTER_FACTORY(ExecuteFilterActionFactory, diff --git a/source/extensions/filters/http/composite/action.h b/source/extensions/filters/http/composite/action.h index 834b757a573fa..ecd9ad9bb076c 100644 --- a/source/extensions/filters/http/composite/action.h +++ b/source/extensions/filters/http/composite/action.h @@ -18,16 +18,26 @@ class ExecuteFilterAction : public Matcher::ActionBase< envoy::extensions::filters::http::composite::v3::ExecuteFilterAction> { public: - explicit ExecuteFilterAction(Http::FilterFactoryCb cb, const std::string& name) - : cb_(std::move(cb)), name_(name) {} + using FilterConfigProvider = std::function()>; + + explicit ExecuteFilterAction( + FilterConfigProvider config_provider, const std::string& name, + const absl::optional& sample, + Runtime::Loader& runtime) + : config_provider_(std::move(config_provider)), name_(name), sample_(sample), + runtime_(runtime) {} void createFilters(Http::FilterChainFactoryCallbacks& callbacks) const; - const std::string& actionName() const { return name_; } + const std::string& actionName() const; + + bool actionSkip() const; private: - Http::FilterFactoryCb cb_; + FilterConfigProvider config_provider_; const std::string name_; + const absl::optional sample_; + Runtime::Loader& runtime_; }; class ExecuteFilterActionFactory @@ -36,29 +46,22 @@ class ExecuteFilterActionFactory public: std::string name() const override { return "composite-action"; } - Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, - Http::Matching::HttpFilterActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, Http::Matching::HttpFilterActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); } - // Rolling the dice to decide whether the action will be sampled. - // By default, if sample_percent is not specified, then it is sampled. - bool isSampled( - const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, - Envoy::Runtime::Loader& runtime); - private: - Matcher::ActionFactoryCb createActionFactoryCbCommon( + Matcher::ActionConstSharedPtr createActionCommon( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, Envoy::Http::FilterFactoryCb& callback, bool is_downstream); template - Matcher::ActionFactoryCb createDynamicActionFactoryCbTyped( + Matcher::ActionConstSharedPtr createDynamicActionTyped( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, const std::string& filter_chain_type, FactoryCtx& factory_context, std::shared_ptr& provider_manager) { @@ -73,35 +76,30 @@ class ExecuteFilterActionFactory server_factory_context.clusterManager(), false, filter_chain_type, nullptr); Envoy::Runtime::Loader& runtime = context.server_factory_context_->runtime(); - return - [provider = std::move(provider), n = std::move(name), - composite_action = std::move(composite_action), &runtime, this]() -> Matcher::ActionPtr { - if (!isSampled(composite_action, runtime)) { - return nullptr; - } - - if (auto config_value = provider->config(); config_value.has_value()) { - return std::make_unique(config_value.ref(), n); - } - // There is no dynamic config available. Apply missing config filter. - return std::make_unique(Envoy::Http::MissingConfigFilterFactory, n); - }; + + return std::make_shared( + [provider]() -> OptRef { return provider->config(); }, name, + composite_action.has_sample_percent() + ? absl::make_optional( + composite_action.sample_percent()) + : absl::nullopt, + runtime); } - Matcher::ActionFactoryCb createDynamicActionFactoryCbDownstream( + Matcher::ActionConstSharedPtr createDynamicActionDownstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context); - Matcher::ActionFactoryCb createDynamicActionFactoryCbUpstream( + Matcher::ActionConstSharedPtr createDynamicActionUpstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context); - Matcher::ActionFactoryCb createStaticActionFactoryCbDownstream( + Matcher::ActionConstSharedPtr createStaticActionDownstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, ProtobufMessage::ValidationVisitor& validation_visitor); - Matcher::ActionFactoryCb createStaticActionFactoryCbUpstream( + Matcher::ActionConstSharedPtr createStaticActionUpstream( const envoy::extensions::filters::http::composite::v3::ExecuteFilterAction& composite_action, Http::Matching::HttpFilterActionContext& context, ProtobufMessage::ValidationVisitor& validation_visitor); diff --git a/source/extensions/filters/http/composite/factory_wrapper.cc b/source/extensions/filters/http/composite/factory_wrapper.cc index c53399f08f6e4..36ba47fcbb506 100644 --- a/source/extensions/filters/http/composite/factory_wrapper.cc +++ b/source/extensions/filters/http/composite/factory_wrapper.cc @@ -7,7 +7,6 @@ namespace Extensions { namespace HttpFilters { namespace Composite { void FactoryCallbacksWrapper::addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr filter) { - ASSERT(!filter_.decoded_headers_); if (filter_to_inject_) { errors_.push_back(absl::InvalidArgumentError( "cannot delegate to decoder filter that instantiates multiple filters")); @@ -18,7 +17,6 @@ void FactoryCallbacksWrapper::addStreamDecoderFilter(Http::StreamDecoderFilterSh } void FactoryCallbacksWrapper::addStreamEncoderFilter(Http::StreamEncoderFilterSharedPtr filter) { - ASSERT(!filter_.decoded_headers_); if (filter_to_inject_) { errors_.push_back(absl::InvalidArgumentError( "cannot delegate to encoder filter that instantiates multiple filters")); @@ -29,7 +27,6 @@ void FactoryCallbacksWrapper::addStreamEncoderFilter(Http::StreamEncoderFilterSh } void FactoryCallbacksWrapper::addStreamFilter(Http::StreamFilterSharedPtr filter) { - ASSERT(!filter_.decoded_headers_); if (filter_to_inject_) { errors_.push_back(absl::InvalidArgumentError( "cannot delegate to stream filter that instantiates multiple filters")); diff --git a/source/extensions/filters/http/composite/filter.cc b/source/extensions/filters/http/composite/filter.cc index 3075685da05b1..f53850ee4b327 100644 --- a/source/extensions/filters/http/composite/filter.cc +++ b/source/extensions/filters/http/composite/filter.cc @@ -30,8 +30,8 @@ template Overloaded(Ts...) -> Overloaded; } // namespace -std::unique_ptr MatchedActionInfo::buildProtoStruct() const { - auto message = std::make_unique(); +std::unique_ptr MatchedActionInfo::buildProtoStruct() const { + auto message = std::make_unique(); auto& fields = *message->mutable_fields(); for (const auto& p : actions_) { fields[p.first] = ValueUtil::stringValue(p.second); @@ -40,8 +40,6 @@ std::unique_ptr MatchedActionInfo::buildProtoStruct() const } Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { - decoded_headers_ = true; - return delegateFilterActionOr(delegated_filter_, &StreamDecoderFilter::decodeHeaders, Http::FilterHeadersStatus::Continue, headers, end_stream); } @@ -96,7 +94,6 @@ void Filter::encodeComplete() { void Filter::onMatchCallback(const Matcher::Action& action) { const auto& composite_action = action.getTyped(); - FactoryCallbacksWrapper wrapper(*this, dispatcher_); composite_action.createFilters(wrapper); @@ -107,11 +104,12 @@ void Filter::onMatchCallback(const Matcher::Action& action) { wrapper.errors_, [](const auto& status) { return status.ToString(); })); return; } - const std::string& action_name = composite_action.actionName(); if (wrapper.filter_to_inject_.has_value()) { stats_.filter_delegation_success_.inc(); + const std::string& action_name = composite_action.actionName(); + auto createDelegatedFilterFn = Overloaded{ [this, action_name](Http::StreamDecoderFilterSharedPtr filter) { delegated_filter_ = std::make_shared(std::move(filter)); @@ -137,7 +135,6 @@ void Filter::onMatchCallback(const Matcher::Action& action) { access_loggers_.insert(access_loggers_.end(), wrapper.access_loggers_.begin(), wrapper.access_loggers_.end()); } - // TODO(snowp): Make it possible for onMatchCallback to fail the stream by issuing a local reply, // either directly or via some return status. } diff --git a/source/extensions/filters/http/composite/filter.h b/source/extensions/filters/http/composite/filter.h index acc86f447bf57..b1ddf8f07ef36 100644 --- a/source/extensions/filters/http/composite/filter.h +++ b/source/extensions/filters/http/composite/filter.h @@ -47,7 +47,7 @@ class MatchedActionInfo : public StreamInfo::FilterState::Object { } private: - std::unique_ptr buildProtoStruct() const; + std::unique_ptr buildProtoStruct() const; absl::flat_hash_map actions_; }; @@ -57,8 +57,7 @@ class Filter : public Http::StreamFilter, Logger::Loggable { public: Filter(FilterStats& stats, Event::Dispatcher& dispatcher, bool is_upstream) - : dispatcher_(dispatcher), decoded_headers_(false), stats_(stats), is_upstream_(is_upstream) { - } + : dispatcher_(dispatcher), stats_(stats), is_upstream_(is_upstream) {} // Http::StreamDecoderFilter Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, @@ -119,12 +118,6 @@ class Filter : public Http::StreamFilter, const std::string& action_name); Event::Dispatcher& dispatcher_; - // Use these to track whether we are allowed to insert a specific kind of filter. These mainly - // serve to surface an easier to understand error, as attempting to insert a filter at a later - // time will result in various FM assertions firing. - // We should be protected against this by the match tree validation that only allows request - // headers, this just provides some additional sanity checking. - bool decoded_headers_ : 1; // Wraps a stream encoder OR a stream decoder filter into a stream filter, making it easier to // delegate calls. diff --git a/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.cc b/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.cc index a228db503b7b8..1c65019d5747a 100644 --- a/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.cc +++ b/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.cc @@ -56,8 +56,8 @@ std::string statusCodeToString(const Grpc::Status::GrpcStatus status) { } } -ProtobufWkt::Struct convertToStruct(const Error& error) { - ProtobufWkt::Struct obj; +Protobuf::Struct convertToStruct(const Error& error) { + Protobuf::Struct obj; (*obj.mutable_fields())["code"] = ValueUtil::stringValue(statusCodeToString(error.code)); if (!error.message.empty()) { @@ -65,7 +65,7 @@ ProtobufWkt::Struct convertToStruct(const Error& error) { } if (!error.details.empty()) { - auto details_list = std::make_unique(); + auto details_list = std::make_unique(); for (const auto& detail : error.details) { const auto& value = detail.value(); *details_list->add_values() = ValueUtil::structValue(MessageUtil::keyValueStruct({ @@ -74,30 +74,30 @@ ProtobufWkt::Struct convertToStruct(const Error& error) { })); } - ProtobufWkt::Value details_value; + Protobuf::Value details_value; details_value.set_allocated_list_value(details_list.release()); (*obj.mutable_fields())["details"] = details_value; } return obj; } -ProtobufWkt::Struct convertToStruct(const EndStreamResponse& response) { - ProtobufWkt::Struct obj; +Protobuf::Struct convertToStruct(const EndStreamResponse& response) { + Protobuf::Struct obj; if (response.error.has_value()) { (*obj.mutable_fields())["error"] = ValueUtil::structValue(convertToStruct(*response.error)); } if (!response.metadata.empty()) { - ProtobufWkt::Struct metadata_obj; + Protobuf::Struct metadata_obj; for (const auto& [name, values] : response.metadata) { - auto values_list = std::make_unique(); + auto values_list = std::make_unique(); for (const auto& value : values) { *values_list->add_values() = ValueUtil::stringValue(value); } - ProtobufWkt::Value values_value; + Protobuf::Value values_value; values_value.set_allocated_list_value(values_list.release()); (*metadata_obj.mutable_fields())[name] = values_value; } @@ -110,12 +110,12 @@ ProtobufWkt::Struct convertToStruct(const EndStreamResponse& response) { } // namespace bool serializeJson(const Error& error, std::string& out) { - ProtobufWkt::Struct message = convertToStruct(error); + Protobuf::Struct message = convertToStruct(error); return Protobuf::util::MessageToJsonString(message, &out).ok(); } bool serializeJson(const EndStreamResponse& response, std::string& out) { - ProtobufWkt::Struct message = convertToStruct(response); + Protobuf::Struct message = convertToStruct(response); return Protobuf::util::MessageToJsonString(message, &out).ok(); } diff --git a/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.h b/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.h index 7a0600fc7bee5..d5f27a12d0bbb 100644 --- a/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.h +++ b/source/extensions/filters/http/connect_grpc_bridge/end_stream_response.h @@ -19,7 +19,7 @@ namespace ConnectGrpcBridge { struct Error { Grpc::Status::GrpcStatus code; std::string message; - std::vector details; + std::vector details; }; struct EndStreamResponse { diff --git a/source/extensions/filters/http/custom_response/config.cc b/source/extensions/filters/http/custom_response/config.cc index 4aa4ec67746fc..84d5c1533b20d 100644 --- a/source/extensions/filters/http/custom_response/config.cc +++ b/source/extensions/filters/http/custom_response/config.cc @@ -61,7 +61,7 @@ PolicySharedPtr FilterConfig::getPolicy(const ::Envoy::Http::ResponseHeaderMap& if (!match_result.isMatch()) { return PolicySharedPtr{}; } - return match_result.action()->getTyped().policy_; + return std::dynamic_pointer_cast(match_result.actionByMove()); } } // namespace CustomResponse diff --git a/source/extensions/filters/http/custom_response/policy.h b/source/extensions/filters/http/custom_response/policy.h index 8a4d98080421d..c45f214a3e0e1 100644 --- a/source/extensions/filters/http/custom_response/policy.h +++ b/source/extensions/filters/http/custom_response/policy.h @@ -19,9 +19,9 @@ namespace CustomResponse { class CustomResponseFilter; // Base class for custom response policies. -class Policy : public std::enable_shared_from_this { +class Policy : public std::enable_shared_from_this, + public Matcher::ActionBase { public: - virtual ~Policy() = default; virtual Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap&, bool, CustomResponseFilter&) const PURE; @@ -42,11 +42,6 @@ struct CustomResponseFilterState : public std::enable_shared_from_this { - explicit CustomResponseMatchAction(PolicySharedPtr policy) : policy_(policy) {} - const PolicySharedPtr policy_; -}; - struct CustomResponseActionFactoryContext { Server::Configuration::ServerFactoryContext& server_; Stats::StatName stats_prefix_; @@ -57,12 +52,10 @@ template class PolicyMatchActionFactory : public Matcher::ActionFactory, Logger::Loggable { public: - Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message& config, - CustomResponseActionFactoryContext& context, - ProtobufMessage::ValidationVisitor&) override { - return [policy = createPolicy(config, context.server_, context.stats_prefix_)] { - return std::make_unique(policy); - }; + Matcher::ActionConstSharedPtr createAction(const Protobuf::Message& config, + CustomResponseActionFactoryContext& context, + ProtobufMessage::ValidationVisitor&) override { + return createPolicy(config, context.server_, context.stats_prefix_); } std::string category() const override { return "envoy.http.custom_response"; } diff --git a/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc b/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc index 4373f1bf13809..8d4f82ad5abcc 100644 --- a/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc +++ b/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc @@ -122,12 +122,7 @@ LoadClusterEntryHandlePtr ProxyFilterConfig::addDynamicCluster( // update. As this cluster lifecycle is managed by DFP cluster, it should not be removed by // CDS. https://github.com/envoyproxy/envoy/issues/35171 absl::Status status = - cluster_manager_ - .addOrUpdateCluster( - cluster, version_info, - Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.avoid_dfp_cluster_removal_on_cds_update")) - .status(); + cluster_manager_.addOrUpdateCluster(cluster, version_info, true).status(); ENVOY_BUG(status.ok(), absl::StrCat("Failed to update DFP cluster due to ", status.message())); }); @@ -308,19 +303,26 @@ Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& hea latchTime(decoder_callbacks_, DNS_START); const bool is_proxying = isProxying(); - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.dfp_fail_on_empty_host_header")) { - if (headers.Host()->value().getStringView().empty()) { - decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, - ResponseStrings::get().EmptyHostHeader, nullptr, - absl::nullopt, RcDetails::get().EmptyHostHeader); - return Http::FilterHeadersStatus::StopIteration; - } + if (headers.Host()->value().getStringView().empty()) { + decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, + ResponseStrings::get().EmptyHostHeader, nullptr, + absl::nullopt, RcDetails::get().EmptyHostHeader); + return Http::FilterHeadersStatus::StopIteration; } // Get host value from the request headers. const auto host_attributes = Http::Utility::parseAuthority(headers.Host()->value().getStringView()); - absl::string_view host = host_attributes.host_; + // For IPv6 numeric addresses, use a copy with square brackets added around the host. + // For any other address type just use the existing unmodified host string. + std::string host_str; + absl::string_view host; + if (host_attributes.is_ip_address_ && absl::StrContains(host_attributes.host_, ":")) { + host_str = absl::StrCat("[", host_attributes.host_, "]"); + host = host_str; + } else { + host = host_attributes.host_; + } uint16_t port = host_attributes.port_.value_or(default_port); // Apply filter state overrides for host and port. diff --git a/source/extensions/filters/http/dynamic_modules/abi_impl.cc b/source/extensions/filters/http/dynamic_modules/abi_impl.cc index 5e19b60c78dfa..0ecdd4c4a449c 100644 --- a/source/extensions/filters/http/dynamic_modules/abi_impl.cc +++ b/source/extensions/filters/http/dynamic_modules/abi_impl.cc @@ -1,5 +1,3 @@ -#include - #include "source/common/http/header_map_impl.h" #include "source/common/http/message_impl.h" #include "source/common/http/utility.h" @@ -11,6 +9,23 @@ namespace Envoy { namespace Extensions { namespace DynamicModules { namespace HttpFilters { + +static Stats::StatNameTagVector +buildTagsForModuleMetric(DynamicModuleHttpFilter& filter, const Stats::StatNameVec& label_names, + envoy_dynamic_module_type_module_buffer* label_values, + size_t label_values_length) { + + ASSERT(label_values_length == label_names.size()); + Stats::StatNameTagVector tags; + tags.reserve(label_values_length); + for (size_t i = 0; i < label_values_length; i++) { + absl::string_view label_value_view(label_values[i].ptr, label_values[i].length); + auto label_value = filter.getStatNamePool().add(label_value_view); + tags.push_back(Stats::StatNameTag(label_names[i], label_value)); + } + return tags; +} + extern "C" { using HeadersMapOptConstRef = OptRef; @@ -71,6 +86,277 @@ bool getSslInfo( return true; } +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_counter( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, size_t* counter_id_ptr) { + auto filter_config = static_cast(filter_config_envoy_ptr); + if (filter_config->stat_creation_frozen_) { + return envoy_dynamic_module_type_metrics_result_Frozen; + } + absl::string_view name_view(name, name_length); + Stats::StatName main_stat_name = filter_config->stat_name_pool_.add(name_view); + Stats::Counter& c = + Stats::Utility::counterFromStatNames(*filter_config->stats_scope_, {main_stat_name}); + *counter_id_ptr = filter_config->addCounter({c}); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_counter_vec( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + envoy_dynamic_module_type_module_buffer* label_names, size_t label_names_length, + size_t* counter_id_ptr) { + auto filter_config = static_cast(filter_config_envoy_ptr); + if (filter_config->stat_creation_frozen_) { + return envoy_dynamic_module_type_metrics_result_Frozen; + } + absl::string_view name_view(name, name_length); + Stats::StatName main_stat_name = filter_config->stat_name_pool_.add(name_view); + Stats::StatNameVec label_names_vec; + for (size_t i = 0; i < label_names_length; i++) { + absl::string_view label_name_view(label_names[i].ptr, label_names[i].length); + label_names_vec.push_back(filter_config->stat_name_pool_.add(label_name_view)); + } + *counter_id_ptr = filter_config->addCounterVec({main_stat_name, label_names_vec}); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_increment_counter( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto counter = filter->getFilterConfig().getCounterById(id); + if (!counter.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + counter->add(value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_increment_counter_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto counter = filter->getFilterConfig().getCounterVecById(id); + if (!counter.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + if (label_values_length != counter->getLabelNames().size()) { + return envoy_dynamic_module_type_metrics_result_InvalidLabels; + } + auto tags = buildTagsForModuleMetric(*filter, counter->getLabelNames(), label_values, + label_values_length); + counter->add(*filter->getFilterConfig().stats_scope_, tags, value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_gauge( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, size_t* gauge_id_ptr) { + auto filter_config = static_cast(filter_config_envoy_ptr); + if (filter_config->stat_creation_frozen_) { + return envoy_dynamic_module_type_metrics_result_Frozen; + } + absl::string_view name_view(name, name_length); + Stats::StatName main_stat_name = filter_config->stat_name_pool_.add(name_view); + Stats::Gauge::ImportMode import_mode = + Stats::Gauge::ImportMode::Accumulate; // TODO: make this configurable? + Stats::Gauge& g = Stats::Utility::gaugeFromStatNames(*filter_config->stats_scope_, + {main_stat_name}, import_mode); + *gauge_id_ptr = filter_config->addGauge({g}); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_gauge_vec( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + envoy_dynamic_module_type_module_buffer* label_names, size_t label_names_length, + size_t* gauge_id_ptr) { + auto filter_config = static_cast(filter_config_envoy_ptr); + if (filter_config->stat_creation_frozen_) { + return envoy_dynamic_module_type_metrics_result_Frozen; + } + absl::string_view name_view(name, name_length); + Stats::StatName main_stat_name = filter_config->stat_name_pool_.add(name_view); + Stats::Gauge::ImportMode import_mode = + Stats::Gauge::ImportMode::Accumulate; // TODO: make this configurable? + Stats::StatNameVec label_names_vec; + for (size_t i = 0; i < label_names_length; i++) { + absl::string_view label_name_view(label_names[i].ptr, label_names[i].length); + label_names_vec.push_back(filter_config->stat_name_pool_.add(label_name_view)); + } + *gauge_id_ptr = filter_config->addGaugeVec({main_stat_name, label_names_vec, import_mode}); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_increase_gauge( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto gauge = filter->getFilterConfig().getGaugeById(id); + if (!gauge.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + gauge->increase(value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_increase_gauge_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto gauge = filter->getFilterConfig().getGaugeVecById(id); + if (!gauge.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + if (label_values_length != gauge->getLabelNames().size()) { + return envoy_dynamic_module_type_metrics_result_InvalidLabels; + } + auto tags = + buildTagsForModuleMetric(*filter, gauge->getLabelNames(), label_values, label_values_length); + gauge->increase(*filter->getFilterConfig().stats_scope_, tags, value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_decrease_gauge( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto gauge = filter->getFilterConfig().getGaugeById(id); + if (!gauge.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + gauge->decrease(value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_decrease_gauge_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto gauge = filter->getFilterConfig().getGaugeVecById(id); + if (!gauge.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + if (label_values_length != gauge->getLabelNames().size()) { + return envoy_dynamic_module_type_metrics_result_InvalidLabels; + } + auto tags = + buildTagsForModuleMetric(*filter, gauge->getLabelNames(), label_values, label_values_length); + gauge->decrease(*filter->getFilterConfig().stats_scope_, tags, value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_set_gauge( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto gauge = filter->getFilterConfig().getGaugeById(id); + if (!gauge.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + gauge->set(value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_http_filter_set_gauge_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto gauge = filter->getFilterConfig().getGaugeVecById(id); + if (!gauge.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + if (label_values_length != gauge->getLabelNames().size()) { + return envoy_dynamic_module_type_metrics_result_InvalidLabels; + } + auto tags = + buildTagsForModuleMetric(*filter, gauge->getLabelNames(), label_values, label_values_length); + gauge->set(*filter->getFilterConfig().stats_scope_, tags, value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_histogram( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + size_t* histogram_id_ptr) { + auto filter_config = static_cast(filter_config_envoy_ptr); + if (filter_config->stat_creation_frozen_) { + return envoy_dynamic_module_type_metrics_result_Frozen; + } + absl::string_view name_view(name, name_length); + Stats::StatName main_stat_name = filter_config->stat_name_pool_.add(name_view); + Stats::Histogram::Unit unit = + Stats::Histogram::Unit::Unspecified; // TODO: make this configurable? + Stats::Histogram& h = + Stats::Utility::histogramFromStatNames(*filter_config->stats_scope_, {main_stat_name}, unit); + *histogram_id_ptr = filter_config->addHistogram({h}); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_config_define_histogram_vec( + envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr name, size_t name_length, + envoy_dynamic_module_type_module_buffer* label_names, size_t label_names_length, + size_t* histogram_id_ptr) { + auto filter_config = static_cast(filter_config_envoy_ptr); + if (filter_config->stat_creation_frozen_) { + return envoy_dynamic_module_type_metrics_result_Frozen; + } + absl::string_view name_view(name, name_length); + Stats::StatName main_stat_name = filter_config->stat_name_pool_.add(name_view); + Stats::Histogram::Unit unit = + Stats::Histogram::Unit::Unspecified; // TODO: make this configurable? + Stats::StatNameVec label_names_vec; + for (size_t i = 0; i < label_names_length; i++) { + absl::string_view label_name_view(label_names[i].ptr, label_names[i].length); + label_names_vec.push_back(filter_config->stat_name_pool_.add(label_name_view)); + } + *histogram_id_ptr = filter_config->addHistogramVec({main_stat_name, label_names_vec, unit}); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_record_histogram_value( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto hist = filter->getFilterConfig().getHistogramById(id); + if (!hist.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + hist->recordValue(value); + return envoy_dynamic_module_type_metrics_result_Success; +} + +envoy_dynamic_module_type_metrics_result +envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, size_t id, + envoy_dynamic_module_type_module_buffer* label_values, size_t label_values_length, + uint64_t value) { + auto filter = static_cast(filter_envoy_ptr); + auto hist = filter->getFilterConfig().getHistogramVecById(id); + if (!hist.has_value()) { + return envoy_dynamic_module_type_metrics_result_MetricNotFound; + } + if (label_values_length != hist->getLabelNames().size()) { + return envoy_dynamic_module_type_metrics_result_InvalidLabels; + } + auto tags = + buildTagsForModuleMetric(*filter, hist->getLabelNames(), label_values, label_values_length); + hist->recordValue(*filter->getFilterConfig().stats_scope_, tags, value); + return envoy_dynamic_module_type_metrics_result_Success; +} + size_t envoy_dynamic_module_callback_http_get_request_header( envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, envoy_dynamic_module_type_buffer_module_ptr key, size_t key_length, @@ -283,7 +569,7 @@ void envoy_dynamic_module_callback_http_send_response( * each variant differs in the returned type of the metadata. For example, route metadata will * return OptRef vs upstream host metadata will return a shared pointer. */ -const ProtobufWkt::Struct* +const Protobuf::Struct* getMetadataNamespaceImpl(const envoy::config::core::v3::Metadata& metadata, envoy_dynamic_module_type_buffer_module_ptr namespace_ptr, size_t namespace_length) { @@ -306,7 +592,7 @@ getMetadataNamespaceImpl(const envoy::config::core::v3::Metadata& metadata, * @param namespace_length is the length of the namespace. * @return the metadata namespace if it exists, nullptr otherwise. */ -const ProtobufWkt::Struct* +const Protobuf::Struct* getMetadataNamespace(envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, envoy_dynamic_module_type_metadata_source metadata_source, envoy_dynamic_module_type_buffer_module_ptr namespace_ptr, @@ -379,7 +665,7 @@ getMetadataNamespace(envoy_dynamic_module_type_http_filter_envoy_ptr filter_envo * @param namespace_length is the length of the namespace. * @return the metadata namespace if it exists, nullptr otherwise. */ -ProtobufWkt::Struct* +Protobuf::Struct* getDynamicMetadataNamespace(envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, envoy_dynamic_module_type_buffer_module_ptr namespace_ptr, size_t namespace_length) { @@ -394,7 +680,7 @@ getDynamicMetadataNamespace(envoy_dynamic_module_type_http_filter_envoy_ptr filt absl::string_view namespace_view(static_cast(namespace_ptr), namespace_length); auto metadata_namespace = metadata->find(namespace_view); if (metadata_namespace == metadata->end()) { - metadata_namespace = metadata->emplace(namespace_view, ProtobufWkt::Struct{}).first; + metadata_namespace = metadata->emplace(namespace_view, Protobuf::Struct{}).first; } return &metadata_namespace->second; } @@ -410,7 +696,7 @@ getDynamicMetadataNamespace(envoy_dynamic_module_type_http_filter_envoy_ptr filt * @param key_length is the length of the key. * @return the metadata value if it exists, nullptr otherwise. */ -const ProtobufWkt::Value* +const Protobuf::Value* getMetadataValue(envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, envoy_dynamic_module_type_metadata_source metadata_source, envoy_dynamic_module_type_buffer_module_ptr namespace_ptr, size_t namespace_length, @@ -441,7 +727,7 @@ bool envoy_dynamic_module_callback_http_set_dynamic_metadata_number( return false; } absl::string_view key_view(static_cast(key_ptr), key_length); - ProtobufWkt::Struct metadata_value; + Protobuf::Struct metadata_value; (*metadata_value.mutable_fields())[key_view].set_number_value(value); metadata_namespace->MergeFrom(metadata_value); return true; @@ -481,7 +767,7 @@ bool envoy_dynamic_module_callback_http_set_dynamic_metadata_string( } absl::string_view key_view(static_cast(key_ptr), key_length); absl::string_view value_view(static_cast(value_ptr), value_length); - ProtobufWkt::Struct metadata_value; + Protobuf::Struct metadata_value; (*metadata_value.mutable_fields())[key_view].set_string_value(value_view); metadata_namespace->MergeFrom(metadata_value); return true; @@ -629,6 +915,20 @@ bool envoy_dynamic_module_callback_http_drain_request_body( return true; } +bool envoy_dynamic_module_callback_http_inject_request_body( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr data, size_t length, bool end_stream) { + auto filter = static_cast(filter_envoy_ptr); + if (!filter->decoder_callbacks_) { + return false; + } + + Buffer::OwnedImpl buffer(static_cast(data), length); + filter->decoder_callbacks_->injectDecodedDataToFilterChain(buffer, end_stream); + + return true; +} + bool envoy_dynamic_module_callback_http_get_response_body_vector( envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, envoy_dynamic_module_type_envoy_buffer* result_buffer_vector) { @@ -705,6 +1005,20 @@ bool envoy_dynamic_module_callback_http_drain_response_body( return true; } +bool envoy_dynamic_module_callback_http_inject_response_body( + envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_buffer_module_ptr data, size_t length, bool end_stream) { + auto filter = static_cast(filter_envoy_ptr); + if (!filter->encoder_callbacks_) { + return false; + } + + Buffer::OwnedImpl buffer(static_cast(data), length); + filter->encoder_callbacks_->injectEncodedDataToFilterChain(buffer, end_stream); + + return true; +} + void envoy_dynamic_module_callback_http_clear_route_cache( envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr) { auto filter = static_cast(filter_envoy_ptr); @@ -1079,6 +1393,36 @@ void envoy_dynamic_module_callback_http_filter_continue_encoding( DynamicModuleHttpFilter* filter = static_cast(filter_envoy_ptr); filter->continueEncoding(); } + +bool envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level level) { + return Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules).level() <= + static_cast(level); +} + +void envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level level, + const char* message_ptr, size_t message_length) { + absl::string_view message_view(static_cast(message_ptr), message_length); + spdlog::logger& logger = Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules); + switch (level) { + case envoy_dynamic_module_type_log_level_Debug: + ENVOY_LOG_TO_LOGGER(logger, debug, message_view); + break; + case envoy_dynamic_module_type_log_level_Info: + ENVOY_LOG_TO_LOGGER(logger, info, message_view); + break; + case envoy_dynamic_module_type_log_level_Warn: + ENVOY_LOG_TO_LOGGER(logger, warn, message_view); + break; + case envoy_dynamic_module_type_log_level_Error: + ENVOY_LOG_TO_LOGGER(logger, error, message_view); + break; + case envoy_dynamic_module_type_log_level_Critical: + ENVOY_LOG_TO_LOGGER(logger, critical, message_view); + break; + default: + break; + } +} } } // namespace HttpFilters } // namespace DynamicModules diff --git a/source/extensions/filters/http/dynamic_modules/factory.cc b/source/extensions/filters/http/dynamic_modules/factory.cc index e91f3c6170b74..695814ef07f79 100644 --- a/source/extensions/filters/http/dynamic_modules/factory.cc +++ b/source/extensions/filters/http/dynamic_modules/factory.cc @@ -8,7 +8,7 @@ namespace Server { namespace Configuration { absl::StatusOr DynamicModuleConfigFactory::createFilterFactoryFromProtoTyped( - const FilterConfig& proto_config, const std::string&, DualInfo, + const FilterConfig& proto_config, const std::string&, DualInfo dual_info, Server::Configuration::ServerFactoryContext& context) { const auto& module_config = proto_config.dynamic_module_config(); @@ -29,16 +29,21 @@ absl::StatusOr DynamicModuleConfigFactory::createFilterFa Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr> filter_config = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - proto_config.filter_name(), config, std::move(dynamic_module.value()), context); + proto_config.filter_name(), config, std::move(dynamic_module.value()), + dual_info.scope, context); if (!filter_config.ok()) { return absl::InvalidArgumentError("Failed to create filter config: " + std::string(filter_config.status().message())); } + + context.api().customStatNamespaces().registerStatNamespace( + Extensions::DynamicModules::HttpFilters::CustomStatNamespace); + return [config = filter_config.value()](Http::FilterChainFactoryCallbacks& callbacks) -> void { auto filter = std::make_shared( - config); + config, config->stats_scope_->symbolTable()); filter->initializeInModuleFilter(); callbacks.addStreamFilter(filter); }; diff --git a/source/extensions/filters/http/dynamic_modules/filter.cc b/source/extensions/filters/http/dynamic_modules/filter.cc index 7ad53c5dcae3e..82e99f119ee8a 100644 --- a/source/extensions/filters/http/dynamic_modules/filter.cc +++ b/source/extensions/filters/http/dynamic_modules/filter.cc @@ -126,8 +126,8 @@ void DynamicModuleHttpFilter::sendLocalReply( Code code, absl::string_view body, std::function modify_headers, const absl::optional grpc_status, absl::string_view details) { - decoder_callbacks_->sendLocalReply(code, body, modify_headers, grpc_status, details); sent_local_reply_ = true; + decoder_callbacks_->sendLocalReply(code, body, modify_headers, grpc_status, details); } void DynamicModuleHttpFilter::encodeComplete() {}; diff --git a/source/extensions/filters/http/dynamic_modules/filter.h b/source/extensions/filters/http/dynamic_modules/filter.h index a1ad0bc36902a..d7ff04554c4a5 100644 --- a/source/extensions/filters/http/dynamic_modules/filter.h +++ b/source/extensions/filters/http/dynamic_modules/filter.h @@ -18,7 +18,9 @@ class DynamicModuleHttpFilter : public Http::StreamFilter, public std::enable_shared_from_this, public Logger::Loggable { public: - DynamicModuleHttpFilter(DynamicModuleHttpFilterConfigSharedPtr config) : config_(config) {} + DynamicModuleHttpFilter(DynamicModuleHttpFilterConfigSharedPtr config, + Stats::SymbolTable& symbol_table) + : config_(config), stat_name_pool_(symbol_table) {} ~DynamicModuleHttpFilter() override; /** @@ -164,6 +166,9 @@ class DynamicModuleHttpFilter : public Http::StreamFilter, sendHttpCallout(uint32_t callout_id, absl::string_view cluster_name, Http::RequestMessagePtr&& message, uint64_t timeout_milliseconds); + const DynamicModuleHttpFilterConfig& getFilterConfig() const { return *config_; } + Stats::StatNameDynamicPool& getStatNamePool() { return stat_name_pool_; } + private: /** * This is a helper function to get the `this` pointer as a void pointer which is passed to the @@ -191,6 +196,7 @@ class DynamicModuleHttpFilter : public Http::StreamFilter, const DynamicModuleHttpFilterConfigSharedPtr config_ = nullptr; envoy_dynamic_module_type_http_filter_module_ptr in_module_filter_ = nullptr; + Stats::StatNameDynamicPool stat_name_pool_; /** * This implementation of the AsyncClient::Callbacks is used to handle the response from the HTTP diff --git a/source/extensions/filters/http/dynamic_modules/filter_config.cc b/source/extensions/filters/http/dynamic_modules/filter_config.cc index 81a8c4d410ae2..9c7768843cbbf 100644 --- a/source/extensions/filters/http/dynamic_modules/filter_config.cc +++ b/source/extensions/filters/http/dynamic_modules/filter_config.cc @@ -7,9 +7,11 @@ namespace HttpFilters { DynamicModuleHttpFilterConfig::DynamicModuleHttpFilterConfig( const absl::string_view filter_name, const absl::string_view filter_config, - Extensions::DynamicModules::DynamicModulePtr dynamic_module, + Extensions::DynamicModules::DynamicModulePtr dynamic_module, Stats::Scope& stats_scope, Server::Configuration::ServerFactoryContext& context) - : cluster_manager_(context.clusterManager()), filter_name_(filter_name), + : cluster_manager_(context.clusterManager()), + stats_scope_(stats_scope.createScope(std::string(CustomStatNamespace) + ".")), + stat_name_pool_(stats_scope_->symbolTable()), filter_name_(filter_name), filter_config_(filter_config), dynamic_module_(std::move(dynamic_module)) {}; DynamicModuleHttpFilterConfig::~DynamicModuleHttpFilterConfig() { @@ -34,7 +36,7 @@ newDynamicModuleHttpPerRouteConfig(const absl::string_view per_route_config_name "envoy_dynamic_module_on_http_filter_per_route_config_new"); RETURN_IF_NOT_OK_REF(constructor.status()); - auto destroy = dynamic_module->getFunctionPointer( + auto destroy = dynamic_module->getFunctionPointer( "envoy_dynamic_module_on_http_filter_per_route_config_destroy"); RETURN_IF_NOT_OK_REF(destroy.status()); @@ -45,21 +47,20 @@ newDynamicModuleHttpPerRouteConfig(const absl::string_view per_route_config_name return absl::InvalidArgumentError("Failed to initialize per-route dynamic module"); } - return std::make_shared(filter_config_envoy_ptr, - destroy.value()); + return std::make_shared( + filter_config_envoy_ptr, destroy.value(), std::move(dynamic_module)); } -absl::StatusOr -newDynamicModuleHttpFilterConfig(const absl::string_view filter_name, - const absl::string_view filter_config, - Extensions::DynamicModules::DynamicModulePtr dynamic_module, - Server::Configuration::ServerFactoryContext& context) { +absl::StatusOr newDynamicModuleHttpFilterConfig( + const absl::string_view filter_name, const absl::string_view filter_config, + Extensions::DynamicModules::DynamicModulePtr dynamic_module, Stats::Scope& stats_scope, + Server::Configuration::ServerFactoryContext& context) { auto constructor = dynamic_module->getFunctionPointer( "envoy_dynamic_module_on_http_filter_config_new"); RETURN_IF_NOT_OK_REF(constructor.status()); - auto on_config_destroy = dynamic_module->getFunctionPointer( + auto on_config_destroy = dynamic_module->getFunctionPointer( "envoy_dynamic_module_on_http_filter_config_destroy"); RETURN_IF_NOT_OK_REF(on_config_destroy.status()); @@ -108,8 +109,8 @@ newDynamicModuleHttpFilterConfig(const absl::string_view filter_name, "envoy_dynamic_module_on_http_filter_scheduled"); RETURN_IF_NOT_OK_REF(on_scheduled.status()); - auto config = std::make_shared(filter_name, filter_config, - std::move(dynamic_module), context); + auto config = std::make_shared( + filter_name, filter_config, std::move(dynamic_module), stats_scope, context); const void* filter_config_envoy_ptr = (*constructor.value())(static_cast(config.get()), filter_name.data(), @@ -118,6 +119,8 @@ newDynamicModuleHttpFilterConfig(const absl::string_view filter_name, return absl::InvalidArgumentError("Failed to initialize dynamic module"); } + config->stat_creation_frozen_ = true; + config->in_module_config_ = filter_config_envoy_ptr; config->on_http_filter_config_destroy_ = on_config_destroy.value(); config->on_http_filter_new_ = on_new_filter.value(); diff --git a/source/extensions/filters/http/dynamic_modules/filter_config.h b/source/extensions/filters/http/dynamic_modules/filter_config.h index 30101ea335966..ad99e74acf8ba 100644 --- a/source/extensions/filters/http/dynamic_modules/filter_config.h +++ b/source/extensions/filters/http/dynamic_modules/filter_config.h @@ -11,10 +11,14 @@ namespace Extensions { namespace DynamicModules { namespace HttpFilters { -using OnHttpConfigDestoryType = decltype(&envoy_dynamic_module_on_http_filter_config_destroy); +// The custom stat namespace which prepends all the user-defined metrics. +// Note that the prefix is removed from the final output of /stats endpoints. +constexpr absl::string_view CustomStatNamespace = "dynamicmodulescustom"; + +using OnHttpConfigDestroyType = decltype(&envoy_dynamic_module_on_http_filter_config_destroy); using OnHttpFilterNewType = decltype(&envoy_dynamic_module_on_http_filter_new); -using OnHttpPerRouteConfigDestoryType = +using OnHttpPerRouteConfigDestroyType = decltype(&envoy_dynamic_module_on_http_filter_per_route_config_destroy); using OnHttpFilterRequestHeadersType = decltype(&envoy_dynamic_module_on_http_filter_request_headers); @@ -49,7 +53,7 @@ class DynamicModuleHttpFilterConfig { */ DynamicModuleHttpFilterConfig(const absl::string_view filter_name, const absl::string_view filter_config, - DynamicModulePtr dynamic_module, + DynamicModulePtr dynamic_module, Stats::Scope& stats_scope, Server::Configuration::ServerFactoryContext& context); ~DynamicModuleHttpFilterConfig(); @@ -60,7 +64,7 @@ class DynamicModuleHttpFilterConfig { // The function pointers for the module related to the HTTP filter. All of them are resolved // during the construction of the config and made sure they are not nullptr after that. - OnHttpConfigDestoryType on_http_filter_config_destroy_ = nullptr; + OnHttpConfigDestroyType on_http_filter_config_destroy_ = nullptr; OnHttpFilterNewType on_http_filter_new_ = nullptr; OnHttpFilterRequestHeadersType on_http_filter_request_headers_ = nullptr; OnHttpFilterRequestBodyType on_http_filter_request_body_ = nullptr; @@ -74,6 +78,187 @@ class DynamicModuleHttpFilterConfig { OnHttpFilterScheduled on_http_filter_scheduled_ = nullptr; Envoy::Upstream::ClusterManager& cluster_manager_; + const Stats::ScopeSharedPtr stats_scope_; + Stats::StatNamePool stat_name_pool_; + // We only allow the module to create stats during envoy_dynamic_module_on_http_filter_config_new, + // and not later during request handling, so that we don't have to wrap the stat storage in a + // lock. + bool stat_creation_frozen_ = false; + + class ModuleCounterHandle { + public: + ModuleCounterHandle(Stats::Counter& counter) : counter_(counter) {} + + void add(uint64_t amount) const { counter_.add(amount); } + + private: + Stats::Counter& counter_; + }; + + class ModuleCounterVecHandle { + public: + ModuleCounterVecHandle(Stats::StatName name, Stats::StatNameVec label_names) + : name_(name), label_names_(label_names) {} + + const Stats::StatNameVec& getLabelNames() const { return label_names_; } + void add(Stats::Scope& scope, Stats::StatNameTagVectorOptConstRef tags, uint64_t amount) const { + ASSERT(tags.has_value()); + Stats::Utility::counterFromElements(scope, {name_}, tags).add(amount); + } + + private: + Stats::StatName name_; + Stats::StatNameVec label_names_; + }; + + class ModuleGaugeHandle { + public: + ModuleGaugeHandle(Stats::Gauge& gauge) : gauge_(gauge) {} + + void increase(uint64_t amount) const { gauge_.add(amount); } + void decrease(uint64_t amount) const { gauge_.sub(amount); } + void set(uint64_t amount) const { gauge_.set(amount); } + + private: + Stats::Gauge& gauge_; + }; + + class ModuleGaugeVecHandle { + public: + ModuleGaugeVecHandle(Stats::StatName name, Stats::StatNameVec label_names, + Stats::Gauge::ImportMode import_mode) + : name_(name), label_names_(label_names), import_mode_(import_mode) {} + + const Stats::StatNameVec& getLabelNames() const { return label_names_; } + + void increase(Stats::Scope& scope, Stats::StatNameTagVectorOptConstRef tags, + uint64_t amount) const { + ASSERT(tags.has_value()); + Stats::Utility::gaugeFromElements(scope, {name_}, import_mode_, tags).add(amount); + } + void decrease(Stats::Scope& scope, Stats::StatNameTagVectorOptConstRef tags, + uint64_t amount) const { + ASSERT(tags.has_value()); + Stats::Utility::gaugeFromElements(scope, {name_}, import_mode_, tags).sub(amount); + } + void set(Stats::Scope& scope, Stats::StatNameTagVectorOptConstRef tags, uint64_t amount) const { + ASSERT(tags.has_value()); + Stats::Utility::gaugeFromElements(scope, {name_}, import_mode_, tags).set(amount); + } + + private: + Stats::StatName name_; + Stats::StatNameVec label_names_; + Stats::Gauge::ImportMode import_mode_; + }; + + class ModuleHistogramHandle { + public: + ModuleHistogramHandle(Stats::Histogram& histogram) : histogram_(histogram) {} + + void recordValue(uint64_t value) const { histogram_.recordValue(value); } + + private: + Stats::Histogram& histogram_; + }; + + class ModuleHistogramVecHandle { + public: + ModuleHistogramVecHandle(Stats::StatName name, Stats::StatNameVec label_names, + Stats::Histogram::Unit unit) + : name_(name), label_names_(label_names), unit_(unit) {} + + const Stats::StatNameVec& getLabelNames() const { return label_names_; } + + void recordValue(Stats::Scope& scope, Stats::StatNameTagVectorOptConstRef tags, + uint64_t value) const { + ASSERT(tags.has_value()); + Stats::Utility::histogramFromElements(scope, {name_}, unit_, tags).recordValue(value); + } + + private: + Stats::StatName name_; + Stats::StatNameVec label_names_; + Stats::Histogram::Unit unit_; + }; + + size_t addCounter(ModuleCounterHandle&& counter) { + size_t id = counters_.size(); + counters_.push_back(std::move(counter)); + return id; + } + + size_t addCounterVec(ModuleCounterVecHandle&& counter_vec) { + size_t id = counter_vecs_.size(); + counter_vecs_.push_back(std::move(counter_vec)); + return id; + } + + OptRef getCounterById(size_t id) const { + if (id >= counters_.size()) { + return {}; + } + return counters_[id]; + } + + OptRef getCounterVecById(size_t id) const { + if (id >= counter_vecs_.size()) { + return {}; + } + return counter_vecs_[id]; + } + + size_t addGauge(ModuleGaugeHandle&& gauge) { + size_t id = gauges_.size(); + gauges_.push_back(std::move(gauge)); + return id; + } + + size_t addGaugeVec(ModuleGaugeVecHandle&& gauge_vec) { + size_t id = gauge_vecs_.size(); + gauge_vecs_.push_back(std::move(gauge_vec)); + return id; + } + + OptRef getGaugeById(size_t id) const { + if (id >= gauges_.size()) { + return {}; + } + return gauges_[id]; + } + + OptRef getGaugeVecById(size_t id) const { + if (id >= gauge_vecs_.size()) { + return {}; + } + return gauge_vecs_[id]; + } + + size_t addHistogram(ModuleHistogramHandle&& hist) { + size_t id = hists_.size(); + hists_.push_back(std::move(hist)); + return id; + } + + OptRef getHistogramById(size_t id) const { + if (id >= hists_.size()) { + return {}; + } + return hists_[id]; + } + + size_t addHistogramVec(ModuleHistogramVecHandle&& hist_vec) { + size_t id = hist_vecs_.size(); + hist_vecs_.push_back(std::move(hist_vec)); + return id; + } + + OptRef getHistogramVecById(size_t id) const { + if (id >= hist_vecs_.size()) { + return {}; + } + return hist_vecs_[id]; + } private: // The name of the filter passed in the constructor. @@ -82,6 +267,14 @@ class DynamicModuleHttpFilterConfig { // The configuration for the module. const std::string filter_config_; + // The cached references to stats and their metadata. + std::vector counters_; + std::vector counter_vecs_; + std::vector gauges_; + std::vector gauge_vecs_; + std::vector hists_; + std::vector hist_vecs_; + // The handle for the module. Extensions::DynamicModules::DynamicModulePtr dynamic_module_; }; @@ -90,14 +283,16 @@ class DynamicModuleHttpPerRouteFilterConfig : public Router::RouteSpecificFilter public: DynamicModuleHttpPerRouteFilterConfig( envoy_dynamic_module_type_http_filter_config_module_ptr config, - OnHttpPerRouteConfigDestoryType destroy) - : config_(config), destroy_(destroy) {} + OnHttpPerRouteConfigDestroyType destroy, + Extensions::DynamicModules::DynamicModulePtr dynamic_module) + : config_(config), destroy_(destroy), dynamic_module_(std::move(dynamic_module)) {} ~DynamicModuleHttpPerRouteFilterConfig() override; envoy_dynamic_module_type_http_filter_config_module_ptr config_; private: - OnHttpPerRouteConfigDestoryType destroy_; + OnHttpPerRouteConfigDestroyType destroy_; + Extensions::DynamicModules::DynamicModulePtr dynamic_module_; }; using DynamicModuleHttpFilterConfigSharedPtr = std::shared_ptr; @@ -117,11 +312,10 @@ newDynamicModuleHttpPerRouteConfig(const absl::string_view per_route_config_name * @param context the server factory context. * @return a shared pointer to the new config object or an error if the module could not be loaded. */ -absl::StatusOr -newDynamicModuleHttpFilterConfig(const absl::string_view filter_name, - const absl::string_view filter_config, - Extensions::DynamicModules::DynamicModulePtr dynamic_module, - Server::Configuration::ServerFactoryContext& context); +absl::StatusOr newDynamicModuleHttpFilterConfig( + const absl::string_view filter_name, const absl::string_view filter_config, + Extensions::DynamicModules::DynamicModulePtr dynamic_module, Stats::Scope& stats_scope, + Server::Configuration::ServerFactoryContext& context); } // namespace HttpFilters } // namespace DynamicModules diff --git a/source/extensions/filters/http/ext_authz/config.cc b/source/extensions/filters/http/ext_authz/config.cc index 83a3c60287ae6..8f7febefa8ccc 100644 --- a/source/extensions/filters/http/ext_authz/config.cc +++ b/source/extensions/filters/http/ext_authz/config.cc @@ -39,7 +39,8 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ &server_context](Http::FilterChainFactoryCallbacks& callbacks) { auto client = std::make_unique( server_context.clusterManager(), client_config); - callbacks.addStreamFilter(std::make_shared(filter_config, std::move(client))); + callbacks.addStreamFilter( + std::make_shared(filter_config, std::move(client), server_context)); }; } else { // gRPC client. @@ -57,7 +58,8 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ THROW_IF_NOT_OK_REF(client_or_error.status()); auto client = std::make_unique( client_or_error.value(), std::chrono::milliseconds(timeout_ms)); - callbacks.addStreamFilter(std::make_shared(filter_config, std::move(client))); + callbacks.addStreamFilter( + std::make_shared(filter_config, std::move(client), server_context)); }; } return callback; diff --git a/source/extensions/filters/http/ext_authz/ext_authz.cc b/source/extensions/filters/http/ext_authz/ext_authz.cc index 87f25714d7151..8984cb37f0ebc 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.cc +++ b/source/extensions/filters/http/ext_authz/ext_authz.cc @@ -1,4 +1,3 @@ -#include "ext_authz.h" #include "source/extensions/filters/http/ext_authz/ext_authz.h" #include @@ -21,6 +20,9 @@ namespace ExtAuthz { namespace { +// Default timeout for per-route gRPC client creation. +constexpr uint32_t kDefaultPerRouteTimeoutMs = 200; + using MetadataProto = ::envoy::config::core::v3::Metadata; using Filters::Common::MutationRules::CheckOperation; using Filters::Common::MutationRules::CheckResult; @@ -172,6 +174,86 @@ void FilterConfigPerRoute::merge(const FilterConfigPerRoute& other) { } } +// Constructor used for merging configurations from different levels (vhost, route, etc.) +FilterConfigPerRoute::FilterConfigPerRoute(const FilterConfigPerRoute& less_specific, + const FilterConfigPerRoute& more_specific) + : context_extensions_(less_specific.context_extensions_), + check_settings_(more_specific.check_settings_), disabled_(more_specific.disabled_), + // Only use the most specific per-route override. Do not inherit overrides from less + // specific configuration. If the more specific configuration has no override, leave both + // unset so that the main filter configuration is used. + grpc_service_(more_specific.grpc_service_.has_value() ? more_specific.grpc_service_ + : absl::nullopt), + http_service_(more_specific.http_service_.has_value() ? more_specific.http_service_ + : absl::nullopt) { + // Merge context extensions from more specific configuration, overriding less specific ones. + for (const auto& extension : more_specific.context_extensions_) { + context_extensions_[extension.first] = extension.second; + } +} + +Filters::Common::ExtAuthz::ClientPtr +Filter::createPerRouteGrpcClient(const envoy::config::core::v3::GrpcService& grpc_service) { + if (server_context_ == nullptr) { + ENVOY_STREAM_LOG( + debug, "ext_authz filter: server context not available for per-route gRPC client creation.", + *decoder_callbacks_); + return nullptr; + } + + // Use the timeout from the gRPC service configuration, use default if not specified. + const uint32_t timeout_ms = + PROTOBUF_GET_MS_OR_DEFAULT(grpc_service, timeout, kDefaultPerRouteTimeoutMs); + + // We can skip transport version check for per-route gRPC service here. + // The transport version is already validated at the main configuration level. + Envoy::Grpc::GrpcServiceConfigWithHashKey config_with_hash_key = + Envoy::Grpc::GrpcServiceConfigWithHashKey(grpc_service); + + auto client_or_error = server_context_->clusterManager() + .grpcAsyncClientManager() + .getOrCreateRawAsyncClientWithHashKey(config_with_hash_key, + server_context_->scope(), true); + if (!client_or_error.ok()) { + ENVOY_STREAM_LOG(warn, + "ext_authz filter: failed to create per-route gRPC client: {}. Falling back " + "to default client.", + *decoder_callbacks_, client_or_error.status().ToString()); + return nullptr; + } + + ENVOY_STREAM_LOG(debug, "ext_authz filter: created per-route gRPC client for cluster: {}.", + *decoder_callbacks_, + grpc_service.has_envoy_grpc() ? grpc_service.envoy_grpc().cluster_name() + : "google_grpc"); + + return std::make_unique( + client_or_error.value(), std::chrono::milliseconds(timeout_ms)); +} + +Filters::Common::ExtAuthz::ClientPtr Filter::createPerRouteHttpClient( + const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service) { + if (server_context_ == nullptr) { + ENVOY_STREAM_LOG( + debug, "ext_authz filter: server context not available for per-route HTTP client creation.", + *decoder_callbacks_); + return nullptr; + } + + // Use the timeout from the HTTP service configuration, use default if not specified. + const uint32_t timeout_ms = + PROTOBUF_GET_MS_OR_DEFAULT(http_service.server_uri(), timeout, kDefaultPerRouteTimeoutMs); + + ENVOY_STREAM_LOG(debug, "ext_authz filter: creating per-route HTTP client for URI: {}.", + *decoder_callbacks_, http_service.server_uri().uri()); + + const auto client_config = std::make_shared( + http_service, config_->headersAsBytes(), timeout_ms, *server_context_); + + return std::make_unique( + server_context_->clusterManager(), client_config); +} + void Filter::initiateCall(const Http::RequestHeaderMap& headers) { if (filter_return_ == FilterReturn::StopDecoding) { return; @@ -205,9 +287,10 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) { for (const FilterConfigPerRoute& cfg : Http::Utility::getAllPerFilterConfig(decoder_callbacks_)) { if (maybe_merged_per_route_config.has_value()) { - maybe_merged_per_route_config.value().merge(cfg); + FilterConfigPerRoute current_config = maybe_merged_per_route_config.value(); + maybe_merged_per_route_config.emplace(current_config, cfg); } else { - maybe_merged_per_route_config = cfg; + maybe_merged_per_route_config.emplace(cfg); } } @@ -216,6 +299,46 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) { context_extensions = maybe_merged_per_route_config.value().takeContextExtensions(); } + // Check if we need to use a per-route service override (gRPC or HTTP). + Filters::Common::ExtAuthz::Client* client_to_use = client_.get(); + if (maybe_merged_per_route_config) { + if (maybe_merged_per_route_config->grpcService().has_value()) { + const auto& grpc_service = maybe_merged_per_route_config->grpcService().value(); + ENVOY_STREAM_LOG(debug, "ext_authz filter: using per-route gRPC service configuration.", + *decoder_callbacks_); + + // Create a new gRPC client for this route. + per_route_client_ = createPerRouteGrpcClient(grpc_service); + if (per_route_client_ != nullptr) { + client_to_use = per_route_client_.get(); + ENVOY_STREAM_LOG(debug, "ext_authz filter: successfully created per-route gRPC client.", + *decoder_callbacks_); + } else { + ENVOY_STREAM_LOG( + warn, + "ext_authz filter: failed to create per-route gRPC client, falling back to default.", + *decoder_callbacks_); + } + } else if (maybe_merged_per_route_config->httpService().has_value()) { + const auto& http_service = maybe_merged_per_route_config->httpService().value(); + ENVOY_STREAM_LOG(debug, "ext_authz filter: using per-route HTTP service configuration.", + *decoder_callbacks_); + + // Create a new HTTP client for this route. + per_route_client_ = createPerRouteHttpClient(http_service); + if (per_route_client_ != nullptr) { + client_to_use = per_route_client_.get(); + ENVOY_STREAM_LOG(debug, "ext_authz filter: successfully created per-route HTTP client.", + *decoder_callbacks_); + } else { + ENVOY_STREAM_LOG( + warn, + "ext_authz filter: failed to create per-route HTTP client, falling back to default.", + *decoder_callbacks_); + } + } + } + // If metadata_context_namespaces or typed_metadata_context_namespaces is specified, // pass matching filter metadata to the ext_authz service. // If metadata key is set in both the connection and request metadata, @@ -241,7 +364,7 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) { config_->destinationLabels(), config_->allowedHeadersMatcher(), config_->disallowedHeadersMatcher()); - ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server", *decoder_callbacks_); + ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server.", *decoder_callbacks_); // Store start time of ext_authz filter call start_time_ = decoder_callbacks_->dispatcher().timeSource().monotonicTime(); @@ -250,8 +373,8 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) { // going to invoke check call. cluster_ = decoder_callbacks_->clusterInfo(); initiating_call_ = true; - client_->check(*this, check_request_, decoder_callbacks_->activeSpan(), - decoder_callbacks_->streamInfo()); + client_to_use->check(*this, check_request_, decoder_callbacks_->activeSpan(), + decoder_callbacks_->streamInfo()); initiating_call_ = false; } @@ -478,6 +601,17 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { updateLoggingInfo(response->grpc_status); + if (response->saw_invalid_append_actions) { + if (config_->validateMutations()) { + ENVOY_STREAM_LOG(trace, "Rejecting response with invalid header append action.", + *decoder_callbacks_); + rejectResponse(); + return; + } + ENVOY_STREAM_LOG(trace, "Ignoring response headers with invalid header append action.", + *decoder_callbacks_); + } + if (!response->dynamic_metadata.fields().empty()) { if (!config_->enableDynamicMetadataIngestion()) { ENVOY_STREAM_LOG(trace, @@ -488,7 +622,7 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { } else { // Add duration of call to dynamic metadata if applicable if (start_time_.has_value() && response->status == CheckStatus::OK) { - ProtobufWkt::Value ext_authz_duration_value; + Protobuf::Value ext_authz_duration_value; auto duration = decoder_callbacks_->dispatcher().timeSource().monotonicTime() - start_time_.value(); ext_authz_duration_value.set_number_value( diff --git a/source/extensions/filters/http/ext_authz/ext_authz.h b/source/extensions/filters/http/ext_authz/ext_authz.h index b1c37426d4a92..123daee1a6154 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.h +++ b/source/extensions/filters/http/ext_authz/ext_authz.h @@ -6,8 +6,10 @@ #include #include "envoy/extensions/filters/http/ext_authz/v3/ext_authz.pb.h" +#include "envoy/grpc/async_client_manager.h" #include "envoy/http/filter.h" #include "envoy/runtime/runtime.h" +#include "envoy/server/factory_context.h" #include "envoy/service/auth/v3/external_auth.pb.h" #include "envoy/stats/scope.h" #include "envoy/stats/stats_macros.h" @@ -17,6 +19,7 @@ #include "source/common/common/logger.h" #include "source/common/common/matchers.h" #include "source/common/common/utility.h" +#include "source/common/grpc/typed_async_client.h" #include "source/common/http/codes.h" #include "source/common/http/header_map_impl.h" #include "source/common/runtime/runtime_protos.h" @@ -54,10 +57,10 @@ struct ExtAuthzFilterStats { class ExtAuthzLoggingInfo : public Envoy::StreamInfo::FilterState::Object { public: - explicit ExtAuthzLoggingInfo(const absl::optional filter_metadata) + explicit ExtAuthzLoggingInfo(const absl::optional filter_metadata) : filter_metadata_(filter_metadata) {} - const absl::optional& filterMetadata() const { return filter_metadata_; } + const absl::optional& filterMetadata() const { return filter_metadata_; } absl::optional latency() const { return latency_; }; absl::optional bytesSent() const { return bytes_sent_; } absl::optional bytesReceived() const { return bytes_received_; } @@ -99,7 +102,7 @@ class ExtAuthzLoggingInfo : public Envoy::StreamInfo::FilterState::Object { void clearUpstreamHost() { upstream_host_ = nullptr; } private: - const absl::optional filter_metadata_; + const absl::optional filter_metadata_; absl::optional latency_; // The following stats are populated for ext_authz filters using Envoy gRPC only. absl::optional bytes_sent_; @@ -198,7 +201,7 @@ class FilterConfig { bool includeTLSSession() const { return include_tls_session_; } const LabelsMap& destinationLabels() const { return destination_labels_; } - const absl::optional& filterMetadata() const { return filter_metadata_; } + const absl::optional& filterMetadata() const { return filter_metadata_; } bool emitFilterStateStats() const { return emit_filter_state_stats_; } @@ -252,7 +255,7 @@ class FilterConfig { Runtime::Loader& runtime_; Http::Context& http_context_; LabelsMap destination_labels_; - const absl::optional filter_metadata_; + const absl::optional filter_metadata_; const bool emit_filter_state_stats_; const absl::optional filter_enabled_; @@ -305,7 +308,13 @@ class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { check_settings_(config.has_check_settings() ? config.check_settings() : envoy::extensions::filters::http::ext_authz::v3::CheckSettings()), - disabled_(config.disabled()) { + disabled_(config.disabled()), + grpc_service_(config.has_check_settings() && config.check_settings().has_grpc_service() + ? absl::make_optional(config.check_settings().grpc_service()) + : absl::nullopt), + http_service_(config.has_check_settings() && config.check_settings().has_http_service() + ? absl::make_optional(config.check_settings().http_service()) + : absl::nullopt) { if (config.has_check_settings() && config.check_settings().disable_request_body_buffering() && config.check_settings().has_with_request_body()) { ExceptionUtil::throwEnvoyException( @@ -314,6 +323,12 @@ class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { } } + // This constructor is used as a way to merge more-specific config into less-specific config in a + // clearly defined way (e.g. route config into VH config). All fields on this class must be const + // and thus must be initialized in the constructor initialization list. + FilterConfigPerRoute(const FilterConfigPerRoute& less_specific, + const FilterConfigPerRoute& more_specific); + void merge(const FilterConfigPerRoute& other); /** @@ -329,12 +344,30 @@ class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { return check_settings_; } + /** + * @return The gRPC service override for this route, if any. + */ + const absl::optional& grpcService() const { + return grpc_service_; + } + + /** + * @return The HTTP service override for this route, if any. + */ + const absl::optional& + httpService() const { + return http_service_; + } + private: // We save the context extensions as a protobuf map instead of a std::map as this allows us to // move it to the CheckRequest, thus avoiding a copy that would incur by converting it. ContextExtensionsMap context_extensions_; envoy::extensions::filters::http::ext_authz::v3::CheckSettings check_settings_; - bool disabled_; + const bool disabled_; + const absl::optional grpc_service_; + const absl::optional + http_service_; }; /** @@ -348,6 +381,12 @@ class Filter : public Logger::Loggable, Filter(const FilterConfigSharedPtr& config, Filters::Common::ExtAuthz::ClientPtr&& client) : config_(config), client_(std::move(client)), stats_(config->stats()) {} + // Constructor that includes server context for per-route service support. + Filter(const FilterConfigSharedPtr& config, Filters::Common::ExtAuthz::ClientPtr&& client, + Server::Configuration::ServerFactoryContext& server_context) + : config_(config), client_(std::move(client)), server_context_(&server_context), + stats_(config->stats()) {} + // Http::StreamFilterBase void onDestroy() override; @@ -383,6 +422,14 @@ class Filter : public Logger::Loggable, // code. void rejectResponse(); + // Create a new gRPC client for per-route gRPC service configuration. + Filters::Common::ExtAuthz::ClientPtr + createPerRouteGrpcClient(const envoy::config::core::v3::GrpcService& grpc_service); + + // Create a new HTTP client for per-route HTTP service configuration. + Filters::Common::ExtAuthz::ClientPtr createPerRouteHttpClient( + const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service); + absl::optional start_time_; void addResponseHeaders(Http::HeaderMap& header_map, const Http::HeaderVector& headers); void initiateCall(const Http::RequestHeaderMap& headers); @@ -410,6 +457,10 @@ class Filter : public Logger::Loggable, Http::HeaderMapPtr getHeaderMap(const Filters::Common::ExtAuthz::ResponsePtr& response); FilterConfigSharedPtr config_; Filters::Common::ExtAuthz::ClientPtr client_; + // Per-route gRPC client that overrides the default client when specified. + Filters::Common::ExtAuthz::ClientPtr per_route_client_; + // Server context for creating per-route clients. + Server::Configuration::ServerFactoryContext* server_context_{nullptr}; Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; Http::RequestHeaderMap* request_headers_; diff --git a/source/extensions/filters/http/ext_proc/config.cc b/source/extensions/filters/http/ext_proc/config.cc index a85a987e334c6..88db5a9807bbe 100644 --- a/source/extensions/filters/http/ext_proc/config.cc +++ b/source/extensions/filters/http/ext_proc/config.cc @@ -50,17 +50,6 @@ absl::Status verifyProcessingModeConfig( "then the response_trailer_mode has to be set to SEND"); } - // Do not support fail open for FULL_DUPLEX_STREAMED body mode. - if (((processing_mode.request_body_mode() == - envoy::extensions::filters::http::ext_proc::v3::ProcessingMode::FULL_DUPLEX_STREAMED) || - (processing_mode.response_body_mode() == - envoy::extensions::filters::http::ext_proc::v3::ProcessingMode::FULL_DUPLEX_STREAMED)) && - config.failure_mode_allow()) { - return absl::InvalidArgumentError( - "If the ext_proc filter has either the request_body_mode or the response_body_mode set " - "to FULL_DUPLEX_STREAMED, then the failure_mode_allow has to be left as false"); - } - return absl::OkStatus(); } diff --git a/source/extensions/filters/http/ext_proc/ext_proc.cc b/source/extensions/filters/http/ext_proc/ext_proc.cc index 81158f9c1ba12..e331f5a6c4437 100644 --- a/source/extensions/filters/http/ext_proc/ext_proc.cc +++ b/source/extensions/filters/http/ext_proc/ext_proc.cc @@ -219,7 +219,7 @@ FilterConfig::FilterConfig(const ExternalProcessor& config, const std::chrono::milliseconds message_timeout, const uint32_t max_message_timeout_ms, Stats::Scope& scope, const std::string& stats_prefix, bool is_upstream, - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder, + Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr builder, Server::Configuration::CommonFactoryContext& context) : failure_mode_allow_(config.failure_mode_allow()), observability_mode_(config.observability_mode()), @@ -258,7 +258,8 @@ FilterConfig::FilterConfig(const ExternalProcessor& config, createOnProcessingResponseCb(config, context, stats_prefix)), thread_local_stream_manager_slot_(context.threadLocal().allocateSlot()), remote_close_timeout_(context.runtime().snapshot().getInteger( - RemoteCloseTimeout, DefaultRemoteCloseTimeoutMilliseconds)) { + RemoteCloseTimeout, DefaultRemoteCloseTimeoutMilliseconds)), + status_on_error_(toErrorCode(config.status_on_error().code())) { if (config.disable_clear_route_cache()) { route_cache_action_ = ExternalProcessor::RETAIN; @@ -326,7 +327,7 @@ ExtProcLoggingInfo::grpcCalls(envoy::config::core::v3::TrafficDirection traffic_ } ProtobufTypes::MessagePtr ExtProcLoggingInfo::serializeAsProto() const { - auto struct_msg = std::make_unique(); + auto struct_msg = std::make_unique(); if (decoding_processor_grpc_calls_.header_stats_) { (*struct_msg->mutable_fields())[RequestHeaderLatencyUsField].set_number_value( @@ -577,7 +578,8 @@ void Filter::onError() { decoding_state_.onFinishProcessorCall(Grpc::Status::Aborted); encoding_state_.onFinishProcessorCall(Grpc::Status::Aborted); ImmediateResponse errorResponse; - errorResponse.mutable_status()->set_code(StatusCode::InternalServerError); + errorResponse.mutable_status()->set_code( + static_cast(static_cast(config_->statusOnError()))); errorResponse.set_details(absl::StrCat(ErrorPrefix, "_HTTP_ERROR")); sendImmediateResponse(errorResponse); } @@ -1311,7 +1313,7 @@ void Filter::logStreamInfo() { } } -void Filter::onNewTimeout(const ProtobufWkt::Duration& override_message_timeout) { +void Filter::onNewTimeout(const Protobuf::Duration& override_message_timeout) { const auto result = DurationUtil::durationToMillisecondsNoThrow(override_message_timeout); if (!result.ok()) { ENVOY_STREAM_LOG(warn, @@ -1456,7 +1458,8 @@ void Filter::handleErrorResponse(absl::Status processing_status) { onFinishProcessorCalls(processing_status.raw_code()); closeStream(); ImmediateResponse invalid_mutation_response; - invalid_mutation_response.mutable_status()->set_code(StatusCode::InternalServerError); + invalid_mutation_response.mutable_status()->set_code( + static_cast(static_cast(config_->statusOnError()))); invalid_mutation_response.set_details(std::string(processing_status.message())); sendImmediateResponse(invalid_mutation_response); } @@ -1631,7 +1634,7 @@ void Filter::onReceiveMessage(std::unique_ptr&& r) { // instance's lifetime to protect us from a malformed server. stats_.failure_mode_allowed_.inc(); closeStream(); - clearAsyncState(); + clearAsyncState(processing_status.raw_code()); processing_complete_ = true; } else { // Send an immediate response if fail close is configured. @@ -1646,7 +1649,8 @@ void Filter::onReceiveMessage(std::unique_ptr&& r) { } void Filter::onGrpcError(Grpc::Status::GrpcStatus status, const std::string& message) { - ENVOY_STREAM_LOG(debug, "Received gRPC error on stream: {}", *decoder_callbacks_, status); + ENVOY_STREAM_LOG(warn, "Received gRPC error on stream: {}, message {}", *decoder_callbacks_, + status, message); stats_.streams_failed_.inc(); if (processing_complete_) { @@ -1654,8 +1658,7 @@ void Filter::onGrpcError(Grpc::Status::GrpcStatus status, const std::string& mes } if (failure_mode_allow_) { - // Ignore this and treat as a successful close - onGrpcClose(); + onGrpcCloseWithStatus(status); stats_.failure_mode_allowed_.inc(); } else { @@ -1665,14 +1668,17 @@ void Filter::onGrpcError(Grpc::Status::GrpcStatus status, const std::string& mes onFinishProcessorCalls(status); closeStream(); ImmediateResponse errorResponse; - errorResponse.mutable_status()->set_code(StatusCode::InternalServerError); + errorResponse.mutable_status()->set_code( + static_cast(static_cast(config_->statusOnError()))); errorResponse.set_details( absl::StrFormat("%s_gRPC_error_%i{%s}", ErrorPrefix, status, message)); sendImmediateResponse(errorResponse); } } -void Filter::onGrpcClose() { +void Filter::onGrpcClose() { onGrpcCloseWithStatus(Grpc::Status::Aborted); } + +void Filter::onGrpcCloseWithStatus(Grpc::Status::GrpcStatus status) { ENVOY_STREAM_LOG(debug, "Received gRPC stream close", *decoder_callbacks_); processing_complete_ = true; @@ -1680,7 +1686,7 @@ void Filter::onGrpcClose() { // Successful close. We can ignore the stream for the rest of our request // and response processing. closeStream(); - clearAsyncState(); + clearAsyncState(status); } void Filter::onMessageTimeout() { @@ -1695,7 +1701,7 @@ void Filter::onMessageTimeout() { processing_complete_ = true; closeStream(); stats_.failure_mode_allowed_.inc(); - clearAsyncState(); + clearAsyncState(Grpc::Status::DeadlineExceeded); } else { // Return an error and stop processing the current stream. @@ -1704,7 +1710,6 @@ void Filter::onMessageTimeout() { decoding_state_.onFinishProcessorCall(Grpc::Status::DeadlineExceeded); encoding_state_.onFinishProcessorCall(Grpc::Status::DeadlineExceeded); ImmediateResponse errorResponse; - errorResponse.mutable_status()->set_code(StatusCode::GatewayTimeout); errorResponse.set_details(absl::StrFormat("%s_per-message_timeout_exceeded", ErrorPrefix)); sendImmediateResponse(errorResponse); @@ -1713,9 +1718,9 @@ void Filter::onMessageTimeout() { // Regardless of the current filter state, reset it to "IDLE", continue // the current callback, and reset timers. This is used in a few error-handling situations. -void Filter::clearAsyncState() { - decoding_state_.clearAsyncState(); - encoding_state_.clearAsyncState(); +void Filter::clearAsyncState(Grpc::Status::GrpcStatus call_status) { + decoding_state_.clearAsyncState(call_status); + encoding_state_.clearAsyncState(call_status); } // Regardless of the current state, ensure that the timers won't fire diff --git a/source/extensions/filters/http/ext_proc/ext_proc.h b/source/extensions/filters/http/ext_proc/ext_proc.h index 1f99582cac485..43a890ae1eab3 100644 --- a/source/extensions/filters/http/ext_proc/ext_proc.h +++ b/source/extensions/filters/http/ext_proc/ext_proc.h @@ -10,6 +10,7 @@ #include "envoy/event/timer.h" #include "envoy/extensions/filters/http/ext_proc/v3/ext_proc.pb.h" #include "envoy/grpc/async_client.h" +#include "envoy/http/codes.h" #include "envoy/http/filter.h" #include "envoy/service/ext_proc/v3/external_processor.pb.h" #include "envoy/stats/scope.h" @@ -58,7 +59,7 @@ struct ExtProcFilterStats { class ExtProcLoggingInfo : public Envoy::StreamInfo::FilterState::Object { public: - explicit ExtProcLoggingInfo(const Envoy::ProtobufWkt::Struct& filter_metadata) + explicit ExtProcLoggingInfo(const Envoy::Protobuf::Struct& filter_metadata) : filter_metadata_(filter_metadata) {} // gRPC call stats for headers and trailers. @@ -120,7 +121,7 @@ class ExtProcLoggingInfo : public Envoy::StreamInfo::FilterState::Object { Upstream::ClusterInfoConstSharedPtr clusterInfo() const { return cluster_info_; } Upstream::HostDescriptionConstSharedPtr upstreamHost() const { return upstream_host_; } const GrpcCalls& grpcCalls(envoy::config::core::v3::TrafficDirection traffic_direction) const; - const Envoy::ProtobufWkt::Struct& filterMetadata() const { return filter_metadata_; } + const Envoy::Protobuf::Struct& filterMetadata() const { return filter_metadata_; } const std::string& httpResponseCodeDetails() const { return http_response_code_details_; } ProtobufTypes::MessagePtr serializeAsProto() const override; @@ -135,7 +136,7 @@ class ExtProcLoggingInfo : public Envoy::StreamInfo::FilterState::Object { GrpcCalls& grpcCalls(envoy::config::core::v3::TrafficDirection traffic_direction); GrpcCalls decoding_processor_grpc_calls_; GrpcCalls encoding_processor_grpc_calls_; - const Envoy::ProtobufWkt::Struct filter_metadata_; + const Envoy::Protobuf::Struct filter_metadata_; // The following stats are populated for ext_proc filters using Envoy gRPC only. // The bytes sent and received are for the entire stream. uint64_t bytes_sent_{0}, bytes_received_{0}; @@ -225,7 +226,7 @@ class FilterConfig { const std::chrono::milliseconds message_timeout, const uint32_t max_message_timeout_ms, Stats::Scope& scope, const std::string& stats_prefix, bool is_upstream, - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder, + Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr builder, Server::Configuration::CommonFactoryContext& context); bool failureModeAllow() const { return failure_mode_allow_; } @@ -264,7 +265,7 @@ class FilterConfig { return disallowed_headers_; } - const ProtobufWkt::Struct& filterMetadata() const { return filter_metadata_; } + const Protobuf::Struct& filterMetadata() const { return filter_metadata_; } const ExpressionManager& expressionManager() const { return expression_manager_; } @@ -305,7 +306,18 @@ class FilterConfig { std::unique_ptr createOnProcessingResponse() const; + Http::Code statusOnError() const { return status_on_error_; } + private: + static Http::Code toErrorCode(uint64_t status) { + const auto code = static_cast(status); + // Only allow 4xx and 5xx status codes. + if (code >= Http::Code::BadRequest && code <= Http::Code::LastUnassignedServerErrorCode) { + return code; + } + return Http::Code::InternalServerError; + } + ExtProcFilterStats generateStats(const std::string& prefix, const std::string& filter_stats_prefix, Stats::Scope& scope) { const std::string final_prefix = absl::StrCat(prefix, "ext_proc.", filter_stats_prefix); @@ -327,7 +339,7 @@ class FilterConfig { ExtProcFilterStats stats_; const envoy::extensions::filters::http::ext_proc::v3::ProcessingMode processing_mode_; const Filters::Common::MutationRules::Checker mutation_checker_; - const ProtobufWkt::Struct filter_metadata_; + const Protobuf::Struct filter_metadata_; // If set to true, allow the processing mode to be modified by the ext_proc response. const bool allow_mode_override_; // If set to true, disable the immediate response from the ext_proc server, which means @@ -353,6 +365,7 @@ class FilterConfig { ThreadLocal::SlotPtr thread_local_stream_manager_slot_; const std::chrono::milliseconds remote_close_timeout_; + const Http::Code status_on_error_; }; using FilterConfigSharedPtr = std::shared_ptr; @@ -471,11 +484,12 @@ class Filter : public Logger::Loggable, std::unique_ptr&& response) override; void onGrpcError(Grpc::Status::GrpcStatus error, const std::string& message) override; void onGrpcClose() override; + void onGrpcCloseWithStatus(Grpc::Status::GrpcStatus status); void logStreamInfoBase(const Envoy::StreamInfo::StreamInfo* stream_info); void logStreamInfo() override; void onMessageTimeout(); - void onNewTimeout(const ProtobufWkt::Duration& override_message_timeout); + void onNewTimeout(const Protobuf::Duration& override_message_timeout); envoy::service::ext_proc::v3::ProcessingRequest setupBodyChunk(ProcessorState& state, const Buffer::Instance& data, bool end_stream); @@ -523,7 +537,7 @@ class Filter : public Logger::Loggable, void halfCloseAndWaitForRemoteClose(); void onFinishProcessorCalls(Grpc::Status::GrpcStatus call_status); - void clearAsyncState(); + void clearAsyncState(Grpc::Status::GrpcStatus call_status = Grpc::Status::Aborted); void sendImmediateResponse(const envoy::service::ext_proc::v3::ImmediateResponse& response); Http::FilterHeadersStatus onHeaders(ProcessorState& state, diff --git a/source/extensions/filters/http/ext_proc/matching_utils.cc b/source/extensions/filters/http/ext_proc/matching_utils.cc index f87f44e89612c..29e36000985ca 100644 --- a/source/extensions/filters/http/ext_proc/matching_utils.cc +++ b/source/extensions/filters/http/ext_proc/matching_utils.cc @@ -25,12 +25,15 @@ ExpressionManager::initExpressions(const Protobuf::RepeatedPtrField parse_status.status().ToString()); } - Filters::Common::Expr::ExpressionPtr expression = - Extensions::Filters::Common::Expr::createExpression(builder_->builder(), - parse_status.value().expr()); - - expressions.emplace( - matcher, ExpressionManager::CelExpression{parse_status.value(), std::move(expression)}); + const auto& parsed_expr = parse_status.value(); + const cel::expr::Expr& cel_expr = parsed_expr.expr(); + auto compiled_expression = + Extensions::Filters::Common::Expr::CompiledExpression::Create(builder_, cel_expr); + if (!compiled_expression.ok()) { + throw EnvoyException( + absl::StrCat("failed to create an expression: ", compiled_expression.status().message())); + } + expressions.emplace(matcher, std::move(compiled_expression.value())); } #else ENVOY_LOG(warn, "CEL expression parsing is not available for use in this environment." @@ -40,19 +43,19 @@ ExpressionManager::initExpressions(const Protobuf::RepeatedPtrField return expressions; } -ProtobufWkt::Struct +Protobuf::Struct ExpressionManager::evaluateAttributes(const Filters::Common::Expr::Activation& activation, const absl::flat_hash_map& expr) { - ProtobufWkt::Struct proto; + Protobuf::Struct proto; if (expr.empty()) { return proto; } for (const auto& hash_entry : expr) { - ProtobufWkt::Arena arena; - const auto result = hash_entry.second.compiled_expr_->Evaluate(activation, &arena); + Protobuf::Arena arena; + const auto result = hash_entry.second.evaluate(activation, &arena); if (!result.ok()) { // TODO: Stats? continue; @@ -82,7 +85,7 @@ ExpressionManager::evaluateAttributes(const Filters::Common::Expr::Activation& a // Handling all value types here would be graceful but is not currently // testable and drives down coverage %. This is not a _great_ reason to // not do it; will get feedback from reviewers. - ProtobufWkt::Value value; + Protobuf::Value value; switch (result.value().type()) { case google::api::expr::runtime::CelValue::Type::kBool: value.set_bool_value(result.value().BoolOrDie()); diff --git a/source/extensions/filters/http/ext_proc/matching_utils.h b/source/extensions/filters/http/ext_proc/matching_utils.h index cfdf038c1d732..112ea9bc04392 100644 --- a/source/extensions/filters/http/ext_proc/matching_utils.h +++ b/source/extensions/filters/http/ext_proc/matching_utils.h @@ -5,6 +5,8 @@ #include "source/common/protobuf/protobuf.h" #include "source/extensions/filters/common/expr/evaluator.h" +#include "cel/expr/syntax.pb.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -12,12 +14,9 @@ namespace ExternalProcessing { class ExpressionManager : public Logger::Loggable { public: - struct CelExpression { - google::api::expr::v1alpha1::ParsedExpr parsed_expr_; - Filters::Common::Expr::ExpressionPtr compiled_expr_; - }; + using CelExpression = Filters::Common::Expr::CompiledExpression; - ExpressionManager(Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder, + ExpressionManager(Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr builder, const LocalInfo::LocalInfo& local_info, const Protobuf::RepeatedPtrField& request_matchers, const Protobuf::RepeatedPtrField& response_matchers) @@ -29,17 +28,17 @@ class ExpressionManager : public Logger::Loggable { bool hasResponseExpr() const { return !response_expr_.empty(); }; - ProtobufWkt::Struct + Protobuf::Struct evaluateRequestAttributes(const Filters::Common::Expr::Activation& activation) const { return evaluateAttributes(activation, request_expr_); } - ProtobufWkt::Struct + Protobuf::Struct evaluateResponseAttributes(const Filters::Common::Expr::Activation& activation) const { return evaluateAttributes(activation, response_expr_); } - static ProtobufWkt::Struct + static Protobuf::Struct evaluateAttributes(const Filters::Common::Expr::Activation& activation, const absl::flat_hash_map& expr); @@ -49,7 +48,7 @@ class ExpressionManager : public Logger::Loggable { absl::flat_hash_map initExpressions(const Protobuf::RepeatedPtrField& matchers); - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder_; + const Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr builder_; const LocalInfo::LocalInfo& local_info_; const absl::flat_hash_map request_expr_; diff --git a/source/extensions/filters/http/ext_proc/processor_state.cc b/source/extensions/filters/http/ext_proc/processor_state.cc index 65fc531eb99e0..1ff3c2a7ba1f3 100644 --- a/source/extensions/filters/http/ext_proc/processor_state.cc +++ b/source/extensions/filters/http/ext_proc/processor_state.cc @@ -90,17 +90,20 @@ bool ProcessorState::restartMessageTimer(const uint32_t message_timeout_ms) { } } +// Process the data being buffered in STREAMED or FULL_DUPLEX_STREAMED mode. void ProcessorState::sendBufferedDataInStreamedMode(bool end_stream) { - // Process the data being buffered in streaming mode. - // Move the current buffer into the queue for remote processing and clear the buffered data. if (hasBufferedData()) { Buffer::OwnedImpl buffered_chunk; modifyBufferedData([&buffered_chunk](Buffer::Instance& data) { buffered_chunk.move(data); }); ENVOY_STREAM_LOG(debug, "Sending a chunk of buffered data ({})", *filter_callbacks_, buffered_chunk.length()); - // Need to first enqueue the data into the chunk queue before sending. auto req = filter_.setupBodyChunk(*this, buffered_chunk, end_stream); - enqueueStreamingChunk(buffered_chunk, end_stream); + if (body_mode_ != ProcessingMode::FULL_DUPLEX_STREAMED) { + // Move the current buffer into the queue for remote processing and clear the buffered data. + enqueueStreamingChunk(buffered_chunk, end_stream); + } else { + buffered_chunk.drain(buffered_chunk.length()); + } filter_.sendBodyChunk(*this, ProcessorState::CallbackState::StreamedBodyCallback, req); } if (queueBelowLowLimit()) { @@ -224,16 +227,12 @@ absl::Status ProcessorState::handleHeaderContinue(const HeadersResponse& respons clearWatermark(); filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection()); return absl::OkStatus(); - } else if (body_mode_ == ProcessingMode::STREAMED) { + } else if (body_mode_ == ProcessingMode::STREAMED || + body_mode_ == ProcessingMode::FULL_DUPLEX_STREAMED) { sendBufferedDataInStreamedMode(false); filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection()); continueIfNecessary(); return absl::OkStatus(); - } else if (body_mode_ == ProcessingMode::FULL_DUPLEX_STREAMED) { - // There is no buffered data in this mode. - filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection()); - continueIfNecessary(); - return absl::OkStatus(); } else if (body_mode_ == ProcessingMode::BUFFERED_PARTIAL) { return handleBufferedPartialMode(response); } @@ -536,8 +535,8 @@ QueuedChunkPtr ProcessorState::dequeueStreamingChunk(Buffer::OwnedImpl& out_data return chunk_queue_.pop(out_data); } -void ProcessorState::clearAsyncState() { - onFinishProcessorCall(Grpc::Status::Aborted); +void ProcessorState::clearAsyncState(Grpc::Status::GrpcStatus call_status) { + onFinishProcessorCall(call_status); if (chunkQueue().receivedData().length() > 0) { const auto& all_data = consolidateStreamedChunks(); ENVOY_STREAM_LOG(trace, "Injecting leftover buffer of {} bytes", *filter_callbacks_, diff --git a/source/extensions/filters/http/ext_proc/processor_state.h b/source/extensions/filters/http/ext_proc/processor_state.h index 26fb4786cf961..1c53b1b72262a 100644 --- a/source/extensions/filters/http/ext_proc/processor_state.h +++ b/source/extensions/filters/http/ext_proc/processor_state.h @@ -210,7 +210,7 @@ class ProcessorState : public Logger::Loggable { virtual void continueProcessing() const PURE; void continueIfNecessary(); - void clearAsyncState(); + void clearAsyncState(Grpc::Status::GrpcStatus call_status = Grpc::Status::Aborted); virtual envoy::service::ext_proc::v3::HttpHeaders* mutableHeaders(envoy::service::ext_proc::v3::ProcessingRequest& request) const PURE; @@ -225,7 +225,7 @@ class ProcessorState : public Logger::Loggable { void setSentAttributes(bool sent) { attributes_sent_ = sent; } - virtual ProtobufWkt::Struct + virtual Protobuf::Struct evaluateAttributes(const ExpressionManager& mgr, const Filters::Common::Expr::Activation& activation) const PURE; @@ -502,7 +502,7 @@ class DecodingProcessorState : public ProcessorState { } const Http::RequestOrResponseHeaderMap* responseHeaders() const override { return nullptr; } - ProtobufWkt::Struct + Protobuf::Struct evaluateAttributes(const ExpressionManager& mgr, const Filters::Common::Expr::Activation& activation) const override { return mgr.evaluateRequestAttributes(activation); @@ -594,7 +594,7 @@ class EncodingProcessorState : public ProcessorState { const Http::RequestOrResponseHeaderMap* responseHeaders() const override { return headers_; } - ProtobufWkt::Struct + Protobuf::Struct evaluateAttributes(const ExpressionManager& mgr, const Filters::Common::Expr::Activation& activation) const override { return mgr.evaluateResponseAttributes(activation); diff --git a/source/extensions/filters/http/fault/fault_filter.h b/source/extensions/filters/http/fault/fault_filter.h index a17538fa0d7ae..1189682daa5d9 100644 --- a/source/extensions/filters/http/fault/fault_filter.h +++ b/source/extensions/filters/http/fault/fault_filter.h @@ -77,7 +77,7 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { return response_rate_limit_percent_runtime_; } bool disableDownstreamClusterStats() const { return disable_downstream_cluster_stats_; } - const Envoy::ProtobufWkt::Struct& filterMetadata() const { return filter_metadata_; } + const Envoy::Protobuf::Struct& filterMetadata() const { return filter_metadata_; } private: class RuntimeKeyValues { @@ -111,7 +111,7 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { const std::string response_rate_limit_percent_runtime_; const bool disable_downstream_cluster_stats_; - const Envoy::ProtobufWkt::Struct filter_metadata_; + const Envoy::Protobuf::Struct filter_metadata_; }; /** diff --git a/source/extensions/filters/http/gcp_authn/BUILD b/source/extensions/filters/http/gcp_authn/BUILD index 2dd2e7daa00ce..c6e0ba93debcd 100644 --- a/source/extensions/filters/http/gcp_authn/BUILD +++ b/source/extensions/filters/http/gcp_authn/BUILD @@ -19,6 +19,7 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/http:message_lib", "//source/common/http:utility_lib", + "//source/common/runtime:runtime_features_lib", "//source/extensions/filters/http/common:factory_base_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", "@envoy_api//envoy/extensions/filters/http/gcp_authn/v3:pkg_cc_proto", diff --git a/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc b/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc index ef0d4e992d68b..c88702c442070 100644 --- a/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc +++ b/source/extensions/filters/http/gcp_authn/gcp_authn_filter.cc @@ -6,6 +6,7 @@ #include "source/common/common/enum_to_int.h" #include "source/common/http/header_map_impl.h" #include "source/common/http/utility.h" +#include "source/common/runtime/runtime_features.h" #include "absl/strings/str_replace.h" @@ -75,11 +76,7 @@ Http::FilterHeadersStatus GcpAuthnFilter::decodeHeaders(Http::RequestHeaderMap& // So, we add the audience from the config to the final url by substituting the `[AUDIENCE]` // with real audience string from the config. - std::string final_url = absl::StrReplaceAll( - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.gcp_authn_use_fixed_url") - ? UrlString - : filter_config_->http_uri().uri(), - {{"[AUDIENCE]", audience_str_}}); + std::string final_url = absl::StrReplaceAll(UrlString, {{"[AUDIENCE]", audience_str_}}); client_->fetchToken(*this, buildRequest(final_url)); initiating_call_ = false; } else { diff --git a/source/extensions/filters/http/grpc_field_extraction/extractor.h b/source/extensions/filters/http/grpc_field_extraction/extractor.h index d321b3651ec4f..f601833f81d11 100644 --- a/source/extensions/filters/http/grpc_field_extraction/extractor.h +++ b/source/extensions/filters/http/grpc_field_extraction/extractor.h @@ -25,7 +25,7 @@ struct RequestField { absl::string_view path; // The request field value. - ProtobufWkt::Value value; + Protobuf::Value value; }; using ExtractionResult = std::vector; diff --git a/source/extensions/filters/http/grpc_field_extraction/extractor_impl.cc b/source/extensions/filters/http/grpc_field_extraction/extractor_impl.cc index 6b78d6bd29d72..95d95d647d21d 100644 --- a/source/extensions/filters/http/grpc_field_extraction/extractor_impl.cc +++ b/source/extensions/filters/http/grpc_field_extraction/extractor_impl.cc @@ -48,7 +48,7 @@ ExtractorImpl::processRequest(Protobuf::field_extraction::MessageData& message) ExtractionResult result; for (const auto& it : per_field_extractors_) { - absl::StatusOr extracted_value = it.second->ExtractValue(message); + absl::StatusOr extracted_value = it.second->ExtractValue(message); if (!extracted_value.ok()) { return extracted_value.status(); } diff --git a/source/extensions/filters/http/grpc_field_extraction/filter.cc b/source/extensions/filters/http/grpc_field_extraction/filter.cc index 7ae1fb12f326b..813a23138c830 100644 --- a/source/extensions/filters/http/grpc_field_extraction/filter.cc +++ b/source/extensions/filters/http/grpc_field_extraction/filter.cc @@ -211,7 +211,7 @@ Filter::HandleDecodeDataStatus Filter::handleDecodeData(Envoy::Buffer::Instance& void Filter::handleExtractionResult(const ExtractionResult& result) { RELEASE_ASSERT(extractor_, "`extractor_ should be inited when extracting fields"); - ProtobufWkt::Struct dest_metadata; + Protobuf::Struct dest_metadata; for (const auto& req_field : result) { RELEASE_ASSERT(!req_field.path.empty(), "`req_field.path` shouldn't be empty"); (*dest_metadata.mutable_fields())[req_field.path] = req_field.value; diff --git a/source/extensions/filters/http/grpc_json_reverse_transcoder/filter_config.cc b/source/extensions/filters/http/grpc_json_reverse_transcoder/filter_config.cc index 600d97b1bce2c..3b1bafbb9cde7 100644 --- a/source/extensions/filters/http/grpc_json_reverse_transcoder/filter_config.cc +++ b/source/extensions/filters/http/grpc_json_reverse_transcoder/filter_config.cc @@ -123,15 +123,15 @@ bool GrpcJsonReverseTranscoderConfig::IsRequestNestedHttpBody( if (http_request_body_field.empty() || http_request_body_field == "*") { return false; } - const ProtobufWkt::Type* request_type = type_helper_->Info()->GetTypeByTypeUrl(request_type_url); - std::vector request_body_field_path; + const Protobuf::Type* request_type = type_helper_->Info()->GetTypeByTypeUrl(request_type_url); + std::vector request_body_field_path; absl::Status status = type_helper_->ResolveFieldPath(*request_type, http_request_body_field, &request_body_field_path); if (!status.ok() || request_body_field_path.empty()) { ENVOY_LOG(error, "Failed to resolve the request type: {}", request_type_url); return false; } - const ProtobufWkt::Type* request_body_type = + const Protobuf::Type* request_body_type = type_helper_->Info()->GetTypeByTypeUrl(request_body_field_path.back()->type_url()); return request_body_type != nullptr && diff --git a/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc index 58ef86e3591ff..16f4f6999507f 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc +++ b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc @@ -20,7 +20,7 @@ namespace { constexpr uint32_t ProtobufLengthDelimitedField = 2; bool parseMessageByFieldPath(CodedInputStream* input, - absl::Span field_path, + absl::Span field_path, Protobuf::Message* message) { if (field_path.empty()) { return message->MergeFromCodedStream(input); @@ -52,9 +52,9 @@ bool parseMessageByFieldPath(CodedInputStream* input, } } // namespace -bool HttpBodyUtils::parseMessageByFieldPath( - ZeroCopyInputStream* stream, const std::vector& field_path, - Protobuf::Message* message) { +bool HttpBodyUtils::parseMessageByFieldPath(ZeroCopyInputStream* stream, + const std::vector& field_path, + Protobuf::Message* message) { CodedInputStream input(stream); input.SetRecursionLimit(field_path.size()); @@ -63,7 +63,7 @@ bool HttpBodyUtils::parseMessageByFieldPath( } void HttpBodyUtils::appendHttpBodyEnvelope( - Buffer::Instance& output, const std::vector& request_body_field_path, + Buffer::Instance& output, const std::vector& request_body_field_path, std::string content_type, uint64_t content_length, const UnknownQueryParams& unknown_params) { // Manually encode the protobuf envelope for the body. // See https://developers.google.com/protocol-buffers/docs/encoding#embedded for wire format. @@ -88,7 +88,7 @@ void HttpBodyUtils::appendHttpBodyEnvelope( std::vector message_sizes; message_sizes.reserve(request_body_field_path.size()); for (auto it = request_body_field_path.rbegin(); it != request_body_field_path.rend(); ++it) { - const ProtobufWkt::Field* field = *it; + const Protobuf::Field* field = *it; const uint64_t message_size = envelope_size + content_length; const uint32_t field_number = (field->number() << 3) | ProtobufLengthDelimitedField; const uint64_t field_size = CodedOutputStream::VarintSize32(field_number) + @@ -105,7 +105,7 @@ void HttpBodyUtils::appendHttpBodyEnvelope( // Serialize body field definition manually to avoid the copy of the body. for (size_t i = 0; i < request_body_field_path.size(); ++i) { - const ProtobufWkt::Field* field = request_body_field_path[i]; + const Protobuf::Field* field = request_body_field_path[i]; const uint32_t field_number = (field->number() << 3) | ProtobufLengthDelimitedField; const uint64_t message_size = message_sizes[i]; coded_stream.WriteTag(field_number); diff --git a/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h index 37b550bde77a2..b75c2af785397 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h +++ b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h @@ -15,11 +15,10 @@ namespace GrpcJsonTranscoder { class HttpBodyUtils { public: static bool parseMessageByFieldPath(Protobuf::io::ZeroCopyInputStream* stream, - const std::vector& field_path, + const std::vector& field_path, Protobuf::Message* message); static void appendHttpBodyEnvelope( - Buffer::Instance& output, - const std::vector& request_body_field_path, + Buffer::Instance& output, const std::vector& request_body_field_path, std::string content_type, uint64_t content_length, const envoy::extensions::filters::http::grpc_json_transcoder::v3::UnknownQueryParams& unknown_params); diff --git a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc index cb92a33abb403..6c822a52bad6c 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc +++ b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc @@ -269,9 +269,9 @@ void JsonTranscoderConfig::addBuiltinSymbolDescriptor(const std::string& symbol_ Status JsonTranscoderConfig::resolveField(const Protobuf::Descriptor* descriptor, const std::string& field_path_str, - std::vector* field_path, + std::vector* field_path, bool* is_http_body) { - const ProtobufWkt::Type* message_type = + const Protobuf::Type* message_type = type_helper_->Info()->GetTypeByTypeUrl(Grpc::Common::typeUrl(descriptor->full_name())); if (message_type == nullptr) { return {StatusCode::kNotFound, @@ -287,7 +287,7 @@ Status JsonTranscoderConfig::resolveField(const Protobuf::Descriptor* descriptor if (field_path->empty()) { *is_http_body = descriptor->full_name() == google::api::HttpBody::descriptor()->full_name(); } else { - const ProtobufWkt::Type* body_type = + const Protobuf::Type* body_type = type_helper_->Info()->GetTypeByTypeUrl(field_path->back()->type_url()); *is_http_body = body_type != nullptr && body_type->name() == google::api::HttpBody::descriptor()->full_name(); @@ -594,12 +594,23 @@ Http::FilterDataStatus JsonTranscoderFilter::decodeData(Buffer::Instance& data, if (method_->request_type_is_http_body_) { stats_->transcoder_request_buffer_bytes_.add(data.length()); request_data_.move(data); - if (decoderBufferLimitReached(request_data_.length())) { + if (!method_->descriptor_->client_streaming() && + decoderBufferLimitReached(request_data_.length())) { return Http::FilterDataStatus::StopIterationNoBuffer; } - // TODO(euroelessar): Upper bound message size for streaming case. - if (end_stream || method_->descriptor_->client_streaming()) { + if (method_->descriptor_->client_streaming()) { + // To avoid sending a grpc frame larger than 4MB (which grpc will by default reject), + // split the input buffer into 1MB pieces until the buffer is smaller than 1MB. + Buffer::OwnedImpl remaining_request_data; + remaining_request_data.move(request_data_); + while (remaining_request_data.length() > 0) { + uint64_t piece_size = + std::min(remaining_request_data.length(), JsonTranscoderConfig::MaxStreamedPieceSize); + request_data_.move(remaining_request_data, piece_size); + maybeSendHttpBodyRequestMessage(&data); + } + } else if (end_stream) { maybeSendHttpBodyRequestMessage(&data); } else { // TODO(euroelessar): Avoid buffering if content length is already known. @@ -667,6 +678,11 @@ void JsonTranscoderFilter::setDecoderFilterCallbacks( Http::FilterHeadersStatus JsonTranscoderFilter::encodeHeaders(Http::ResponseHeaderMap& headers, bool end_stream) { + if (error_ || !transcoder_) { + ENVOY_STREAM_LOG(debug, "Response headers is passed through", *encoder_callbacks_); + return Http::FilterHeadersStatus::Continue; + } + if (!Grpc::Common::isGrpcResponseHeaders(headers, end_stream)) { ENVOY_STREAM_LOG( debug, @@ -674,10 +690,6 @@ Http::FilterHeadersStatus JsonTranscoderFilter::encodeHeaders(Http::ResponseHead "without transcoding.", *encoder_callbacks_); error_ = true; - } - - if (error_ || !transcoder_) { - ENVOY_STREAM_LOG(debug, "Response headers is passed through", *encoder_callbacks_); return Http::FilterHeadersStatus::Continue; } @@ -960,7 +972,7 @@ bool JsonTranscoderFilter::buildResponseFromHttpBodyOutput( encoder_callbacks_->resetStream(); return true; } - const auto& body = http_body.data(); + const auto& body = MessageUtil::bytesToString(http_body.data()); data.add(body); diff --git a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h index 6871171d38a21..1eebd85a21ff4 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h +++ b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h @@ -27,8 +27,8 @@ namespace GrpcJsonTranscoder { struct MethodInfo { const Protobuf::MethodDescriptor* descriptor_ = nullptr; - std::vector request_body_field_path; - std::vector response_body_field_path; + std::vector request_body_field_path; + std::vector response_body_field_path; bool request_type_is_http_body_ = false; bool response_type_is_http_body_ = false; }; @@ -50,6 +50,11 @@ class JsonTranscoderConfig : public Logger::Loggable, proto_config, Api::Api& api); + // grpc by default doesn't like a frame larger than 4MB. Splitting streamed data + // into 1MB pieces should keep that threshold from being exceeded when data comes + // in as a large buffer. + static constexpr size_t MaxStreamedPieceSize = 1024 * 1024; + /** * Create an instance of Transcoder interface based on incoming request. * @param headers headers received from decoder. @@ -113,7 +118,7 @@ class JsonTranscoderConfig : public Logger::Loggable, void addFileDescriptor(const Protobuf::FileDescriptorProto& file); absl::Status resolveField(const Protobuf::Descriptor* descriptor, const std::string& field_path_str, - std::vector* field_path, bool* is_http_body); + std::vector* field_path, bool* is_http_body); absl::Status createMethodInfo(const Protobuf::MethodDescriptor* descriptor, const google::api::HttpRule& http_rule, MethodInfoSharedPtr& method_info); diff --git a/source/extensions/filters/http/grpc_stats/response_frame_counter.cc b/source/extensions/filters/http/grpc_stats/response_frame_counter.cc index 1c5dd742e572a..5a7a7b686a40a 100644 --- a/source/extensions/filters/http/grpc_stats/response_frame_counter.cc +++ b/source/extensions/filters/http/grpc_stats/response_frame_counter.cc @@ -53,7 +53,7 @@ void ResponseFrameCounter::frameDataEnd() { ASSERT(connect_eos_buffer_ != nullptr); bool has_unknown_field; - ProtobufWkt::Struct message; + Protobuf::Struct message; auto status = MessageUtil::loadFromJsonNoThrow(connect_eos_buffer_->toString(), message, has_unknown_field); if (!has_unknown_field && !status.ok()) { diff --git a/source/extensions/filters/http/grpc_web/grpc_web_filter.cc b/source/extensions/filters/http/grpc_web/grpc_web_filter.cc index 1d4dfca44184a..754e84a4d5d7f 100644 --- a/source/extensions/filters/http/grpc_web/grpc_web_filter.cc +++ b/source/extensions/filters/http/grpc_web/grpc_web_filter.cc @@ -251,6 +251,10 @@ Http::FilterHeadersStatus GrpcWebFilter::encodeHeaders(Http::ResponseHeaderMap& needs_transformation_for_non_proto_encoded_response_ = needsTransformationForNonProtoEncodedResponse(headers, end_stream); + // If upstream sets a content length, we must remove it because we're going to change the + // length of the body + headers.removeContentLength(); + if (is_text_response_) { headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.GrpcWebTextProto); } else { diff --git a/source/extensions/filters/http/header_mutation/BUILD b/source/extensions/filters/http/header_mutation/BUILD index 48f12a0525023..0d999deb96ae1 100644 --- a/source/extensions/filters/http/header_mutation/BUILD +++ b/source/extensions/filters/http/header_mutation/BUILD @@ -28,6 +28,9 @@ envoy_cc_extension( name = "config", srcs = ["config.cc"], hdrs = ["config.h"], + extra_visibility = [ + "//test/integration:__subpackages__", + ], deps = [ ":header_mutation_lib", "//envoy/registry", diff --git a/source/extensions/filters/http/header_mutation/config.cc b/source/extensions/filters/http/header_mutation/config.cc index 096b3870c45a5..7788d7e5c89e1 100644 --- a/source/extensions/filters/http/header_mutation/config.cc +++ b/source/extensions/filters/http/header_mutation/config.cc @@ -12,9 +12,9 @@ namespace HeaderMutation { absl::StatusOr HeaderMutationFactoryConfig::createFilterFactoryFromProtoTyped( const ProtoConfig& config, const std::string&, DualInfo, - Server::Configuration::ServerFactoryContext&) { + Server::Configuration::ServerFactoryContext& context) { absl::Status creation_status = absl::OkStatus(); - auto filter_config = std::make_shared(config, creation_status); + auto filter_config = std::make_shared(config, context, creation_status); RETURN_IF_NOT_OK_REF(creation_status); return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { @@ -24,10 +24,11 @@ HeaderMutationFactoryConfig::createFilterFactoryFromProtoTyped( absl::StatusOr HeaderMutationFactoryConfig::createRouteSpecificFilterConfigTyped( - const PerRouteProtoConfig& proto_config, Server::Configuration::ServerFactoryContext&, + const PerRouteProtoConfig& proto_config, Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) { absl::Status creation_status = absl::OkStatus(); - auto route_config = std::make_shared(proto_config, creation_status); + auto route_config = + std::make_shared(proto_config, context, creation_status); RETURN_IF_NOT_OK_REF(creation_status); return route_config; } diff --git a/source/extensions/filters/http/header_mutation/header_mutation.cc b/source/extensions/filters/http/header_mutation/header_mutation.cc index b4a76c97fc623..5b582c7c1611f 100644 --- a/source/extensions/filters/http/header_mutation/header_mutation.cc +++ b/source/extensions/filters/http/header_mutation/header_mutation.cc @@ -41,22 +41,24 @@ void QueryParameterMutationAppend::mutateQueryParameter( } } -Mutations::Mutations(const MutationsProto& config, absl::Status& creation_status) { - auto request_mutations_or_error = HeaderMutations::create(config.request_mutations()); +Mutations::Mutations(const MutationsProto& config, + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status) { + auto request_mutations_or_error = HeaderMutations::create(config.request_mutations(), context); SET_AND_RETURN_IF_NOT_OK(request_mutations_or_error.status(), creation_status); request_mutations_ = std::move(request_mutations_or_error.value()); - auto response_mutations_or_error = HeaderMutations::create(config.response_mutations()); + auto response_mutations_or_error = HeaderMutations::create(config.response_mutations(), context); SET_AND_RETURN_IF_NOT_OK(response_mutations_or_error.status(), creation_status); response_mutations_ = std::move(response_mutations_or_error.value()); auto response_trailers_mutations_or_error = - HeaderMutations::create(config.response_trailers_mutations()); + HeaderMutations::create(config.response_trailers_mutations(), context); SET_AND_RETURN_IF_NOT_OK(response_trailers_mutations_or_error.status(), creation_status); response_trailers_mutations_ = std::move(response_trailers_mutations_or_error.value()); auto request_trailers_mutations_or_error = - HeaderMutations::create(config.request_trailers_mutations()); + HeaderMutations::create(config.request_trailers_mutations(), context); SET_AND_RETURN_IF_NOT_OK(request_trailers_mutations_or_error.status(), creation_status); request_trailers_mutations_ = std::move(request_trailers_mutations_or_error.value()); @@ -133,11 +135,14 @@ void Mutations::mutateRequestTrailers(Http::RequestTrailerMap& trailers, } PerRouteHeaderMutation::PerRouteHeaderMutation(const PerRouteProtoConfig& config, + Server::Configuration::ServerFactoryContext& context, absl::Status& creation_status) - : mutations_(config.mutations(), creation_status) {} + : mutations_(config.mutations(), context, creation_status) {} -HeaderMutationConfig::HeaderMutationConfig(const ProtoConfig& config, absl::Status& creation_status) - : mutations_(config.mutations(), creation_status), +HeaderMutationConfig::HeaderMutationConfig(const ProtoConfig& config, + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status) + : mutations_(config.mutations(), context, creation_status), most_specific_header_mutations_wins_(config.most_specific_header_mutations_wins()) {} void HeaderMutation::maybeInitializeRouteConfigs(Http::StreamFilterCallbacks* callbacks) { @@ -167,7 +172,8 @@ void HeaderMutation::maybeInitializeRouteConfigs(Http::StreamFilterCallbacks* ca } Http::FilterHeadersStatus HeaderMutation::decodeHeaders(Http::RequestHeaderMap& headers, bool) { - Formatter::HttpFormatterContext context{&headers}; + const Formatter::HttpFormatterContext context{&headers, {}, {}, + {}, {}, &decoder_callbacks_->activeSpan()}; config_->mutations().mutateRequestHeaders(headers, context, decoder_callbacks_->streamInfo()); maybeInitializeRouteConfigs(decoder_callbacks_); @@ -181,7 +187,9 @@ Http::FilterHeadersStatus HeaderMutation::decodeHeaders(Http::RequestHeaderMap& } Http::FilterHeadersStatus HeaderMutation::encodeHeaders(Http::ResponseHeaderMap& headers, bool) { - Formatter::HttpFormatterContext context{encoder_callbacks_->requestHeaders().ptr(), &headers}; + Formatter::HttpFormatterContext context{ + encoder_callbacks_->requestHeaders().ptr(), &headers, {}, {}, {}, + &encoder_callbacks_->activeSpan()}; config_->mutations().mutateResponseHeaders(headers, context, encoder_callbacks_->streamInfo()); // Note if the filter before this one has send local reply then the decodeHeaders() will not be @@ -199,7 +207,11 @@ Http::FilterHeadersStatus HeaderMutation::encodeHeaders(Http::ResponseHeaderMap& Http::FilterTrailersStatus HeaderMutation::encodeTrailers(Http::ResponseTrailerMap& trailers) { Formatter::HttpFormatterContext context{encoder_callbacks_->requestHeaders().ptr(), - encoder_callbacks_->responseHeaders().ptr(), &trailers}; + encoder_callbacks_->responseHeaders().ptr(), + &trailers, + {}, + {}, + &encoder_callbacks_->activeSpan()}; config_->mutations().mutateResponseTrailers(trailers, context, encoder_callbacks_->streamInfo()); maybeInitializeRouteConfigs(encoder_callbacks_); @@ -214,14 +226,16 @@ Http::FilterTrailersStatus HeaderMutation::encodeTrailers(Http::ResponseTrailerM Http::FilterTrailersStatus HeaderMutation::decodeTrailers(Http::RequestTrailerMap& trailers) { // TODO(davinci26): if `HttpFormatterContext` supports request trailers we can also pass the // trailers to the context so we can support substitutions from other trailers. - Formatter::HttpFormatterContext context{encoder_callbacks_->requestHeaders().ptr()}; - config_->mutations().mutateRequestTrailers(trailers, context, encoder_callbacks_->streamInfo()); + Formatter::HttpFormatterContext context{ + decoder_callbacks_->requestHeaders().ptr(), {}, {}, {}, {}, + &decoder_callbacks_->activeSpan()}; + config_->mutations().mutateRequestTrailers(trailers, context, decoder_callbacks_->streamInfo()); - maybeInitializeRouteConfigs(encoder_callbacks_); + maybeInitializeRouteConfigs(decoder_callbacks_); for (const PerRouteHeaderMutation& route_config : route_configs_) { route_config.mutations().mutateRequestTrailers(trailers, context, - encoder_callbacks_->streamInfo()); + decoder_callbacks_->streamInfo()); } return Http::FilterTrailersStatus::Continue; } diff --git a/source/extensions/filters/http/header_mutation/header_mutation.h b/source/extensions/filters/http/header_mutation/header_mutation.h index 4a6c44a06b667..4ab718d194bb7 100644 --- a/source/extensions/filters/http/header_mutation/header_mutation.h +++ b/source/extensions/filters/http/header_mutation/header_mutation.h @@ -83,7 +83,8 @@ class Mutations { public: using HeaderMutations = Http::HeaderMutations; - Mutations(const MutationsProto& config, absl::Status& creation_status); + Mutations(const MutationsProto& config, Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status); void mutateRequestHeaders(Http::RequestHeaderMap& headers, const Formatter::HttpFormatterContext& context, @@ -109,7 +110,9 @@ class Mutations { class PerRouteHeaderMutation : public Router::RouteSpecificFilterConfig { public: - PerRouteHeaderMutation(const PerRouteProtoConfig& config, absl::Status& creation_status); + PerRouteHeaderMutation(const PerRouteProtoConfig& config, + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status); const Mutations& mutations() const { return mutations_; } @@ -120,7 +123,9 @@ using PerRouteHeaderMutationSharedPtr = std::shared_ptr; class HeaderMutationConfig { public: - HeaderMutationConfig(const ProtoConfig& config, absl::Status& creation_status); + HeaderMutationConfig(const ProtoConfig& config, + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status); const Mutations& mutations() const { return mutations_; } @@ -151,10 +156,10 @@ class HeaderMutation : public Http::PassThroughFilter, public Logger::Loggable, 4> route_configs_{}; + absl::InlinedVector, 4> route_configs_; }; } // namespace HeaderMutation diff --git a/source/extensions/filters/http/header_to_metadata/config.cc b/source/extensions/filters/http/header_to_metadata/config.cc index 3cf74a3115bea..7c49a73e2a8d4 100644 --- a/source/extensions/filters/http/header_to_metadata/config.cc +++ b/source/extensions/filters/http/header_to_metadata/config.cc @@ -17,8 +17,8 @@ namespace HeaderToMetadataFilter { absl::StatusOr HeaderToMetadataConfig::createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::header_to_metadata::v3::Config& proto_config, const std::string&, Server::Configuration::FactoryContext& context) { - absl::StatusOr filter_config_or = - Config::create(proto_config, context.serverFactoryContext().regexEngine(), false); + absl::StatusOr filter_config_or = Config::create( + proto_config, context.serverFactoryContext().regexEngine(), context.scope(), false); RETURN_IF_ERROR(filter_config_or.status()); return [filter_config = std::move(filter_config_or.value())]( @@ -32,7 +32,8 @@ absl::StatusOr HeaderToMetadataConfig::createRouteSpecificFilterConfigTyped( const envoy::extensions::filters::http::header_to_metadata::v3::Config& config, Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) { - absl::StatusOr config_or = Config::create(config, context.regexEngine(), true); + absl::StatusOr config_or = + Config::create(config, context.regexEngine(), context.scope(), true); RETURN_IF_ERROR(config_or.status()); return std::move(config_or.value()); } diff --git a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc index 405aa54a9fe32..784448dca965b 100644 --- a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc +++ b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc @@ -111,15 +111,16 @@ Rule::Rule(const ProtoRule& rule, Regex::Engine& regex_engine, absl::Status& cre absl::StatusOr Config::create(const envoy::extensions::filters::http::header_to_metadata::v3::Config& config, - Regex::Engine& regex_engine, bool per_route) { + Regex::Engine& regex_engine, Stats::Scope& scope, bool per_route) { absl::Status creation_status = absl::OkStatus(); - auto cfg = ConfigSharedPtr(new Config(config, regex_engine, per_route, creation_status)); + auto cfg = ConfigSharedPtr(new Config(config, regex_engine, scope, per_route, creation_status)); RETURN_IF_NOT_OK_REF(creation_status); return cfg; } Config::Config(const envoy::extensions::filters::http::header_to_metadata::v3::Config config, - Regex::Engine& regex_engine, const bool per_route, absl::Status& creation_status) { + Regex::Engine& regex_engine, Stats::Scope& scope, const bool per_route, + absl::Status& creation_status) { absl::StatusOr request_set_or = Config::configToVector(config.request_rules(), request_rules_, regex_engine); SET_AND_RETURN_IF_NOT_OK(request_set_or.status(), creation_status); @@ -130,6 +131,11 @@ Config::Config(const envoy::extensions::filters::http::header_to_metadata::v3::C SET_AND_RETURN_IF_NOT_OK(response_set_or.status(), creation_status); response_set_ = response_set_or.value(); + // Generate stats only if stat_prefix is configured (opt-in behavior). + if (!config.stat_prefix().empty()) { + stats_.emplace(generateStats(config.stat_prefix(), scope)); + } + // Note: empty configs are fine for the global config, which would be the case for enabling // the filter globally without rules and then applying them at the virtual host or // route level. At the virtual or route level, it makes no sense to have an empty @@ -158,6 +164,51 @@ absl::StatusOr Config::configToVector(const ProtobufRepeatedRule& proto_ru return true; } +HeaderToMetadataFilterStats Config::generateStats(const std::string& stat_prefix, + Stats::Scope& scope) { + const std::string final_prefix = fmt::format("http_filter_name.{}", stat_prefix); + return {ALL_HEADER_TO_METADATA_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; +} + +void Config::chargeStat(StatsEvent event, HeaderDirection direction) const { + if (!stats_.has_value()) { + return; + } + + switch (event) { + case StatsEvent::RulesProcessed: + if (direction == HeaderDirection::Request) { + stats_->request_rules_processed_.inc(); + } else { + stats_->response_rules_processed_.inc(); + } + break; + case StatsEvent::MetadataAdded: + if (direction == HeaderDirection::Request) { + stats_->request_metadata_added_.inc(); + } else { + stats_->response_metadata_added_.inc(); + } + break; + case StatsEvent::HeaderNotFound: + if (direction == HeaderDirection::Request) { + stats_->request_header_not_found_.inc(); + } else { + stats_->response_header_not_found_.inc(); + } + break; + case StatsEvent::Base64DecodeFailed: + stats_->base64_decode_failed_.inc(); + break; + case StatsEvent::HeaderValueTooLong: + stats_->header_value_too_long_.inc(); + break; + case StatsEvent::RegexSubstitutionFailed: + stats_->regex_substitution_failed_.inc(); + break; + } +} + HeaderToMetadataFilter::HeaderToMetadataFilter(const ConfigSharedPtr config) : config_(config) {} HeaderToMetadataFilter::~HeaderToMetadataFilter() = default; @@ -166,7 +217,8 @@ Http::FilterHeadersStatus HeaderToMetadataFilter::decodeHeaders(Http::RequestHea bool) { const auto* config = getConfig(); if (config->doRequest()) { - writeHeaderToMetadata(headers, config->requestRules(), *decoder_callbacks_); + writeHeaderToMetadata(headers, config->requestRules(), *decoder_callbacks_, + HeaderDirection::Request); } return Http::FilterHeadersStatus::Continue; @@ -181,7 +233,8 @@ Http::FilterHeadersStatus HeaderToMetadataFilter::encodeHeaders(Http::ResponseHe bool) { const auto* config = getConfig(); if (config->doResponse()) { - writeHeaderToMetadata(headers, config->responseRules(), *encoder_callbacks_); + writeHeaderToMetadata(headers, config->responseRules(), *encoder_callbacks_, + HeaderDirection::Response); } return Http::FilterHeadersStatus::Continue; } @@ -193,14 +246,16 @@ void HeaderToMetadataFilter::setEncoderFilterCallbacks( bool HeaderToMetadataFilter::addMetadata(StructMap& struct_map, const std::string& meta_namespace, const std::string& key, std::string value, ValueType type, - ValueEncode encode) const { - ProtobufWkt::Value val; + ValueEncode encode, HeaderDirection direction) const { + Protobuf::Value val; + const auto* config = getConfig(); ASSERT(!value.empty()); if (value.size() >= MAX_HEADER_VALUE_LEN) { // Too long, go away. ENVOY_LOG(debug, "metadata value is too long"); + config->chargeStat(StatsEvent::HeaderValueTooLong, direction); return false; } @@ -208,6 +263,7 @@ bool HeaderToMetadataFilter::addMetadata(StructMap& struct_map, const std::strin value = Base64::decodeWithoutPadding(value); if (value.empty()) { ENVOY_LOG(debug, "Base64 decode failed"); + config->chargeStat(StatsEvent::Base64DecodeFailed, direction); return false; } } @@ -240,6 +296,9 @@ bool HeaderToMetadataFilter::addMetadata(StructMap& struct_map, const std::strin auto& keyval = struct_map[meta_namespace]; (*keyval.mutable_fields())[key] = std::move(val); + // Increment metadata_added stat if stats are enabled. + config->chargeStat(StatsEvent::MetadataAdded, direction); + return true; } @@ -249,18 +308,27 @@ const std::string& HeaderToMetadataFilter::decideNamespace(const std::string& ns // add metadata['key']= value depending on header present or missing case void HeaderToMetadataFilter::applyKeyValue(std::string&& value, const Rule& rule, - const KeyValuePair& keyval, StructMap& np) { + const KeyValuePair& keyval, StructMap& np, + HeaderDirection direction) { + const auto* config = getConfig(); + if (!keyval.value().empty()) { value = keyval.value(); } else { const auto& matcher = rule.regexRewrite(); if (matcher != nullptr) { + const bool was_non_empty = !value.empty(); value = matcher->replaceAll(value, rule.regexSubstitution()); + // If we had a non-empty input but got an empty result from regex, it could indicate a + // failure. + if (was_non_empty && value.empty()) { + config->chargeStat(StatsEvent::RegexSubstitutionFailed, direction); + } } } if (!value.empty()) { const auto& nspace = decideNamespace(keyval.metadata_namespace()); - addMetadata(np, nspace, keyval.key(), value, keyval.type(), keyval.encode()); + addMetadata(np, nspace, keyval.key(), value, keyval.type(), keyval.encode(), direction); } else { ENVOY_LOG(debug, "value is empty, not adding metadata"); } @@ -268,19 +336,26 @@ void HeaderToMetadataFilter::applyKeyValue(std::string&& value, const Rule& rule void HeaderToMetadataFilter::writeHeaderToMetadata(Http::HeaderMap& headers, const HeaderToMetadataRules& rules, - Http::StreamFilterCallbacks& callbacks) { + Http::StreamFilterCallbacks& callbacks, + HeaderDirection direction) { StructMap structs_by_namespace; + const auto* config = getConfig(); for (const auto& rule : rules) { const auto& proto_rule = rule.rule(); absl::optional value = rule.selector_->extract(headers); + // Increment rules_processed stat if stats are enabled. + config->chargeStat(StatsEvent::RulesProcessed, direction); + if (value && proto_rule.has_on_header_present()) { applyKeyValue(std::move(value).value_or(""), rule, proto_rule.on_header_present(), - structs_by_namespace); + structs_by_namespace, direction); } else if (!value && proto_rule.has_on_header_missing()) { + // Increment header_not_found stat if stats are enabled. + config->chargeStat(StatsEvent::HeaderNotFound, direction); applyKeyValue(std::move(value).value_or(""), rule, proto_rule.on_header_missing(), - structs_by_namespace); + structs_by_namespace, direction); } } // Any matching rules? diff --git a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h index ef5db118f1e99..3c62dabdfac0c 100644 --- a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h +++ b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h @@ -17,11 +17,49 @@ namespace Extensions { namespace HttpFilters { namespace HeaderToMetadataFilter { +/** + * All stats for the Header-To-Metadata filter. @see stats_macros.h + */ +#define ALL_HEADER_TO_METADATA_FILTER_STATS(COUNTER) \ + COUNTER(request_rules_processed) \ + COUNTER(response_rules_processed) \ + COUNTER(request_metadata_added) \ + COUNTER(response_metadata_added) \ + COUNTER(request_header_not_found) \ + COUNTER(response_header_not_found) \ + COUNTER(base64_decode_failed) \ + COUNTER(header_value_too_long) \ + COUNTER(regex_substitution_failed) + +/** + * Wrapper struct for header-to-metadata filter stats. @see stats_macros.h + */ +struct HeaderToMetadataFilterStats { + ALL_HEADER_TO_METADATA_FILTER_STATS(GENERATE_COUNTER_STRUCT) +}; + using ProtoRule = envoy::extensions::filters::http::header_to_metadata::v3::Config::Rule; using ValueType = envoy::extensions::filters::http::header_to_metadata::v3::Config::ValueType; using ValueEncode = envoy::extensions::filters::http::header_to_metadata::v3::Config::ValueEncode; using KeyValuePair = envoy::extensions::filters::http::header_to_metadata::v3::Config::KeyValuePair; +/** + * Enum to distinguish between request and response processing for stats collection. + */ +enum class HeaderDirection { Request, Response }; + +/** + * Enum of all discrete events for which the filter records statistics. + */ +enum class StatsEvent { + RulesProcessed, + MetadataAdded, + HeaderNotFound, + Base64DecodeFailed, + HeaderValueTooLong, + RegexSubstitutionFailed, +}; + // Interface for getting values from a cookie or a header. class ValueSelector { public: @@ -98,18 +136,26 @@ class Config : public ::Envoy::Router::RouteSpecificFilterConfig, public: static absl::StatusOr> create(const envoy::extensions::filters::http::header_to_metadata::v3::Config& config, - Regex::Engine& regex_engine, bool per_route = false); + Regex::Engine& regex_engine, Stats::Scope& scope, bool per_route = false); const HeaderToMetadataRules& requestRules() const { return request_rules_; } const HeaderToMetadataRules& responseRules() const { return response_rules_; } bool doResponse() const { return response_set_; } bool doRequest() const { return request_set_; } + const absl::optional& stats() const { return stats_; } + + /** + * Increment the appropriate statistic for the given event and traffic direction. + * No-op if statistics were not configured. + */ + void chargeStat(StatsEvent event, HeaderDirection direction) const; private: using ProtobufRepeatedRule = Protobuf::RepeatedPtrField; Config(const envoy::extensions::filters::http::header_to_metadata::v3::Config config, - Regex::Engine& regex_engine, bool per_route, absl::Status& creation_status); + Regex::Engine& regex_engine, Stats::Scope& scope, bool per_route, + absl::Status& creation_status); /** * configToVector is a helper function for converting from configuration (protobuf types) into @@ -125,12 +171,23 @@ class Config : public ::Envoy::Router::RouteSpecificFilterConfig, static absl::StatusOr configToVector(const ProtobufRepeatedRule&, HeaderToMetadataRules&, Regex::Engine&); + /** + * Generate stats for the header-to-metadata filter. + * @param stat_prefix the prefix to use for stats. + * @param scope the stats scope. + * @return HeaderToMetadataFilterStats the generated stats. + */ + static HeaderToMetadataFilterStats generateStats(const std::string& stat_prefix, + Stats::Scope& scope); + const std::string& decideNamespace(const std::string& nspace) const; HeaderToMetadataRules request_rules_; HeaderToMetadataRules response_rules_; bool response_set_; bool request_set_; + // Mutable to allow stats charging from const contexts. + mutable absl::optional stats_; }; using ConfigSharedPtr = std::shared_ptr; @@ -177,7 +234,7 @@ class HeaderToMetadataFilter : public Http::StreamFilter, private: friend class HeaderToMetadataTest; - using StructMap = std::map; + using StructMap = std::map; const ConfigSharedPtr config_; mutable const Config* effective_config_{nullptr}; @@ -193,12 +250,14 @@ class HeaderToMetadataFilter : public Http::StreamFilter, * @param rules the header-to-metadata mapping set in configuration. * @param callbacks the callback used to fetch the StreamInfo (which is then used to get * metadata). Callable with both encoder_callbacks_ and decoder_callbacks_. + * @param direction whether processing request or response headers for stats collection. */ void writeHeaderToMetadata(Http::HeaderMap& headers, const HeaderToMetadataRules& rules, - Http::StreamFilterCallbacks& callbacks); + Http::StreamFilterCallbacks& callbacks, HeaderDirection direction); bool addMetadata(StructMap&, const std::string&, const std::string&, std::string, ValueType, - ValueEncode) const; - void applyKeyValue(std::string&&, const Rule&, const KeyValuePair&, StructMap&); + ValueEncode, HeaderDirection direction) const; + void applyKeyValue(std::string&&, const Rule&, const KeyValuePair&, StructMap&, + HeaderDirection direction); const std::string& decideNamespace(const std::string& nspace) const; const Config* getConfig() const; }; diff --git a/source/extensions/filters/http/health_check/config.cc b/source/extensions/filters/http/health_check/config.cc index dcf464deb74b8..f2448ac592060 100644 --- a/source/extensions/filters/http/health_check/config.cc +++ b/source/extensions/filters/http/health_check/config.cc @@ -44,6 +44,10 @@ Http::FilterFactoryCb HealthCheckFilterConfig::createFilterFactoryFromProtoTyped if (!pass_through_mode && !proto_config.cluster_min_healthy_percentages().empty()) { auto cluster_to_percentage = std::make_unique(); for (const auto& item : proto_config.cluster_min_healthy_percentages()) { + if (std::isnan(item.second.value())) { + throw EnvoyException(absl::StrCat( + "cluster_min_healthy_percentages contains a NaN value for cluster: ", item.first)); + } cluster_to_percentage->emplace(std::make_pair(item.first, item.second.value())); } cluster_min_healthy_percentages = std::move(cluster_to_percentage); diff --git a/source/extensions/filters/http/json_to_metadata/filter.cc b/source/extensions/filters/http/json_to_metadata/filter.cc index ede9136ee630c..3a61fa1811f61 100644 --- a/source/extensions/filters/http/json_to_metadata/filter.cc +++ b/source/extensions/filters/http/json_to_metadata/filter.cc @@ -35,27 +35,27 @@ struct JsonValueToDoubleConverter { }; struct JsonValueToProtobufValueConverter { - absl::StatusOr operator()(bool&& val) { - ProtobufWkt::Value protobuf_value; + absl::StatusOr operator()(bool&& val) { + Protobuf::Value protobuf_value; protobuf_value.set_bool_value(val); return protobuf_value; } - absl::StatusOr operator()(int64_t&& val) { - ProtobufWkt::Value protobuf_value; + absl::StatusOr operator()(int64_t&& val) { + Protobuf::Value protobuf_value; protobuf_value.set_number_value(val); return protobuf_value; } - absl::StatusOr operator()(double&& val) { - ProtobufWkt::Value protobuf_value; + absl::StatusOr operator()(double&& val) { + Protobuf::Value protobuf_value; protobuf_value.set_number_value(val); return protobuf_value; } - absl::StatusOr operator()(std::string&& val) { + absl::StatusOr operator()(std::string&& val) { if (val.size() > MAX_PAYLOAD_VALUE_LEN) { return absl::InternalError( fmt::format("metadata value is too long. value.length: {}", val.size())); } - ProtobufWkt::Value protobuf_value; + Protobuf::Value protobuf_value; protobuf_value.set_string_value(std::move(val)); return protobuf_value; } @@ -171,20 +171,20 @@ void Filter::applyKeyValue(const std::string& value, const KeyValuePair& keyval, StructMap& struct_map, Http::StreamFilterCallbacks& filter_callback) { ASSERT(!value.empty()); - ProtobufWkt::Value val; + Protobuf::Value val; val.set_string_value(value); applyKeyValue(std::move(val), keyval, struct_map, filter_callback); } void Filter::applyKeyValue(double value, const KeyValuePair& keyval, StructMap& struct_map, Http::StreamFilterCallbacks& filter_callback) { - ProtobufWkt::Value val; + Protobuf::Value val; val.set_number_value(value); applyKeyValue(std::move(val), keyval, struct_map, filter_callback); } -void Filter::applyKeyValue(ProtobufWkt::Value value, const KeyValuePair& keyval, - StructMap& struct_map, Http::StreamFilterCallbacks& filter_callback) { +void Filter::applyKeyValue(Protobuf::Value value, const KeyValuePair& keyval, StructMap& struct_map, + Http::StreamFilterCallbacks& filter_callback) { const auto& nspace = decideNamespace(keyval.metadata_namespace()); addMetadata(nspace, keyval.key(), std::move(value), keyval.preserve_existing_metadata_value(), struct_map, filter_callback); @@ -195,7 +195,7 @@ const std::string& Filter::decideNamespace(const std::string& nspace) const { } bool Filter::addMetadata(const std::string& meta_namespace, const std::string& key, - ProtobufWkt::Value val, const bool preserve_existing_metadata_value, + Protobuf::Value val, const bool preserve_existing_metadata_value, StructMap& struct_map, Http::StreamFilterCallbacks& filter_callback) { if (preserve_existing_metadata_value) { diff --git a/source/extensions/filters/http/json_to_metadata/filter.h b/source/extensions/filters/http/json_to_metadata/filter.h index 690276a70f648..60491c093f398 100644 --- a/source/extensions/filters/http/json_to_metadata/filter.h +++ b/source/extensions/filters/http/json_to_metadata/filter.h @@ -106,7 +106,7 @@ class Filter : public Http::PassThroughFilter, Logger::Loggable; + using StructMap = absl::flat_hash_map; // Handle on_missing case of the `rule` and store in `struct_map`. void handleOnMissing(const Rule& rule, StructMap& struct_map, Http::StreamFilterCallbacks& filter_callback); @@ -132,14 +132,14 @@ class Filter : public Http::PassThroughFilter, Logger::Loggable, void handleGoodJwt(bool cache_hit); // Normalize and set the payload metadata. - void setPayloadMetadata(const ProtobufWkt::Struct& jwt_payload); + void setPayloadMetadata(const Protobuf::Struct& jwt_payload); // Calls the callback with status. void doneWithStatus(const Status& status); @@ -333,23 +333,23 @@ void AuthenticatorImpl::verifyKey() { bool AuthenticatorImpl::addJWTClaimToHeader(const std::string& claim_name, const std::string& header_name) { StructUtils payload_getter(jwt_->payload_pb_); - const ProtobufWkt::Value* claim_value; + const Protobuf::Value* claim_value; const auto status = payload_getter.GetValue(claim_name, claim_value); std::string str_claim_value; if (status == StructUtils::OK) { switch (claim_value->kind_case()) { - case Envoy::ProtobufWkt::Value::kStringValue: + case Envoy::Protobuf::Value::kStringValue: str_claim_value = claim_value->string_value(); break; - case Envoy::ProtobufWkt::Value::kNumberValue: + case Envoy::Protobuf::Value::kNumberValue: str_claim_value = convertClaimDoubleToString(claim_value->number_value()); break; - case Envoy::ProtobufWkt::Value::kBoolValue: + case Envoy::Protobuf::Value::kBoolValue: str_claim_value = claim_value->bool_value() ? "true" : "false"; break; - case Envoy::ProtobufWkt::Value::kStructValue: + case Envoy::Protobuf::Value::kStructValue: ABSL_FALLTHROUGH_INTENDED; - case Envoy::ProtobufWkt::Value::kListValue: { + case Envoy::Protobuf::Value::kListValue: { std::string output; auto status = claim_value->has_struct_value() ? ProtobufUtil::MessageToJsonString(claim_value->struct_value(), &output) @@ -422,14 +422,14 @@ void AuthenticatorImpl::handleGoodJwt(bool cache_hit) { doneWithStatus(Status::Ok); } -void AuthenticatorImpl::setPayloadMetadata(const ProtobufWkt::Struct& jwt_payload) { +void AuthenticatorImpl::setPayloadMetadata(const Protobuf::Struct& jwt_payload) { const auto& provider = jwks_data_->getJwtProvider(); const auto& normalize = provider.normalize_payload_in_metadata(); if (normalize.space_delimited_claims().empty()) { set_extracted_jwt_data_cb_(provider.payload_in_metadata(), jwt_payload); } // Make a temporary copy to normalize the JWT struct. - ProtobufWkt::Struct out_payload = jwt_payload; + Protobuf::Struct out_payload = jwt_payload; for (const auto& claim : normalize.space_delimited_claims()) { const auto& it = jwt_payload.fields().find(claim); if (it != jwt_payload.fields().end() && it->second.has_string_value()) { @@ -462,7 +462,7 @@ void AuthenticatorImpl::doneWithStatus(const Status& status) { if (!failed_status_in_metadata.empty()) { - ProtobufWkt::Struct failed_status; + Protobuf::Struct failed_status; auto& failed_status_fields = *failed_status.mutable_fields(); failed_status_fields["code"].set_number_value(enumToInt(status)); failed_status_fields["message"].set_string_value(google::jwt_verify::getStatusString(status)); diff --git a/source/extensions/filters/http/jwt_authn/authenticator.h b/source/extensions/filters/http/jwt_authn/authenticator.h index d54157a472f40..45b89d770987f 100644 --- a/source/extensions/filters/http/jwt_authn/authenticator.h +++ b/source/extensions/filters/http/jwt_authn/authenticator.h @@ -20,7 +20,7 @@ using AuthenticatorPtr = std::unique_ptr; using AuthenticatorCallback = std::function; using SetExtractedJwtDataCallback = - std::function; + std::function; using ClearRouteCacheCallback = std::function; diff --git a/source/extensions/filters/http/jwt_authn/extractor.cc b/source/extensions/filters/http/jwt_authn/extractor.cc index b38e20f063908..9dd7c31ccbbb8 100644 --- a/source/extensions/filters/http/jwt_authn/extractor.cc +++ b/source/extensions/filters/http/jwt_authn/extractor.cc @@ -108,17 +108,14 @@ class JwtParamLocation : public JwtLocationBase { : JwtLocationBase(token, issuer_checker), param_(param) {} void removeJwt(Http::RequestHeaderMap& headers) const override { - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.jwt_authn_remove_jwt_from_query_params")) { - absl::string_view path = headers.getPathValue(); - Http::Utility::QueryParamsMulti query_params = - Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(path); + absl::string_view path = headers.getPathValue(); + Http::Utility::QueryParamsMulti query_params = + Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(path); - query_params.remove(param_); + query_params.remove(param_); - const auto updated_path = query_params.replaceQueryString(headers.Path()->value()); - headers.setPath(updated_path); - } + const auto updated_path = query_params.replaceQueryString(headers.Path()->value()); + headers.setPath(updated_path); } private: diff --git a/source/extensions/filters/http/jwt_authn/filter.cc b/source/extensions/filters/http/jwt_authn/filter.cc index 10b92ace9403d..574076e7e93f2 100644 --- a/source/extensions/filters/http/jwt_authn/filter.cc +++ b/source/extensions/filters/http/jwt_authn/filter.cc @@ -102,7 +102,7 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, return Http::FilterHeadersStatus::StopIteration; } -void Filter::setExtractedData(const ProtobufWkt::Struct& extracted_data) { +void Filter::setExtractedData(const Protobuf::Struct& extracted_data) { decoder_callbacks_->streamInfo().setDynamicMetadata("envoy.filters.http.jwt_authn", extracted_data); } diff --git a/source/extensions/filters/http/jwt_authn/filter.h b/source/extensions/filters/http/jwt_authn/filter.h index 461f4bc9deb74..ef2a6ac574ed1 100644 --- a/source/extensions/filters/http/jwt_authn/filter.h +++ b/source/extensions/filters/http/jwt_authn/filter.h @@ -31,8 +31,8 @@ class Filter : public Http::StreamDecoderFilter, private: // Following two functions are for Verifier::Callbacks interface. - // Pass the extracted data from a verified JWT as an opaque ProtobufWkt::Struct. - void setExtractedData(const ProtobufWkt::Struct& extracted_data) override; + // Pass the extracted data from a verified JWT as an opaque Protobuf::Struct. + void setExtractedData(const Protobuf::Struct& extracted_data) override; void clearRouteCache() override; // It will be called when its verify() call is completed. void onComplete(const ::google::jwt_verify::Status& status) override; diff --git a/source/extensions/filters/http/jwt_authn/filter_config.cc b/source/extensions/filters/http/jwt_authn/filter_config.cc index 2a1a733b4caf8..84f873cd31e55 100644 --- a/source/extensions/filters/http/jwt_authn/filter_config.cc +++ b/source/extensions/filters/http/jwt_authn/filter_config.cc @@ -28,16 +28,14 @@ FilterConfigImpl::FilterConfigImpl( // Validate provider URIs. // Note that the PGV well-known regex for URI is not implemented in C++, otherwise we could add a // PGV rule instead of doing this check manually. - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.jwt_authn_validate_uri")) { - for (const auto& provider_pair : proto_config_.providers()) { - const auto provider_value = std::get<1>(provider_pair); - if (provider_value.has_remote_jwks()) { - absl::string_view provider_uri = provider_value.remote_jwks().http_uri().uri(); - Http::Utility::Url url; - if (!url.initialize(provider_uri, /*is_connect=*/false)) { - throw EnvoyException(fmt::format("Provider '{}' has an invalid URI: '{}'", - std::get<0>(provider_pair), provider_uri)); - } + for (const auto& provider_pair : proto_config_.providers()) { + const auto provider_value = std::get<1>(provider_pair); + if (provider_value.has_remote_jwks()) { + absl::string_view provider_uri = provider_value.remote_jwks().http_uri().uri(); + Http::Utility::Url url; + if (!url.initialize(provider_uri, /*is_connect=*/false)) { + throw EnvoyException(fmt::format("Provider '{}' has an invalid URI: '{}'", + std::get<0>(provider_pair), provider_uri)); } } } diff --git a/source/extensions/filters/http/jwt_authn/jwks_cache.cc b/source/extensions/filters/http/jwt_authn/jwks_cache.cc index 19d5b3447c9f7..0a9cf92757aa0 100644 --- a/source/extensions/filters/http/jwt_authn/jwks_cache.cc +++ b/source/extensions/filters/http/jwt_authn/jwks_cache.cc @@ -46,7 +46,7 @@ class JwksDataImpl : public JwksCache::JwksData, public Logger::Loggable completion_states_; std::vector auths_; - ProtobufWkt::Struct extracted_data_; + Protobuf::Struct extracted_data_; }; // base verifier for provider_name, provider_and_audiences, and allow_missing_or_failed. @@ -120,7 +120,7 @@ class ProviderVerifierImpl : public BaseVerifierImpl { extractor_->sanitizeHeaders(ctximpl.headers()); auth->verify( ctximpl.headers(), ctximpl.parentSpan(), extractor_->extract(ctximpl.headers()), - [&ctximpl](const std::string& name, const ProtobufWkt::Struct& extracted_data) { + [&ctximpl](const std::string& name, const Protobuf::Struct& extracted_data) { ctximpl.addExtractedData(name, extracted_data); }, [this, &ctximpl](const Status& status) { onComplete(status, ctximpl); }, @@ -170,7 +170,7 @@ class AllowFailedVerifierImpl : public BaseVerifierImpl { extractor_->sanitizeHeaders(ctximpl.headers()); auth->verify( ctximpl.headers(), ctximpl.parentSpan(), extractor_->extract(ctximpl.headers()), - [&ctximpl](const std::string& name, const ProtobufWkt::Struct& extracted_data) { + [&ctximpl](const std::string& name, const Protobuf::Struct& extracted_data) { ctximpl.addExtractedData(name, extracted_data); }, [this, &ctximpl](const Status& status) { onComplete(status, ctximpl); }, @@ -203,7 +203,7 @@ class AllowMissingVerifierImpl : public BaseVerifierImpl { extractor_->sanitizeHeaders(ctximpl.headers()); auth->verify( ctximpl.headers(), ctximpl.parentSpan(), extractor_->extract(ctximpl.headers()), - [&ctximpl](const std::string& name, const ProtobufWkt::Struct& extracted_data) { + [&ctximpl](const std::string& name, const Protobuf::Struct& extracted_data) { ctximpl.addExtractedData(name, extracted_data); }, [this, &ctximpl](const Status& status) { onComplete(status, ctximpl); }, diff --git a/source/extensions/filters/http/jwt_authn/verifier.h b/source/extensions/filters/http/jwt_authn/verifier.h index 26a6964dac8e8..b235d3a9319de 100644 --- a/source/extensions/filters/http/jwt_authn/verifier.h +++ b/source/extensions/filters/http/jwt_authn/verifier.h @@ -32,7 +32,7 @@ class Verifier { * This function is called before onComplete() function. * It will not be called if no payload to write. */ - virtual void setExtractedData(const ProtobufWkt::Struct& payload) PURE; + virtual void setExtractedData(const Protobuf::Struct& payload) PURE; /** * JWT payloads added to headers may require clearing the cached route. diff --git a/source/extensions/filters/http/lua/lua_filter.cc b/source/extensions/filters/http/lua/lua_filter.cc index e6de884b1cedc..64e2a21733ebd 100644 --- a/source/extensions/filters/http/lua/lua_filter.cc +++ b/source/extensions/filters/http/lua/lua_filter.cc @@ -105,9 +105,9 @@ void parseOptionsFromTable(lua_State* state, int index, } } -const ProtobufWkt::Struct& getMetadata(Http::StreamFilterCallbacks* callbacks) { +const Protobuf::Struct& getMetadata(Http::StreamFilterCallbacks* callbacks) { if (callbacks->route() == nullptr) { - return ProtobufWkt::Struct::default_instance(); + return Protobuf::Struct::default_instance(); } const auto& metadata = callbacks->route()->metadata(); @@ -123,7 +123,7 @@ const ProtobufWkt::Struct& getMetadata(Http::StreamFilterCallbacks* callbacks) { } } - return ProtobufWkt::Struct::default_instance(); + return Protobuf::Struct::default_instance(); } // Okay to return non-const reference because this doesn't ever get changed. @@ -206,11 +206,14 @@ PerLuaCodeSetup::PerLuaCodeSetup(const std::string& lua_code, ThreadLocal::SlotA lua_state_.registerType(); lua_state_.registerType(); lua_state_.registerType(); + lua_state_.registerType(); lua_state_.registerType(); lua_state_.registerType(); lua_state_.registerType(); lua_state_.registerType(); lua_state_.registerType(); + lua_state_.registerType(); + lua_state_.registerType(); const Filters::Common::Lua::InitializerList initializers( // EnvoyTimestampResolution "enum". @@ -627,6 +630,29 @@ int StreamHandleWrapper::luaMetadata(lua_State* state) { return 1; } +int StreamHandleWrapper::luaVirtualHost(lua_State* state) { + ASSERT(state_ == State::Running); + if (virtual_host_wrapper_.get() != nullptr) { + virtual_host_wrapper_.pushStack(); + } else { + virtual_host_wrapper_.reset( + VirtualHostWrapper::create(state, callbacks_.streamInfo(), callbacks_.filterConfigName()), + true); + } + return 1; +} + +int StreamHandleWrapper::luaRoute(lua_State* state) { + ASSERT(state_ == State::Running); + if (route_wrapper_.get() != nullptr) { + route_wrapper_.pushStack(); + } else { + route_wrapper_.reset( + RouteWrapper::create(state, callbacks_.streamInfo(), callbacks_.filterConfigName()), true); + } + return 1; +} + int StreamHandleWrapper::luaStreamInfo(lua_State* state) { ASSERT(state_ == State::Running); if (stream_info_wrapper_.get() != nullptr) { @@ -962,7 +988,7 @@ void Filter::DecoderCallbacks::respond(Http::ResponseHeaderMapPtr&& headers, Buf HttpResponseCodeDetails::get().LuaResponse); } -const ProtobufWkt::Struct& Filter::DecoderCallbacks::metadata() const { +const Protobuf::Struct& Filter::DecoderCallbacks::metadata() const { return getMetadata(callbacks_); } @@ -973,7 +999,7 @@ void Filter::EncoderCallbacks::respond(Http::ResponseHeaderMapPtr&&, Buffer::Ins luaL_error(state, "respond not currently supported in the response path"); } -const ProtobufWkt::Struct& Filter::EncoderCallbacks::metadata() const { +const Protobuf::Struct& Filter::EncoderCallbacks::metadata() const { return getMetadata(callbacks_); } diff --git a/source/extensions/filters/http/lua/lua_filter.h b/source/extensions/filters/http/lua/lua_filter.h index 7863d0cf115d0..c73a459c2527d 100644 --- a/source/extensions/filters/http/lua/lua_filter.h +++ b/source/extensions/filters/http/lua/lua_filter.h @@ -91,10 +91,10 @@ class FilterCallbacks { lua_State* state) PURE; /** - * @return const ProtobufWkt::Struct& the value of metadata inside the lua filter scope of current + * @return const Protobuf::Struct& the value of metadata inside the lua filter scope of current * route entry. */ - virtual const ProtobufWkt::Struct& metadata() const PURE; + virtual const Protobuf::Struct& metadata() const PURE; /** * @return StreamInfo::StreamInfo& the current stream info handle. This handle is mutable to @@ -124,11 +124,16 @@ class FilterCallbacks { virtual void clearRouteCache() PURE; /** - * @return const ProtobufWkt::Struct& the filter context from the most specific filter config + * @return const Protobuf::Struct& the filter context from the most specific filter config * from the route or virtual host. Empty struct will be returned if no route or virtual host is * found. */ - virtual const ProtobufWkt::Struct& filterContext() const PURE; + virtual const Protobuf::Struct& filterContext() const PURE; + + /** + * @return absl::string_view the value of filter config name. + */ + virtual const absl::string_view filterConfigName() const PURE; }; class Filter; @@ -202,7 +207,9 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject connection_stream_info_wrapper_; Filters::Common::Lua::LuaDeathRef connection_wrapper_; Filters::Common::Lua::LuaDeathRef public_key_wrapper_; + Filters::Common::Lua::LuaDeathRef virtual_host_wrapper_; + Filters::Common::Lua::LuaDeathRef route_wrapper_; State state_{State::Running}; std::function yield_callback_; Http::AsyncClient::Request* http_request_{}; @@ -464,13 +485,13 @@ class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { bool disabled() const { return disabled_; } absl::string_view name() const { return name_; } PerLuaCodeSetup* perLuaCodeSetup() const { return per_lua_code_setup_ptr_.get(); } - const ProtobufWkt::Struct& filterContext() const { return filter_context_; } + const Protobuf::Struct& filterContext() const { return filter_context_; } private: const bool disabled_; const std::string name_; PerLuaCodeSetupPtr per_lua_code_setup_ptr_; - const ProtobufWkt::Struct filter_context_; + const Protobuf::Struct filter_context_; }; /** @@ -550,7 +571,7 @@ class Filter : public Http::StreamFilter, private Filters::Common::Lua::LuaLogga void respond(Http::ResponseHeaderMapPtr&& headers, Buffer::Instance* body, lua_State* state) override; - const ProtobufWkt::Struct& metadata() const override; + const Protobuf::Struct& metadata() const override; StreamInfo::StreamInfo& streamInfo() override { return callbacks_->streamInfo(); } const Network::Connection* connection() const override { return callbacks_->connection().ptr(); @@ -564,7 +585,10 @@ class Filter : public Http::StreamFilter, private Filters::Common::Lua::LuaLogga cb->clearRouteCache(); } } - const ProtobufWkt::Struct& filterContext() const override { return parent_.filterContext(); } + const Protobuf::Struct& filterContext() const override { return parent_.filterContext(); } + const absl::string_view filterConfigName() const override { + return callbacks_->filterConfigName(); + } Filter& parent_; Http::StreamDecoderFilterCallbacks* callbacks_{}; @@ -583,7 +607,7 @@ class Filter : public Http::StreamFilter, private Filters::Common::Lua::LuaLogga void respond(Http::ResponseHeaderMapPtr&& headers, Buffer::Instance* body, lua_State* state) override; - const ProtobufWkt::Struct& metadata() const override; + const Protobuf::Struct& metadata() const override; StreamInfo::StreamInfo& streamInfo() override { return callbacks_->streamInfo(); } const Network::Connection* connection() const override { return callbacks_->connection().ptr(); @@ -593,7 +617,10 @@ class Filter : public Http::StreamFilter, private Filters::Common::Lua::LuaLogga UNREFERENCED_PARAMETER(host_and_strict); } void clearRouteCache() override {} - const ProtobufWkt::Struct& filterContext() const override { return parent_.filterContext(); } + const Protobuf::Struct& filterContext() const override { return parent_.filterContext(); } + const absl::string_view filterConfigName() const override { + return callbacks_->filterConfigName(); + } Filter& parent_; Http::StreamEncoderFilterCallbacks* callbacks_{}; @@ -625,8 +652,8 @@ class Filter : public Http::StreamFilter, private Filters::Common::Lua::LuaLogga return config_->perLuaCodeSetup(); } - const ProtobufWkt::Struct& filterContext() const { - return per_route_config_ == nullptr ? ProtobufWkt::Struct::default_instance() + const Protobuf::Struct& filterContext() const { + return per_route_config_ == nullptr ? Protobuf::Struct::default_instance() : per_route_config_->filterContext(); } diff --git a/source/extensions/filters/http/lua/wrappers.cc b/source/extensions/filters/http/lua/wrappers.cc index 8462fec213e07..a9aa071e9987b 100644 --- a/source/extensions/filters/http/lua/wrappers.cc +++ b/source/extensions/filters/http/lua/wrappers.cc @@ -180,6 +180,15 @@ int StreamInfoWrapper::luaDynamicTypedMetadata(lua_State* state) { state, typed_metadata); } +int StreamInfoWrapper::luaFilterState(lua_State* state) { + if (filter_state_wrapper_.get() != nullptr) { + filter_state_wrapper_.pushStack(); + } else { + filter_state_wrapper_.reset(FilterStateWrapper::create(state, *this), true); + } + return 1; +} + int ConnectionStreamInfoWrapper::luaConnectionDynamicMetadata(lua_State* state) { if (connection_dynamic_metadata_wrapper_.get() != nullptr) { connection_dynamic_metadata_wrapper_.pushStack(); @@ -328,7 +337,7 @@ int DynamicMetadataMapWrapper::luaSet(lua_State* state) { // so push a copy of the 3rd arg ("value") to the top. lua_pushvalue(state, 4); - ProtobufWkt::Struct value; + Protobuf::Struct value; (*value.mutable_fields())[key] = Filters::Common::Lua::MetadataMapHelper::loadValue(state); streamInfo().setDynamicMetadata(filter_name, value); @@ -380,6 +389,114 @@ int PublicKeyWrapper::luaGet(lua_State* state) { return 1; } +StreamInfo::StreamInfo& FilterStateWrapper::streamInfo() { return parent_.stream_info_; } + +int FilterStateWrapper::luaGet(lua_State* state) { + const char* object_name = luaL_checkstring(state, 2); + const StreamInfo::FilterStateSharedPtr filter_state = streamInfo().filterState(); + + // Check if filter state exists. + if (filter_state == nullptr) { + return 0; // Return nil if filter state is null. + } + + // Get the filter state object by name. + const StreamInfo::FilterState::Object* object = filter_state->getDataReadOnlyGeneric(object_name); + if (object == nullptr) { + return 0; // Return nil if object not found. + } + + // Check if there's an optional third parameter for field access. + if (lua_gettop(state) >= 3 && !lua_isnil(state, 3)) { + const char* field_name = luaL_checkstring(state, 3); + if (object->hasFieldSupport()) { + auto field_value = object->getField(field_name); + + // Convert the field value to the appropriate Lua type. + if (absl::holds_alternative(field_value)) { + const auto& str_value = absl::get(field_value); + lua_pushlstring(state, str_value.data(), str_value.size()); + return 1; + } + + if (absl::holds_alternative(field_value)) { + lua_pushnumber(state, absl::get(field_value)); + return 1; + } + + // Return nil if field is not found. + return 0; + } + + // Object doesn't support field access, return nil. + return 0; + } + + absl::optional string_value = object->serializeAsString(); + if (string_value.has_value()) { + const std::string& value = string_value.value(); + + // Return the filter state value as a string. + lua_pushlstring(state, value.data(), value.size()); + return 1; + } + + // If string serialization is not supported, return nil. + return 0; +} + +const Protobuf::Struct& VirtualHostWrapper::getMetadata() const { + const auto& virtual_host = stream_info_.virtualHost(); + if (virtual_host == nullptr) { + return Protobuf::Struct::default_instance(); + } + + const auto& metadata = virtual_host->metadata(); + auto filter_it = metadata.filter_metadata().find(filter_config_name_); + + if (filter_it != metadata.filter_metadata().end()) { + return filter_it->second; + } + + return Protobuf::Struct::default_instance(); +} + +int VirtualHostWrapper::luaMetadata(lua_State* state) { + if (metadata_wrapper_.get() != nullptr) { + metadata_wrapper_.pushStack(); + } else { + metadata_wrapper_.reset(Filters::Common::Lua::MetadataMapWrapper::create(state, getMetadata()), + true); + } + return 1; +} + +const Protobuf::Struct& RouteWrapper::getMetadata() const { + const auto& route = stream_info_.route(); + if (route == nullptr) { + return Protobuf::Struct::default_instance(); + } + + const auto& metadata = route->metadata(); + auto filter_it = metadata.filter_metadata().find(filter_config_name_); + + if (filter_it != metadata.filter_metadata().end()) { + return filter_it->second; + } + + return Protobuf::Struct::default_instance(); +} + +int RouteWrapper::luaMetadata(lua_State* state) { + if (metadata_wrapper_.get() != nullptr) { + metadata_wrapper_.pushStack(); + } else { + metadata_wrapper_.reset(Filters::Common::Lua::MetadataMapWrapper::create(state, getMetadata()), + true); + } + return 1; +} + } // namespace Lua } // namespace HttpFilters } // namespace Extensions diff --git a/source/extensions/filters/http/lua/wrappers.h b/source/extensions/filters/http/lua/wrappers.h index ddaae767f1f69..1e457fa31b42d 100644 --- a/source/extensions/filters/http/lua/wrappers.h +++ b/source/extensions/filters/http/lua/wrappers.h @@ -147,7 +147,7 @@ class DynamicMetadataMapIterator private: DynamicMetadataMapWrapper& parent_; - Protobuf::Map::const_iterator current_; + Protobuf::Map::const_iterator current_; }; /** @@ -165,7 +165,7 @@ class ConnectionDynamicMetadataMapIterator private: ConnectionDynamicMetadataMapWrapper& parent_; - Protobuf::Map::const_iterator current_; + Protobuf::Map::const_iterator current_; }; /** @@ -260,6 +260,28 @@ class ConnectionDynamicMetadataMapWrapper friend class ConnectionDynamicMetadataMapIterator; }; +/** + * Lua wrapper for accessing filter state objects. + */ +class FilterStateWrapper : public Filters::Common::Lua::BaseLuaObject { +public: + FilterStateWrapper(StreamInfoWrapper& parent) : parent_(parent) {} + static ExportedFunctions exportedFunctions() { return {{"get", static_luaGet}}; } + +private: + /** + * Get a filter state object by name, with an optional field name. + * @param 1 (string): object name. + * @param 2 (string, optional): field name for objects that support field access. + * @return filter state value as string, or nil if not found. + */ + DECLARE_LUA_FUNCTION(FilterStateWrapper, luaGet); + + StreamInfo::StreamInfo& streamInfo(); + + StreamInfoWrapper& parent_; +}; + /** * Lua wrapper for a stream info. */ @@ -270,6 +292,7 @@ class StreamInfoWrapper : public Filters::Common::Lua::BaseLuaObject dynamic_metadata_wrapper_; + Filters::Common::Lua::LuaDeathRef filter_state_wrapper_; Filters::Common::Lua::LuaDeathRef downstream_ssl_connection_; friend class DynamicMetadataMapWrapper; + friend class FilterStateWrapper; }; /** @@ -422,6 +454,54 @@ class Timestamp { enum Resolution { Millisecond, Microsecond, Undefined }; }; +class VirtualHostWrapper : public Filters::Common::Lua::BaseLuaObject { +public: + VirtualHostWrapper(const StreamInfo::StreamInfo& stream_info, + const absl::string_view filter_config_name) + : stream_info_{stream_info}, filter_config_name_{filter_config_name} {} + + static ExportedFunctions exportedFunctions() { return {{"metadata", static_luaMetadata}}; } + +private: + /** + * @return a handle to the metadata. + */ + DECLARE_LUA_FUNCTION(VirtualHostWrapper, luaMetadata); + + const Protobuf::Struct& getMetadata() const; + + // Filters::Common::Lua::BaseLuaObject + void onMarkDead() override { metadata_wrapper_.reset(); } + + const StreamInfo::StreamInfo& stream_info_; + const absl::string_view filter_config_name_; + Filters::Common::Lua::LuaDeathRef metadata_wrapper_; +}; + +class RouteWrapper : public Filters::Common::Lua::BaseLuaObject { +public: + RouteWrapper(const StreamInfo::StreamInfo& stream_info, + const absl::string_view filter_config_name) + : stream_info_{stream_info}, filter_config_name_{filter_config_name} {} + + static ExportedFunctions exportedFunctions() { return {{"metadata", static_luaMetadata}}; } + +private: + /** + * @return a handle to the metadata. + */ + DECLARE_LUA_FUNCTION(RouteWrapper, luaMetadata); + + const Protobuf::Struct& getMetadata() const; + + // Filters::Common::Lua::BaseLuaObject + void onMarkDead() override { metadata_wrapper_.reset(); } + + const StreamInfo::StreamInfo& stream_info_; + const absl::string_view filter_config_name_; + Filters::Common::Lua::LuaDeathRef metadata_wrapper_; +}; + } // namespace Lua } // namespace HttpFilters } // namespace Extensions diff --git a/source/extensions/filters/http/match_delegate/config.cc b/source/extensions/filters/http/match_delegate/config.cc index 86d32d5e63222..c80e56c9fc92f 100644 --- a/source/extensions/filters/http/match_delegate/config.cc +++ b/source/extensions/filters/http/match_delegate/config.cc @@ -24,10 +24,10 @@ class SkipActionFactory : public Matcher::ActionFactory { public: std::string name() const override { return "skip"; } - Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message&, - Envoy::Http::Matching::HttpFilterActionContext&, - ProtobufMessage::ValidationVisitor&) override { - return []() { return std::make_unique(); }; + Matcher::ActionConstSharedPtr createAction(const Protobuf::Message&, + Envoy::Http::Matching::HttpFilterActionContext&, + ProtobufMessage::ValidationVisitor&) override { + return std::make_shared(); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); @@ -120,8 +120,8 @@ void DelegatingStreamFilter::FilterMatchState::evaluateMatchTree( match_tree_evaluated_ = match_result.isComplete(); if (match_tree_evaluated_ && match_result.isMatch()) { - const Matcher::ActionPtr result = match_result.action(); - if ((result == nullptr) || (SkipAction().typeUrl() == result->typeUrl())) { + const auto& result = match_result.action(); + if (result == nullptr || SkipAction().typeUrl() == result->typeUrl()) { skip_filter_ = true; } else { ASSERT(base_filter_ != nullptr); diff --git a/source/extensions/filters/http/oauth2/filter.cc b/source/extensions/filters/http/oauth2/filter.cc index e4d9ab6e2646d..e03857ce71a2e 100644 --- a/source/extensions/filters/http/oauth2/filter.cc +++ b/source/extensions/filters/http/oauth2/filter.cc @@ -469,9 +469,10 @@ FilterConfig::FilterConfig( ? CookieSettings(proto_config.cookie_configs().code_verifier_cookie_config()) : CookieSettings()) { if (!context.clusterManager().clusters().hasCluster(oauth_token_endpoint_.cluster())) { - throw EnvoyException(fmt::format("OAuth2 filter: unknown cluster '{}' in config. Please " - "specify which cluster to direct OAuth requests to.", - oauth_token_endpoint_.cluster())); + // This is not necessarily a configuration error — sometimes cluster is sent later than the + // listener in the xDS stream. + ENVOY_LOG(warn, "OAuth2 filter: unknown cluster '{}' in config. ", + oauth_token_endpoint_.cluster()); } if (!authorization_endpoint_url_.initialize(authorization_endpoint_, /*is_connect_request=*/false)) { @@ -508,11 +509,7 @@ FilterStats FilterConfig::generateStats(const std::string& prefix, bool FilterConfig::shouldUseRefreshToken( const envoy::extensions::filters::http::oauth2::v3::OAuth2Config& proto_config) const { - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.oauth2_use_refresh_token")) { - return PROTOBUF_GET_WRAPPED_OR_DEFAULT(proto_config, use_refresh_token, true); - } - - return proto_config.use_refresh_token().value(); + return PROTOBUF_GET_WRAPPED_OR_DEFAULT(proto_config, use_refresh_token, true); } void OAuth2CookieValidator::setParams(const Http::RequestHeaderMap& headers, @@ -578,24 +575,22 @@ OAuth2Filter::OAuth2Filter(FilterConfigSharedPtr config, * 5) user is unauthorized */ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { - // Decrypt the OAuth tokens and update the corresponding cookies in the request headers - // before forwarding the request upstream. This step must occur early to ensure that - // other parts of the filter can access the decrypted tokens—for example, to calculate - // the HMAC for the cookies. - decryptAndUpdateOAuthTokenCookies(headers); - - // Skip Filter and continue chain if a Passthrough header is matching - // Must be done before the sanitation of the authorization header, - // otherwise the authorization header might be altered or removed + // Skip Filter and continue chain if a Passthrough header is matching. + // Only increment counters here; do not modify request headers, as there may be + // other instances of this filter configured that still need to process the request. for (const auto& matcher : config_->passThroughMatchers()) { if (matcher->matchesHeaders(headers)) { config_->stats().oauth_passthrough_.inc(); - // Remove OAuth flow cookies to prevent them from being sent upstream. - removeOAuthFlowCookies(headers); return Http::FilterHeadersStatus::Continue; } } + // Decrypt the OAuth tokens and update the corresponding cookies in the request headers + // before forwarding the request upstream. This step must occur early to ensure that + // other parts of the filter can access the decrypted tokens—for example, to calculate + // the HMAC for the cookies. + decryptAndUpdateOAuthTokenCookies(headers); + // Only sanitize the Authorization header if preserveAuthorizationHeader is false if (!config_->preserveAuthorizationHeader()) { // Sanitize the Authorization header, since we have no way to validate its content. Also, @@ -637,7 +632,7 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 const CallbackValidationResult result = validateOAuthCallback(headers, path_str); if (!result.is_valid_) { - sendUnauthorizedResponse(); + sendUnauthorizedResponse(result.error_details_); return Http::FilterHeadersStatus::StopIteration; } @@ -646,9 +641,9 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he Http::Utility::Url original_request_url; original_request_url.initialize(result.original_request_url_, false); if (config_->redirectPathMatcher().match(original_request_url.pathAndQueryParams())) { - ENVOY_LOG(debug, "state url query params {} matches the redirect path matcher", - original_request_url.pathAndQueryParams()); - sendUnauthorizedResponse(); + sendUnauthorizedResponse( + fmt::format("State url query params matches the redirect path matcher: {}", + original_request_url.pathAndQueryParams())); return Http::FilterHeadersStatus::StopIteration; } @@ -691,8 +686,8 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he redirectToOAuthServer(headers); return Http::FilterHeadersStatus::StopIteration; } else { - ENVOY_LOG(debug, "unauthorized, redirecting to OAuth server is not allowed", path_str); - sendUnauthorizedResponse(); + sendUnauthorizedResponse(fmt::format( + "Unauthorized, and redirecting to OAuth server is not allowed: {}", path_str)); return Http::FilterHeadersStatus::StopIteration; } } @@ -702,7 +697,7 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he // token. const CallbackValidationResult result = validateOAuthCallback(headers, path_str); if (!result.is_valid_) { - sendUnauthorizedResponse(); + sendUnauthorizedResponse(result.error_details_); return Http::FilterHeadersStatus::StopIteration; } @@ -716,16 +711,14 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he std::string encrypted_code_verifier = Http::Utility::parseCookieValue(headers, config_->cookieNames().code_verifier_); if (encrypted_code_verifier.empty()) { - ENVOY_LOG(error, "code verifier cookie is missing in the request"); - sendUnauthorizedResponse(); + sendUnauthorizedResponse("Code verifier cookie is missing in the request"); return Http::FilterHeadersStatus::StopIteration; } DecryptResult decrypt_result = decrypt(encrypted_code_verifier, config_->hmacSecret()); if (decrypt_result.error.has_value()) { - ENVOY_LOG(error, "failed to decrypt code verifier: {}, error: {}", encrypted_code_verifier, - decrypt_result.error.value()); - sendUnauthorizedResponse(); + sendUnauthorizedResponse(fmt::format("Failed to decrypt code verifier: {}, error: {}", + encrypted_code_verifier, decrypt_result.error.value())); return Http::FilterHeadersStatus::StopIteration; } @@ -947,30 +940,24 @@ Http::FilterHeadersStatus OAuth2Filter::signOutUser(const Http::RequestHeaderMap cookie_domain = fmt::format(CookieDomainFormatString, config_->cookieDomain()); } - response_headers->addReferenceKey( - Http::Headers::get().SetCookie, - absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().oauth_hmac_), - cookie_domain)); - response_headers->addReferenceKey( - Http::Headers::get().SetCookie, - absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().bearer_token_), - cookie_domain)); - response_headers->addReferenceKey( - Http::Headers::get().SetCookie, - absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().id_token_), - cookie_domain)); - response_headers->addReferenceKey( - Http::Headers::get().SetCookie, - absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().refresh_token_), - cookie_domain)); - response_headers->addReferenceKey( - Http::Headers::get().SetCookie, - absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().oauth_nonce_), - cookie_domain)); - response_headers->addReferenceKey( - Http::Headers::get().SetCookie, - absl::StrCat(fmt::format(CookieDeleteFormatString, config_->cookieNames().code_verifier_), - cookie_domain)); + const std::vector cookie_names{ + config_->cookieNames().oauth_hmac_, config_->cookieNames().bearer_token_, + config_->cookieNames().id_token_, config_->cookieNames().refresh_token_, + config_->cookieNames().oauth_nonce_, config_->cookieNames().code_verifier_, + }; + + for (const auto& cookie_name : cookie_names) { + // Cookie names prefixed with "__Secure-" or "__Host-" are special. They MUST be set with the + // Secure attribute so that the browser handles their deletion properly. + const bool add_secure_attr = + cookie_name.starts_with("__Secure-") || cookie_name.starts_with("__Host-"); + const absl::string_view maybe_secure_attr = add_secure_attr ? "; Secure" : ""; + + response_headers->addReferenceKey( + Http::Headers::get().SetCookie, + absl::StrCat(fmt::format(CookieDeleteFormatString, cookie_name), cookie_domain, + maybe_secure_attr)); + } const std::string post_logout_redirect_url = absl::StrCat(headers.getSchemeValue(), "://", host_, "/"); @@ -1219,7 +1206,8 @@ void OAuth2Filter::onRefreshAccessTokenFailure() { if (canRedirectToOAuthServer(*request_headers_)) { redirectToOAuthServer(*request_headers_); } else { - sendUnauthorizedResponse(); + sendUnauthorizedResponse( + "Failed to refresh the access token, and redirecting to OAuth server is not allowed"); } } @@ -1280,10 +1268,11 @@ void OAuth2Filter::addResponseCookies(Http::ResponseHeaderMap& headers, } } -void OAuth2Filter::sendUnauthorizedResponse() { +void OAuth2Filter::sendUnauthorizedResponse(const std::string& details) { + ENVOY_LOG(warn, "Responding with 401 Unauthorized. Cause: {}", details); config_->stats().oauth_failure_.inc(); decoder_callbacks_->sendLocalReply(Http::Code::Unauthorized, UnauthorizedBodyMessage, nullptr, - absl::nullopt, EMPTY_STRING); + absl::nullopt, details); } // Validates the OAuth callback request. @@ -1297,16 +1286,17 @@ OAuth2Filter::validateOAuthCallback(const Http::RequestHeaderMap& headers, // Return 401 unauthorized if the query parameters contain an error response. const auto query_parameters = Http::Utility::QueryParamsMulti::parseQueryString(path_str); if (query_parameters.getFirstValue(queryParamsError).has_value()) { - ENVOY_LOG(debug, "OAuth server returned an error: \n{}", query_parameters.data()); - return {false, "", ""}; + return {false, "", "", + fmt::format("OAuth server returned an error: {}", query_parameters.toString())}; } // Return 401 unauthorized if the query parameters do not contain the code and state. auto codeVal = query_parameters.getFirstValue(queryParamsCode); auto stateVal = query_parameters.getFirstValue(queryParamsState); if (!codeVal.has_value() || !stateVal.has_value()) { - ENVOY_LOG(error, "code or state query param does not exist: \n{}", query_parameters.data()); - return {false, "", ""}; + return { + false, "", "", + fmt::format("Code or state query param does not exist: {}", query_parameters.toString())}; } // Return 401 unauthorized if the state query parameter does not contain the original request URL @@ -1314,19 +1304,18 @@ OAuth2Filter::validateOAuthCallback(const Http::RequestHeaderMap& headers, // Decode the state parameter to get the original request URL and the CSRF token. const std::string state = Base64Url::decode(stateVal.value()); bool has_unknown_field; - ProtobufWkt::Struct message; + Protobuf::Struct message; auto status = MessageUtil::loadFromJsonNoThrow(state, message, has_unknown_field); if (!status.ok()) { - ENVOY_LOG(error, "state query param is not a valid JSON: \n{}", state); - return {false, "", ""}; + return {false, "", "", fmt::format("State query param is not a valid JSON: {}", state)}; } const auto& filed_value_pair = message.fields(); if (!filed_value_pair.contains(stateParamsUrl) || !filed_value_pair.contains(stateParamsCsrfToken)) { - ENVOY_LOG(error, "state query param does not contain url or CSRF token: \n{}", state); - return {false, "", ""}; + return {false, "", "", + fmt::format("State query param does not contain url or CSRF token: {}", state)}; } // Return 401 unauthorized if the CSRF token cookie does not match the CSRF token in the state. @@ -1337,19 +1326,18 @@ OAuth2Filter::validateOAuthCallback(const Http::RequestHeaderMap& headers, // More information can be found at https://datatracker.ietf.org/doc/html/rfc6819#section-5.3.5 std::string csrf_token = filed_value_pair.at(stateParamsCsrfToken).string_value(); if (!validateCsrfToken(headers, csrf_token)) { - ENVOY_LOG(error, "csrf token validation failed"); - return {false, "", ""}; + return {false, "", "", "CSRF token validation failed"}; } const std::string original_request_url = filed_value_pair.at(stateParamsUrl).string_value(); // Return 401 unauthorized if the URL in the state is not valid. Http::Utility::Url url; if (!url.initialize(original_request_url, false)) { - ENVOY_LOG(error, "state url {} can not be initialized", original_request_url); - return {false, "", ""}; + return {false, "", "", + fmt::format("State url can not be initialized: {}", original_request_url)}; } - return {true, codeVal.value(), original_request_url}; + return {true, codeVal.value(), original_request_url, ""}; } // Validates the csrf_token in the state parameter against the one in the cookie. diff --git a/source/extensions/filters/http/oauth2/filter.h b/source/extensions/filters/http/oauth2/filter.h index 37c7921250005..3c0906cd9f516 100644 --- a/source/extensions/filters/http/oauth2/filter.h +++ b/source/extensions/filters/http/oauth2/filter.h @@ -126,7 +126,7 @@ struct CookieNames { * This class encapsulates all data needed for the filter to operate so that we don't pass around * raw protobufs and other arbitrary data. */ -class FilterConfig { +class FilterConfig : public Logger::Loggable { public: FilterConfig(const envoy::extensions::filters::http::oauth2::v3::OAuth2Config& proto_config, Server::Configuration::CommonFactoryContext& context, @@ -300,6 +300,7 @@ struct CallbackValidationResult { bool is_valid_; std::string auth_code_; std::string original_request_url_; + std::string error_details_; }; /** @@ -331,7 +332,7 @@ class OAuth2Filter : public Http::PassThroughFilter, // a catch-all function used for request failures. we don't retry, as a user can simply refresh // the page in the case of a network blip. - void sendUnauthorizedResponse() override; + void sendUnauthorizedResponse(const std::string& details) override; void finishGetAccessTokenFlow(); void finishRefreshAccessTokenFlow(); diff --git a/source/extensions/filters/http/oauth2/oauth.h b/source/extensions/filters/http/oauth2/oauth.h index f7850d35fd99f..ea851a4c7d923 100644 --- a/source/extensions/filters/http/oauth2/oauth.h +++ b/source/extensions/filters/http/oauth2/oauth.h @@ -30,7 +30,7 @@ class FilterCallbacks { virtual void onRefreshAccessTokenFailure() PURE; - virtual void sendUnauthorizedResponse() PURE; + virtual void sendUnauthorizedResponse(const std::string& details) PURE; }; /** diff --git a/source/extensions/filters/http/oauth2/oauth_client.cc b/source/extensions/filters/http/oauth2/oauth_client.cc index 562ccd48c0a8c..13f6d40f83adf 100644 --- a/source/extensions/filters/http/oauth2/oauth_client.cc +++ b/source/extensions/filters/http/oauth2/oauth_client.cc @@ -121,7 +121,7 @@ void OAuth2ClientImpl::dispatchRequest(Http::RequestMessagePtr&& msg) { in_flight_request_ = thread_local_cluster->httpAsyncClient().send(std::move(msg), *this, options); } else { - parent_->sendUnauthorizedResponse(); + parent_->sendUnauthorizedResponse("Token endpoint cluster not found"); } } @@ -142,7 +142,9 @@ void OAuth2ClientImpl::onSuccess(const Http::AsyncClient::Request&, ENVOY_LOG(debug, "Oauth response body: {}", message->bodyAsString()); switch (oldState) { case OAuthState::PendingAccessToken: - parent_->sendUnauthorizedResponse(); + parent_->sendUnauthorizedResponse( + fmt::format("Failed to get access token, response code: {}, response body: {}", + response_code, message->bodyAsString())); break; case OAuthState::PendingAccessTokenByRefreshToken: parent_->onRefreshAccessTokenFailure(); @@ -160,17 +162,16 @@ void OAuth2ClientImpl::onSuccess(const Http::AsyncClient::Request&, MessageUtil::loadFromJson(response_body, response, ProtobufMessage::getNullValidationVisitor()); } END_TRY catch (EnvoyException& e) { - ENVOY_LOG(debug, "Error parsing response body, received exception: {}", e.what()); - ENVOY_LOG(debug, "Response body: {}", response_body); - parent_->sendUnauthorizedResponse(); + parent_->sendUnauthorizedResponse(fmt::format( + "Failed to parse oauth response body: {}, exception: {}", response_body, e.what())); return; } // TODO(snowp): Should this be a pgv validation instead? A more readable log // message might be good enough reason to do this manually? if (!response.has_access_token()) { - ENVOY_LOG(debug, "No access token after asyncGetAccessToken"); - parent_->sendUnauthorizedResponse(); + parent_->sendUnauthorizedResponse( + fmt::format("No access token found in the token exchange response: {}", response_body)); return; } @@ -183,8 +184,9 @@ void OAuth2ClientImpl::onSuccess(const Http::AsyncClient::Request&, expires_in = std::chrono::seconds{response.expires_in().value()}; } if (expires_in <= 0s) { - ENVOY_LOG(debug, "No default or explicit access token expiration after asyncGetAccessToken"); - parent_->sendUnauthorizedResponse(); + parent_->sendUnauthorizedResponse(fmt::format( + "No default or explicit access token expiration found in the token exchange response: {}", + response_body)); return; } @@ -209,7 +211,7 @@ void OAuth2ClientImpl::onFailure(const Http::AsyncClient::Request&, switch (oldState) { case OAuthState::PendingAccessToken: - parent_->sendUnauthorizedResponse(); + parent_->sendUnauthorizedResponse("Failed to get access token due to HTTP request failure"); break; case OAuthState::PendingAccessTokenByRefreshToken: parent_->onRefreshAccessTokenFailure(); diff --git a/source/extensions/filters/http/on_demand/on_demand_update.cc b/source/extensions/filters/http/on_demand/on_demand_update.cc index 2dcb7a9cd2aa3..ca08d4a8de96d 100644 --- a/source/extensions/filters/http/on_demand/on_demand_update.cc +++ b/source/extensions/filters/http/on_demand/on_demand_update.cc @@ -6,6 +6,7 @@ #include "source/common/config/xds_resource.h" #include "source/common/http/codes.h" #include "source/common/http/utility.h" +#include "source/common/runtime/runtime_features.h" #include "source/common/upstream/od_cds_api_impl.h" #include "source/extensions/filters/http/well_known_names.h" @@ -58,20 +59,50 @@ DecodeHeadersBehaviorPtr createDecodeHeadersBehavior( if (!odcds_config.has_value()) { return DecodeHeadersBehavior::rds(); } - Upstream::OdCdsApiHandlePtr odcds; - if (odcds_config->resources_locator().empty()) { - odcds = THROW_OR_RETURN_VALUE(cm.allocateOdCdsApi(&Upstream::OdCdsApiImpl::create, - odcds_config->source(), absl::nullopt, - validation_visitor), - Upstream::OdCdsApiHandlePtr); - } else { - auto locator = THROW_OR_RETURN_VALUE( - Config::XdsResourceIdentifier::decodeUrl(odcds_config->resources_locator()), - xds::core::v3::ResourceLocator); - odcds = THROW_OR_RETURN_VALUE(cm.allocateOdCdsApi(&Upstream::OdCdsApiImpl::create, - odcds_config->source(), locator, - validation_visitor), - Upstream::OdCdsApiHandlePtr); + Upstream::OdCdsApiHandlePtr odcds = nullptr; + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions")) { + // For xDS-TP based configs, both the odcds_config->source and + // odcds_config->resources_locator must be empty. + if (!odcds_config->has_source() && odcds_config->resources_locator().empty()) { + odcds = THROW_OR_RETURN_VALUE(cm.allocateOdCdsApi(&Upstream::XdstpOdCdsApiImpl::create, + odcds_config->source(), absl::nullopt, + validation_visitor), + Upstream::OdCdsApiHandlePtr); + } + } + // TODO(adisuissa): Once the + // "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions" runtime flag is + // deprecated, change "if (odcds == nullptr)" to "else" (and further merge the else with the "if + // (odcds_config->resources_locator().empty())"). + if (odcds == nullptr) { + if (odcds_config->resources_locator().empty()) { + // If the config-source is ADS, use a singleton-subscription mechanism, + // similar to xDS-TP based configs. + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.odcds_over_ads_fix")) { + if (odcds_config->source().config_source_specifier_case() == + envoy::config::core::v3::ConfigSource::ConfigSourceSpecifierCase::kAds) { + odcds = THROW_OR_RETURN_VALUE(cm.allocateOdCdsApi(&Upstream::XdstpOdCdsApiImpl::create, + odcds_config->source(), absl::nullopt, + validation_visitor), + Upstream::OdCdsApiHandlePtr); + } + } + if (odcds == nullptr) { + odcds = THROW_OR_RETURN_VALUE(cm.allocateOdCdsApi(&Upstream::OdCdsApiImpl::create, + odcds_config->source(), absl::nullopt, + validation_visitor), + Upstream::OdCdsApiHandlePtr); + } + } else { + auto locator = THROW_OR_RETURN_VALUE( + Config::XdsResourceIdentifier::decodeUrl(odcds_config->resources_locator()), + xds::core::v3::ResourceLocator); + odcds = THROW_OR_RETURN_VALUE(cm.allocateOdCdsApi(&Upstream::OdCdsApiImpl::create, + odcds_config->source(), locator, + validation_visitor), + Upstream::OdCdsApiHandlePtr); + } } // If changing the default timeout, please update the documentation in on_demand.proto too. auto timeout = diff --git a/source/extensions/filters/http/proto_api_scrubber/filter_config.h b/source/extensions/filters/http/proto_api_scrubber/filter_config.h index bb246bfcb8224..6fa6245a78683 100644 --- a/source/extensions/filters/http/proto_api_scrubber/filter_config.h +++ b/source/extensions/filters/http/proto_api_scrubber/filter_config.h @@ -118,10 +118,10 @@ class RemoveFieldAction : public Matcher::ActionBase { public: - Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message&, - ProtoApiScrubberRemoveFieldAction&, - ProtobufMessage::ValidationVisitor&) override { - return []() { return std::make_unique(); }; + Matcher::ActionConstSharedPtr createAction(const Protobuf::Message&, + ProtoApiScrubberRemoveFieldAction&, + ProtobufMessage::ValidationVisitor&) override { + return std::make_shared(); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { diff --git a/source/extensions/filters/http/proto_message_extraction/extraction_util/BUILD b/source/extensions/filters/http/proto_message_extraction/extraction_util/BUILD index 3e43405088b0e..e44f22d9a3f6c 100644 --- a/source/extensions/filters/http/proto_message_extraction/extraction_util/BUILD +++ b/source/extensions/filters/http/proto_message_extraction/extraction_util/BUILD @@ -15,7 +15,6 @@ envoy_cc_library( ], deps = [ "//source/common/protobuf", - "//source/common/protobuf:cc_wkt_protos", "@com_google_absl//absl/container:flat_hash_map", "@com_google_protofieldextraction//:all_libs", ], @@ -34,7 +33,6 @@ envoy_cc_library( ":proto_extractor_interface", "//source/common/common:regex_lib", "//source/common/protobuf", - "//source/common/protobuf:cc_wkt_protos", "@com_google_protoconverter//:all", "@com_google_protofieldextraction//:all_libs", "@com_google_protoprocessinglib//proto_processing_lib/proto_scrubber", @@ -54,7 +52,6 @@ envoy_cc_library( ":proto_extractor_interface", "//source/common/common:regex_lib", "//source/common/protobuf", - "//source/common/protobuf:cc_wkt_protos", "@com_google_protoconverter//:all", "@com_google_protofieldextraction//:all_libs", "@com_google_protoprocessinglib//proto_processing_lib/proto_scrubber", diff --git a/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.cc b/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.cc index 7e027c1af1e34..324375635bdc9 100644 --- a/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.cc +++ b/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.cc @@ -48,7 +48,9 @@ namespace { using ::Envoy::Protobuf::Field; using ::Envoy::Protobuf::Map; +using ::Envoy::Protobuf::Struct; using ::Envoy::Protobuf::Type; +using ::Envoy::Protobuf::Value; using ::Envoy::Protobuf::field_extraction::FieldExtractor; using ::Envoy::Protobuf::internal::WireFormatLite; using ::Envoy::Protobuf::io::CodedInputStream; @@ -57,8 +59,6 @@ using ::Envoy::Protobuf::io::CordOutputStream; using ::Envoy::Protobuf::util::JsonParseOptions; using ::Envoy::Protobuf::util::converter::JsonObjectWriter; using ::Envoy::Protobuf::util::converter::ProtoStreamObjectSource; -using ::Envoy::ProtobufWkt::Struct; -using ::Envoy::ProtobufWkt::Value; std::string kLocationRegionExtractorPattern = R"((?:^|/)(?:locations|regions)/([^/]+))"; @@ -388,7 +388,7 @@ absl::Status RedactStructRecursively(std::vector::const_iterator pa } absl::Status ConvertToStruct(const Protobuf::field_extraction::MessageData& message, - const Envoy::ProtobufWkt::Type& type, + const Envoy::Protobuf::Type& type, ::Envoy::Protobuf::util::TypeResolver* type_resolver, Struct* message_struct) { // Convert from message data to JSON using absl::Cord. @@ -413,15 +413,15 @@ absl::Status ConvertToStruct(const Protobuf::field_extraction::MessageData& mess } (*message_struct->mutable_fields())[kTypeProperty].set_string_value( - google::protobuf::util::converter::GetFullTypeWithUrl(type.name())); + ProtobufUtil::converter::GetFullTypeWithUrl(type.name())); return absl::OkStatus(); } bool ScrubToStruct(const proto_processing_lib::proto_scrubber::ProtoScrubber* scrubber, - const Envoy::ProtobufWkt::Type& type, + const Envoy::Protobuf::Type& type, const ::google::grpc::transcoding::TypeHelper& type_helper, Protobuf::field_extraction::MessageData* message, - Envoy::ProtobufWkt::Struct* message_struct) { + Envoy::Protobuf::Struct* message_struct) { message_struct->Clear(); // When scrubber or message is nullptr, it indicates that there's nothing to diff --git a/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.h b/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.h index 1442657e6183a..f2885b42fa6c8 100644 --- a/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.h +++ b/source/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util.h @@ -44,7 +44,7 @@ constexpr int kProtoTranslationMaxRecursionDepth = 64; ABSL_CONST_INIT const char* const kStructTypeUrl = "type.googleapis.com/google.protobuf.Struct"; -bool IsEmptyStruct(const ProtobufWkt::Struct& message_struct); +bool IsEmptyStruct(const Protobuf::Struct& message_struct); bool IsLabelName(absl::string_view value); @@ -86,10 +86,9 @@ absl::string_view ExtractLocationIdFromResourceName(absl::string_view resource_n // Recursively redacts the path_pieces in the enclosing proto_struct. void RedactPath(std::vector::const_iterator path_begin, - std::vector::const_iterator path_end, - ProtobufWkt::Struct* proto_struct); + std::vector::const_iterator path_end, Protobuf::Struct* proto_struct); -void RedactPaths(absl::Span paths_to_redact, ProtobufWkt::Struct* proto_struct); +void RedactPaths(absl::Span paths_to_redact, Protobuf::Struct* proto_struct); // Finds the last value of the non-repeated string field after the first // value. Returns an empty string if there is only one string field. Returns @@ -116,15 +115,15 @@ ExtractStringFieldValue(const Protobuf::Type& type, absl::Status RedactStructRecursively(std::vector::const_iterator path_pieces_begin, std::vector::const_iterator path_pieces_end, - ProtobufWkt::Struct* message_struct); + Protobuf::Struct* message_struct); // Converts given proto message to Struct. It also adds // a "@type" property with proto type url to the generated Struct. Expects the // TypeResolver to handle types prefixed with "type.googleapis.com/". absl::Status ConvertToStruct(const Protobuf::field_extraction::MessageData& message, - const Envoy::ProtobufWkt::Type& type, + const Envoy::Protobuf::Type& type, ::Envoy::Protobuf::util::TypeResolver* type_resolver, - ::Envoy::ProtobufWkt::Struct* message_struct); + ::Envoy::Protobuf::Struct* message_struct); // Extracts given proto message and convert the extracted proto to Struct. // @@ -133,10 +132,10 @@ absl::Status ConvertToStruct(const Protobuf::field_extraction::MessageData& mess // (2) error during scrubbing/converting; // (3) the message is empty after scrubbing; bool ScrubToStruct(const proto_processing_lib::proto_scrubber::ProtoScrubber* scrubber, - const Envoy::ProtobufWkt::Type& type, + const Envoy::Protobuf::Type& type, const ::google::grpc::transcoding::TypeHelper& type_helper, Protobuf::field_extraction::MessageData* message, - Envoy::ProtobufWkt::Struct* message_struct); + Envoy::Protobuf::Struct* message_struct); } // namespace ProtoMessageExtraction } // namespace HttpFilters diff --git a/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.cc b/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.cc index d31926425f63b..66673ccb85370 100644 --- a/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.cc +++ b/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.cc @@ -35,18 +35,17 @@ namespace Extensions { namespace HttpFilters { namespace ProtoMessageExtraction { +using ::Envoy::Protobuf::Type; using ::Envoy::Protobuf::util::JsonParseOptions; using ::Envoy::ProtobufUtil::FieldMaskUtil; -using ::Envoy::ProtobufWkt::Type; using ::google::grpc::transcoding::TypeHelper; using ::proto_processing_lib::proto_scrubber::FieldCheckerInterface; using ::proto_processing_lib::proto_scrubber::FieldMaskPathChecker; using ::proto_processing_lib::proto_scrubber::ScrubberContext; using ::proto_processing_lib::proto_scrubber::UnknownFieldChecker; -const google::protobuf::FieldMask& -ProtoExtractor::FindWithDefault(ExtractedMessageDirective directive) { - static const google::protobuf::FieldMask default_field_mask; +const Protobuf::FieldMask& ProtoExtractor::FindWithDefault(ExtractedMessageDirective directive) { + static const Protobuf::FieldMask default_field_mask; auto it = directives_mapping_.find(directive); if (it != directives_mapping_.end()) { @@ -145,8 +144,7 @@ ProtoExtractor::ExtractMessage(const Protobuf::field_extraction::MessageData& ra // property. if (scrubber_ == nullptr) { (*extracted_message_metadata.extracted_message.mutable_fields())[kTypeProperty] - .set_string_value( - google::protobuf::util::converter::GetFullTypeWithUrl(message_type_->name())); + .set_string_value(ProtobufUtil::converter::GetFullTypeWithUrl(message_type_->name())); return extracted_message_metadata; } @@ -164,7 +162,7 @@ ProtoExtractor::ExtractMessage(const Protobuf::field_extraction::MessageData& ra // resulting proto struct keys are in camel case. std::vector redact_paths_camel_case; for (const std::string& path : redact_field_mask->second.paths()) { - redact_paths_camel_case.push_back(google::protobuf::util::converter::ToCamelCase(path)); + redact_paths_camel_case.push_back(ProtobufUtil::converter::ToCamelCase(path)); } RedactPaths(redact_paths_camel_case, &extracted_message_metadata.extracted_message); } diff --git a/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.h b/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.h index 82e37cccd4d2b..9f6f7f911e03a 100644 --- a/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.h +++ b/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor.h @@ -27,8 +27,7 @@ class ProtoExtractor : public ProtoExtractorInterface { static std::unique_ptr Create(proto_processing_lib::proto_scrubber::ScrubberContext scrubber_context, const google::grpc::transcoding::TypeHelper* type_helper, - const ::Envoy::ProtobufWkt::Type* message_type, - const FieldPathToExtractType& field_policies); + const ::Envoy::Protobuf::Type* message_type, const FieldPathToExtractType& field_policies); // Input message must be a message data. ExtractedMessageMetadata @@ -38,7 +37,7 @@ class ProtoExtractor : public ProtoExtractorInterface { // Initializes an instance of ProtoExtractor using FieldPolicies. ProtoExtractor(proto_processing_lib::proto_scrubber::ScrubberContext scrubber_context, const google::grpc::transcoding::TypeHelper* type_helper, - const ::Envoy::ProtobufWkt::Type* message_type, + const ::Envoy::Protobuf::Type* message_type, const FieldPathToExtractType& field_policies); // Populate the target resource or the target resource callback in the extracted message @@ -48,14 +47,14 @@ class ProtoExtractor : public ProtoExtractorInterface { bool callback, ExtractedMessageMetadata* extracted_message_metadata) const; // Function to get the value associated with a key - const ProtobufWkt::FieldMask& FindWithDefault(ExtractedMessageDirective directive); + const Protobuf::FieldMask& FindWithDefault(ExtractedMessageDirective directive); const google::grpc::transcoding::TypeHelper* type_helper_; - const ::Envoy::ProtobufWkt::Type* message_type_; + const ::Envoy::Protobuf::Type* message_type_; // We use std::map instead of absl::flat_hash_map because of flat_hash_map's // rehash behavior. - std::map directives_mapping_; - std::function type_finder_; + std::map directives_mapping_; + std::function type_finder_; std::unique_ptr field_checker_; std::unique_ptr scrubber_; // A field path for 'location_selector' associated with the field marked as diff --git a/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor_interface.h b/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor_interface.h index 9c9e680501ccd..29f42b283bc2a 100644 --- a/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor_interface.h +++ b/source/extensions/filters/http/proto_message_extraction/extraction_util/proto_extractor_interface.h @@ -29,7 +29,7 @@ struct ExtractedMessageMetadata { absl::optional target_resource; absl::optional target_resource_callback; absl::optional resource_location; - ProtobufWkt::Struct extracted_message; + Protobuf::Struct extracted_message; }; // A proto-extraction interface for extracting that converts a source message diff --git a/source/extensions/filters/http/proto_message_extraction/extractor.h b/source/extensions/filters/http/proto_message_extraction/extractor.h index 42e0fdffd8d54..dce6b2ac13751 100644 --- a/source/extensions/filters/http/proto_message_extraction/extractor.h +++ b/source/extensions/filters/http/proto_message_extraction/extractor.h @@ -24,7 +24,7 @@ namespace ProtoMessageExtraction { using ::Envoy::Protobuf::Type; -using TypeFinder = std::function; +using TypeFinder = std::function; struct ExtractedMessageResult { const TypeFinder* type_finder; @@ -35,8 +35,8 @@ struct ExtractedMessageResult { response_data; // Extracted struct with a "@type" field. - ProtobufWkt::Struct request_type_struct; - ProtobufWkt::Struct response_type_struct; + Protobuf::Struct request_type_struct; + Protobuf::Struct response_type_struct; }; class Extractor { diff --git a/source/extensions/filters/http/proto_message_extraction/extractor_impl.cc b/source/extensions/filters/http/proto_message_extraction/extractor_impl.cc index 7cbbbf9781769..dc33916adc640 100644 --- a/source/extensions/filters/http/proto_message_extraction/extractor_impl.cc +++ b/source/extensions/filters/http/proto_message_extraction/extractor_impl.cc @@ -58,7 +58,7 @@ std::string getFullTypeWithUrl(absl::string_view simple_type) { return absl::StrCat(kTypeServiceBaseUrl, "/", simple_type); } -void fillStructWithType(const ::Envoy::ProtobufWkt::Type& type, ::Envoy::ProtobufWkt::Struct& out) { +void fillStructWithType(const ::Envoy::Protobuf::Type& type, ::Envoy::Protobuf::Struct& out) { (*out.mutable_fields())[kTypeProperty].set_string_value(getFullTypeWithUrl(type.name())); } diff --git a/source/extensions/filters/http/proto_message_extraction/extractor_impl.h b/source/extensions/filters/http/proto_message_extraction/extractor_impl.h index 9d9bf6312fdb3..bbe639e714f8c 100644 --- a/source/extensions/filters/http/proto_message_extraction/extractor_impl.h +++ b/source/extensions/filters/http/proto_message_extraction/extractor_impl.h @@ -24,8 +24,8 @@ class ExtractorImpl : public Extractor { ExtractorImpl( const TypeFinder& type_finder, const google::grpc::transcoding::TypeHelper& type_helper, absl::string_view request_type_url, absl::string_view response_type_url, - // const Envoy::ProtobufWkt::Type* request_type, - // const Envoy::ProtobufWkt::Type* response_type, + // const Envoy::Protobuf::Type* request_type, + // const Envoy::Protobuf::Type* response_type, const envoy::extensions::filters::http::proto_message_extraction::v3::MethodExtraction& method_extraction) : method_extraction_(method_extraction), request_type_url_(request_type_url), diff --git a/source/extensions/filters/http/proto_message_extraction/filter.cc b/source/extensions/filters/http/proto_message_extraction/filter.cc index 7d686ec54aebc..4a48d1ccc55ff 100644 --- a/source/extensions/filters/http/proto_message_extraction/filter.cc +++ b/source/extensions/filters/http/proto_message_extraction/filter.cc @@ -365,7 +365,7 @@ Filter::HandleDataStatus Filter::handleEncodeData(Envoy::Buffer::Instance& data, void Filter::handleRequestExtractionResult(const std::vector& result) { RELEASE_ASSERT(extractor_, "`extractor_` should be initialized when extracting fields"); - Envoy::ProtobufWkt::Struct dest_metadata; + Envoy::Protobuf::Struct dest_metadata; auto addResultToMetadata = [&](const std::string& category, const std::string& key, const ExtractedMessageMetadata& metadata) { @@ -399,7 +399,7 @@ void Filter::handleRequestExtractionResult(const std::vector& result) { RELEASE_ASSERT(extractor_, "`extractor_` should be initialized when extracting fields"); - Envoy::ProtobufWkt::Struct dest_metadata; + Envoy::Protobuf::Struct dest_metadata; auto addResultToMetadata = [&](const std::string& category, const std::string& key, const ExtractedMessageMetadata& metadata) { diff --git a/source/extensions/filters/http/proto_message_extraction/filter_config.cc b/source/extensions/filters/http/proto_message_extraction/filter_config.cc index 8bfd86ed307d2..ef017c6b0db18 100644 --- a/source/extensions/filters/http/proto_message_extraction/filter_config.cc +++ b/source/extensions/filters/http/proto_message_extraction/filter_config.cc @@ -35,7 +35,7 @@ FilterConfig::FilterConfig(const ProtoMessageExtractionConfig& proto_config, Envoy::Grpc::Common::typeUrlPrefix(), descriptor_pool_.get())); type_finder_ = std::make_unique( - [this](absl::string_view type_url) -> const ::Envoy::ProtobufWkt::Type* { + [this](absl::string_view type_url) -> const ::Envoy::Protobuf::Type* { return type_helper_->Info()->GetTypeByTypeUrl(type_url); }); diff --git a/source/extensions/filters/http/rate_limit_quota/BUILD b/source/extensions/filters/http/rate_limit_quota/BUILD index 09bfd31ef640b..0437fb46f8237 100644 --- a/source/extensions/filters/http/rate_limit_quota/BUILD +++ b/source/extensions/filters/http/rate_limit_quota/BUILD @@ -39,6 +39,7 @@ envoy_cc_extension( srcs = ["config.cc"], hdrs = ["config.h"], deps = [ + ":filter_persistence", ":global_client_lib", ":rate_limit_quota", "//envoy/grpc:async_client_manager_interface", @@ -116,3 +117,24 @@ envoy_cc_library( "@envoy_api//envoy/service/rate_limit_quota/v3:pkg_cc_proto", ], ) + +envoy_cc_library( + name = "filter_persistence", + srcs = ["filter_persistence.cc"], + hdrs = ["filter_persistence.h"], + deps = [ + ":global_client_lib", + ":quota_bucket_cache", + "//envoy/grpc:async_client_manager_interface", + "//envoy/registry", + "//source/common/http:headers_lib", + "//source/common/http:message_lib", + "//source/common/http:utility_lib", + "//source/common/http/matching:data_impl_lib", + "//source/common/matcher:matcher_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "//source/extensions/matching/input_matchers/cel_matcher:config", + ], +) diff --git a/source/extensions/filters/http/rate_limit_quota/client_impl.cc b/source/extensions/filters/http/rate_limit_quota/client_impl.cc index 7d95b03072006..8731678923b93 100644 --- a/source/extensions/filters/http/rate_limit_quota/client_impl.cc +++ b/source/extensions/filters/http/rate_limit_quota/client_impl.cc @@ -21,11 +21,10 @@ void LocalRateLimitClientImpl::createBucket( const BucketId& bucket_id, size_t id, const BucketAction& default_bucket_action, std::unique_ptr fallback_action, std::chrono::milliseconds fallback_ttl, bool initial_request_allowed) { - std::shared_ptr global_client = getGlobalClient(); // Intentionally crash if the local client is initialized with a null global // client or TLS slot due to a bug. - global_client->createBucket(bucket_id, id, default_bucket_action, std::move(fallback_action), - fallback_ttl, initial_request_allowed); + global_client_->createBucket(bucket_id, id, default_bucket_action, std::move(fallback_action), + fallback_ttl, initial_request_allowed); } std::shared_ptr LocalRateLimitClientImpl::getBucket(size_t id) { diff --git a/source/extensions/filters/http/rate_limit_quota/client_impl.h b/source/extensions/filters/http/rate_limit_quota/client_impl.h index 1d17e88a923da..70fc2df48849b 100644 --- a/source/extensions/filters/http/rate_limit_quota/client_impl.h +++ b/source/extensions/filters/http/rate_limit_quota/client_impl.h @@ -31,9 +31,9 @@ class LocalRateLimitClientImpl : public RateLimitClient, public Logger::Loggable { public: explicit LocalRateLimitClientImpl( - Envoy::ThreadLocal::TypedSlot& global_client_tls, + GlobalRateLimitClientImpl* global_client, Envoy::ThreadLocal::TypedSlot& buckets_cache_tls) - : global_client_tls_(global_client_tls), buckets_cache_tls_(buckets_cache_tls) {} + : global_client_(global_client), buckets_cache_tls_(buckets_cache_tls) {} void createBucket(const BucketId& bucket_id, size_t id, const BucketAction& default_bucket_action, std::unique_ptr fallback_action, @@ -44,24 +44,20 @@ class LocalRateLimitClientImpl : public RateLimitClient, std::shared_ptr getBucket(size_t id) override; private: - inline std::shared_ptr getGlobalClient() { - return (global_client_tls_.get().has_value()) ? global_client_tls_.get()->global_client - : nullptr; - } inline std::shared_ptr getBucketsCache() { return (buckets_cache_tls_.get().has_value()) ? buckets_cache_tls_.get()->quota_buckets_ : nullptr; } // Lockless access to global resources via TLS. - ThreadLocal::TypedSlot& global_client_tls_; + GlobalRateLimitClientImpl* global_client_; ThreadLocal::TypedSlot& buckets_cache_tls_; }; -inline std::unique_ptr createLocalRateLimitClient( - ThreadLocal::TypedSlot& global_client_tls, - ThreadLocal::TypedSlot& buckets_cache_tls_) { - return std::make_unique(global_client_tls, buckets_cache_tls_); +inline std::unique_ptr +createLocalRateLimitClient(GlobalRateLimitClientImpl* global_client, + ThreadLocal::TypedSlot& buckets_cache_tls_) { + return std::make_unique(global_client, buckets_cache_tls_); } } // namespace RateLimitQuota diff --git a/source/extensions/filters/http/rate_limit_quota/config.cc b/source/extensions/filters/http/rate_limit_quota/config.cc index eb005e4e5cdeb..085643fac13c3 100644 --- a/source/extensions/filters/http/rate_limit_quota/config.cc +++ b/source/extensions/filters/http/rate_limit_quota/config.cc @@ -16,6 +16,7 @@ #include "source/extensions/filters/http/rate_limit_quota/client_impl.h" #include "source/extensions/filters/http/rate_limit_quota/filter.h" +#include "source/extensions/filters/http/rate_limit_quota/filter_persistence.h" #include "source/extensions/filters/http/rate_limit_quota/global_client_impl.h" #include "source/extensions/filters/http/rate_limit_quota/quota_bucket_cache.h" @@ -24,15 +25,7 @@ namespace Extensions { namespace HttpFilters { namespace RateLimitQuota { -// Object to hold TLS slots after the factory itself has been cleaned up. -struct TlsStore { - TlsStore(Server::Configuration::FactoryContext& context) - : global_client_tls(context.serverFactoryContext().threadLocal()), - buckets_tls(context.serverFactoryContext().threadLocal()) {} - - ThreadLocal::TypedSlot global_client_tls; - ThreadLocal::TypedSlot buckets_tls; -}; +using TlsStore = GlobalTlsStores::TlsStore; Http::FilterFactoryCb RateLimitQuotaFilterFactory::createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaFilterConfig& @@ -47,32 +40,6 @@ Http::FilterFactoryCb RateLimitQuotaFilterFactory::createFilterFactoryFromProtoT Grpc::GrpcServiceConfigWithHashKey config_with_hash_key = Grpc::GrpcServiceConfigWithHashKey(config->rlqs_server()); - // Quota bucket & global client TLS objects are created with the config and - // kept alive via shared_ptr to a storage struct. The local rate limit client - // in each filter instance assumes that the slot will outlive them. - std::shared_ptr tls_store = std::make_shared(context); - auto tl_buckets_cache = - std::make_shared(std::make_shared()); - tls_store->buckets_tls.set( - [tl_buckets_cache]([[maybe_unused]] Envoy::Event::Dispatcher& dispatcher) { - return tl_buckets_cache; - }); - - // TODO(bsurber): Implement report timing & usage aggregation based on each - // bucket's reporting_interval field. Currently this is not supported and all - // usage is reported on a hardcoded interval. - std::chrono::milliseconds reporting_interval(5000); - - // Create the global client resource to be shared via TLS to all worker - // threads (accessed through a filter-specific LocalRateLimitClient). - auto tl_global_client = std::make_shared( - createGlobalRateLimitClientImpl(context, filter_config.domain(), reporting_interval, - tls_store->buckets_tls, config_with_hash_key)); - tls_store->global_client_tls.set( - [tl_global_client]([[maybe_unused]] Envoy::Event::Dispatcher& dispatcher) { - return tl_global_client; - }); - RateLimitOnMatchActionContext action_context; RateLimitQuotaValidationVisitor visitor; Matcher::MatchTreeFactory matcher_factory( @@ -83,10 +50,18 @@ Http::FilterFactoryCb RateLimitQuotaFilterFactory::createFilterFactoryFromProtoT matcher = matcher_factory.create(config->bucket_matchers())(); } + std::string rlqs_server_target = config->rlqs_server().has_envoy_grpc() + ? config->rlqs_server().envoy_grpc().cluster_name() + : config->rlqs_server().google_grpc().target_uri(); + + // Get the TLS store from the global map, or create one if it doesn't exist. + std::shared_ptr tls_store = GlobalTlsStores::getTlsStore( + config_with_hash_key, context, rlqs_server_target, filter_config.domain()); + return [&, config = std::move(config), config_with_hash_key, tls_store = std::move(tls_store), matcher = std::move(matcher)](Http::FilterChainFactoryCallbacks& callbacks) -> void { std::unique_ptr local_client = - createLocalRateLimitClient(tls_store->global_client_tls, tls_store->buckets_tls); + createLocalRateLimitClient(tls_store->global_client.get(), tls_store->buckets_tls); callbacks.addStreamFilter(std::make_shared( config, context, std::move(local_client), config_with_hash_key, matcher)); diff --git a/source/extensions/filters/http/rate_limit_quota/filter.cc b/source/extensions/filters/http/rate_limit_quota/filter.cc index 34c74d7e4a6e6..04a4697472816 100644 --- a/source/extensions/filters/http/rate_limit_quota/filter.cc +++ b/source/extensions/filters/http/rate_limit_quota/filter.cc @@ -56,8 +56,9 @@ inline Envoy::Http::Code getDenyResponseCode(const DenyResponseSettings& setting inline std::function addDenyResponseHeadersCb(const DenyResponseSettings& settings) { - if (settings.response_headers_to_add().empty()) + if (settings.response_headers_to_add().empty()) { return nullptr; + } // Headers copied from settings for thread-safety. return [headers_to_add = settings.response_headers_to_add()](Http::ResponseHeaderMap& headers) { for (const envoy::config::core::v3::HeaderValueOption& header : headers_to_add) { @@ -69,7 +70,8 @@ addDenyResponseHeadersCb(const DenyResponseSettings& settings) { Http::FilterHeadersStatus sendDenyResponse(Http::StreamDecoderFilterCallbacks* cb, const DenyResponseSettings& settings, StreamInfo::CoreResponseFlag flag) { - cb->sendLocalReply(getDenyResponseCode(settings), settings.http_body().value(), + cb->sendLocalReply(getDenyResponseCode(settings), + MessageUtil::bytesToString(settings.http_body().value()), addDenyResponseHeadersCb(settings), absl::nullopt, ""); cb->streamInfo().setResponseFlag(flag); return Envoy::Http::FilterHeadersStatus::StopIteration; @@ -79,7 +81,7 @@ Http::FilterHeadersStatus RateLimitQuotaFilter::decodeHeaders(Http::RequestHeade bool end_stream) { ENVOY_LOG(trace, "decodeHeaders: end_stream = {}", end_stream); // First, perform the request matching. - absl::StatusOr match_result = requestMatching(headers); + absl::StatusOr match_result = requestMatching(headers); if (!match_result.ok()) { // When the request is not matched by any matchers, it is ALLOWED by default // (i.e., fail-open) and its quota usage will not be reported to RLQS @@ -110,7 +112,7 @@ Http::FilterHeadersStatus RateLimitQuotaFilter::decodeHeaders(Http::RequestHeade bucket_id, bucket_id_proto.DebugString()); // Add the matched bucket_id to dynamic metadata for logging. - ProtobufWkt::Struct bucket_log; + Protobuf::Struct bucket_log; auto* bucket_log_fields = bucket_log.mutable_fields(); for (const auto& bucket : bucket_id_proto.bucket()) { (*bucket_log_fields)[bucket.first] = ValueUtil::stringValue(bucket.second); @@ -184,7 +186,7 @@ Http::FilterHeadersStatus RateLimitQuotaFilter::decodeHeaders(Http::RequestHeade // TODO(tyxia) Currently request matching is only performed on the request // header. -absl::StatusOr +absl::StatusOr RateLimitQuotaFilter::requestMatching(const Http::RequestHeaderMap& headers) { // Initialize the data pointer on first use and reuse it for subsequent // requests. This avoids creating the data object for every request, which @@ -215,7 +217,7 @@ RateLimitQuotaFilter::requestMatching(const Http::RequestHeaderMap& headers) { return absl::NotFoundError("Matching completed but no match result was found."); } // Return the matched result for `on_match` case. - return match_result.action(); + return match_result.actionByMove(); } void RateLimitQuotaFilter::onDestroy() { diff --git a/source/extensions/filters/http/rate_limit_quota/filter.h b/source/extensions/filters/http/rate_limit_quota/filter.h index dcaf8b45a8d51..2920594c5d5f3 100644 --- a/source/extensions/filters/http/rate_limit_quota/filter.h +++ b/source/extensions/filters/http/rate_limit_quota/filter.h @@ -65,7 +65,8 @@ class RateLimitQuotaFilter : public Http::PassThroughFilter, // Perform request matching. It returns the generated bucket ids if the // matching succeeded, error status otherwise. - absl::StatusOr requestMatching(const Http::RequestHeaderMap& headers); + absl::StatusOr + requestMatching(const Http::RequestHeaderMap& headers); Http::Matching::HttpMatchingDataImpl matchingData() { ASSERT(data_ptr_ != nullptr); diff --git a/source/extensions/filters/http/rate_limit_quota/filter_persistence.cc b/source/extensions/filters/http/rate_limit_quota/filter_persistence.cc new file mode 100644 index 0000000000000..f5920d6367b57 --- /dev/null +++ b/source/extensions/filters/http/rate_limit_quota/filter_persistence.cc @@ -0,0 +1,82 @@ +#include "source/extensions/filters/http/rate_limit_quota/filter_persistence.h" + +#include +#include +#include +#include + +#include "envoy/grpc/async_client_manager.h" +#include "envoy/server/factory_context.h" + +#include "source/extensions/filters/http/rate_limit_quota/global_client_impl.h" +#include "source/extensions/filters/http/rate_limit_quota/quota_bucket_cache.h" + +#include "absl/base/no_destructor.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RateLimitQuota { + +using TlsStore = GlobalTlsStores::TlsStore; + +// Helper to initialize a new TLS store based on a rate_limit_quota config's +// settings. +std::shared_ptr +initTlsStore(const Grpc::GrpcServiceConfigWithHashKey& config_with_hash_key, + Server::Configuration::FactoryContext& context, absl::string_view target_address, + absl::string_view domain) { + // Quota bucket & global client TLS objects are created with the config and + // kept alive via shared_ptr to a storage struct. The local rate limit client + // in each filter instance assumes that the slot will outlive them. + std::shared_ptr tls_store = std::make_shared(context, target_address, domain); + auto tl_buckets_cache = + std::make_shared(std::make_shared()); + tls_store->buckets_tls.set( + [tl_buckets_cache]([[maybe_unused]] Envoy::Event::Dispatcher& dispatcher) { + return tl_buckets_cache; + }); + + // TODO(bsurber): Implement report timing & usage aggregation based on each + // bucket's reporting_interval field. Currently this is not supported and all + // usage is reported on a hardcoded interval. + std::chrono::milliseconds reporting_interval(5000); + + // Create the global client resource to be shared via TLS to all worker + // threads (accessed through a filter-specific LocalRateLimitClient). + std::unique_ptr tl_global_client = createGlobalRateLimitClientImpl( + context, domain, reporting_interval, tls_store->buckets_tls, config_with_hash_key); + tls_store->global_client = std::move(tl_global_client); + + return tls_store; +} + +// References a statically shared map. This is not thread-safe so it should +// only be called during RLQS filter factory creation on the main thread. +std::shared_ptr +GlobalTlsStores::getTlsStore(const Grpc::GrpcServiceConfigWithHashKey& config_with_hash_key, + Server::Configuration::FactoryContext& context, + absl::string_view target_address, absl::string_view domain) { + TlsStoreIndex index = std::make_pair(std::string(target_address), std::string(domain)); + // Find existing TlsStore or initialize a new one. + auto it = stores().find(index); + if (it != stores().end()) { + ENVOY_LOG(debug, "Found existing cache & RLQS client for target ({}) and domain ({}).", + index.first, index.second); + return it->second.lock(); + } + ENVOY_LOG(debug, "Creating a new cache & RLQS client for target ({}) and domain ({}).", + index.first, index.second); + std::shared_ptr tls_store = + initTlsStore(config_with_hash_key, context, index.first, index.second); + // Save weak_ptr as an unowned reference. + stores()[index] = tls_store; + return tls_store; +} + +} // namespace RateLimitQuota +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/rate_limit_quota/filter_persistence.h b/source/extensions/filters/http/rate_limit_quota/filter_persistence.h new file mode 100644 index 0000000000000..0eff70664976d --- /dev/null +++ b/source/extensions/filters/http/rate_limit_quota/filter_persistence.h @@ -0,0 +1,108 @@ +#pragma once + +#ifndef THIRD_PARTY_ENVOY_SRC_SOURCE_EXTENSIONS_FILTERS_HTTP_RATE_LIMIT_QUOTA_FILTER_PERSISTENCE_H_ +#define THIRD_PARTY_ENVOY_SRC_SOURCE_EXTENSIONS_FILTERS_HTTP_RATE_LIMIT_QUOTA_FILTER_PERSISTENCE_H_ + +#include +#include +#include + +#include "envoy/event/deferred_deletable.h" +#include "envoy/event/timer.h" +#include "envoy/grpc/async_client_manager.h" +#include "envoy/server/factory_context.h" + +#include "source/extensions/filters/http/rate_limit_quota/global_client_impl.h" +#include "source/extensions/filters/http/rate_limit_quota/quota_bucket_cache.h" + +#include "absl/base/no_destructor.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RateLimitQuota { + +// GlobalTlsStores holds a singleton hash map of rate_limit_quota TLS stores, +// indexed by their combined RLQS server targets & domains. +// +// This follows the data sharing model of FactoryRegistry, and similarly does +// not guarantee thread-safety. Additions or removals of indices can only be +// done on the main thread, as part of filter factory creation and garbage +// collection respectively. +// +// Note, multiple RLQS clients with different configs (e.g. timeouts) can hit +// the same index (destination + domain). The global map does not guarantee +// which config will be selected for the client creation. +class GlobalTlsStores : public Logger::Loggable { +public: + // Object to hold TLS slots after the factory itself has been cleaned up. + struct TlsStore { + TlsStore(Server::Configuration::FactoryContext& context, absl::string_view target_address, + absl::string_view domain) + : buckets_tls(context.serverFactoryContext().threadLocal()), + target_address_(target_address), domain_(domain), + main_dispatcher_(context.serverFactoryContext().mainThreadDispatcher()) {} + + ~TlsStore() { + // Clean up the index from the global map. This is not thread-safe, so + // it's only called after asserting that we're on the main thread. + ASSERT_IS_MAIN_OR_TEST_THREAD(); + // The global client must be cleaned up by the server main thread before + // it shuts down. + if (global_client != nullptr) { + main_dispatcher_.deferredDelete(std::move(global_client)); + } + GlobalTlsStores::clearTlsStore(std::make_pair(target_address_, domain_)); + } + + std::unique_ptr global_client = nullptr; + ThreadLocal::TypedSlot buckets_tls; + + private: + std::string target_address_; + std::string domain_; + Envoy::Event::Dispatcher& main_dispatcher_; + }; + + // Get an existing TLS store by index, or create one if not found. + static std::shared_ptr + getTlsStore(const Grpc::GrpcServiceConfigWithHashKey& config_with_hash_key, + Server::Configuration::FactoryContext& context, absl::string_view target_address, + absl::string_view domain); + + // Test-only: only thread-safe if filter factories are stable. + static size_t size() { return stores().size(); } + + // Test-only: unsafely clear the global map, used in testing to reset static + // state. A safer alternative is to delete all rate_limit_quota filters from + // config with LDS & let the garbage collector handle cleanup. + static void clear() { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + stores().clear(); + } + +private: + // The index is a pair of . + using TlsStoreIndex = std::pair; + + // Map of rate_limit_quota TLS stores & looping garbage collection timer. + using TlsStoreMap = absl::flat_hash_map>; + + // Static reference to shared map of rate_limit_quota TLS stores (follows the + // data sharing model of FactoryRegistry::factories()). + static TlsStoreMap& stores() { + static absl::NoDestructor tls_stores{}; + return *tls_stores; + } + + // Clear a specified index when it is no longer captured by any filter factories. + static void clearTlsStore(const TlsStoreIndex& index) { stores().erase(index); } +}; + +} // namespace RateLimitQuota +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy + +#endif // THIRD_PARTY_ENVOY_SRC_SOURCE_EXTENSIONS_FILTERS_HTTP_RATE_LIMIT_QUOTA_FILTER_PERSISTENCE_H_ diff --git a/source/extensions/filters/http/rate_limit_quota/global_client_impl.cc b/source/extensions/filters/http/rate_limit_quota/global_client_impl.cc index f666533d301bf..99e3efd959c28 100644 --- a/source/extensions/filters/http/rate_limit_quota/global_client_impl.cc +++ b/source/extensions/filters/http/rate_limit_quota/global_client_impl.cc @@ -38,12 +38,6 @@ namespace Extensions { namespace HttpFilters { namespace RateLimitQuota { -Grpc::RawAsyncClientSharedPtr -getOrThrow(absl::StatusOr client_or_error) { - THROW_IF_NOT_OK_REF(client_or_error.status()); - return client_or_error.value(); -} - using BucketAction = RateLimitQuotaResponse::BucketAction; using envoy::type::v3::RateLimitStrategy; @@ -53,15 +47,36 @@ GlobalRateLimitClientImpl::GlobalRateLimitClientImpl( std::chrono::milliseconds send_reports_interval, Envoy::ThreadLocal::TypedSlot& buckets_tls, Envoy::Event::Dispatcher& main_dispatcher) - : domain_name_(domain_name), - aync_client_(getOrThrow( - context.serverFactoryContext() - .clusterManager() - .grpcAsyncClientManager() - .getOrCreateRawAsyncClientWithHashKey(config_with_hash_key, context.scope(), true))), - buckets_tls_(buckets_tls), send_reports_interval_(send_reports_interval), + : domain_name_(domain_name), buckets_tls_(buckets_tls), + send_reports_interval_(send_reports_interval), time_source_(context.serverFactoryContext().mainThreadDispatcher().timeSource()), - main_dispatcher_(main_dispatcher) {} + main_dispatcher_(main_dispatcher) { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + + absl::StatusOr rlqs_stream_client_factory = + context.serverFactoryContext() + .clusterManager() + .grpcAsyncClientManager() + .factoryForGrpcService(config_with_hash_key.config(), context.scope(), true); + if (!rlqs_stream_client_factory.ok()) { + throw EnvoyException(std::string(rlqs_stream_client_factory.status().message())); + } + + absl::StatusOr rlqs_stream_client = + (*rlqs_stream_client_factory)->createUncachedRawAsyncClient(); + if (!rlqs_stream_client.ok()) { + throw EnvoyException(std::string(rlqs_stream_client.status().message())); + } + async_client_ = GrpcAsyncClient(std::move(*rlqs_stream_client)); +} + +void GlobalRateLimitClientImpl::deleteIsPending() { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + // Deleting the async client also triggers stream_ to reset, if active. + // The client & stream must be destroyed before the GlobalRateLimitClientImpl, + // as it provides the stream callbacks. + async_client_->reset(); +} void getUsageFromBucket(const CachedBucket& cached_bucket, TimeSource& time_source, BucketQuotaUsage& usage) { @@ -366,21 +381,21 @@ void GlobalRateLimitClientImpl::onQuotaResponseImpl(const RateLimitQuotaResponse void GlobalRateLimitClientImpl::onRemoteClose(Grpc::Status::GrpcStatus status, const std::string& message) { // TODO(tyxia) Revisit later, maybe add some logging. - main_dispatcher_.post([&, status, message]() { - // Stream is already closed and cannot be referenced further. - ENVOY_LOG(debug, "gRPC stream closed remotely with status {}: {}", status, message); - stream_ = nullptr; - }); + ASSERT_IS_MAIN_OR_TEST_THREAD(); + // Stream is already closed and cannot be referenced further. + ENVOY_LOG(debug, "gRPC stream closed remotely with status {}: {}", status, message); + stream_ = nullptr; } bool GlobalRateLimitClientImpl::startStreamImpl() { // Starts stream if it has not been opened yet. + ASSERT_IS_MAIN_OR_TEST_THREAD(); if (stream_ == nullptr) { ENVOY_LOG(debug, "Trying to start the new gRPC stream"); - stream_ = aync_client_.start(*Protobuf::DescriptorPool::generated_pool()->FindMethodByName( - "envoy.service.rate_limit_quota.v3.RateLimitQuotaService." - "StreamRateLimitQuotas"), - *this, Http::AsyncClient::RequestOptions()); + stream_ = async_client_->start(*Protobuf::DescriptorPool::generated_pool()->FindMethodByName( + "envoy.service.rate_limit_quota.v3.RateLimitQuotaService." + "StreamRateLimitQuotas"), + *this, Http::AsyncClient::RequestOptions()); } // Returns error status if start failed (i.e., stream_ is nullptr). return (stream_ != nullptr); diff --git a/source/extensions/filters/http/rate_limit_quota/global_client_impl.h b/source/extensions/filters/http/rate_limit_quota/global_client_impl.h index f72ffcff31e7e..7259973a529da 100644 --- a/source/extensions/filters/http/rate_limit_quota/global_client_impl.h +++ b/source/extensions/filters/http/rate_limit_quota/global_client_impl.h @@ -57,8 +57,11 @@ class GlobalRateLimitClientCallbacks { // worker threads' local RateLimitClients. class GlobalRateLimitClientImpl : public Grpc::AsyncStreamCallbacks< envoy::service::rate_limit_quota::v3::RateLimitQuotaResponse>, + public Event::DeferredDeletable, public Logger::Loggable { public: + // Note: rlqs_client is owned directly to ensure that it does not outlive the + // GlobalRateLimitClientImpl (as the impl provides stream callbacks). GlobalRateLimitClientImpl(const Grpc::GrpcServiceConfigWithHashKey& config_with_hash_key, Server::Configuration::FactoryContext& context, absl::string_view domain_name, @@ -75,6 +78,11 @@ class GlobalRateLimitClientImpl : public Grpc::AsyncStreamCallbacks< void onReceiveTrailingMetadata(Http::ResponseTrailerMapPtr&&) override {} void onRemoteClose(Grpc::Status::GrpcStatus status, const std::string& message) override; + // DeferredDeletable + // Cleanup resources that have to be deleted on the main thread before this deferred deletion. + // Not thread-safe & should only be called by the main thread. + void deleteIsPending() override; + // Functions needed by LocalRateLimitClientImpl to make unsafe modifications // to global resources. All are non-blocking & safely callable by worker // threads and make unsafe changes by ensuring that all such changes are done @@ -153,9 +161,9 @@ class GlobalRateLimitClientImpl : public Grpc::AsyncStreamCallbacks< // Domain from filter configuration. The same domain name throughout the // whole lifetime of client. std::string domain_name_; - // Client is stored as the bare object since there is no ownership transfer - // involved. - GrpcAsyncClient aync_client_; + // Client is stored as the bare object since GrpcAsyncClient already takes ownership of the given + // raw AsyncClientPtr. + GrpcAsyncClient async_client_; Grpc::AsyncStream stream_{}; // Reference to TLS slot for the global quota bucket cache. It outlives @@ -180,29 +188,18 @@ class GlobalRateLimitClientImpl : public Grpc::AsyncStreamCallbacks< * Create a shared rate limit client. It should be shared to each worker * thread via TLS. */ -inline std::shared_ptr +inline std::unique_ptr createGlobalRateLimitClientImpl(Server::Configuration::FactoryContext& context, absl::string_view domain_name, std::chrono::milliseconds send_reports_interval, ThreadLocal::TypedSlot& buckets_tls, - Grpc::GrpcServiceConfigWithHashKey& config_with_hash_key) { + const Grpc::GrpcServiceConfigWithHashKey& config_with_hash_key) { Envoy::Event::Dispatcher& main_dispatcher = context.serverFactoryContext().mainThreadDispatcher(); - return std::make_shared(config_with_hash_key, context, domain_name, + return std::make_unique(config_with_hash_key, context, domain_name, send_reports_interval, buckets_tls, main_dispatcher); } -struct ThreadLocalGlobalRateLimitClientImpl : public Envoy::ThreadLocal::ThreadLocalObject, - Logger::Loggable { -public: - ThreadLocalGlobalRateLimitClientImpl(std::shared_ptr global_client) - : global_client(global_client) {} - - // Thread-unsafe operations like index creation should only be done by the - // global ThreadLocalClient. - std::shared_ptr global_client; -}; - } // namespace RateLimitQuota } // namespace HttpFilters } // namespace Extensions diff --git a/source/extensions/filters/http/rate_limit_quota/matcher.h b/source/extensions/filters/http/rate_limit_quota/matcher.h index 38eb7ee3921c7..ee6fc0a21bdc1 100644 --- a/source/extensions/filters/http/rate_limit_quota/matcher.h +++ b/source/extensions/filters/http/rate_limit_quota/matcher.h @@ -51,17 +51,15 @@ class RateLimitOnMatchActionFactory : public Matcher::ActionFactory(config, validation_visitor); - return [bucket_settings = std::move(bucket_settings)]() { - return std::make_unique(std::move(bucket_settings)); - }; + return std::make_shared(std::move(bucket_settings)); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { diff --git a/source/extensions/filters/http/ratelimit/config.cc b/source/extensions/filters/http/ratelimit/config.cc index 0f07f356ee907..d69d2d6d44bfd 100644 --- a/source/extensions/filters/http/ratelimit/config.cc +++ b/source/extensions/filters/http/ratelimit/config.cc @@ -26,7 +26,7 @@ absl::StatusOr RateLimitFilterConfig::createFilterFactory absl::Status status = absl::OkStatus(); FilterConfigSharedPtr filter_config(new FilterConfig(proto_config, server_context.localInfo(), context.scope(), server_context.runtime(), - server_context.httpContext(), status)); + server_context, status)); RETURN_IF_NOT_OK_REF(status); const std::chrono::milliseconds timeout = std::chrono::milliseconds(PROTOBUF_GET_MS_OR_DEFAULT(proto_config, timeout, 20)); diff --git a/source/extensions/filters/http/ratelimit/ratelimit.cc b/source/extensions/filters/http/ratelimit/ratelimit.cc index 96e2f0b6f3369..b334fad497f22 100644 --- a/source/extensions/filters/http/ratelimit/ratelimit.cc +++ b/source/extensions/filters/http/ratelimit/ratelimit.cc @@ -96,6 +96,12 @@ void Filter::populateRateLimitDescriptors(std::vectorhasRateLimitConfigs()) { + config_->populateDescriptors(headers, callbacks_->streamInfo(), descriptors, on_stream_done); + return; + } + // Get all applicable rate limit policy entries for the route. populateRateLimitDescriptorsForPolicy(route_entry->rateLimitPolicy(), descriptors, headers, on_stream_done); @@ -127,11 +133,11 @@ double Filter::getHitAddend() { } Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + request_headers_ = &headers; if (!config_->enabled()) { return Http::FilterHeadersStatus::Continue; } - request_headers_ = &headers; initiateCall(headers); return (state_ == State::Calling || state_ == State::Responded) ? Http::FilterHeadersStatus::StopIteration @@ -184,15 +190,22 @@ void Filter::onDestroy() { if (state_ == State::Calling) { state_ = State::Complete; client_->cancel(); - } else if (client_ != nullptr) { + } else if (client_ != nullptr && request_headers_ != nullptr) { std::vector descriptors; populateRateLimitDescriptors(descriptors, *request_headers_, true); if (!descriptors.empty()) { + // If the limit() call fails directly then the callback and client will be destroyed + // when calling the limit() function. To make sure we can call the detach() function + // safely, we convert the client_ to a shared_ptr. + + std::shared_ptr shared_client = std::move(client_); // Since this filter is being destroyed, we need to keep the client alive until the request // is complete by leaking the client with OnStreamDoneCallBack. - auto callback = new OnStreamDoneCallBack(std::move(client_)); + auto callback = new OnStreamDoneCallBack(shared_client); callback->client().limit(*callback, getDomain(), descriptors, Tracing::NullSpan::instance(), - absl::nullopt, getHitAddend()); + callbacks_->streamInfo(), getHitAddend()); + // If the limit() call fails directly then the detach() will be no-op. + shared_client->detach(); } } } diff --git a/source/extensions/filters/http/ratelimit/ratelimit.h b/source/extensions/filters/http/ratelimit/ratelimit.h index 688cc85a0338c..39ecfe0822773 100644 --- a/source/extensions/filters/http/ratelimit/ratelimit.h +++ b/source/extensions/filters/http/ratelimit/ratelimit.h @@ -37,6 +37,8 @@ enum class FilterRequestType { Internal, External, Both }; */ enum class VhRateLimitOptions { Override, Include, Ignore }; +using RateLimitConfig = Extensions::Filters::Common::RateLimit::RateLimitConfig; + /** * Global configuration for the HTTP rate limit filter. */ @@ -44,7 +46,8 @@ class FilterConfig { public: FilterConfig(const envoy::extensions::filters::http::ratelimit::v3::RateLimit& config, const LocalInfo::LocalInfo& local_info, Stats::Scope& scope, - Runtime::Loader& runtime, Http::Context& http_context, absl::Status& creation_status) + Runtime::Loader& runtime, Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status) : domain_(config.domain()), stage_(static_cast(config.stage())), request_type_(config.request_type().empty() ? stringToType("both") : stringToType(config.request_type())), @@ -58,7 +61,8 @@ class FilterConfig { config.rate_limited_as_resource_exhausted() ? absl::make_optional(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted) : absl::nullopt), - http_context_(http_context), stat_names_(scope.symbolTable(), config.stat_prefix()), + http_context_(context.httpContext()), + stat_names_(scope.symbolTable(), config.stat_prefix()), rate_limited_status_(toErrorCode(config.rate_limited_status().code())), status_on_error_(toRatelimitServerErrorCode(config.status_on_error().code())), filter_enabled_( @@ -80,6 +84,8 @@ class FilterConfig { Envoy::Router::HeaderParser::configure(config.response_headers_to_add()); SET_AND_RETURN_IF_NOT_OK(response_headers_parser_or_.status(), creation_status); response_headers_parser_ = std::move(response_headers_parser_or_.value()); + rate_limit_config_ = std::make_unique( + config.rate_limits(), context, creation_status); } const std::string& domain() const { return domain_; } @@ -106,6 +112,18 @@ class FilterConfig { Http::Code statusOnError() const { return status_on_error_; } bool enabled() const; bool enforced() const; + bool hasRateLimitConfigs() const { + ASSERT(rate_limit_config_ != nullptr); + return !rate_limit_config_->empty(); + } + void populateDescriptors(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info, + Filters::Common::RateLimit::RateLimitDescriptors& descriptors, + bool on_stream_done) const { + ASSERT(rate_limit_config_ != nullptr); + rate_limit_config_->populateDescriptors(headers, info, local_info_.clusterName(), descriptors, + on_stream_done); + } private: static FilterRequestType stringToType(const std::string& request_type) { @@ -153,6 +171,7 @@ class FilterConfig { const absl::optional filter_enabled_; const absl::optional filter_enforced_; const absl::optional failure_mode_deny_percent_; + std::unique_ptr rate_limit_config_; }; using FilterConfigSharedPtr = std::shared_ptr; @@ -195,7 +214,7 @@ class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { const envoy::extensions::filters::http::ratelimit::v3::RateLimitPerRoute::VhRateLimitsOptions vh_rate_limits_; const std::string domain_; - std::unique_ptr rate_limit_config_; + std::unique_ptr rate_limit_config_; }; using FilterConfigPerRouteSharedPtr = std::shared_ptr; @@ -273,7 +292,8 @@ class Filter : public Http::StreamFilter, public Filters::Common::RateLimit::Req */ class OnStreamDoneCallBack : public Filters::Common::RateLimit::RequestCallbacks { public: - OnStreamDoneCallBack(Filters::Common::RateLimit::ClientPtr client) : client_(std::move(client)) {} + OnStreamDoneCallBack(std::shared_ptr client) + : client_(std::move(client)) {} ~OnStreamDoneCallBack() override = default; // RateLimit::RequestCallbacks @@ -285,7 +305,7 @@ class OnStreamDoneCallBack : public Filters::Common::RateLimit::RequestCallbacks Filters::Common::RateLimit::Client& client() { return *client_; } private: - Filters::Common::RateLimit::ClientPtr client_; + std::shared_ptr client_; }; } // namespace RateLimitFilter diff --git a/source/extensions/filters/http/rbac/rbac_filter.cc b/source/extensions/filters/http/rbac/rbac_filter.cc index 164b0d55b3449..ab49448d494c9 100644 --- a/source/extensions/filters/http/rbac/rbac_filter.cc +++ b/source/extensions/filters/http/rbac/rbac_filter.cc @@ -146,7 +146,7 @@ RoleBasedAccessControlRouteSpecificFilterConfig::RoleBasedAccessControlRouteSpec // Evaluates the shadow engine policy and updates metrics accordingly bool RoleBasedAccessControlFilter::evaluateShadowEngine(const Http::RequestHeaderMap& headers, - ProtobufWkt::Struct& metrics) const { + Protobuf::Struct& metrics) const { const auto shadow_engine = config_->engine(callbacks_, Filters::Common::RBAC::EnforcementMode::Shadow); if (shadow_engine == nullptr) { @@ -193,7 +193,7 @@ bool RoleBasedAccessControlFilter::evaluateShadowEngine(const Http::RequestHeade // Evaluates the enforced engine policy and returns the appropriate filter status Http::FilterHeadersStatus RoleBasedAccessControlFilter::evaluateEnforcedEngine(Http::RequestHeaderMap& headers, - ProtobufWkt::Struct& metrics) const { + Protobuf::Struct& metrics) const { const auto engine = config_->engine(callbacks_, Filters::Common::RBAC::EnforcementMode::Enforced); if (engine == nullptr) { return Http::FilterHeadersStatus::Continue; @@ -263,7 +263,7 @@ RoleBasedAccessControlFilter::decodeHeaders(Http::RequestHeaderMap& headers, boo headers, callbacks_->streamInfo().dynamicMetadata().DebugString()); // Create metrics structure to hold results - ProtobufWkt::Struct metrics; + Protobuf::Struct metrics; // Evaluate shadow engine if it exists const bool shadow_engine_evaluated = evaluateShadowEngine(headers, metrics); diff --git a/source/extensions/filters/http/rbac/rbac_filter.h b/source/extensions/filters/http/rbac/rbac_filter.h index c5432c3b435fc..f0dc61074a269 100644 --- a/source/extensions/filters/http/rbac/rbac_filter.h +++ b/source/extensions/filters/http/rbac/rbac_filter.h @@ -126,12 +126,11 @@ class RoleBasedAccessControlFilter final : public Http::StreamDecoderFilter, private: // Handles shadow engine evaluation and updates metrics - bool evaluateShadowEngine(const Http::RequestHeaderMap& headers, - ProtobufWkt::Struct& metrics) const; + bool evaluateShadowEngine(const Http::RequestHeaderMap& headers, Protobuf::Struct& metrics) const; // Handles enforced engine evaluation and updates metrics Http::FilterHeadersStatus evaluateEnforcedEngine(Http::RequestHeaderMap& headers, - ProtobufWkt::Struct& metrics) const; + Protobuf::Struct& metrics) const; RoleBasedAccessControlFilterConfigSharedPtr config_; Http::StreamDecoderFilterCallbacks* callbacks_{}; diff --git a/source/extensions/filters/http/reverse_conn/BUILD b/source/extensions/filters/http/reverse_conn/BUILD new file mode 100644 index 0000000000000..cc94afbd25467 --- /dev/null +++ b/source/extensions/filters/http/reverse_conn/BUILD @@ -0,0 +1,45 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + visibility = ["//visibility:public"], + deps = [ + ":reverse_conn_filter_lib", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/reverse_conn/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "reverse_conn_filter_lib", + srcs = ["reverse_conn_filter.cc"], + hdrs = ["reverse_conn_filter.h"], + visibility = ["//visibility:public"], + deps = [ + "//envoy/http:filter_interface", + "//envoy/server:filter_config_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/http:codes_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "//source/common/json:json_loader_lib", + "//source/common/network:connection_socket_lib", + "//source/common/network:filter_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_handshake_cc_proto", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "@envoy_api//envoy/extensions/filters/http/reverse_conn/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/reverse_conn/config.cc b/source/extensions/filters/http/reverse_conn/config.cc new file mode 100644 index 0000000000000..93a7a9767f3be --- /dev/null +++ b/source/extensions/filters/http/reverse_conn/config.cc @@ -0,0 +1,36 @@ +#include "source/extensions/filters/http/reverse_conn/config.h" + +#include "envoy/registry/registry.h" + +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/filters/http/reverse_conn/reverse_conn_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ReverseConn { + +Http::FilterFactoryCb ReverseConnFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::reverse_conn::v3::ReverseConn& proto_config, + const std::string&, Server::Configuration::FactoryContext&) { + ReverseConnFilterConfigSharedPtr config = + std::make_shared(ReverseConnFilterConfig(proto_config)); + + // The filter now uses the upstream socket interface directly, no need for registry + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter(std::make_shared(config)); + }; +} + +/** + * Static registration for the reverse_conn filter. @see RegisterFactory. + */ +static Envoy::Registry::RegisterFactory + register_; + +} // namespace ReverseConn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/reverse_conn/config.h b/source/extensions/filters/http/reverse_conn/config.h new file mode 100644 index 0000000000000..b9a29a45959de --- /dev/null +++ b/source/extensions/filters/http/reverse_conn/config.h @@ -0,0 +1,30 @@ +#pragma once + +#include "envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.pb.h" +#include "envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.pb.validate.h" + +#include "source/extensions/filters/http/common/factory_base.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ReverseConn { + +/** + * Config registration for the reverse_conn filter. @see NamedHttpFilterConfigFactory. + */ +class ReverseConnFilterConfigFactory + : public Common::FactoryBase { +public: + ReverseConnFilterConfigFactory() : FactoryBase("reverse_conn") {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::reverse_conn::v3::ReverseConn& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace ReverseConn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/reverse_conn/reverse_conn_filter.cc b/source/extensions/filters/http/reverse_conn/reverse_conn_filter.cc new file mode 100644 index 0000000000000..d2b35fb058fa4 --- /dev/null +++ b/source/extensions/filters/http/reverse_conn/reverse_conn_filter.cc @@ -0,0 +1,457 @@ +#include "source/extensions/filters/http/reverse_conn/reverse_conn_filter.h" + +#include "envoy/http/codes.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/empty_string.h" +#include "source/common/common/enum_to_int.h" +#include "source/common/common/logger.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/http/headers.h" +#include "source/common/http/message_impl.h" +#include "source/common/http/utility.h" +#include "source/common/json/json_loader.h" +#include "source/common/network/connection_socket_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ReverseConn { + +// Using statement for the new proto namespace +namespace ReverseConnectionHandshake = envoy::extensions::bootstrap::reverse_tunnel; + +const std::string ReverseConnFilter::reverse_connections_path = "/reverse_connections"; +const std::string ReverseConnFilter::reverse_connections_request_path = + "/reverse_connections/request"; +const std::string ReverseConnFilter::node_id_param = "node_id"; +const std::string ReverseConnFilter::cluster_id_param = "cluster_id"; +const std::string ReverseConnFilter::tenant_id_param = "tenant_id"; +const std::string ReverseConnFilter::role_param = "role"; +const std::string ReverseConnFilter::rc_accepted_response = "reverse connection accepted"; + +ReverseConnFilter::ReverseConnFilter(ReverseConnFilterConfigSharedPtr config) + : config_(config), is_accept_request_(false), accept_rev_conn_proto_(Buffer::OwnedImpl()) {} + +ReverseConnFilter::~ReverseConnFilter() {} + +void ReverseConnFilter::onDestroy() {} + +std::string ReverseConnFilter::getQueryParam(const std::string& key) { + if (query_params_.data().empty()) { + query_params_ = Http::Utility::QueryParamsMulti::parseQueryString( + request_headers_->Path()->value().getStringView()); + } + auto item = query_params_.getFirstValue(key); + if (item.has_value()) { + return item.value(); + } else { + return ""; + } +} + +void ReverseConnFilter::getClusterDetailsUsingProtobuf(std::string* node_uuid, + std::string* cluster_uuid, + std::string* tenant_uuid) { + + ReverseConnectionHandshake::ReverseConnHandshakeArg arg; + const std::string request_body = accept_rev_conn_proto_.toString(); + ENVOY_STREAM_LOG(debug, "Received protobuf request body length: {}", *decoder_callbacks_, + request_body.length()); + if (!arg.ParseFromString(request_body)) { + ENVOY_STREAM_LOG(error, "Failed to parse protobuf from request body", *decoder_callbacks_); + return; + } + ENVOY_STREAM_LOG(debug, "Successfully parsed protobuf: {}", *decoder_callbacks_, + arg.DebugString()); + ENVOY_STREAM_LOG(debug, "Extracted values - tenant='{}', cluster='{}', node='{}'", + *decoder_callbacks_, arg.tenant_uuid(), arg.cluster_uuid(), arg.node_uuid()); + + if (node_uuid) { + *node_uuid = arg.node_uuid(); + } + if (cluster_uuid) { + *cluster_uuid = arg.cluster_uuid(); + } + if (tenant_uuid) { + *tenant_uuid = arg.tenant_uuid(); + } +} + +Http::FilterDataStatus ReverseConnFilter::acceptReverseConnection() { + std::string node_uuid, cluster_uuid, tenant_uuid; + + ReverseConnectionHandshake::ReverseConnHandshakeRet ret; + getClusterDetailsUsingProtobuf(&node_uuid, &cluster_uuid, &tenant_uuid); + if (node_uuid.empty()) { + ret.set_status(ReverseConnectionHandshake::ReverseConnHandshakeRet::REJECTED); + ret.set_status_message("Failed to parse request message or required fields missing"); + decoder_callbacks_->sendLocalReply(Http::Code::BadGateway, ret.SerializeAsString(), nullptr, + absl::nullopt, ""); + return Http::FilterDataStatus::StopIterationNoBuffer; + } + + Network::Connection* connection = + &const_cast(*decoder_callbacks_->connection()); + Envoy::Ssl::ConnectionInfoConstSharedPtr ssl = connection->ssl(); + ENVOY_STREAM_LOG( + info, + "Received accept reverse connection request. tenant '{}', cluster '{}', node '{}' FD: {}", + *decoder_callbacks_, tenant_uuid, cluster_uuid, node_uuid, + connection->getSocket()->ioHandle().fdDoNotUse()); + + if ((ssl != nullptr) && (ssl->peerCertificatePresented())) { + absl::Span dnsSans = ssl->dnsSansPeerCertificate(); + for (const std::string& dns : dnsSans) { + auto parts = StringUtil::splitToken(dns, "="); + if (parts.size() == 2) { + if (parts[0] == "tenantId") { + tenant_uuid = std::string(parts[1]); + } else if (parts[0] == "clusterId") { + cluster_uuid = std::string(parts[1]); + } + } + } + } + + ENVOY_STREAM_LOG(info, "Accepting reverse connection", *decoder_callbacks_); + ret.set_status(ReverseConnectionHandshake::ReverseConnHandshakeRet::ACCEPTED); + ENVOY_STREAM_LOG(info, "return value", *decoder_callbacks_); + + // Create response with explicit Content-Length + std::string response_body = ret.SerializeAsString(); + ENVOY_STREAM_LOG(info, "Response body length: {}, content: '{}'", *decoder_callbacks_, + response_body.length(), response_body); + ENVOY_STREAM_LOG(info, "Protobuf debug string: '{}'", *decoder_callbacks_, ret.DebugString()); + + decoder_callbacks_->sendLocalReply( + Http::Code::OK, response_body, + [&response_body](Http::ResponseHeaderMap& headers) { + headers.setContentType("application/octet-stream"); + headers.setContentLength(response_body.length()); + headers.setConnection("close"); + }, + absl::nullopt, ""); + + ENVOY_STREAM_LOG(info, "DEBUG: About to save connection with node_uuid='{}' cluster_uuid='{}'", + *decoder_callbacks_, node_uuid, cluster_uuid); + saveDownstreamConnection(*connection, node_uuid, cluster_uuid); + + // Reset file events on the connection socket + if (connection->getSocket()) { + connection->getSocket()->ioHandle().resetFileEvents(); + } + + connection->close(Network::ConnectionCloseType::NoFlush, "accepted_reverse_conn"); + return Http::FilterDataStatus::StopIterationNoBuffer; +} + +Http::FilterHeadersStatus ReverseConnFilter::getReverseConnectionInfo() { + // Determine role based on query param or auto-detect from available interfaces + std::string role = getQueryParam(role_param); + if (role.empty()) { + role = determineRole(); + ENVOY_LOG(debug, "Auto-detected role: {}", role); + } + + bool is_responder = (role == "responder" || role == "both"); + bool is_initiator = (role == "initiator" || role == "both"); + + const std::string& remote_node = getQueryParam(node_id_param); + const std::string& remote_cluster = getQueryParam(cluster_id_param); + ENVOY_LOG( + info, + "Received reverse connection info request with role: {}, remote node: {}, remote cluster: {}", + role, remote_node, remote_cluster); + + // Handle based on role + if (is_responder) { + return handleResponderInfo(remote_node, remote_cluster); + } else if (is_initiator) { + return handleInitiatorInfo(remote_node, remote_cluster); + } else { + ENVOY_LOG(error, "Unknown role: {}", role); + decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Unknown role", nullptr, + absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } +} + +Http::FilterHeadersStatus +ReverseConnFilter::handleResponderInfo(const std::string& remote_node, + const std::string& remote_cluster) { + ENVOY_LOG(debug, + "ReverseConnFilter: Received reverse connection info request with remote_node: {} " + "remote_cluster: {}", + remote_node, remote_cluster); + + // Production-ready cross-thread aggregation + auto* upstream_extension = getUpstreamSocketInterfaceExtension(); + if (upstream_extension == nullptr) { + ENVOY_LOG(error, "No upstream extension available for stats collection"); + std::string response = R"({"accepted":[],"connected":[]})"; + decoder_callbacks_->sendLocalReply(Http::Code::OK, response, nullptr, absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } + + // For specific node or cluster query + if (!remote_node.empty() || !remote_cluster.empty()) { + // Get connection count for specific remote node/cluster using stats + auto stats_map = upstream_extension->getCrossWorkerStatMap(); + size_t num_connections = 0; + + if (!remote_node.empty()) { + std::string node_stat_name = fmt::format("reverse_connections.nodes.{}", remote_node); + // Search for the stat with scope prefix since getCrossWorkerStatMap returns full stat names + for (const auto& [stat_name, value] : stats_map) { + if (stat_name.find(node_stat_name) != std::string::npos) { + num_connections = value; + break; + } + } + } else { + std::string cluster_stat_name = + fmt::format("reverse_connections.clusters.{}", remote_cluster); + // Search for the stat with scope prefix since getCrossWorkerStatMap returns full stat names + for (const auto& [stat_name, value] : stats_map) { + if (stat_name.find(cluster_stat_name) != std::string::npos) { + num_connections = value; + break; + } + } + } + + std::string response = fmt::format("{{\"available_connections\":{}}}", num_connections); + ENVOY_LOG(info, "handleResponderInfo response for {}: {}", + remote_node.empty() ? remote_cluster : remote_node, response); + decoder_callbacks_->sendLocalReply(Http::Code::OK, response, nullptr, absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } + + ENVOY_LOG(debug, "ReverseConnFilter: Using upstream socket manager to get connection stats"); + + auto [connected_nodes, accepted_connections] = + upstream_extension->getConnectionStatsSync(std::chrono::milliseconds(1000)); + + // Convert vectors to lists for JSON serialization + std::list accepted_connections_list(accepted_connections.begin(), + accepted_connections.end()); + std::list connected_nodes_list(connected_nodes.begin(), connected_nodes.end()); + + ENVOY_LOG(debug, "Stats aggregation completed: {} connected nodes, {} accepted connections", + connected_nodes.size(), accepted_connections.size()); + + // Create JSON response + std::string response = fmt::format("{{\"accepted\":{},\"connected\":{}}}", + Json::Factory::listAsJsonString(accepted_connections_list), + Json::Factory::listAsJsonString(connected_nodes_list)); + + ENVOY_LOG(info, "handleResponderInfo production stats-based response: {}", response); + decoder_callbacks_->sendLocalReply(Http::Code::OK, response, nullptr, absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; +} + +Http::FilterHeadersStatus +ReverseConnFilter::handleInitiatorInfo(const std::string& remote_node, + const std::string& remote_cluster) { + ENVOY_LOG(debug, "Getting reverse connection info for initiator role"); + + // Check if downstream socket interface is available + auto* downstream_interface = getDownstreamSocketInterface(); + if (downstream_interface == nullptr) { + ENVOY_LOG(error, "Failed to get downstream socket interface for initiator role"); + decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, + "Failed to get downstream socket interface", nullptr, + absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } + + // Get the downstream socket interface extension to check established connections + auto* downstream_extension = getDownstreamSocketInterfaceExtension(); + if (downstream_extension == nullptr) { + ENVOY_LOG(error, "Failed to get downstream socket interface extension for initiator role"); + std::string response = R"({"accepted":[],"connected":[]})"; + decoder_callbacks_->sendLocalReply(Http::Code::OK, response, nullptr, absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } + + // For specific node or cluster query + if (!remote_node.empty() || !remote_cluster.empty()) { + // Get connection count for specific remote node/cluster using stats + // For initiator, stats format includes state suffix: reverse_connections.nodes..connected + auto stats_map = downstream_extension->getCrossWorkerStatMap(); + size_t num_connections = 0; + + if (!remote_node.empty()) { + std::string node_stat_name = + fmt::format("reverse_connections.host.{}.connected", remote_node); + // Search for the stat with scope prefix since getCrossWorkerStatMap returns full stat names + for (const auto& [stat_name, value] : stats_map) { + if (stat_name.find(node_stat_name) != std::string::npos) { + num_connections = value; + break; + } + } + } else { + std::string cluster_stat_name = + fmt::format("reverse_connections.cluster.{}.connected", remote_cluster); + // Search for the stat with scope prefix since getCrossWorkerStatMap returns full stat names + for (const auto& [stat_name, value] : stats_map) { + if (stat_name.find(cluster_stat_name) != std::string::npos) { + num_connections = value; + break; + } + } + } + + std::string response = fmt::format("{{\"available_connections\":{}}}", num_connections); + ENVOY_LOG(info, "handleInitiatorInfo response for {}: {}", + remote_node.empty() ? remote_cluster : remote_node, response); + decoder_callbacks_->sendLocalReply(Http::Code::OK, response, nullptr, absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; + } + + ENVOY_LOG(debug, "ReverseConnFilter: Using downstream socket manager to get connection stats"); + + // Use the production stats-based approach with Envoy's proven stats system + auto [connected_nodes, accepted_connections] = + downstream_extension->getConnectionStatsSync(std::chrono::milliseconds(1000)); + + // Convert vectors to lists for JSON serialization + std::list accepted_connections_list(accepted_connections.begin(), + accepted_connections.end()); + std::list connected_nodes_list(connected_nodes.begin(), connected_nodes.end()); + + ENVOY_LOG(debug, "Stats aggregation completed: {} connected nodes, {} accepted connections", + connected_nodes.size(), accepted_connections.size()); + + // Create production-ready JSON response for multi-tenant environment + std::string response = fmt::format("{{\"accepted\":{},\"connected\":{}}}", + Json::Factory::listAsJsonString(accepted_connections_list), + Json::Factory::listAsJsonString(connected_nodes_list)); + + ENVOY_LOG(info, "handleInitiatorInfo production stats-based response: {}", response); + decoder_callbacks_->sendLocalReply(Http::Code::OK, response, nullptr, absl::nullopt, ""); + return Http::FilterHeadersStatus::StopIteration; +} + +Http::FilterHeadersStatus ReverseConnFilter::decodeHeaders(Http::RequestHeaderMap& request_headers, + bool) { + // check that request path starts with "/reverse_connections" + const absl::string_view request_path = request_headers.Path()->value().getStringView(); + const bool should_intercept_request = + matchRequestPath(request_path, ReverseConnFilter::reverse_connections_path); + + if (!should_intercept_request) { + ENVOY_STREAM_LOG(trace, "Not intercepting HTTP request for path ", *decoder_callbacks_, + request_path); + return Http::FilterHeadersStatus::Continue; + } + + ENVOY_STREAM_LOG(trace, "Intercepting HTTP request for path ", *decoder_callbacks_, request_path); + request_headers_ = &request_headers; + + const absl::string_view method = request_headers.Method()->value().getStringView(); + if (method == Http::Headers::get().MethodValues.Post) { + is_accept_request_ = + matchRequestPath(request_path, ReverseConnFilter::reverse_connections_request_path); + if (is_accept_request_) { + absl::string_view length = + request_headers_->get(Http::Headers::get().ContentLength)[0]->value().getStringView(); + expected_proto_size_ = static_cast(std::stoi(std::string(length))); + ENVOY_STREAM_LOG(info, "Expecting a reverse connection accept request with {} bytes", + *decoder_callbacks_, length); + return Http::FilterHeadersStatus::StopIteration; + } + } else if (method == Http::Headers::get().MethodValues.Get) { + return getReverseConnectionInfo(); + } + return Http::FilterHeadersStatus::Continue; +} + +bool ReverseConnFilter::matchRequestPath(const absl::string_view& request_path, + const std::string& api_path) { + if (request_path.compare(0, api_path.size(), api_path) == 0) { + return true; + } + return false; +} + +void ReverseConnFilter::saveDownstreamConnection(Network::Connection& downstream_connection, + const std::string& node_id, + const std::string& cluster_id) { + ENVOY_STREAM_LOG(debug, "Adding downstream connection socket to upstream socket manager", + *decoder_callbacks_); + + auto* socket_manager = getUpstreamSocketManager(); + if (!socket_manager) { + ENVOY_STREAM_LOG(error, "Failed to get upstream socket manager", *decoder_callbacks_); + return; + } + + // Instead of moving the socket, duplicate the file descriptor + const Network::ConnectionSocketPtr& original_socket = downstream_connection.getSocket(); + if (!original_socket || !original_socket->isOpen()) { + ENVOY_STREAM_LOG(error, "Original socket is not available or not open", *decoder_callbacks_); + return; + } + + // Duplicate the file descriptor + Network::IoHandlePtr duplicated_handle = original_socket->ioHandle().duplicate(); + if (!duplicated_handle || !duplicated_handle->isOpen()) { + ENVOY_STREAM_LOG(error, "Failed to duplicate file descriptor", *decoder_callbacks_); + return; + } + + ENVOY_STREAM_LOG(debug, + "Successfully duplicated file descriptor: original_fd={}, duplicated_fd={}", + *decoder_callbacks_, original_socket->ioHandle().fdDoNotUse(), + duplicated_handle->fdDoNotUse()); + + // Create a new socket with the duplicated handle + Network::ConnectionSocketPtr duplicated_socket = std::make_unique( + std::move(duplicated_handle), original_socket->connectionInfoProvider().localAddress(), + original_socket->connectionInfoProvider().remoteAddress()); + + // Reset file events on the duplicated socket to clear any inherited events + duplicated_socket->ioHandle().resetFileEvents(); + + // Add the duplicated socket to the manager + socket_manager->addConnectionSocket(node_id, cluster_id, std::move(duplicated_socket), + config_->pingInterval(), false /* rebalanced */); + + ENVOY_STREAM_LOG(debug, + "Successfully added duplicated socket to upstream socket manager. Original " + "connection remains functional.", + *decoder_callbacks_); +} + +Http::FilterDataStatus ReverseConnFilter::decodeData(Buffer::Instance& data, bool) { + if (is_accept_request_) { + accept_rev_conn_proto_.move(data); + if (expected_proto_size_ > 0 && accept_rev_conn_proto_.length() < expected_proto_size_) { + ENVOY_STREAM_LOG(debug, + "Waiting for more data, expected_proto_size_={}, current_buffer_size={}", + *decoder_callbacks_, expected_proto_size_, accept_rev_conn_proto_.length()); + return Http::FilterDataStatus::StopIterationAndBuffer; + } else { + return acceptReverseConnection(); + } + } + return Http::FilterDataStatus::Continue; +} + +Http::FilterTrailersStatus ReverseConnFilter::decodeTrailers(Http::RequestTrailerMap&) { + return Http::FilterTrailersStatus::Continue; +} + +void ReverseConnFilter::setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) { + decoder_callbacks_ = &callbacks; +} + +} // namespace ReverseConn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/reverse_conn/reverse_conn_filter.h b/source/extensions/filters/http/reverse_conn/reverse_conn_filter.h new file mode 100644 index 0000000000000..74b20715f6f54 --- /dev/null +++ b/source/extensions/filters/http/reverse_conn/reverse_conn_filter.h @@ -0,0 +1,269 @@ +#pragma once + +#include "envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.pb.h" +#include "envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.pb.validate.h" +#include "envoy/http/async_client.h" +#include "envoy/http/filter.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/http/headers.h" +#include "source/common/http/utility.h" +#include "source/common/network/filter_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_handshake.pb.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +#include "absl/types/optional.h" + +namespace Envoy { + +namespace Http { +namespace Utility { +using QueryParams = std::map; +std::string queryParamsToString(const QueryParams& query_params); +} // namespace Utility +} // namespace Http + +namespace Extensions { +namespace HttpFilters { +namespace ReverseConn { + +namespace ReverseConnection = Envoy::Extensions::Bootstrap::ReverseConnection; + +using ClusterNodeStorage = absl::flat_hash_map>; +using ClusterNodeStorageSharedPtr = std::shared_ptr; + +using TenantClusterStorage = absl::flat_hash_map>; +using TenantClusterStorageSharedPtr = std::shared_ptr; + +class ReverseConnFilterConfig { +public: + ReverseConnFilterConfig( + const envoy::extensions::filters::http::reverse_conn::v3::ReverseConn& config) + : ping_interval_(getValidPingInterval(config)) {} + + std::chrono::seconds pingInterval() const { return ping_interval_; } + +private: + static std::chrono::seconds getValidPingInterval( + const envoy::extensions::filters::http::reverse_conn::v3::ReverseConn& config) { + if (config.has_ping_interval()) { + uint32_t value = config.ping_interval().value(); + // If ping_interval is explicitly set to 0, use default value + return value > 0 ? std::chrono::seconds(value) : std::chrono::seconds(2); + } + // If ping_interval is not set, use default value + return std::chrono::seconds(2); + } + + const std::chrono::seconds ping_interval_; +}; + +using ReverseConnFilterConfigSharedPtr = std::shared_ptr; +static const char CRLF[] = "\r\n"; +static const char DOUBLE_CRLF[] = "\r\n\r\n"; + +/** + * Reverse connection filter for HTTP handshake processing. + * This filter handles HTTP requests for reverse tunnel handshakes. + */ +class ReverseConnFilter : Logger::Loggable, public Http::StreamDecoderFilter { + friend class ReverseConnFilterTest; + +public: + ReverseConnFilter(ReverseConnFilterConfigSharedPtr config); + ~ReverseConnFilter(); + + // Http::StreamFilterBase + void onDestroy() override; + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override; + Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap&) override; + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks&) override; + + static const std::string reverse_connections_path; + static const std::string reverse_connections_request_path; + static const std::string stats_path; + static const std::string tenant_path; + static const std::string node_id_param; + static const std::string cluster_id_param; + static const std::string tenant_id_param; + static const std::string role_param; + static const std::string rc_accepted_response; + +private: + void saveDownstreamConnection(Network::Connection& downstream_connection, + const std::string& node_id, const std::string& cluster_id); + std::string getQueryParam(const std::string& key); + // API to get reverse connection information for the local envoy. + // The API accepts the following headers: + // - role: The role of the local envoy; can be either initiator or acceptor. + // - node_id: The node ID of the remote envoy. + // - cluster_id: The cluster ID of the remote envoy. + // For info about the established reverse connections with the local envoy + // as initiator, the API expects the cluster ID or node ID of the remote envoy. + // For info about the reverse connections accepted by the local envoy as responder, + // the API expects the cluster ID of the remote envoy that initiated the connections. + // In both the above cases, the API returns a JSON response in the format: + // "{available_connections: }" + // In the default case (a request param is not provided), the API returns a JSON + // object with the full list of nodes/clusters with which reverse connections are present + // in the format: {"accepted": ["cluster_1", "cluster_2"], "connected": ["cluster_3"]}. + Http::FilterHeadersStatus getReverseConnectionInfo(); + + // Handle reverse connection info for responder role (uses upstream socket manager) + Http::FilterHeadersStatus handleResponderInfo(const std::string& remote_node, + const std::string& remote_cluster); + + // Handle reverse connection info for initiator role (uses downstream socket interface) + Http::FilterHeadersStatus handleInitiatorInfo(const std::string& remote_node, + const std::string& remote_cluster); + // API to accept a reverse connection request. The handler obtains the cluster, tenant, etc + // from the query parameters from the request and calls the UpstreamSocketManager to cache + // the socket. + Http::FilterDataStatus acceptReverseConnection(); + + // Gets the details of the remote cluster such as the node UUID, cluster UUID, + // and tenant UUID from the protobuf payload and populates them in the corresponding + // out parameters. + void getClusterDetailsUsingProtobuf(std::string* node_uuid, std::string* cluster_uuid, + std::string* tenant_uuid); + + bool matchRequestPath(const absl::string_view& request_path, const std::string& api_path); + + // Get the upstream socket manager from the thread-local registry + ReverseConnection::UpstreamSocketManager* getUpstreamSocketManager() { + auto* upstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (!upstream_interface) { + ENVOY_LOG(debug, "Upstream reverse socket interface not found"); + return nullptr; + } + + auto* upstream_socket_interface = + dynamic_cast(upstream_interface); + if (!upstream_socket_interface) { + ENVOY_LOG(error, "Failed to cast to ReverseTunnelAcceptor"); + return nullptr; + } + + auto* tls_registry = upstream_socket_interface->getLocalRegistry(); + if (!tls_registry) { + ENVOY_LOG(error, "Thread local registry not found for upstream socket interface"); + return nullptr; + } + + return tls_registry->socketManager(); + } + + // Get the downstream socket interface (for initiator role) + const ReverseConnection::ReverseTunnelInitiator* getDownstreamSocketInterface() { + auto* downstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.downstream_socket_interface"); + if (!downstream_interface) { + ENVOY_LOG(debug, "Downstream reverse socket interface not found"); + return nullptr; + } + + auto* downstream_socket_interface = + dynamic_cast(downstream_interface); + if (!downstream_socket_interface) { + ENVOY_LOG(error, "Failed to cast to ReverseTunnelInitiator"); + return nullptr; + } + + return downstream_socket_interface; + } + + // Get the upstream socket interface extension for production cross-thread aggregation + ReverseConnection::ReverseTunnelAcceptorExtension* getUpstreamSocketInterfaceExtension() { + auto* upstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (!upstream_interface) { + ENVOY_LOG(debug, "Upstream reverse socket interface not found"); + return nullptr; + } + + auto* upstream_socket_interface = + dynamic_cast(upstream_interface); + if (!upstream_socket_interface) { + ENVOY_LOG(error, "Failed to cast to ReverseTunnelAcceptor"); + return nullptr; + } + + // Get the extension which provides cross-thread aggregation capabilities + return upstream_socket_interface->getExtension(); + } + + // Get the downstream socket interface extension for production cross-thread aggregation + ReverseConnection::ReverseTunnelInitiatorExtension* getDownstreamSocketInterfaceExtension() { + auto* downstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.downstream_socket_interface"); + if (!downstream_interface) { + ENVOY_LOG(debug, "Downstream reverse socket interface not found"); + return nullptr; + } + + auto* downstream_socket_interface = + dynamic_cast(downstream_interface); + if (!downstream_socket_interface) { + ENVOY_LOG(error, "Failed to cast to ReverseTunnelInitiator"); + return nullptr; + } + + // Get the extension which provides cross-thread aggregation capabilities + return downstream_socket_interface->getExtension(); + } + + // Determine the role of this envoy instance based on available socket interfaces + std::string determineRole() { + auto* upstream_manager = getUpstreamSocketManager(); + auto* downstream_interface = getDownstreamSocketInterface(); + + if (upstream_manager && !downstream_interface) { + return "responder"; // Cloud envoy - accepts reverse connections + } else if (!upstream_manager && downstream_interface) { + return "initiator"; // On-prem envoy - initiates reverse connections + } else if (upstream_manager && downstream_interface) { + return "both"; // Supports both roles + } else { + return "unknown"; // No reverse connection interfaces available + } + } + + const ReverseConnFilterConfigSharedPtr config_; + Http::StreamDecoderFilterCallbacks* decoder_callbacks_; + Network::ClientConnectionPtr connection_; + + Http::RequestHeaderMap* request_headers_; + Http::Utility::QueryParamsMulti query_params_; + + // Cluster where outgoing RC request is being sent to + std::string remote_cluster_id_; + + // True, if the request path indicate that is an accept request that is not + // meant to initiate reverse connections. + bool is_accept_request_; + + // Holds the body size parsed from the Content-length header. Will be used by + // decodeData() to decide if it has to wait for more data before parsing the + // bytes into a protobuf object. + uint32_t expected_proto_size_; + + // Data collection buffer used to maintain all the bytes of the + // serialised 'ReverseConnHandshakeArg' proto. + Buffer::OwnedImpl accept_rev_conn_proto_; +}; + +} // namespace ReverseConn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/set_metadata/set_metadata_filter.cc b/source/extensions/filters/http/set_metadata/set_metadata_filter.cc index 129fba5af7d29..24d695f630b77 100644 --- a/source/extensions/filters/http/set_metadata/set_metadata_filter.cc +++ b/source/extensions/filters/http/set_metadata/set_metadata_filter.cc @@ -60,7 +60,7 @@ Http::FilterHeadersStatus SetMetadataFilter::decodeHeaders(Http::RequestHeaderMa mut_untyped_metadata[entry.metadata_namespace] = entry.value; } else if (entry.allow_overwrite) { // Get the existing metadata at this key for merging. - ProtobufWkt::Struct& orig_fields = mut_untyped_metadata[entry.metadata_namespace]; + Protobuf::Struct& orig_fields = mut_untyped_metadata[entry.metadata_namespace]; const auto& to_merge = entry.value; // Merge the new metadata into the existing metadata. diff --git a/source/extensions/filters/http/set_metadata/set_metadata_filter.h b/source/extensions/filters/http/set_metadata/set_metadata_filter.h index e293690a32abd..9aff8a68650e3 100644 --- a/source/extensions/filters/http/set_metadata/set_metadata_filter.h +++ b/source/extensions/filters/http/set_metadata/set_metadata_filter.h @@ -27,12 +27,12 @@ struct FilterStats { struct UntypedMetadataEntry { bool allow_overwrite{}; std::string metadata_namespace; - ProtobufWkt::Struct value; + Protobuf::Struct value; }; struct TypedMetadataEntry { bool allow_overwrite{}; std::string metadata_namespace; - ProtobufWkt::Any value; + Protobuf::Any value; }; class Config : public ::Envoy::Router::RouteSpecificFilterConfig, public Logger::Loggable { diff --git a/source/extensions/filters/http/stateful_session/stateful_session.cc b/source/extensions/filters/http/stateful_session/stateful_session.cc index 80f4c7525cb21..fd1616352027d 100644 --- a/source/extensions/filters/http/stateful_session/stateful_session.cc +++ b/source/extensions/filters/http/stateful_session/stateful_session.cc @@ -59,7 +59,7 @@ Http::FilterHeadersStatus StatefulSession::decodeHeaders(Http::RequestHeaderMap& if (route_config->disabled()) { return Http::FilterHeadersStatus::Continue; } - config = route_config->statefuleSessionConfig(); + config = route_config->statefulSessionConfig(); } session_state_ = config->createSessionState(headers); if (session_state_ == nullptr) { diff --git a/source/extensions/filters/http/stateful_session/stateful_session.h b/source/extensions/filters/http/stateful_session/stateful_session.h index c92b0254f75de..5112a3fabb26d 100644 --- a/source/extensions/filters/http/stateful_session/stateful_session.h +++ b/source/extensions/filters/http/stateful_session/stateful_session.h @@ -47,7 +47,7 @@ class PerRouteStatefulSession : public Router::RouteSpecificFilterConfig { Server::Configuration::GenericFactoryContext& context); bool disabled() const { return disabled_; } - StatefulSessionConfig* statefuleSessionConfig() const { return config_.get(); } + StatefulSessionConfig* statefulSessionConfig() const { return config_.get(); } private: bool disabled_{}; diff --git a/source/extensions/filters/http/tap/tap_config.h b/source/extensions/filters/http/tap/tap_config.h index d318686b608e8..04219d76110d5 100644 --- a/source/extensions/filters/http/tap/tap_config.h +++ b/source/extensions/filters/http/tap/tap_config.h @@ -66,12 +66,12 @@ class HttpTapConfig : public virtual Extensions::Common::Tap::TapConfig { public: /** * @return a new per-request HTTP tapper which is used to handle tapping of a discrete request. - * @param tap_config provides http tap config - * @param stream_id supplies the owning HTTP stream ID. + * @param tap_config provides http tap config. + * @param decoder_callbacks supplies all needed information for HTTP tap. */ virtual HttpPerRequestTapperPtr createPerRequestTapper(const envoy::extensions::filters::http::tap::v3::Tap& tap_config, - uint64_t stream_id, OptRef connection) PURE; + Http::StreamDecoderFilterCallbacks& decoder_callbacks) PURE; /** * @return time source to use for timestamp diff --git a/source/extensions/filters/http/tap/tap_config_impl.cc b/source/extensions/filters/http/tap/tap_config_impl.cc index d26d5ebe1485f..b021642327a68 100644 --- a/source/extensions/filters/http/tap/tap_config_impl.cc +++ b/source/extensions/filters/http/tap/tap_config_impl.cc @@ -36,16 +36,19 @@ HttpTapConfigImpl::HttpTapConfigImpl(const envoy::config::tap::v3::TapConfig& pr time_source_(context.serverFactoryContext().mainThreadDispatcher().timeSource()) {} HttpPerRequestTapperPtr HttpTapConfigImpl::createPerRequestTapper( - const envoy::extensions::filters::http::tap::v3::Tap& tap_config, uint64_t stream_id, - OptRef connection) { - return std::make_unique(shared_from_this(), tap_config, stream_id, - connection); + const envoy::extensions::filters::http::tap::v3::Tap& tap_config, + Http::StreamDecoderFilterCallbacks& decoder_callbacks) { + return std::make_unique(shared_from_this(), tap_config, + decoder_callbacks); } void HttpPerRequestTapperImpl::streamRequestHeaders() { TapCommon::TraceWrapperPtr trace = makeTraceSegment(); - request_headers_->iterate(fillHeaderList( - trace->mutable_http_streamed_trace_segment()->mutable_request_headers()->mutable_headers())); + if (request_headers_ != nullptr) { + request_headers_->iterate(fillHeaderList(trace->mutable_http_streamed_trace_segment() + ->mutable_request_headers() + ->mutable_headers())); + } sink_handle_->submitTrace(std::move(trace)); } @@ -163,6 +166,24 @@ void HttpPerRequestTapperImpl::onResponseTrailers(const Http::ResponseTrailerMap } } +void HttpPerRequestTapperImpl::setUpstreamConnection( + envoy::data::tap::v3::Connection& up_stream_conn) { + envoy::config::core::v3::Address local_address; + envoy::config::core::v3::Address remote_address; + auto& stream_info = decoder_callbacks_.streamInfo(); + if (stream_info.upstreamInfo() && stream_info.upstreamInfo()->upstreamLocalAddress()) { + Envoy::Network::Utility::addressToProtobufAddress( + *stream_info.upstreamInfo()->upstreamLocalAddress(), local_address); + up_stream_conn.mutable_local_address()->MergeFrom(local_address); + } + + if (stream_info.upstreamInfo() && stream_info.upstreamInfo()->upstreamRemoteAddress()) { + Envoy::Network::Utility::addressToProtobufAddress( + *stream_info.upstreamInfo()->upstreamRemoteAddress(), remote_address); + up_stream_conn.mutable_remote_address()->MergeFrom(remote_address); + } +} + bool HttpPerRequestTapperImpl::onDestroyLog() { if (config_->streaming() || !config_->rootMatcher().matchStatus(statuses_).matches_) { return config_->rootMatcher().matchStatus(statuses_).matches_; @@ -207,6 +228,10 @@ bool HttpPerRequestTapperImpl::onDestroyLog() { downstream_remote_address); } + if (should_record_upstream_connection_) { + setUpstreamConnection(*http_trace.mutable_upstream_connection()); + } + ENVOY_LOG(debug, "submitting buffered trace sink"); // move is safe as onDestroyLog is the last method called. sink_handle_->submitTrace(std::move(buffered_full_trace_)); diff --git a/source/extensions/filters/http/tap/tap_config_impl.h b/source/extensions/filters/http/tap/tap_config_impl.h index 042b6bbe91de9..e02f0da17b048 100644 --- a/source/extensions/filters/http/tap/tap_config_impl.h +++ b/source/extensions/filters/http/tap/tap_config_impl.h @@ -26,7 +26,7 @@ class HttpTapConfigImpl : public Extensions::Common::Tap::TapConfigBaseImpl, // TapFilter::HttpTapConfig HttpPerRequestTapperPtr createPerRequestTapper(const envoy::extensions::filters::http::tap::v3::Tap& tap_config, - uint64_t stream_id, OptRef connection) override; + Http::StreamDecoderFilterCallbacks& decoder_callbacks) override; TimeSource& timeSource() const override { return time_source_; } @@ -38,12 +38,14 @@ class HttpPerRequestTapperImpl : public HttpPerRequestTapper, Logger::Loggable connection) - : config_(std::move(config)), + Http::StreamDecoderFilterCallbacks& callbacks) + : config_(std::move(config)), decoder_callbacks_(callbacks), should_record_headers_received_time_(tap_config.record_headers_received_time()), should_record_downstream_connection_(tap_config.record_downstream_connection()), - stream_id_(stream_id), sink_handle_(config_->createPerTapSinkHandleManager(stream_id)), - statuses_(config_->createMatchStatusVector()), connection_(connection) { + should_record_upstream_connection_(tap_config.record_upstream_connection()), + stream_id_(callbacks.streamId()), + sink_handle_(config_->createPerTapSinkHandleManager(callbacks.streamId())), + statuses_(config_->createMatchStatusVector()), connection_(callbacks.connection()) { config_->rootMatcher().onNewStream(statuses_); } @@ -91,10 +93,13 @@ class HttpPerRequestTapperImpl : public HttpPerRequestTapper, Logger::LoggabletimeSource().systemTime().time_since_epoch()) .count(); } + void setUpstreamConnection(envoy::data::tap::v3::Connection& up_stream_conn); HttpTapConfigSharedPtr config_; + Http::StreamDecoderFilterCallbacks& decoder_callbacks_; const bool should_record_headers_received_time_; const bool should_record_downstream_connection_; + const bool should_record_upstream_connection_; const uint64_t stream_id_; Extensions::Common::Tap::PerTapSinkHandleManagerPtr sink_handle_; Extensions::Common::Tap::Matcher::MatchStatusVector statuses_; diff --git a/source/extensions/filters/http/tap/tap_filter.h b/source/extensions/filters/http/tap/tap_filter.h index b779e6f8426b8..232e5f5c35856 100644 --- a/source/extensions/filters/http/tap/tap_filter.h +++ b/source/extensions/filters/http/tap/tap_filter.h @@ -100,9 +100,7 @@ class Filter : public Http::StreamFilter, public AccessLog::Instance { void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override { HttpTapConfigSharedPtr config = config_->currentConfig(); if (config != nullptr) { - auto streamId = callbacks.streamId(); - auto connection = callbacks.connection(); - tapper_ = config->createPerRequestTapper(config_->getTapConfig(), streamId, connection); + tapper_ = config->createPerRequestTapper(config_->getTapConfig(), callbacks); } else { tapper_ = nullptr; } diff --git a/source/extensions/filters/http/thrift_to_metadata/filter.cc b/source/extensions/filters/http/thrift_to_metadata/filter.cc index 23753568d326f..071e95067c2bb 100644 --- a/source/extensions/filters/http/thrift_to_metadata/filter.cc +++ b/source/extensions/filters/http/thrift_to_metadata/filter.cc @@ -24,13 +24,12 @@ Rule::Rule(const ProtoRule& rule) : rule_(rule) { switch (rule_.field()) { PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; case envoy::extensions::filters::http::thrift_to_metadata::v3::METHOD_NAME: - protobuf_value_extracter_ = - [](MessageMetadataSharedPtr metadata, - const ThriftDecoderHandler&) -> absl::optional { + protobuf_value_extracter_ = [](MessageMetadataSharedPtr metadata, + const ThriftDecoderHandler&) -> absl::optional { if (!metadata->hasMethodName()) { return absl::nullopt; } - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value(metadata->methodName()); return value; }; @@ -38,8 +37,8 @@ Rule::Rule(const ProtoRule& rule) : rule_(rule) { case envoy::extensions::filters::http::thrift_to_metadata::v3::PROTOCOL: protobuf_value_extracter_ = [](MessageMetadataSharedPtr, - const ThriftDecoderHandler& handler) -> absl::optional { - ProtobufWkt::Value value; + const ThriftDecoderHandler& handler) -> absl::optional { + Protobuf::Value value; value.set_string_value(handler.protocolName()); return value; }; @@ -47,56 +46,52 @@ Rule::Rule(const ProtoRule& rule) : rule_(rule) { case envoy::extensions::filters::http::thrift_to_metadata::v3::TRANSPORT: protobuf_value_extracter_ = [](MessageMetadataSharedPtr, - const ThriftDecoderHandler& handler) -> absl::optional { - ProtobufWkt::Value value; + const ThriftDecoderHandler& handler) -> absl::optional { + Protobuf::Value value; value.set_string_value(handler.transportName()); return value; }; break; case envoy::extensions::filters::http::thrift_to_metadata::v3::HEADER_FLAGS: - protobuf_value_extracter_ = - [](MessageMetadataSharedPtr metadata, - const ThriftDecoderHandler&) -> absl::optional { + protobuf_value_extracter_ = [](MessageMetadataSharedPtr metadata, + const ThriftDecoderHandler&) -> absl::optional { if (!metadata->hasHeaderFlags()) { return absl::nullopt; } - ProtobufWkt::Value value; + Protobuf::Value value; value.set_number_value(metadata->headerFlags()); return value; }; break; case envoy::extensions::filters::http::thrift_to_metadata::v3::SEQUENCE_ID: - protobuf_value_extracter_ = - [](MessageMetadataSharedPtr metadata, - const ThriftDecoderHandler&) -> absl::optional { + protobuf_value_extracter_ = [](MessageMetadataSharedPtr metadata, + const ThriftDecoderHandler&) -> absl::optional { if (!metadata->hasSequenceId()) { return absl::nullopt; } - ProtobufWkt::Value value; + Protobuf::Value value; value.set_number_value(metadata->sequenceId()); return value; }; break; case envoy::extensions::filters::http::thrift_to_metadata::v3::MESSAGE_TYPE: - protobuf_value_extracter_ = - [](MessageMetadataSharedPtr metadata, - const ThriftDecoderHandler&) -> absl::optional { + protobuf_value_extracter_ = [](MessageMetadataSharedPtr metadata, + const ThriftDecoderHandler&) -> absl::optional { if (!metadata->hasMessageType()) { return absl::nullopt; } - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value(MessageTypeNames::get().fromType(metadata->messageType())); return value; }; break; case envoy::extensions::filters::http::thrift_to_metadata::v3::REPLY_TYPE: - protobuf_value_extracter_ = - [](MessageMetadataSharedPtr metadata, - const ThriftDecoderHandler&) -> absl::optional { + protobuf_value_extracter_ = [](MessageMetadataSharedPtr metadata, + const ThriftDecoderHandler&) -> absl::optional { if (!metadata->hasReplyType()) { return absl::nullopt; } - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value(ReplyTypeNames::get().fromType(metadata->replyType())); return value; }; @@ -298,7 +293,7 @@ void Filter::processMetadata(MessageMetadataSharedPtr metadata, const Rules& rul ThriftToMetadataStats& stats, bool& processing_finished_flag) { StructMap struct_map; for (const auto& rule : rules) { - absl::optional val_opt = rule.extract_value(metadata, *handler); + absl::optional val_opt = rule.extract_value(metadata, *handler); if (val_opt.has_value()) { handleOnPresent(std::move(val_opt).value(), rule, struct_map); @@ -310,7 +305,7 @@ void Filter::processMetadata(MessageMetadataSharedPtr metadata, const Rules& rul finalizeDynamicMetadata(filter_callback, struct_map, processing_finished_flag); } -void Filter::handleOnPresent(ProtobufWkt::Value&& value, const Rule& rule, StructMap& struct_map) { +void Filter::handleOnPresent(Protobuf::Value&& value, const Rule& rule, StructMap& struct_map) { if (!rule.rule().has_on_present()) { return; } @@ -335,7 +330,7 @@ void Filter::handleAllOnMissing(const Rules& rules, Http::StreamFilterCallbacks& finalizeDynamicMetadata(filter_callback, struct_map, processing_finished_flag); } -void Filter::applyKeyValue(ProtobufWkt::Value value, const KeyValuePair& keyval, +void Filter::applyKeyValue(Protobuf::Value value, const KeyValuePair& keyval, StructMap& struct_map) { const auto& metadata_namespace = decideNamespace(keyval.metadata_namespace()); const auto& key = keyval.key(); diff --git a/source/extensions/filters/http/thrift_to_metadata/filter.h b/source/extensions/filters/http/thrift_to_metadata/filter.h index f5d9c50fc7032..ec9a9fa7d8021 100644 --- a/source/extensions/filters/http/thrift_to_metadata/filter.h +++ b/source/extensions/filters/http/thrift_to_metadata/filter.h @@ -47,19 +47,19 @@ struct ThriftToMetadataStats { using ProtoRule = envoy::extensions::filters::http::thrift_to_metadata::v3::Rule; using KeyValuePair = envoy::extensions::filters::http::thrift_to_metadata::v3::KeyValuePair; -using StructMap = absl::flat_hash_map; +using StructMap = absl::flat_hash_map; using namespace Envoy::Extensions::NetworkFilters::ThriftProxy; class ThriftDecoderHandler; -using ThriftMetadataToProtobufValue = std::function( +using ThriftMetadataToProtobufValue = std::function( MessageMetadataSharedPtr, const ThriftDecoderHandler&)>; class Rule { public: Rule(const ProtoRule& rule); const ProtoRule& rule() const { return rule_; } - absl::optional extract_value(MessageMetadataSharedPtr metadata, - const ThriftDecoderHandler& handler) const { + absl::optional extract_value(MessageMetadataSharedPtr metadata, + const ThriftDecoderHandler& handler) const { return protobuf_value_extracter_(metadata, handler); } @@ -198,12 +198,12 @@ class Filter : public Http::PassThroughFilter, ThriftDecoderHandlerPtr& handler, Http::StreamFilterCallbacks& filter_callback, ThriftToMetadataStats& stats, bool& processing_finished_flag); - void handleOnPresent(ProtobufWkt::Value&& value, const Rule& rule, StructMap& struct_map); + void handleOnPresent(Protobuf::Value&& value, const Rule& rule, StructMap& struct_map); void handleOnMissing(const Rule& rule, StructMap& struct_map); void handleAllOnMissing(const Rules& rules, Http::StreamFilterCallbacks& filter_callback, bool& processing_finished_flag); - void applyKeyValue(ProtobufWkt::Value value, const KeyValuePair& keyval, StructMap& struct_map); + void applyKeyValue(Protobuf::Value value, const KeyValuePair& keyval, StructMap& struct_map); void finalizeDynamicMetadata(Http::StreamFilterCallbacks& filter_callback, const StructMap& struct_map, bool& processing_finished_flag); const std::string& decideNamespace(const std::string& nspace) const; diff --git a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc index 3e1d5a8eee885..90a5d857e609b 100644 --- a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc +++ b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc @@ -557,37 +557,30 @@ bool Filter::parseTlvs(const uint8_t* buf, size_t len) { std::string metadata_key = key_value_pair->metadata_namespace().empty() ? "envoy.filters.listener.proxy_protocol" : key_value_pair->metadata_namespace(); - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener")) { - auto& typed_filter_metadata = (*cb_->dynamicMetadata().mutable_typed_filter_metadata()); - - const auto typed_proxy_filter_metadata = typed_filter_metadata.find(metadata_key); - envoy::data::core::v3::TlvsMetadata tlvs_metadata; - auto status = absl::OkStatus(); - if (typed_proxy_filter_metadata != typed_filter_metadata.end()) { - status = MessageUtil::unpackTo(typed_proxy_filter_metadata->second, tlvs_metadata); - } - if (!status.ok()) { - ENVOY_LOG_PERIODIC(warn, std::chrono::seconds(1), - "proxy_protocol: Failed to unpack typed metadata for TLV type ", - tlv_type); - } else { - Protobuf::BytesValue tlv_byte_value; - tlv_byte_value.set_value(tlv_value.data(), tlv_value.size()); - tlvs_metadata.mutable_typed_metadata()->insert( - {key_value_pair->key(), tlv_byte_value.value()}); - ProtobufWkt::Any typed_metadata; - typed_metadata.PackFrom(tlvs_metadata); - cb_->setDynamicTypedMetadata(metadata_key, typed_metadata); - } + auto& typed_filter_metadata = (*cb_->dynamicMetadata().mutable_typed_filter_metadata()); + + const auto typed_proxy_filter_metadata = typed_filter_metadata.find(metadata_key); + envoy::data::core::v3::TlvsMetadata tlvs_metadata; + auto status = absl::OkStatus(); + if (typed_proxy_filter_metadata != typed_filter_metadata.end()) { + status = MessageUtil::unpackTo(typed_proxy_filter_metadata->second, tlvs_metadata); + } + if (!status.ok()) { + ENVOY_LOG_PERIODIC(warn, std::chrono::seconds(1), + "proxy_protocol: Failed to unpack typed metadata for TLV type ", + tlv_type); + } else { + (*tlvs_metadata.mutable_typed_metadata())[key_value_pair->key()] = tlv_value; + Protobuf::Any typed_metadata; + typed_metadata.PackFrom(tlvs_metadata); + cb_->setDynamicTypedMetadata(metadata_key, typed_metadata); } // Always populate untyped metadata for backwards compatibility. - ProtobufWkt::Value metadata_value; + Protobuf::Value metadata_value; // Sanitize any non utf8 characters. auto sanitised_tlv_value = MessageUtil::sanitizeUtf8String(tlv_value); metadata_value.set_string_value(sanitised_tlv_value.data(), sanitised_tlv_value.size()); - ProtobufWkt::Struct metadata( - (*cb_->dynamicMetadata().mutable_filter_metadata())[metadata_key]); + Protobuf::Struct metadata((*cb_->dynamicMetadata().mutable_filter_metadata())[metadata_key]); metadata.mutable_fields()->insert({key_value_pair->key(), metadata_value}); cb_->setDynamicMetadata(metadata_key, metadata); } else { diff --git a/source/extensions/filters/listener/reverse_connection/BUILD b/source/extensions/filters/listener/reverse_connection/BUILD new file mode 100644 index 0000000000000..5a28db2c9ec5d --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/BUILD @@ -0,0 +1,49 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config_lib", + srcs = ["config.cc"], + hdrs = ["config.h"], + visibility = ["//visibility:public"], + deps = [ + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/extensions/filters/listener/reverse_connection/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "reverse_connection_lib", + srcs = ["reverse_connection.cc"], + hdrs = ["reverse_connection.h"], + visibility = ["//visibility:public"], + deps = [ + ":config_lib", + "//envoy/event:timer_interface", + "//envoy/network:filter_interface", + "//source/common/api:os_sys_calls_lib", + "//source/common/common:logger_lib", + "//source/extensions/bootstrap/reverse_tunnel/common:reverse_connection_utility_lib", + ], +) + +envoy_cc_extension( + name = "config_factory_lib", + srcs = ["config_factory.cc"], + hdrs = ["config_factory.h"], + visibility = ["//visibility:public"], + deps = [ + ":config_lib", + ":reverse_connection_lib", + "//envoy/registry", + "//envoy/server:filter_config_interface", + "@envoy_api//envoy/extensions/filters/listener/reverse_connection/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/listener/reverse_connection/config.cc b/source/extensions/filters/listener/reverse_connection/config.cc new file mode 100644 index 0000000000000..2f789187a8650 --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/config.cc @@ -0,0 +1,18 @@ +#include "source/extensions/filters/listener/reverse_connection/config.h" + +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace ReverseConnection { + +Config::Config( + const envoy::extensions::filters::listener::reverse_connection::v3::ReverseConnection& config) + : ping_wait_timeout_( + std::chrono::seconds(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, ping_wait_timeout, 10))) {} + +} // namespace ReverseConnection +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/listener/reverse_connection/config.h b/source/extensions/filters/listener/reverse_connection/config.h new file mode 100644 index 0000000000000..f25ed14c2b7f3 --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/config.h @@ -0,0 +1,24 @@ +#pragma once + +#include "envoy/extensions/filters/listener/reverse_connection/v3/reverse_connection.pb.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace ReverseConnection { + +class Config { +public: + Config(const envoy::extensions::filters::listener::reverse_connection::v3::ReverseConnection& + config); + + std::chrono::seconds pingWaitTimeout() const { return ping_wait_timeout_; } + +private: + const std::chrono::seconds ping_wait_timeout_; +}; + +} // namespace ReverseConnection +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/listener/reverse_connection/config_factory.cc b/source/extensions/filters/listener/reverse_connection/config_factory.cc new file mode 100644 index 0000000000000..d9f496cb2d81c --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/config_factory.cc @@ -0,0 +1,42 @@ +#include "source/extensions/filters/listener/reverse_connection/config_factory.h" + +#include "envoy/extensions/filters/listener/reverse_connection/v3/reverse_connection.pb.h" +#include "envoy/extensions/filters/listener/reverse_connection/v3/reverse_connection.pb.validate.h" + +#include "source/extensions/filters/listener/reverse_connection/config.h" +#include "source/extensions/filters/listener/reverse_connection/reverse_connection.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace ReverseConnection { + +Network::ListenerFilterFactoryCb +ReverseConnectionConfigFactory::createListenerFilterFactoryFromProto( + const Protobuf::Message& message, + const Network::ListenerFilterMatcherSharedPtr& listener_filter_matcher, + Server::Configuration::ListenerFactoryContext& context) { + auto proto_config = MessageUtil::downcastAndValidate< + const envoy::extensions::filters::listener::reverse_connection::v3::ReverseConnection&>( + message, context.messageValidationVisitor()); + + // Create the configuration from the protobuf message + + Config config(proto_config); + return [listener_filter_matcher, config](Network::ListenerFilterManager& filter_manager) -> void { + filter_manager.addAcceptFilter(listener_filter_matcher, std::make_unique(config)); + }; +} + +ProtobufTypes::MessagePtr ReverseConnectionConfigFactory::createEmptyConfigProto() { + return std::make_unique< + envoy::extensions::filters::listener::reverse_connection::v3::ReverseConnection>(); +} + +REGISTER_FACTORY(ReverseConnectionConfigFactory, + Server::Configuration::NamedListenerFilterConfigFactory); + +} // namespace ReverseConnection +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/listener/reverse_connection/config_factory.h b/source/extensions/filters/listener/reverse_connection/config_factory.h new file mode 100644 index 0000000000000..16576b3d162e2 --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/config_factory.h @@ -0,0 +1,27 @@ +#pragma once + +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace ReverseConnection { + +class ReverseConnectionConfigFactory + : public Server::Configuration::NamedListenerFilterConfigFactory { +public: + Network::ListenerFilterFactoryCb createListenerFilterFactoryFromProto( + const Protobuf::Message& config, + const Network::ListenerFilterMatcherSharedPtr& listener_filter_matcher, + Server::Configuration::ListenerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() const override { return "envoy.listener.reverse_connection"; } +}; + +} // namespace ReverseConnection +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/listener/reverse_connection/reverse_connection.cc b/source/extensions/filters/listener/reverse_connection/reverse_connection.cc new file mode 100644 index 0000000000000..6565118b640cf --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/reverse_connection.cc @@ -0,0 +1,153 @@ +#include "source/extensions/filters/listener/reverse_connection/reverse_connection.h" + +#include +#include +#include + +#include +#include + +#include "envoy/common/exception.h" +#include "envoy/network/listen_socket.h" + +#include "source/common/api/os_sys_calls_impl.h" +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/assert.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" + +// #include "source/common/network/io_socket_handle_impl.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace ReverseConnection { + +// Use centralized constants from utility +using ::Envoy::Extensions::Bootstrap::ReverseConnection::ReverseConnectionUtility; + +Filter::Filter(const Config& config) : config_(config) { + ENVOY_LOG(debug, "reverse_connection: ping_wait_timeout is {}", + config_.pingWaitTimeout().count()); +} + +int Filter::fd() { return cb_->socket().ioHandle().fdDoNotUse(); } + +Filter::~Filter() { + ENVOY_LOG(debug, + "reverse_connection: filter destroyed socket().isOpen(): {} connection_used_: {}", + cb_->socket().isOpen(), connection_used_); + // Only close the socket if the connection was not used (i.e., no data was received) + // If connection_used_ is true, Envoy needs the socket for the new connection + if (!connection_used_ && cb_->socket().isOpen()) { + ENVOY_LOG(debug, "reverse_connection: closing unused socket in destructor, fd {}", fd()); + cb_->socket().close(); + } +} + +void Filter::onClose() { + ENVOY_LOG(debug, "reverse_connection: close"); + + const std::string& connectionKey = + cb_->socket().connectionInfoProvider().localAddress()->asString(); + + ENVOY_LOG(debug, "reverse_connection: onClose: connectionKey: {} connection_used_ {}", + connectionKey, connection_used_); + + // If a connection is closed before data is received, mark the socket dead. + if (!connection_used_) { + ENVOY_LOG(debug, "reverse_connection: marking the socket dead, fd {}", fd()); + cb_->socket().ioHandle().close(); + } +} + +Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) { + ENVOY_LOG(debug, "reverse_connection: New connection accepted"); + connection_used_ = false; + cb_ = &cb; + ping_wait_timer_ = cb.dispatcher().createTimer([this]() { onPingWaitTimeout(); }); + ping_wait_timer_->enableTimer(config_.pingWaitTimeout()); + + // Wait for data. + return Network::FilterStatus::StopIteration; +} + +size_t Filter::maxReadBytes() const { return ReverseConnectionUtility::PING_MESSAGE.length(); } + +void Filter::onPingWaitTimeout() { + ENVOY_LOG(debug, "reverse_connection: timed out waiting for ping request"); + const std::string& connectionKey = + cb_->socket().connectionInfoProvider().localAddress()->asString(); + ENVOY_LOG(debug, + "Connection socket FD: {} local address: {} remote address: {} closed; Reporting to " + "RCManager.", + fd(), connectionKey, + cb_->socket().connectionInfoProvider().remoteAddress()->asStringView()); + + // Connection timed out waiting for data - close and continue filter chain + + cb_->continueFilterChain(false); +} + +Network::FilterStatus Filter::onData(Network::ListenerFilterBuffer& buffer) { + const ReadOrParseState read_state = parseBuffer(buffer); + switch (read_state) { + case ReadOrParseState::Error: + return Network::FilterStatus::StopIteration; + case ReadOrParseState::TryAgainLater: + return Network::FilterStatus::StopIteration; + case ReadOrParseState::Done: + ENVOY_LOG(debug, "reverse_connection: marking the socket ready for use, fd {}", fd()); + // Mark the connection as used and continue with normal processing + const std::string& connectionKey = + cb_->socket().connectionInfoProvider().localAddress()->asString(); + ENVOY_LOG(debug, "reverse_connection: marking the socket ready for use, connectionKey: {}", + connectionKey); + connection_used_ = true; + return Network::FilterStatus::Continue; + } + return Network::FilterStatus::Continue; +} + +ReadOrParseState Filter::parseBuffer(Network::ListenerFilterBuffer& buffer) { + auto raw_slice = buffer.rawSlice(); + auto buf = absl::string_view(static_cast(raw_slice.mem_), raw_slice.len_); + + ENVOY_LOG(debug, "reverse_connection: Data received, len: {}", buf.length()); + if (buf.length() == 0) { + // Remote closed. + return ReadOrParseState::Error; + } + + // Use utility to check for RPING messages (raw or HTTP-embedded) + if (ReverseConnectionUtility::isPingMessage(buf)) { + ENVOY_LOG(debug, "reverse_connection: Received RPING msg on fd {}", fd()); + + if (!buffer.drain(buf.length())) { + ENVOY_LOG(error, "reverse_connection: could not drain buffer for ping message"); + } + + // Use utility to send RPING response + const Api::IoCallUint64Result write_result = + ReverseConnectionUtility::sendPingResponse(cb_->socket().ioHandle()); + + if (write_result.ok()) { + ENVOY_LOG(trace, "reverse_connection: fd {} sent ping response, bytes: {}", fd(), + write_result.return_value_); + } else { + ENVOY_LOG(trace, "reverse_connection: fd {} failed to send ping response, error: {}", fd(), + write_result.err_->getErrorDetails()); + } + + ping_wait_timer_->enableTimer(config_.pingWaitTimeout()); + // Return a status to wait for data. + return ReadOrParseState::TryAgainLater; + } + + ENVOY_LOG(debug, "reverse_connection: fd {} received data, stopping RPINGs", fd()); + return ReadOrParseState::Done; +} + +} // namespace ReverseConnection +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/listener/reverse_connection/reverse_connection.h b/source/extensions/filters/listener/reverse_connection/reverse_connection.h new file mode 100644 index 0000000000000..2f0e1f1b05b88 --- /dev/null +++ b/source/extensions/filters/listener/reverse_connection/reverse_connection.h @@ -0,0 +1,59 @@ +#pragma once + +#include "envoy/event/dispatcher.h" +#include "envoy/event/file_event.h" +#include "envoy/network/filter.h" + +#include "source/common/common/logger.h" + +#include "absl/strings/string_view.h" + +// Configuration header for reverse connection listener filter +#include "source/extensions/filters/listener/reverse_connection/config.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace ReverseConnection { + +enum class ReadOrParseState { Done, TryAgainLater, Error }; + +/** + * Listener filter to store reverse connections until they are actively used. + */ +class Filter : public Network::ListenerFilter, Logger::Loggable { +public: + Filter(const Config& config); + ~Filter(); + + // Network::ListenerFilter + Network::FilterStatus onAccept(Network::ListenerFilterCallbacks& cb) override; + size_t maxReadBytes() const override; + Network::FilterStatus onData(Network::ListenerFilterBuffer&) override; + void onClose() override; + + // Helper method to get file descriptor + int fd(); + +private: + // RPING/PROXY messages now handled by ReverseConnectionUtility + + void onPingWaitTimeout(); + ReadOrParseState parseBuffer(Network::ListenerFilterBuffer&); + + Config config_; + + Network::ListenerFilterCallbacks* cb_{}; + Event::FileEventPtr file_event_; + + Event::TimerPtr ping_wait_timer_; + + // Tracks whether data has been received on the connection. If the connection + // is closed by the peer before data is received, the socket is marked dead. + bool connection_used_; +}; + +} // namespace ReverseConnection +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/listener/tls_inspector/tls_inspector.cc b/source/extensions/filters/listener/tls_inspector/tls_inspector.cc index aac723a945a0f..0d785cde20455 100644 --- a/source/extensions/filters/listener/tls_inspector/tls_inspector.cc +++ b/source/extensions/filters/listener/tls_inspector/tls_inspector.cc @@ -175,6 +175,13 @@ Network::FilterStatus Filter::onData(Network::ListenerFilterBuffer& buffer) { return Network::FilterStatus::StopIteration; } +void Filter::setDynamicMetadata(absl::string_view failure_reason) { + Protobuf::Struct metadata; + auto& fields = *metadata.mutable_fields(); + fields[failureReasonKey()].set_string_value(failure_reason); + cb_->setDynamicMetadata(dynamicMetadataKey(), metadata); +} + ParseState Filter::parseClientHello(const void* data, size_t len, uint64_t bytes_already_processed) { // Ownership remains here though we pass a reference to it in `SSL_set0_rbio()`. @@ -194,15 +201,16 @@ ParseState Filter::parseClientHello(const void* data, size_t len, ParseState state = [this, ret]() { switch (SSL_get_error(ssl_.get(), ret)) { case SSL_ERROR_WANT_READ: - if (read_ == maxConfigReadBytes()) { + if (read_ >= maxConfigReadBytes()) { // We've hit the specified size limit. This is an unreasonably large ClientHello; // indicate failure. config_->stats().client_hello_too_large_.inc(); + setDynamicMetadata(failureReasonClientHelloTooLarge()); return ParseState::Error; } - if (read_ == requested_read_bytes_) { + if (read_ >= requested_read_bytes_) { // Double requested bytes up to the maximum configured. - requested_read_bytes_ = std::min(2 * requested_read_bytes_, maxConfigReadBytes()); + requested_read_bytes_ = std::min(2 * read_, maxConfigReadBytes()); } return ParseState::Continue; case SSL_ERROR_SSL: @@ -219,9 +227,11 @@ ParseState Filter::parseClientHello(const void* data, size_t len, // We've hit the specified size limit. This is an unreasonably large ClientHello; // indicate failure. config_->stats().client_hello_too_large_.inc(); + setDynamicMetadata(failureReasonClientHelloTooLarge()); return ParseState::Error; } config_->stats().tls_not_found_.inc(); + setDynamicMetadata(failureReasonClientHelloNotDetected()); ENVOY_LOG( debug, "tls inspector: parseClientHello failed: {}", Extensions::TransportSockets::Tls::Utility::getLastCryptoError().value_or("unknown")); @@ -369,6 +379,22 @@ void Filter::createJA4Hash(const SSL_CLIENT_HELLO* ssl_client_hello) { cb_->socket().setJA4Hash(fingerprint); } +const std::string& Filter::dynamicMetadataKey() { + CONSTRUCT_ON_FIRST_USE(std::string, "envoy.filters.listener.tls_inspector"); +} + +const std::string& Filter::failureReasonKey() { + CONSTRUCT_ON_FIRST_USE(std::string, "failure_reason"); +} + +const std::string& Filter::failureReasonClientHelloTooLarge() { + CONSTRUCT_ON_FIRST_USE(std::string, "ClientHelloTooLarge"); +} + +const std::string& Filter::failureReasonClientHelloNotDetected() { + CONSTRUCT_ON_FIRST_USE(std::string, "ClientHelloNotDetected"); +} + } // namespace TlsInspector } // namespace ListenerFilters } // namespace Extensions diff --git a/source/extensions/filters/listener/tls_inspector/tls_inspector.h b/source/extensions/filters/listener/tls_inspector/tls_inspector.h index 47a347acc5d86..9fd1087166deb 100644 --- a/source/extensions/filters/listener/tls_inspector/tls_inspector.h +++ b/source/extensions/filters/listener/tls_inspector/tls_inspector.h @@ -47,6 +47,7 @@ enum class ParseState { // Parser reports unrecoverable error. Error }; + /** * Global configuration for TLS inspector. */ @@ -93,6 +94,11 @@ class Filter : public Network::ListenerFilter, Logger::LoggablemaxClientHelloSize(); } + void setDynamicMetadata(absl::string_view failure_reason); ConfigSharedPtr config_; Network::ListenerFilterCallbacks* cb_{}; diff --git a/source/extensions/filters/network/common/redis/supported_commands.cc b/source/extensions/filters/network/common/redis/supported_commands.cc index 069f544dd5128..f5c3352b44bfa 100644 --- a/source/extensions/filters/network/common/redis/supported_commands.cc +++ b/source/extensions/filters/network/common/redis/supported_commands.cc @@ -12,7 +12,7 @@ bool SupportedCommands::isSupportedCommand(const std::string& command) { transactionCommands().contains(command) || auth() == command || echo() == command || mget() == command || mset() == command || keys() == command || ping() == command || time() == command || quit() == command || select() == command || scan() == command || - info() == command); + info() == command || role() == command); } } // namespace Redis diff --git a/source/extensions/filters/network/common/redis/supported_commands.h b/source/extensions/filters/network/common/redis/supported_commands.h index 57177b60422cc..2bb6e77398fbb 100644 --- a/source/extensions/filters/network/common/redis/supported_commands.h +++ b/source/extensions/filters/network/common/redis/supported_commands.h @@ -23,18 +23,23 @@ struct SupportedCommands { absl::flat_hash_set, "append", "bf.add", "bf.card", "bf.exists", "bf.info", "bf.insert", "bf.loadchunk", "bf.madd", "bf.mexists", "bf.reserve", "bf.scandump", "bitcount", "bitfield", "bitpos", "decr", "decrby", "dump", "expire", "expireat", "geoadd", - "geodist", "geohash", "geopos", "georadius_ro", "georadiusbymember_ro", "get", "getbit", - "getdel", "getrange", "getset", "hdel", "hexists", "hget", "hgetall", "hincrby", - "hincrbyfloat", "hkeys", "hlen", "hmget", "hmset", "hscan", "hset", "hsetnx", "hstrlen", - "hvals", "incr", "incrby", "incrbyfloat", "lindex", "linsert", "llen", "lmove", "lpop", - "lpush", "lpushx", "lrange", "lrem", "lset", "ltrim", "persist", "pexpire", "pexpireat", - "pfadd", "pfcount", "psetex", "pttl", "publish", "restore", "rpop", "rpush", "rpushx", - "sadd", "scard", "set", "setbit", "setex", "setnx", "setrange", "sismember", "smembers", - "spop", "srandmember", "srem", "sscan", "strlen", "ttl", "type", "xack", "xadd", + "geodist", "geohash", "geopos", "georadius_ro", "georadiusbymember_ro", "geosearch", "get", + "getbit", "getdel", "getex", "getrange", "getset", "hdel", "hexists", "hget", "hgetall", + "hincrby", "hincrbyfloat", "hkeys", "hlen", "hmget", "hmset", "hscan", "hset", "hsetnx", + "hstrlen", "hvals", "incr", "incrby", "incrbyfloat", "lindex", "linsert", "llen", "lmove", + "lpop", "lpush", "lpushx", "lrange", "lrem", "lset", "ltrim", "persist", "pexpire", + "pexpireat", "pfadd", "pfcount", "psetex", "pttl", "publish", "restore", "rpop", "rpush", + "rpushx", "sadd", "scard", "set", "setbit", "setex", "setnx", "setrange", "sismember", + "smembers", "spop", "srandmember", "srem", "sscan", "strlen", "ttl", "type", "xack", "xadd", "xautoclaim", "xclaim", "xdel", "xlen", "xpending", "xrange", "xrevrange", "xtrim", "zadd", "zcard", "zcount", "zincrby", "zlexcount", "zpopmin", "zpopmax", "zrange", "zrangebylex", "zrangebyscore", "zrank", "zrem", "zremrangebylex", "zremrangebyrank", "zremrangebyscore", - "zrevrange", "zrevrangebylex", "zrevrangebyscore", "zrevrank", "zscan", "zscore"); + "zrevrange", "zrevrangebylex", "zrevrangebyscore", "zrevrank", "zscan", "zscore", "copy", + "rpoplpush", "smove", "sunion", "sdiff", "sinter", "sinterstore", "zunionstore", + "zinterstore", "pfmerge", "georadius", "georadiusbymember", "rename", "sort", "sort_ro", + "zmscore", "sdiffstore", "msetnx", "substr", "zrangestore", "zunion", "zdiff", + "sunionstore", "smismember", "hrandfield", "geosearchstore", "zdiffstore", "zinter", + "zrandmember", "bitop", "lpos", "renamenx"); } /** @@ -42,7 +47,7 @@ struct SupportedCommands { */ static const absl::flat_hash_set& multiKeyCommands() { CONSTRUCT_ON_FIRST_USE(absl::flat_hash_set, "del", "mget", "mset", "touch", - "unlink"); + "unlink", "msetnx"); } /** @@ -122,19 +127,26 @@ struct SupportedCommands { */ static const std::string& info() { CONSTRUCT_ON_FIRST_USE(std::string, "info"); } + /** + * @return role command + */ + static const std::string& role() { CONSTRUCT_ON_FIRST_USE(std::string, "role"); } + /** * @return commands which alters the state of redis */ static const absl::flat_hash_set& writeCommands() { - CONSTRUCT_ON_FIRST_USE(absl::flat_hash_set, "append", "bitfield", "decr", "decrby", - "del", "discard", "exec", "expire", "expireat", "eval", "evalsha", - "geoadd", "getdel", "hdel", "hincrby", "hincrbyfloat", "hmset", "hset", - "hsetnx", "incr", "incrby", "incrbyfloat", "linsert", "lmove", "lpop", - "lpush", "lpushx", "lrem", "lset", "ltrim", "mset", "multi", "persist", - "pexpire", "pexpireat", "pfadd", "psetex", "restore", "rpop", "rpush", - "rpushx", "sadd", "set", "setbit", "setex", "setnx", "setrange", "spop", - "srem", "zadd", "zincrby", "touch", "zpopmin", "zpopmax", "zrem", - "zremrangebylex", "zremrangebyrank", "zremrangebyscore", "unlink"); + CONSTRUCT_ON_FIRST_USE( + absl::flat_hash_set, "append", "bitfield", "decr", "decrby", "del", "discard", + "exec", "expire", "expireat", "eval", "evalsha", "geoadd", "getdel", "hdel", "hincrby", + "hincrbyfloat", "hmset", "hset", "hsetnx", "incr", "incrby", "incrbyfloat", "linsert", + "lmove", "lpop", "lpush", "lpushx", "lrem", "lset", "ltrim", "mset", "multi", "persist", + "pexpire", "pexpireat", "pfadd", "psetex", "restore", "rpop", "rpush", "rpushx", "sadd", + "set", "setbit", "setex", "setnx", "setrange", "spop", "srem", "zadd", "zincrby", "touch", + "zpopmin", "zpopmax", "zrem", "zremrangebylex", "zremrangebyrank", "zremrangebyscore", + "unlink", "copy", "rpoplpush", "smove", "sinterstore", "zunionstore", "zinterstore", + "pfmerge", "georadius", "georadiusbymember", "rename", "sort", "sdiffstore", "msetnx", + "zrangestore", "sunionstore", "geosearchstore", "zdiffstore", "bitop", "renamenx"); } static bool isReadCommand(const std::string& command) { diff --git a/source/extensions/filters/network/ext_authz/ext_authz.cc b/source/extensions/filters/network/ext_authz/ext_authz.cc index 2a8276a1f6809..b54ffc301f20c 100644 --- a/source/extensions/filters/network/ext_authz/ext_authz.cc +++ b/source/extensions/filters/network/ext_authz/ext_authz.cc @@ -78,7 +78,7 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { config_->stats().ok_.inc(); // Add duration of call to dynamic metadata if applicable if (start_time_.has_value()) { - ProtobufWkt::Value ext_authz_duration_value; + Protobuf::Value ext_authz_duration_value; auto duration = filter_callbacks_->connection().dispatcher().timeSource().monotonicTime() - start_time_.value(); ext_authz_duration_value.set_number_value( diff --git a/source/extensions/filters/network/generic_proxy/access_log.cc b/source/extensions/filters/network/generic_proxy/access_log.cc index b946f30275620..546af3bb0b49d 100644 --- a/source/extensions/filters/network/generic_proxy/access_log.cc +++ b/source/extensions/filters/network/generic_proxy/access_log.cc @@ -24,7 +24,7 @@ StringValueFormatterProvider::formatWithContext(const FormatterContext& context, } return optional_str; } -ProtobufWkt::Value StringValueFormatterProvider::formatValueWithContext( +Protobuf::Value StringValueFormatterProvider::formatValueWithContext( const FormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { return ValueUtil::optionalStringValue(formatWithContext(context, stream_info)); } @@ -37,7 +37,7 @@ GenericStatusCodeFormatterProvider::formatWithContext(const FormatterContext& co return std::to_string(code); } -ProtobufWkt::Value +Protobuf::Value GenericStatusCodeFormatterProvider::formatValueWithContext(const FormatterContext& context, const StreamInfo::StreamInfo&) const { CHECK_DATA_OR_RETURN(context, response_, ValueUtil::nullValue()); diff --git a/source/extensions/filters/network/generic_proxy/access_log.h b/source/extensions/filters/network/generic_proxy/access_log.h index 9ebb528789109..f7eb5e6044ddf 100644 --- a/source/extensions/filters/network/generic_proxy/access_log.h +++ b/source/extensions/filters/network/generic_proxy/access_log.h @@ -42,9 +42,8 @@ class StringValueFormatterProvider : public FormatterProvider { absl::optional formatWithContext(const FormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const FormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const FormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; private: ValueExtractor value_extractor_; @@ -58,8 +57,8 @@ class GenericStatusCodeFormatterProvider : public FormatterProvider { // FormatterProvider absl::optional formatWithContext(const FormatterContext& context, const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValueWithContext(const FormatterContext& context, - const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValueWithContext(const FormatterContext& context, + const StreamInfo::StreamInfo&) const override; }; Formatter::CommandParserPtr createGenericProxyCommandParser(); diff --git a/source/extensions/filters/network/generic_proxy/codecs/dubbo/config.h b/source/extensions/filters/network/generic_proxy/codecs/dubbo/config.h index a64cee7171313..46627cce43415 100644 --- a/source/extensions/filters/network/generic_proxy/codecs/dubbo/config.h +++ b/source/extensions/filters/network/generic_proxy/codecs/dubbo/config.h @@ -164,10 +164,18 @@ class DubboDecoderBase : public DubboCodecBase, public CodecType { } void decode(Buffer::Instance& buffer, bool) override { - while (buffer.length() > 0) { + decoding_buffer_.move(buffer); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.generic_proxy_codec_buffer_limit")) { + if (decoding_buffer_.length() > callback_->connection()->bufferLimit()) { + callback_->onDecodingFailure(); + return; + } + } + while (decoding_buffer_.length() > 0) { // Continue decoding if the buffer has more data and the previous decoding is // successful. - if (decodeOne(buffer) != Common::Dubbo::DecodeStatus::Success) { + if (decodeOne(decoding_buffer_) != Common::Dubbo::DecodeStatus::Success) { break; } } @@ -191,6 +199,7 @@ class DubboDecoderBase : public DubboCodecBase, public CodecType { CallBackType* callback_{}; private: + Buffer::OwnedImpl decoding_buffer_; Buffer::OwnedImpl encoding_buffer_; }; diff --git a/source/extensions/filters/network/generic_proxy/codecs/http1/config.h b/source/extensions/filters/network/generic_proxy/codecs/http1/config.h index ab9fd02062764..56d8df2b2a0d4 100644 --- a/source/extensions/filters/network/generic_proxy/codecs/http1/config.h +++ b/source/extensions/filters/network/generic_proxy/codecs/http1/config.h @@ -301,6 +301,14 @@ class Http1ServerCodec : public Http1CodecBase, public ServerCodec { void setCodecCallbacks(ServerCodecCallbacks& callbacks) override { callbacks_ = &callbacks; } void decode(Envoy::Buffer::Instance& buffer, bool) override { + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.generic_proxy_codec_buffer_limit")) { + if (decoding_buffer_.length() + buffer.length() > callbacks_->connection()->bufferLimit()) { + callbacks_->onDecodingFailure(); + return; + } + } + if (!decodeBuffer(buffer)) { onDecodingFailure(); } @@ -362,6 +370,13 @@ class Http1ClientCodec : public Http1CodecBase, public ClientCodec { void setCodecCallbacks(ClientCodecCallbacks& callbacks) override { callbacks_ = &callbacks; } void decode(Envoy::Buffer::Instance& buffer, bool) override { + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.generic_proxy_codec_buffer_limit")) { + if (decoding_buffer_.length() + buffer.length() > callbacks_->connection()->bufferLimit()) { + callbacks_->onDecodingFailure(); + return; + } + } if (!decodeBuffer(buffer)) { onDecodingFailure(); } diff --git a/source/extensions/filters/network/generic_proxy/config.cc b/source/extensions/filters/network/generic_proxy/config.cc index a922cb83dbaa3..bbc9b9661b739 100644 --- a/source/extensions/filters/network/generic_proxy/config.cc +++ b/source/extensions/filters/network/generic_proxy/config.cc @@ -50,7 +50,7 @@ Factory::routeConfigProviderFromProto(const ProxyConfig& config, } std::vector -Factory::filtersFactoryFromProto(const ProtobufWkt::RepeatedPtrField& filters, +Factory::filtersFactoryFromProto(const Protobuf::RepeatedPtrField& filters, const TypedExtensionConfig& codec_config, const std::string stats_prefix, Envoy::Server::Configuration::FactoryContext& context) { @@ -118,7 +118,7 @@ Factory::createFilterFactoryFromProtoTyped(const ProxyConfig& proto_config, if (proto_config.tracing().has_provider()) { tracer = tracer_manager->getOrCreateTracer(&proto_config.tracing().provider()); } - tracing_config = std::make_unique( + tracing_config = std::make_unique( context.listenerInfo().direction(), proto_config.tracing()); } diff --git a/source/extensions/filters/network/generic_proxy/config.h b/source/extensions/filters/network/generic_proxy/config.h index 8708223d1e654..6e746f72a81ff 100644 --- a/source/extensions/filters/network/generic_proxy/config.h +++ b/source/extensions/filters/network/generic_proxy/config.h @@ -31,7 +31,7 @@ class Factory : public Envoy::Extensions::NetworkFilters::Common::FactoryBase - filtersFactoryFromProto(const ProtobufWkt::RepeatedPtrField& filters, + filtersFactoryFromProto(const Protobuf::RepeatedPtrField& filters, const TypedExtensionConfig& codec_config, const std::string stats_prefix, Server::Configuration::FactoryContext& context); }; diff --git a/source/extensions/filters/network/generic_proxy/interface/codec.h b/source/extensions/filters/network/generic_proxy/interface/codec.h index 2b1ba02ceefc7..682a2ea5fbf55 100644 --- a/source/extensions/filters/network/generic_proxy/interface/codec.h +++ b/source/extensions/filters/network/generic_proxy/interface/codec.h @@ -22,12 +22,20 @@ class ServerCodec { virtual ~ServerCodec() = default; /** - * Set callbacks of server codec. + * Set callbacks of server codec. Called before onConnected(). * @param callbacks callbacks of server codec. This callback will have same or longer * lifetime as the server codec. */ virtual void setCodecCallbacks(ServerCodecCallbacks& callbacks) PURE; + /** + * Called when the downstream connection is established. + * + * The connection obtained from ServerCodecCallbacks::connection() will be valid when this + * callback is invoked. It should not be relied upon to be valid until this point. + */ + virtual void onConnected() {} + /** * Decode request frame from downstream connection. * @param buffer data to decode. diff --git a/source/extensions/filters/network/generic_proxy/proxy.cc b/source/extensions/filters/network/generic_proxy/proxy.cc index bb2e3ca8919cd..075c80046bad5 100644 --- a/source/extensions/filters/network/generic_proxy/proxy.cc +++ b/source/extensions/filters/network/generic_proxy/proxy.cc @@ -89,9 +89,15 @@ Tracing::OperationName ActiveStream::operationName() const { return conn_manager_tracing_config_->operationName(); } -const Tracing::CustomTagMap* ActiveStream::customTags() const { +void ActiveStream::modifySpan(Tracing::Span& span) const { ASSERT(conn_manager_tracing_config_.has_value()); - return &conn_manager_tracing_config_->getCustomTags(); + + const TraceContextBridge trace_context{*request_header_frame_}; + const Tracing::CustomTagContext ctx{trace_context, stream_info_}; + + for (const auto& it : conn_manager_tracing_config_->getCustomTags()) { + it.second->applySpan(span, ctx); + } } bool ActiveStream::verbose() const { @@ -621,8 +627,7 @@ void ActiveStream::completeStream(absl::optional re parent_.stats_helper_.onRequestComplete(stream_info_, local_reply_, error_reply); if (active_span_) { - const TraceContextBridge context{*request_header_frame_}; - Tracing::TracerUtility::finalizeSpan(*active_span_, context, stream_info_, *this, false); + Tracing::TracerUtility::finalizeSpan(*active_span_, stream_info_, *this, false); } for (const auto& access_log : parent_.config_->accessLogs()) { diff --git a/source/extensions/filters/network/generic_proxy/proxy.h b/source/extensions/filters/network/generic_proxy/proxy.h index 4a80b98aa30ac..f6d220401e775 100644 --- a/source/extensions/filters/network/generic_proxy/proxy.h +++ b/source/extensions/filters/network/generic_proxy/proxy.h @@ -296,7 +296,7 @@ class ActiveStream : public FilterChainManager, // returned by the public tracingConfig() method. // Tracing::TracingConfig Tracing::OperationName operationName() const override; - const Tracing::CustomTagMap* customTags() const override; + void modifySpan(Tracing::Span& span) const override; bool verbose() const override; uint32_t maxPathTagLength() const override; bool spawnUpstreamSpan() const override; @@ -370,6 +370,7 @@ class Filter : public Envoy::Network::ReadFilter, // Envoy::Network::ReadFilter Envoy::Network::FilterStatus onData(Envoy::Buffer::Instance& data, bool end_stream) override; Envoy::Network::FilterStatus onNewConnection() override { + server_codec_->onConnected(); return Envoy::Network::FilterStatus::Continue; } void initializeReadFilterCallbacks(Envoy::Network::ReadFilterCallbacks& callbacks) override { @@ -442,7 +443,7 @@ class Filter : public Envoy::Network::ReadFilter, bool downstream_connection_closed_{}; - FilterConfigSharedPtr config_{}; + FilterConfigSharedPtr config_; GenericFilterStatsHelper stats_helper_; const Network::DrainDecision& drain_decision_; diff --git a/source/extensions/filters/network/generic_proxy/route_impl.cc b/source/extensions/filters/network/generic_proxy/route_impl.cc index 60fd548e078f3..18d37d2f28087 100644 --- a/source/extensions/filters/network/generic_proxy/route_impl.cc +++ b/source/extensions/filters/network/generic_proxy/route_impl.cc @@ -18,7 +18,7 @@ namespace NetworkFilters { namespace GenericProxy { RouteSpecificFilterConfigConstSharedPtr RouteEntryImpl::createRouteSpecificFilterConfig( - const std::string& name, const ProtobufWkt::Any& typed_config, + const std::string& name, const Protobuf::Any& typed_config, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator) { @@ -61,13 +61,12 @@ RouteEntryImpl::RouteEntryImpl(const ProtoRouteAction& route_action, } } -Matcher::ActionFactoryCb RouteMatchActionFactory::createActionFactoryCb( - const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) { +Matcher::ActionConstSharedPtr +RouteMatchActionFactory::createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& route_action = MessageUtil::downcastAndValidate(config, validation_visitor); - auto route = std::make_shared(route_action, context.factory_context); - return [route]() { return std::make_unique(route); }; + return std::make_shared(route_action, context.factory_context); } REGISTER_FACTORY(RouteMatchActionFactory, Matcher::ActionFactory); @@ -92,15 +91,12 @@ RouteEntryConstSharedPtr VirtualHostImpl::routeEntry(const MatchInput& request) Matcher::MatchResult match_result = Matcher::evaluateMatch(*matcher_, request); if (match_result.isMatch()) { - Matcher::ActionPtr action = match_result.action(); - + Matcher::ActionConstSharedPtr action = match_result.actionByMove(); // The only possible action that can be used within the route matching context // is the RouteMatchAction, so this must be true. - ASSERT(action->typeUrl() == RouteMatchAction::staticTypeUrl()); - ASSERT(dynamic_cast(action.get())); - const RouteMatchAction& route_action = static_cast(*action); - - return route_action.route(); + ASSERT(action->typeUrl() == RouteEntryImpl::staticTypeUrl()); + ASSERT(dynamic_cast(action.get())); + return std::dynamic_pointer_cast(std::move(action)); } ENVOY_LOG(debug, "failed to match incoming request: {}", diff --git a/source/extensions/filters/network/generic_proxy/route_impl.h b/source/extensions/filters/network/generic_proxy/route_impl.h index 62118b8856f6e..d8eae22e55477 100644 --- a/source/extensions/filters/network/generic_proxy/route_impl.h +++ b/source/extensions/filters/network/generic_proxy/route_impl.h @@ -32,7 +32,7 @@ using ProtoRouteConfiguration = using ProtoVirtualHost = envoy::extensions::filters::network::generic_proxy::v3::VirtualHost; using ProtoRetryPolicy = envoy::config::core::v3::RetryPolicy; -class RouteEntryImpl : public RouteEntry { +class RouteEntryImpl : public RouteEntry, public Matcher::ActionBase { public: RouteEntryImpl(const ProtoRouteAction& route, Envoy::Server::Configuration::ServerFactoryContext& context); @@ -52,7 +52,7 @@ class RouteEntryImpl : public RouteEntry { const RetryPolicy& retryPolicy() const override { return retry_policy_; } RouteSpecificFilterConfigConstSharedPtr - createRouteSpecificFilterConfig(const std::string& name, const ProtobufWkt::Any& typed_config, + createRouteSpecificFilterConfig(const std::string& name, const Protobuf::Any& typed_config, Server::Configuration::ServerFactoryContext& factory_context, ProtobufMessage::ValidationVisitor& validator); @@ -76,17 +76,6 @@ struct RouteActionContext { Server::Configuration::ServerFactoryContext& factory_context; }; -// Action used with the matching tree to specify route to use for an incoming stream. -class RouteMatchAction : public Matcher::ActionBase { -public: - explicit RouteMatchAction(RouteEntryConstSharedPtr route) : route_(std::move(route)) {} - - RouteEntryConstSharedPtr route() const { return route_; } - -private: - RouteEntryConstSharedPtr route_; -}; - class RouteActionValidationVisitor : public Matcher::MatchTreeValidationVisitor { public: absl::Status performDataInputValidation(const Matcher::DataInputFactory&, @@ -98,9 +87,9 @@ class RouteActionValidationVisitor : public Matcher::MatchTreeValidationVisitor< // Registered factory for RouteMatchAction. class RouteMatchActionFactory : public Matcher::ActionFactory { public: - Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; std::string name() const override { return "envoy.matching.action.generic_proxy.route"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); diff --git a/source/extensions/filters/network/generic_proxy/router/router.cc b/source/extensions/filters/network/generic_proxy/router/router.cc index c219aef86db45..396def6c3770e 100644 --- a/source/extensions/filters/network/generic_proxy/router/router.cc +++ b/source/extensions/filters/network/generic_proxy/router/router.cc @@ -92,9 +92,7 @@ void UpstreamRequest::resetStream(StreamResetReason reason, absl::string_view re if (span_ != nullptr) { span_->setTag(Tracing::Tags::get().Error, Tracing::Tags::get().True); span_->setTag(Tracing::Tags::get().ErrorReason, resetReasonToViewAndFlag(reason).view); - TraceContextBridge trace_context{*parent_.request_stream_}; - Tracing::TracerUtility::finalizeSpan(*span_, trace_context, stream_info_, - tracing_config_.value().get(), true); + Tracing::TracerUtility::finalizeSpan(*span_, stream_info_, tracing_config_.value().get(), true); } // Notify the parent filter that the upstream request has been reset. @@ -110,9 +108,7 @@ void UpstreamRequest::clearStream(bool close_connection) { close_connection); if (span_ != nullptr) { - TraceContextBridge trace_context{*parent_.request_stream_}; - Tracing::TracerUtility::finalizeSpan(*span_, trace_context, stream_info_, - tracing_config_.value().get(), true); + Tracing::TracerUtility::finalizeSpan(*span_, stream_info_, tracing_config_.value().get(), true); } generic_upstream_->removeUpstreamRequest(stream_id_); @@ -245,6 +241,13 @@ void UpstreamRequest::onDecodingSuccess(ResponseHeaderFramePtr response_header_f upstream_info_->upstreamTiming().onFirstUpstreamRxByteReceived(parent_.time_source_); } + if (!response_header_frame->status().ok()) { + if (span_ != nullptr) { + span_->setTag(Tracing::Tags::get().Error, Tracing::Tags::get().True); + span_->setTag(Tracing::Tags::get().ErrorReason, "upstream_failure"); + } + } + if (response_header_frame->frameFlags().endStream()) { onUpstreamResponseComplete(response_header_frame->frameFlags().drainClose()); } diff --git a/source/extensions/filters/network/generic_proxy/router/upstream.cc b/source/extensions/filters/network/generic_proxy/router/upstream.cc index 0cbef86bddea4..adc6d56dfc63d 100644 --- a/source/extensions/filters/network/generic_proxy/router/upstream.cc +++ b/source/extensions/filters/network/generic_proxy/router/upstream.cc @@ -222,6 +222,12 @@ void BoundGenericUpstream::onEvent(Network::ConnectionEvent event) { encoder_decoder_->onConnectionClose(event); } + // Remove the connection event watcher callbacks since the upstream is already closed. + // If the upstream connection closes shortly after a frame that ends the stream is sent by the + // client codec, the downstream connection may end up being closed after this object has already + // been destroyed. + downstream_conn_.removeConnectionCallbacks(connection_event_watcher_); + // If the downstream connection is not closed, close it. downstream_conn_.close(Network::ConnectionCloseType::FlushWrite); } diff --git a/source/extensions/filters/network/http_connection_manager/BUILD b/source/extensions/filters/network/http_connection_manager/BUILD index 515e5a0efb269..63e691817a2df 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -38,6 +38,7 @@ envoy_cc_extension( "//source/common/access_log:access_log_lib", "//source/common/common:minimal_logger_lib", "//source/common/config:utility_lib", + "//source/common/config:xds_resource_lib", "//source/common/filter:config_discovery_lib", "//source/common/http:conn_manager_lib", "//source/common/http:default_server_string_lib", diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 8d1942464bf00..7f59880891068 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -22,6 +22,7 @@ #include "source/common/access_log/access_log_impl.h" #include "source/common/common/fmt.h" #include "source/common/config/utility.h" +#include "source/common/config/xds_resource.h" #include "source/common/http/conn_manager_config.h" #include "source/common/http/conn_manager_utility.h" #include "source/common/http/default_server_string.h" @@ -80,15 +81,6 @@ std::unique_ptr createInternalAddressConfig( creation_status); } - if (!Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.explicit_internal_address_config")) { - ENVOY_LOG_ONCE_MISC(warn, - "internal_address_config is not configured. The prior default behaviour " - "trusted RFC1918 IP addresses, but this was changed in the 1.32 release. " - "Please explictily config internal address config if you need it before " - "envoy.reloadable_features.explicit_internal_address_config is removed."); - } - return std::make_unique(); } @@ -215,6 +207,20 @@ createHeaderValidatorFactory([[maybe_unused]] const envoy::extensions::filters:: return header_validator_factory; } +// Validates that an RDS config either has a config_source or an xdstp +// route_config_name defined. +absl::Status +validateRds(const envoy::extensions::filters::network::http_connection_manager::v3::Rds& rds) { + if (!rds.has_config_source() && + !Config::XdsResourceIdentifier::hasXdsTpScheme(rds.route_config_name())) { + return absl::InvalidArgumentError( + fmt::format("An RDS config must have either a 'config_source' or an xDS-TP based " + "'route_config_name'. Error while parsing RDS config:\n{}", + rds.DebugString())); + } + return absl::OkStatus(); +} + } // namespace // Singleton registration via macro defined in envoy/singleton/manager.h @@ -389,6 +395,8 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( PROTOBUF_GET_OPTIONAL_MS(config.common_http_protocol_options(), max_stream_duration)), stream_idle_timeout_( PROTOBUF_GET_MS_OR_DEFAULT(config, stream_idle_timeout, StreamIdleTimeoutMs)), + stream_flush_timeout_( + PROTOBUF_GET_MS_OR_DEFAULT(config, stream_flush_timeout, stream_idle_timeout_.count())), request_timeout_(PROTOBUF_GET_MS_OR_DEFAULT(config, request_timeout, RequestTimeoutMs)), request_headers_timeout_( PROTOBUF_GET_MS_OR_DEFAULT(config, request_headers_timeout, RequestHeaderTimeoutMs)), @@ -554,6 +562,7 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( switch (config.route_specifier_case()) { case envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: RouteSpecifierCase::kRds: + SET_AND_RETURN_IF_NOT_OK(validateRds(config.rds()), creation_status); route_config_provider_ = route_config_provider_manager.createRdsRouteConfigProvider( // At the creation of a RDS route config provider, the factory_context's initManager is // always valid, though the init manager may go away later when the listener goes away. diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index 51e4e651f404d..60c8c8daf7087 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -184,6 +184,9 @@ class HttpConnectionManagerConfig : Logger::Loggable, return http1_safe_max_connection_duration_; } std::chrono::milliseconds streamIdleTimeout() const override { return stream_idle_timeout_; } + absl::optional streamFlushTimeout() const override { + return stream_flush_timeout_; + } std::chrono::milliseconds requestTimeout() const override { return request_timeout_; } std::chrono::milliseconds requestHeadersTimeout() const override { return request_headers_timeout_; @@ -330,6 +333,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, const bool http1_safe_max_connection_duration_; absl::optional max_stream_duration_; std::chrono::milliseconds stream_idle_timeout_; + absl::optional stream_flush_timeout_; std::chrono::milliseconds request_timeout_; std::chrono::milliseconds request_headers_timeout_; Router::RouteConfigProviderSharedPtr route_config_provider_; diff --git a/source/extensions/filters/network/match_delegate/config.cc b/source/extensions/filters/network/match_delegate/config.cc index b176034ad65ff..adfa6938a7e8f 100644 --- a/source/extensions/filters/network/match_delegate/config.cc +++ b/source/extensions/filters/network/match_delegate/config.cc @@ -18,10 +18,9 @@ namespace Factory { class SkipActionFactory : public Matcher::ActionFactory { public: std::string name() const override { return "skip"; } - Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message&, - NetworkFilterActionContext&, - ProtobufMessage::ValidationVisitor&) override { - return []() { return std::make_unique(); }; + Matcher::ActionConstSharedPtr createAction(const Protobuf::Message&, NetworkFilterActionContext&, + ProtobufMessage::ValidationVisitor&) override { + return std::make_shared(); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); @@ -107,8 +106,8 @@ void DelegatingNetworkFilter::FilterMatchState::evaluateMatchTree() { match_tree_evaluated_ = match_result.isComplete(); if (match_tree_evaluated_ && match_result.isMatch()) { - const Matcher::ActionPtr result = match_result.action(); - if ((result == nullptr) || (SkipAction().typeUrl() == result->typeUrl())) { + const auto& result = match_result.action(); + if (result == nullptr || SkipAction().typeUrl() == result->typeUrl()) { skip_filter_ = true; } else { // TODO(botengyao) this would be similar to `base_filter_->onMatchCallback(*result);` @@ -192,7 +191,7 @@ Envoy::Network::FilterFactoryCb MatchDelegateConfig::createFilterFactory( auto message = Config::Utility::translateAnyToFactoryConfig( proto_config.extension_config().typed_config(), validation, factory); auto filter_factory_or_error = factory.createFilterFactoryFromProto(*message, context); - THROW_IF_NOT_OK(filter_factory_or_error.status()); + THROW_IF_NOT_OK_REF(filter_factory_or_error.status()); auto filter_factory = filter_factory_or_error.value(); Factory::MatchTreeValidationVisitor validation_visitor(*factory.matchingRequirements()); diff --git a/source/extensions/filters/network/mongo_proxy/proxy.cc b/source/extensions/filters/network/mongo_proxy/proxy.cc index d817267b20fe1..3f1aeebb33d0a 100644 --- a/source/extensions/filters/network/mongo_proxy/proxy.cc +++ b/source/extensions/filters/network/mongo_proxy/proxy.cc @@ -75,7 +75,7 @@ ProxyFilter::ProxyFilter(const std::string& stat_prefix, Stats::Scope& scope, ProxyFilter::~ProxyFilter() { ASSERT(!delay_timer_); } void ProxyFilter::setDynamicMetadata(std::string operation, std::string resource) { - ProtobufWkt::Struct metadata( + Protobuf::Struct metadata( (*read_callbacks_->connection() .streamInfo() .dynamicMetadata() diff --git a/source/extensions/filters/network/rbac/rbac_filter.cc b/source/extensions/filters/network/rbac/rbac_filter.cc index 501935e70a0aa..6e2fc12b08972 100644 --- a/source/extensions/filters/network/rbac/rbac_filter.cc +++ b/source/extensions/filters/network/rbac/rbac_filter.cc @@ -36,6 +36,9 @@ absl::Status ActionValidationVisitor::performDataInputValidation( {TypeUtil::descriptorFullNameToTypeUrl( envoy::extensions::matching::common_inputs::network::v3::ServerNameInput::descriptor() ->full_name())}, + {TypeUtil::descriptorFullNameToTypeUrl(envoy::extensions::matching::common_inputs::network:: + v3::NetworkNamespaceInput::descriptor() + ->full_name())}, {TypeUtil::descriptorFullNameToTypeUrl( envoy::extensions::matching::common_inputs::ssl::v3::UriSanInput::descriptor() ->full_name())}, @@ -170,7 +173,7 @@ void RoleBasedAccessControlFilter::onEvent(Network::ConnectionEvent event) { void RoleBasedAccessControlFilter::setDynamicMetadata(const std::string& shadow_engine_result, const std::string& shadow_policy_id) const { - ProtobufWkt::Struct metrics; + Protobuf::Struct metrics; auto& fields = *metrics.mutable_fields(); if (!shadow_policy_id.empty()) { fields[config_->shadowEffectivePolicyIdField()].set_string_value(shadow_policy_id); diff --git a/source/extensions/filters/network/redis_proxy/BUILD b/source/extensions/filters/network/redis_proxy/BUILD index ffe7a6e69c59f..425eeb1c13acb 100644 --- a/source/extensions/filters/network/redis_proxy/BUILD +++ b/source/extensions/filters/network/redis_proxy/BUILD @@ -67,7 +67,7 @@ envoy_cc_library( "//envoy/stats:timespan_interface", "//source/common/common:assert_lib", "//source/common/common:minimal_logger_lib", - "//source/common/common:trie_lookup_table_lib", + "//source/common/common:radix_tree_lib", "//source/common/stats:timespan_lib", "//source/extensions/filters/network/common/redis:client_lib", "//source/extensions/filters/network/common/redis:fault_lib", @@ -172,7 +172,7 @@ envoy_cc_library( "//envoy/stream_info:stream_info_interface", "//envoy/thread_local:thread_local_interface", "//envoy/upstream:cluster_manager_interface", - "//source/common/common:trie_lookup_table_lib", + "//source/common/common:radix_tree_lib", "//source/extensions/filters/network/common/redis:codec_lib", "//source/extensions/filters/network/common/redis:supported_commands_lib", "//source/extensions/filters/network/common/redis:utility_lib", diff --git a/source/extensions/filters/network/redis_proxy/command_splitter_impl.cc b/source/extensions/filters/network/redis_proxy/command_splitter_impl.cc index 07d7bc2a52e82..5d1beb89e28d2 100644 --- a/source/extensions/filters/network/redis_proxy/command_splitter_impl.cc +++ b/source/extensions/filters/network/redis_proxy/command_splitter_impl.cc @@ -785,6 +785,79 @@ void SplitKeysSumResultRequest::onChildResponse(Common::Redis::RespValuePtr&& va } } +SplitRequestPtr RoleRequest::create(Router& router, Common::Redis::RespValuePtr&& incoming_request, + SplitCallbacks& callbacks, CommandStats& command_stats, + TimeSource& time_source, bool delay_command_latency, + const StreamInfo::StreamInfo& stream_info) { + std::string key = ""; + const auto route = router.upstreamPool(key, stream_info); + uint32_t shard_size = + route ? route->upstream(incoming_request->asArray()[0].asString())->shardSize() : 0; + if (shard_size == 0) { + command_stats.error_.inc(); + callbacks.onResponse(Common::Redis::Utility::makeError(Response::get().NoUpstreamHost)); + return nullptr; + } + + std::unique_ptr request_ptr{ + new RoleRequest(callbacks, command_stats, time_source, delay_command_latency)}; + request_ptr->num_pending_responses_ = shard_size; + request_ptr->pending_requests_.reserve(request_ptr->num_pending_responses_); + + request_ptr->pending_response_ = std::make_unique(); + request_ptr->pending_response_->type(Common::Redis::RespType::Array); + + Common::Redis::RespValueSharedPtr base_request = std::move(incoming_request); + for (uint32_t shard_index = 0; shard_index < shard_size; shard_index++) { + request_ptr->pending_requests_.emplace_back(*request_ptr, shard_index); + PendingRequest& pending_request = request_ptr->pending_requests_.back(); + + ENVOY_LOG(debug, "role request shard index {}: {}", shard_index, base_request->toString()); + pending_request.handle_ = + makeFragmentedRequestToShard(route, base_request->asArray()[0].asString(), shard_index, + *base_request, pending_request, callbacks.transaction()); + + if (!pending_request.handle_) { + pending_request.onResponse(Common::Redis::Utility::makeError(Response::get().NoUpstreamHost)); + } + } + + if (request_ptr->num_pending_responses_ > 0) { + return request_ptr; + } + + return nullptr; +} + +void RoleRequest::onChildResponse(Common::Redis::RespValuePtr&& value, uint32_t index) { + pending_requests_[index].handle_ = nullptr; + switch (value->type()) { + case Common::Redis::RespType::Array: { + pending_response_->asArray().push_back(std::move(*value)); + break; + } + case Common::Redis::RespType::BulkString: { + pending_response_->asArray().push_back(std::move(*value)); + break; + } + default: { + error_count_++; + break; + } + } + + ASSERT(num_pending_responses_ > 0); + if (--num_pending_responses_ == 0) { + updateStats(error_count_ == 0); + if (error_count_ == 0) { + callbacks_.onResponse(std::move(pending_response_)); + } else { + callbacks_.onResponse(Common::Redis::Utility::makeError( + fmt::format("finished with {} error(s)", error_count_))); + } + } +} + SplitRequestPtr TransactionRequest::create(Router& router, Common::Redis::RespValuePtr&& incoming_request, SplitCallbacks& callbacks, CommandStats& command_stats, @@ -928,7 +1001,7 @@ InstanceImpl::InstanceImpl(RouterPtr&& router, Stats::Scope& scope, const std::s : router_(std::move(router)), simple_command_handler_(*router_), eval_command_handler_(*router_), mget_handler_(*router_), mset_handler_(*router_), keys_handler_(*router_), scan_handler_(*router_), info_handler_(*router_), - select_handler_(*router_), split_keys_sum_result_handler_(*router_), + select_handler_(*router_), role_handler_(*router_), split_keys_sum_result_handler_(*router_), transaction_handler_(*router_), stats_{ALL_COMMAND_SPLITTER_STATS(POOL_COUNTER_PREFIX(scope, stat_prefix + "splitter."))}, time_source_(time_source), fault_manager_(std::move(fault_manager)), @@ -964,6 +1037,9 @@ InstanceImpl::InstanceImpl(RouterPtr&& router, Stats::Scope& scope, const std::s addHandler(scope, stat_prefix, Common::Redis::SupportedCommands::select(), latency_in_micros, select_handler_); + addHandler(scope, stat_prefix, Common::Redis::SupportedCommands::role(), latency_in_micros, + role_handler_); + for (const std::string& command : Common::Redis::SupportedCommands::transactionCommands()) { addHandler(scope, stat_prefix, command, latency_in_micros, transaction_handler_); } @@ -1098,6 +1174,21 @@ SplitRequestPtr InstanceImpl::makeRequest(Common::Redis::RespValuePtr&& request, } } + if (command_name == Common::Redis::SupportedCommands::role()) { + if (request->asArray().size() > 1) { + callbacks.onResponse(Common::Redis::Utility::makeError(fmt::format( + "ERR wrong number of arguments for '{}' command", request->asArray()[0].asString()))); + return nullptr; + } + + auto handler = handler_lookup_table_.find(command_name.c_str()); + ASSERT(handler != nullptr); + + handler->command_stats_.total_.inc(); + return handler->handler_.get().startRequest( + std::move(request), callbacks, handler->command_stats_, time_source_, false, stream_info); + } + if (command_name == Common::Redis::SupportedCommands::quit()) { callbacks.onQuit(); return nullptr; diff --git a/source/extensions/filters/network/redis_proxy/command_splitter_impl.h b/source/extensions/filters/network/redis_proxy/command_splitter_impl.h index a32ad9ed6a93f..936e3d832cc96 100644 --- a/source/extensions/filters/network/redis_proxy/command_splitter_impl.h +++ b/source/extensions/filters/network/redis_proxy/command_splitter_impl.h @@ -9,7 +9,7 @@ #include "envoy/stats/timespan.h" #include "source/common/common/logger.h" -#include "source/common/common/trie_lookup_table.h" +#include "source/common/common/radix_tree.h" #include "source/common/stats/timespan_impl.h" #include "source/extensions/filters/network/common/redis/client_impl.h" #include "source/extensions/filters/network/common/redis/fault_impl.h" @@ -392,6 +392,25 @@ class SplitKeysSumResultRequest : public FragmentedRequest { int64_t total_{0}; }; +/** + * RoleRequest sends the ROLE command to all Redis servers. The ROLE command is used to + * get the role of the Redis server. + */ +class RoleRequest : public FragmentedRequest { +public: + static SplitRequestPtr create(Router& router, Common::Redis::RespValuePtr&& incoming_request, + SplitCallbacks& callbacks, CommandStats& command_stats, + TimeSource& time_source, bool delay_command_latency, + const StreamInfo::StreamInfo& stream_info); + +private: + RoleRequest(SplitCallbacks& callbacks, CommandStats& command_stats, TimeSource& time_source, + bool delay_command_latency) + : FragmentedRequest(callbacks, command_stats, time_source, delay_command_latency) {} + // RedisProxy::CommandSplitter::FragmentedRequest + void onChildResponse(Common::Redis::RespValuePtr&& value, uint32_t index) override; +}; + /** * MSETRequest takes each key and value pair from the command and sends a SET for each to the * appropriate Redis server. The response is an OK if all commands succeeded or an ERR if any @@ -479,9 +498,10 @@ class InstanceImpl : public Instance, Logger::Loggable { CommandHandlerFactory scan_handler_; CommandHandlerFactory info_handler_; CommandHandlerFactory select_handler_; + CommandHandlerFactory role_handler_; CommandHandlerFactory split_keys_sum_result_handler_; CommandHandlerFactory transaction_handler_; - TrieLookupTable handler_lookup_table_; + RadixTree handler_lookup_table_; InstanceStats stats_; TimeSource& time_source_; Common::Redis::FaultManagerPtr fault_manager_; diff --git a/source/extensions/filters/network/redis_proxy/conn_pool_impl.cc b/source/extensions/filters/network/redis_proxy/conn_pool_impl.cc index 2facc0d8852e8..76eabcc5cf103 100644 --- a/source/extensions/filters/network/redis_proxy/conn_pool_impl.cc +++ b/source/extensions/filters/network/redis_proxy/conn_pool_impl.cc @@ -170,10 +170,9 @@ void InstanceImpl::ThreadLocalPool::onClusterAddOrUpdateNonVirtual( ASSERT(host_set_member_update_cb_handle_ == nullptr); host_set_member_update_cb_handle_ = cluster_->prioritySet().addMemberUpdateCb( [this](const std::vector& hosts_added, - const std::vector& hosts_removed) -> absl::Status { + const std::vector& hosts_removed) { onHostsAdded(hosts_added); onHostsRemoved(hosts_removed); - return absl::OkStatus(); }); ASSERT(host_address_map_.empty()); diff --git a/source/extensions/filters/network/redis_proxy/external_auth.h b/source/extensions/filters/network/redis_proxy/external_auth.h index dc37148c360e7..763029a5d4495 100644 --- a/source/extensions/filters/network/redis_proxy/external_auth.h +++ b/source/extensions/filters/network/redis_proxy/external_auth.h @@ -48,7 +48,7 @@ struct AuthenticateResponse { std::string message; // The expiration time of the authentication. - ProtobufWkt::Timestamp expiration; + Protobuf::Timestamp expiration; }; using AuthenticateResponsePtr = std::unique_ptr; diff --git a/source/extensions/filters/network/redis_proxy/router_impl.h b/source/extensions/filters/network/redis_proxy/router_impl.h index 8a7b841ca7b48..4d353fb629e12 100644 --- a/source/extensions/filters/network/redis_proxy/router_impl.h +++ b/source/extensions/filters/network/redis_proxy/router_impl.h @@ -13,7 +13,7 @@ #include "envoy/type/v3/percent.pb.h" #include "envoy/upstream/cluster_manager.h" -#include "source/common/common/trie_lookup_table.h" +#include "source/common/common/radix_tree.h" #include "source/common/http/header_map_impl.h" #include "source/common/stream_info/stream_info_impl.h" #include "source/extensions/filters/network/common/redis/supported_commands.h" @@ -86,7 +86,7 @@ class PrefixRoutes : public Router, public Logger::Loggable { const StreamInfo::StreamInfo& stream_info); private: - TrieLookupTable prefix_lookup_table_; + RadixTree prefix_lookup_table_; const bool case_insensitive_; Upstreams upstreams_; PrefixSharedPtr catch_all_route_; diff --git a/source/extensions/filters/network/reverse_tunnel/BUILD b/source/extensions/filters/network/reverse_tunnel/BUILD new file mode 100644 index 0000000000000..56466ccedf0df --- /dev/null +++ b/source/extensions/filters/network/reverse_tunnel/BUILD @@ -0,0 +1,57 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":reverse_tunnel_filter_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "reverse_tunnel_filter_lib", + srcs = ["reverse_tunnel_filter.cc"], + hdrs = ["reverse_tunnel_filter.h"], + deps = [ + "//envoy/buffer:buffer_interface", + "//envoy/http:codec_interface", + "//envoy/network:connection_interface", + "//envoy/network:filter_interface", + "//envoy/ssl:connection_interface", + "//envoy/thread_local:thread_local_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/http:codes_lib", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "//source/common/http/http1:codec_lib", + "//source/common/http/http1:codec_stats_lib", + "//source/common/http/http1:settings_lib", + "//source/common/network:connection_socket_lib", + "//source/common/protobuf", + "//source/common/protobuf:message_validator_lib", + "//source/common/protobuf:utility_lib", + "//source/common/router:string_accessor_lib", + "//source/common/stream_info:stream_info_lib", + "//source/extensions/bootstrap/reverse_tunnel/common:reverse_connection_utility_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_includes", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:upstream_socket_manager_lib", + "//source/server:null_overload_manager_lib", + "@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/network/reverse_tunnel/config.cc b/source/extensions/filters/network/reverse_tunnel/config.cc new file mode 100644 index 0000000000000..31aca98c60520 --- /dev/null +++ b/source/extensions/filters/network/reverse_tunnel/config.cc @@ -0,0 +1,33 @@ +#include "source/extensions/filters/network/reverse_tunnel/config.h" + +#include "source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { + +Network::FilterFactoryCb ReverseTunnelFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config, + Server::Configuration::FactoryContext& context) { + auto config = std::make_shared(proto_config, context); + // Capture scope and overload manager pointers to avoid dangling references. + Stats::Scope* scope = &context.scope(); + Server::OverloadManager* overload_manager = &context.serverFactoryContext().overloadManager(); + + return [config, scope, overload_manager](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter( + std::make_shared(config, *scope, *overload_manager)); + }; +} + +/** + * Static registration for the reverse tunnel filter. + */ +REGISTER_FACTORY(ReverseTunnelFilterConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory); + +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/reverse_tunnel/config.h b/source/extensions/filters/network/reverse_tunnel/config.h new file mode 100644 index 0000000000000..fd1e24ccf77ef --- /dev/null +++ b/source/extensions/filters/network/reverse_tunnel/config.h @@ -0,0 +1,34 @@ +#pragma once + +#include "envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.pb.h" +#include "envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.pb.validate.h" + +#include "source/extensions/filters/network/common/factory_base.h" +#include "source/extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { + +/** + * Config registration for the reverse tunnel network filter. + */ +class ReverseTunnelFilterConfigFactory + : public Common::FactoryBase< + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel> { +public: + // Always mark the reverse tunnel filter as terminal filter. + ReverseTunnelFilterConfigFactory() + : FactoryBase(NetworkFilterNames::get().ReverseTunnel, true /* isTerminalFilter */) {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.cc b/source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.cc new file mode 100644 index 0000000000000..92fa88664a386 --- /dev/null +++ b/source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.cc @@ -0,0 +1,293 @@ +#include "source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h" + +#include "envoy/buffer/buffer.h" +#include "envoy/network/connection.h" +#include "envoy/server/overload/overload_manager.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/http/codes.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/http/headers.h" +#include "source/common/http/http1/codec_impl.h" +#include "source/common/network/connection_socket_impl.h" +#include "source/common/router/string_accessor_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { + +// Stats helper implementation. +ReverseTunnelFilter::ReverseTunnelStats +ReverseTunnelFilter::ReverseTunnelStats::generateStats(const std::string& prefix, + Stats::Scope& scope) { + return {ALL_REVERSE_TUNNEL_HANDSHAKE_STATS(POOL_COUNTER_PREFIX(scope, prefix))}; +} + +// ReverseTunnelFilterConfig implementation. +ReverseTunnelFilterConfig::ReverseTunnelFilterConfig( + const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config, + Server::Configuration::FactoryContext&) + : ping_interval_(proto_config.has_ping_interval() + ? std::chrono::milliseconds( + DurationUtil::durationToMilliseconds(proto_config.ping_interval())) + : std::chrono::milliseconds(2000)), + auto_close_connections_( + proto_config.auto_close_connections() ? proto_config.auto_close_connections() : false), + request_path_(proto_config.request_path().empty() ? "/reverse_connections/request" + : proto_config.request_path()), + request_method_string_([&proto_config]() -> std::string { + envoy::config::core::v3::RequestMethod method = proto_config.request_method(); + if (method == envoy::config::core::v3::METHOD_UNSPECIFIED) { + method = envoy::config::core::v3::GET; + } + return envoy::config::core::v3::RequestMethod_Name(method); + }()) {} + +// ReverseTunnelFilter implementation. +ReverseTunnelFilter::ReverseTunnelFilter(ReverseTunnelFilterConfigSharedPtr config, + Stats::Scope& stats_scope, + Server::OverloadManager& overload_manager) + : config_(std::move(config)), stats_scope_(stats_scope), overload_manager_(overload_manager), + stats_(ReverseTunnelStats::generateStats("reverse_tunnel.handshake.", stats_scope_)) {} + +Network::FilterStatus ReverseTunnelFilter::onNewConnection() { + ENVOY_CONN_LOG(debug, "reverse_tunnel: new connection established", + read_callbacks_->connection()); + return Network::FilterStatus::Continue; +} + +Network::FilterStatus ReverseTunnelFilter::onData(Buffer::Instance& data, bool) { + if (!codec_) { + Http::Http1Settings http1_settings; + Http::Http1::CodecStats::AtomicPtr http1_stats_ptr; + auto& http1_stats = Http::Http1::CodecStats::atomicGet(http1_stats_ptr, stats_scope_); + codec_ = std::make_unique( + read_callbacks_->connection(), http1_stats, *this, http1_settings, + Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT, + envoy::config::core::v3::HttpProtocolOptions::ALLOW, overload_manager_); + } + + const Http::Status status = codec_->dispatch(data); + if (!status.ok()) { + ENVOY_CONN_LOG(debug, "reverse_tunnel: codec dispatch error: {}", read_callbacks_->connection(), + status.message()); + // Close connection on codec error. + read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + return Network::FilterStatus::StopIteration; + } + return Network::FilterStatus::StopIteration; +} + +void ReverseTunnelFilter::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) { + read_callbacks_ = &callbacks; +} + +Http::RequestDecoder& ReverseTunnelFilter::newStream(Http::ResponseEncoder& response_encoder, + bool) { + active_decoder_ = std::make_unique(*this, response_encoder); + return *active_decoder_; +} + +// Private methods. + +// RequestDecoderImpl +void ReverseTunnelFilter::RequestDecoderImpl::decodeHeaders( + Http::RequestHeaderMapSharedPtr&& headers, bool end_stream) { + headers_ = std::move(headers); + if (end_stream) { + processIfComplete(true); + } +} + +void ReverseTunnelFilter::RequestDecoderImpl::decodeData(Buffer::Instance& data, bool end_stream) { + body_.add(data); + if (end_stream) { + processIfComplete(true); + } +} + +void ReverseTunnelFilter::RequestDecoderImpl::decodeTrailers(Http::RequestTrailerMapPtr&&) { + processIfComplete(true); +} + +void ReverseTunnelFilter::RequestDecoderImpl::decodeMetadata(Http::MetadataMapPtr&&) {} + +void ReverseTunnelFilter::RequestDecoderImpl::sendLocalReply( + Http::Code code, absl::string_view body, + const std::function& modify_headers, + const absl::optional, absl::string_view) { + auto headers = Http::ResponseHeaderMapImpl::create(); + headers->setStatus(static_cast(code)); + headers->setReferenceContentType(Http::Headers::get().ContentTypeValues.Text); + if (modify_headers) { + modify_headers(*headers); + } + const bool end_stream = body.empty(); + encoder_.encodeHeaders(*headers, end_stream); + if (!end_stream) { + Buffer::OwnedImpl buf(body); + encoder_.encodeData(buf, true); + } +} + +StreamInfo::StreamInfo& ReverseTunnelFilter::RequestDecoderImpl::streamInfo() { + return stream_info_; +} + +AccessLog::InstanceSharedPtrVector ReverseTunnelFilter::RequestDecoderImpl::accessLogHandlers() { + return {}; +} + +Http::RequestDecoderHandlePtr ReverseTunnelFilter::RequestDecoderImpl::getRequestDecoderHandle() { + return nullptr; +} + +void ReverseTunnelFilter::RequestDecoderImpl::processIfComplete(bool end_stream) { + if (!end_stream || complete_) { + return; + } + complete_ = true; + + // Validate method/path. + const absl::string_view method = headers_->getMethodValue(); + const absl::string_view path = headers_->getPathValue(); + ENVOY_LOG(trace, + "ReverseTunnelFilter::RequestDecoderImpl::processIfComplete: method: {}, path: {}", + method, path); + if (!absl::EqualsIgnoreCase(method, parent_.config_->requestMethod()) || + path != parent_.config_->requestPath()) { + sendLocalReply(Http::Code::NotFound, "Not a reverse tunnel request", nullptr, absl::nullopt, + "reverse_tunnel_not_found"); + // Close the connection after sending the response. + parent_.read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + return; + } + + // Extract node/cluster/tenant identifiers from HTTP headers. + const auto node_vals = + headers_->get(Extensions::Bootstrap::ReverseConnection::reverseTunnelNodeIdHeader()); + const auto cluster_vals = + headers_->get(Extensions::Bootstrap::ReverseConnection::reverseTunnelClusterIdHeader()); + const auto tenant_vals = + headers_->get(Extensions::Bootstrap::ReverseConnection::reverseTunnelTenantIdHeader()); + + if (node_vals.empty() || cluster_vals.empty() || tenant_vals.empty()) { + parent_.stats_.parse_error_.inc(); + ENVOY_CONN_LOG(debug, "reverse_tunnel: missing required headers (node/cluster/tenant)", + parent_.read_callbacks_->connection()); + sendLocalReply(Http::Code::BadRequest, "Missing required reverse tunnel headers", nullptr, + absl::nullopt, "reverse_tunnel_missing_headers"); + // Close the connection after sending the response. + parent_.read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + return; + } + + const absl::string_view node_id = node_vals[0]->value().getStringView(); + const absl::string_view cluster_id = cluster_vals[0]->value().getStringView(); + const absl::string_view tenant_id = tenant_vals[0]->value().getStringView(); + + // Respond with 200 OK. + auto resp_headers = Http::ResponseHeaderMapImpl::create(); + resp_headers->setStatus(200); + encoder_.encodeHeaders(*resp_headers, true); + + parent_.processAcceptedConnection(node_id, cluster_id, tenant_id); + parent_.stats_.accepted_.inc(); + + // Close the connection if configured to do so after handling the request. + if (parent_.config_->autoCloseConnections()) { + parent_.read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + } +} + +void ReverseTunnelFilter::processAcceptedConnection(absl::string_view node_id, + absl::string_view cluster_id, + absl::string_view tenant_id) { + ENVOY_CONN_LOG(debug, + "reverse_tunnel: connection accepted for node '{}' in cluster '{}' (tenant: '{}')", + read_callbacks_->connection(), node_id, cluster_id, tenant_id); + + Network::Connection& connection = read_callbacks_->connection(); + + // Lookup the reverse tunnel acceptor socket interface to retrieve the TLS registry. + // Note: This is a global lookup that should be thread-safe but may return nullptr + // if the socket interface isn't registered or we're in a test environment. + auto* base_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (base_interface == nullptr) { + ENVOY_CONN_LOG(debug, "reverse_tunnel: socket interface not registered, skipping socket reuse", + connection); + return; + } + + const auto* acceptor = + dynamic_cast( + base_interface); + if (acceptor == nullptr) { + ENVOY_CONN_LOG(error, "reverse_tunnel: reverse tunnel socket interface not found", connection); + return; + } + + // The TLS registry access must be done on the same thread where it was created. + // In integration tests, this might not always be the case. + auto* tls_registry = acceptor->getLocalRegistry(); + if (tls_registry == nullptr) { + ENVOY_CONN_LOG(debug, "reverse_tunnel: thread local registry not available on this thread", + connection); + return; + } + + auto* socket_manager = tls_registry->socketManager(); + if (socket_manager == nullptr) { + ENVOY_CONN_LOG(error, "reverse_tunnel: socket manager not available", connection); + return; + } + + // Wrap the downstream socket with our custom IO handle to manage its lifecycle. + const Network::ConnectionSocketPtr& socket = connection.getSocket(); + if (!socket || !socket->isOpen()) { + ENVOY_CONN_LOG(debug, "reverse_tunnel: original socket not available or not open", + read_callbacks_->connection()); + return; + } + + // Duplicate the original socket's IO handle for reuse. + Network::IoHandlePtr wrapped_handle = socket->ioHandle().duplicate(); + if (!wrapped_handle || !wrapped_handle->isOpen()) { + ENVOY_CONN_LOG(error, "reverse_tunnel: failed to duplicate socket handle", connection); + return; + } + + // Build a new ConnectionSocket from the duplicated handle, preserving addressing info. + auto wrapped_socket = std::make_unique( + std::move(wrapped_handle), socket->connectionInfoProvider().localAddress(), + socket->connectionInfoProvider().remoteAddress()); + + // Reset file events on the new socket. + wrapped_socket->ioHandle().resetFileEvents(); + + // Convert ping interval to seconds as required by the manager API. + const std::chrono::seconds ping_seconds = + std::chrono::duration_cast(config_->pingInterval()); + + // Register the wrapped socket for reuse under the provided identifiers. + // Note: The socket manager is expected to be thread-safe. + if (socket_manager != nullptr) { + ENVOY_CONN_LOG(trace, "reverse_tunnel: registering wrapped socket for reuse", connection); + socket_manager->addConnectionSocket(std::string(node_id), std::string(cluster_id), + std::move(wrapped_socket), ping_seconds, + /*rebalanced=*/false); + ENVOY_CONN_LOG(debug, "reverse_tunnel: successfully registered wrapped socket for reuse", + connection); + } +} + +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h b/source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h new file mode 100644 index 0000000000000..15558283b399f --- /dev/null +++ b/source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h @@ -0,0 +1,136 @@ +#pragma once + +#include "envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.pb.h" +#include "envoy/http/codec.h" +#include "envoy/network/filter.h" +#include "envoy/server/factory_context.h" +#include "envoy/server/overload/overload_manager.h" +#include "envoy/stats/stats_macros.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/stream_info/stream_info_impl.h" + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { + +/** + * Configuration for the reverse tunnel network filter. + */ +class ReverseTunnelFilterConfig { +public: + ReverseTunnelFilterConfig( + const envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel& proto_config, + Server::Configuration::FactoryContext& context); + + std::chrono::milliseconds pingInterval() const { return ping_interval_; } + bool autoCloseConnections() const { return auto_close_connections_; } + const std::string& requestPath() const { return request_path_; } + const std::string& requestMethod() const { return request_method_string_; } + +private: + const std::chrono::milliseconds ping_interval_; + const bool auto_close_connections_; + const std::string request_path_; + const std::string request_method_string_; +}; + +using ReverseTunnelFilterConfigSharedPtr = std::shared_ptr; + +/** + * Network filter that handles reverse tunnel connection acceptance/rejection. + * This filter processes HTTP requests to a specific endpoint and uses + * HTTP headers to receive required identifiers. + * + * The filter operates as a terminal filter when processing reverse tunnel requests, + * meaning it stops the filter chain after processing and manages connection lifecycle. + */ +class ReverseTunnelFilter : public Network::ReadFilter, + public Http::ServerConnectionCallbacks, + public Logger::Loggable { +public: + ReverseTunnelFilter(ReverseTunnelFilterConfigSharedPtr config, Stats::Scope& stats_scope, + Server::OverloadManager& overload_manager); + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; + Network::FilterStatus onNewConnection() override; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override; + + // Http::ServerConnectionCallbacks + Http::RequestDecoder& newStream(Http::ResponseEncoder& response_encoder, + bool is_internally_created) override; + void onGoAway(Http::GoAwayErrorCode) override {} + +private: +// Stats definition. +#define ALL_REVERSE_TUNNEL_HANDSHAKE_STATS(COUNTER) \ + COUNTER(parse_error) \ + COUNTER(accepted) \ + COUNTER(rejected) + + struct ReverseTunnelStats { + ALL_REVERSE_TUNNEL_HANDSHAKE_STATS(GENERATE_COUNTER_STRUCT) + static ReverseTunnelStats generateStats(const std::string& prefix, Stats::Scope& scope); + }; + + // Process reverse tunnel connection. + void processAcceptedConnection(absl::string_view node_id, absl::string_view cluster_id, + absl::string_view tenant_id); + + ReverseTunnelFilterConfigSharedPtr config_; + Network::ReadFilterCallbacks* read_callbacks_{nullptr}; + + // HTTP/1 codec and wiring. + Http::ServerConnectionPtr codec_; + Stats::Scope& stats_scope_; + Server::OverloadManager& overload_manager_; + + // Stats counters. + ReverseTunnelStats stats_; + + // Per-request decoder to buffer body and respond via encoder. + class RequestDecoderImpl : public Http::RequestDecoder { + public: + RequestDecoderImpl(ReverseTunnelFilter& parent, Http::ResponseEncoder& encoder) + : parent_(parent), encoder_(encoder), + stream_info_(parent_.read_callbacks_->connection().streamInfo().timeSource(), nullptr, + StreamInfo::FilterState::LifeSpan::Connection) {} + + void decodeHeaders(Http::RequestHeaderMapSharedPtr&& headers, bool end_stream) override; + void decodeData(Buffer::Instance& data, bool end_stream) override; + void decodeTrailers(Http::RequestTrailerMapPtr&&) override; + void decodeMetadata(Http::MetadataMapPtr&&) override; + void sendLocalReply(Http::Code code, absl::string_view body, + const std::function&, + const absl::optional, absl::string_view) override; + StreamInfo::StreamInfo& streamInfo() override; + AccessLog::InstanceSharedPtrVector accessLogHandlers() override; + Http::RequestDecoderHandlePtr getRequestDecoderHandle() override; + + private: + void processIfComplete(bool end_stream); + + ReverseTunnelFilter& parent_; + Http::ResponseEncoder& encoder_; + Http::RequestHeaderMapSharedPtr headers_; + Buffer::OwnedImpl body_; + bool complete_{false}; + StreamInfo::StreamInfoImpl stream_info_; + }; + + std::unique_ptr active_decoder_; +}; + +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/thrift_proxy/conn_manager.cc b/source/extensions/filters/network/thrift_proxy/conn_manager.cc index f83cedb319b72..d342ec5679b46 100644 --- a/source/extensions/filters/network/thrift_proxy/conn_manager.cc +++ b/source/extensions/filters/network/thrift_proxy/conn_manager.cc @@ -831,7 +831,7 @@ void ConnectionManager::ActiveRpc::recordResponseAccessLog( void ConnectionManager::ActiveRpc::recordResponseAccessLog(const std::string& message_type, const std::string& reply_type) { - ProtobufWkt::Struct stats_obj; + Protobuf::Struct stats_obj; auto& fields_map = *stats_obj.mutable_fields(); auto& response_fields_map = *fields_map["response"].mutable_struct_value()->mutable_fields(); @@ -880,18 +880,26 @@ FilterStatus ConnectionManager::ActiveRpc::messageBegin(MessageMetadataSharedPtr metadata->hasFrameSize() ? static_cast(metadata->frameSize()) : -1; if (error.has_value()) { - parent_.stats_.request_internal_error_.inc(); - std::ostringstream oss; - parent_.read_callbacks_->connection().dumpState(oss, 0); - ENVOY_STREAM_LOG(error, - "Catch exception: {}. Request seq_id: {}, method: {}, frame size: {}, cluster " - "name: {}, downstream connection state {}, headers:\n{}", - *this, error.value(), metadata_->sequenceId(), method, frame_size, - cluster_name, oss.str(), metadata->requestHeaders()); + // If downstream connection is closing, we won't be able to proxy and expect this exception. + // In this case, just propagate the error and do *not* increase the internal error counter. + if (parent_.read_callbacks_->connection().state() == Network::Connection::State::Closing) { + ENVOY_CONN_LOG(debug, "thrift: downstream connection closing, not proxying", + parent_.read_callbacks_->connection()); + } else { + parent_.stats_.request_internal_error_.inc(); + std::ostringstream oss; + parent_.read_callbacks_->connection().dumpState(oss, 0); + ENVOY_STREAM_LOG( + error, + "Catch exception: {}. Request seq_id: {}, method: {}, frame size: {}, cluster " + "name: {}, downstream connection state {}, headers:\n{}", + *this, error.value(), metadata_->sequenceId(), method, frame_size, cluster_name, + oss.str(), metadata->requestHeaders()); + } throw EnvoyException(error.value()); } - ProtobufWkt::Struct stats_obj; + Protobuf::Struct stats_obj; auto& fields_map = *stats_obj.mutable_fields(); fields_map["cluster"] = ValueUtil::stringValue(cluster_name); fields_map["method"] = ValueUtil::stringValue(method); diff --git a/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.cc b/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.cc index f29f4bdf07b0b..3b376ce674e06 100644 --- a/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.cc +++ b/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.cc @@ -78,7 +78,7 @@ const std::string& HeaderToMetadataFilter::decideNamespace(const std::string& ns bool HeaderToMetadataFilter::addMetadata(StructMap& struct_map, const std::string& meta_namespace, const std::string& key, std::string value, ValueType type, ValueEncode encode) const { - ProtobufWkt::Value val; + Protobuf::Value val; ASSERT(!value.empty()); diff --git a/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.h b/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.h index 1f5f3ac66fb2c..7701a1b50d5f7 100644 --- a/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.h +++ b/source/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter.h @@ -81,7 +81,7 @@ class HeaderToMetadataFilter : public ThriftProxy::ThriftFilters::PassThroughDec private: using ProtobufRepeatedRule = Protobuf::RepeatedPtrField; - using StructMap = std::map; + using StructMap = std::map; /** * writeHeaderToMetadata encapsulates (1) searching for the header and (2) writing it to the diff --git a/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.cc b/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.cc index d94cfa6e3ab65..294879f864193 100644 --- a/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.cc +++ b/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.cc @@ -242,7 +242,7 @@ const std::string& PayloadToMetadataFilter::decideNamespace(const std::string& n bool PayloadToMetadataFilter::addMetadata(const std::string& meta_namespace, const std::string& key, std::string value, ValueType type) { - ProtobufWkt::Value val; + Protobuf::Value val; ASSERT(!value.empty()); if (value.size() >= MAX_PAYLOAD_VALUE_LEN) { diff --git a/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.h b/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.h index ee5532bb22169..c21ea96044108 100644 --- a/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.h +++ b/source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter.h @@ -170,7 +170,7 @@ class PayloadToMetadataFilter : public MetadataHandler, } // TODO(kuochunghsu): extract the metadata handling logic form header/payload to metadata filters. - using StructMap = std::map; + using StructMap = std::map; bool addMetadata(const std::string&, const std::string&, std::string, ValueType); void applyKeyValue(std::string, const Rule&, const KeyValuePair&); const std::string& decideNamespace(const std::string& nspace) const; diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index 8eaa39bd402ac..45483e7a66d80 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -63,6 +63,8 @@ class NetworkFilterNameValues { const std::string NetworkExternalProcessor = "envoy.filters.network.ext_proc"; // Network match delegate filter const std::string NetworkMatchDelegate = "envoy.filters.network.match_delegate"; + // Reverse tunnel filter + const std::string ReverseTunnel = "envoy.filters.network.reverse_tunnel"; }; using NetworkFilterNames = ConstSingleton; diff --git a/source/extensions/filters/network/zookeeper_proxy/filter.cc b/source/extensions/filters/network/zookeeper_proxy/filter.cc index 29e599ba782b0..fe9ef9e17f708 100644 --- a/source/extensions/filters/network/zookeeper_proxy/filter.cc +++ b/source/extensions/filters/network/zookeeper_proxy/filter.cc @@ -229,12 +229,12 @@ void ZooKeeperFilter::setDynamicMetadata( const std::vector>& data) { envoy::config::core::v3::Metadata& dynamic_metadata = read_callbacks_->connection().streamInfo().dynamicMetadata(); - ProtobufWkt::Struct metadata( + Protobuf::Struct metadata( (*dynamic_metadata.mutable_filter_metadata())[NetworkFilterNames::get().ZooKeeperProxy]); auto& fields = *metadata.mutable_fields(); for (const auto& pair : data) { - auto val = ProtobufWkt::Value(); + auto val = Protobuf::Value(); val.set_string_value(pair.second); fields.insert({pair.first, val}); } diff --git a/source/extensions/filters/udp/dns_filter/BUILD b/source/extensions/filters/udp/dns_filter/BUILD index e4315f1acc15d..f327a12d84d3b 100644 --- a/source/extensions/filters/udp/dns_filter/BUILD +++ b/source/extensions/filters/udp/dns_filter/BUILD @@ -34,8 +34,8 @@ envoy_cc_library( "//envoy/network:listener_interface", "//source/common/buffer:buffer_lib", "//source/common/common:empty_string", + "//source/common/common:radix_tree_lib", "//source/common/common:safe_memcpy_lib", - "//source/common/common:trie_lookup_table_lib", "//source/common/config:config_provider_lib", "//source/common/config:datasource_lib", "//source/common/network:address_lib", diff --git a/source/extensions/filters/udp/dns_filter/dns_filter.cc b/source/extensions/filters/udp/dns_filter/dns_filter.cc index c4c1fefa3c09d..9d6e92c67fd6f 100644 --- a/source/extensions/filters/udp/dns_filter/dns_filter.cc +++ b/source/extensions/filters/udp/dns_filter/dns_filter.cc @@ -175,8 +175,22 @@ DnsFilterEnvoyConfig::DnsFilterEnvoyConfig( client_config, resolver_timeout, DEFAULT_RESOLVER_TIMEOUT.count())); max_pending_lookups_ = client_config.max_pending_lookups(); } else { - // In case client_config doesn't exist, create default DNS resolver factory and save it. - dns_resolver_factory_ = &Network::createDefaultDnsResolverFactory(typed_dns_resolver_config_); + // In case client_config doesn't exist, use the bootstrap DNS resolver if it is configured. + if (context.serverFactoryContext().bootstrap().has_typed_dns_resolver_config() && + !context.serverFactoryContext() + .bootstrap() + .typed_dns_resolver_config() + .typed_config() + .type_url() + .empty()) { + typed_dns_resolver_config_.MergeFrom( + context.serverFactoryContext().bootstrap().typed_dns_resolver_config()); + dns_resolver_factory_ = + &Network::createDnsResolverFactoryFromTypedConfig(typed_dns_resolver_config_); + } else { + // Otherwise create default DNS resolver factory and save it. + dns_resolver_factory_ = &Network::createDefaultDnsResolverFactory(typed_dns_resolver_config_); + } max_pending_lookups_ = 0; } } diff --git a/source/extensions/filters/udp/dns_filter/dns_filter.h b/source/extensions/filters/udp/dns_filter/dns_filter.h index d1e5262d1146e..726120b21b41d 100644 --- a/source/extensions/filters/udp/dns_filter/dns_filter.h +++ b/source/extensions/filters/udp/dns_filter/dns_filter.h @@ -6,7 +6,7 @@ #include "envoy/network/filter.h" #include "source/common/buffer/buffer_impl.h" -#include "source/common/common/trie_lookup_table.h" +#include "source/common/common/radix_tree.h" #include "source/common/config/config_provider_impl.h" #include "source/common/network/utility.h" #include "source/extensions/filters/udp/dns_filter/dns_filter_resolver.h" @@ -97,9 +97,7 @@ class DnsFilterEnvoyConfig : public Logger::Loggable { } const Network::DnsResolverFactory& dnsResolverFactory() const { return *dns_resolver_factory_; } Api::Api& api() const { return api_; } - const TrieLookupTable& getDnsTrie() const { - return dns_lookup_trie_; - } + const RadixTree& getDnsTrie() const { return dns_lookup_trie_; } private: static DnsFilterStats generateStats(const std::string& stat_prefix, Stats::Scope& scope) { @@ -123,7 +121,7 @@ class DnsFilterEnvoyConfig : public Logger::Loggable { mutable DnsFilterStats stats_; - TrieLookupTable dns_lookup_trie_; + RadixTree dns_lookup_trie_; absl::flat_hash_map domain_ttl_; bool forward_queries_; uint64_t retry_count_; diff --git a/source/extensions/filters/udp/udp_proxy/router/router_impl.cc b/source/extensions/filters/udp/udp_proxy/router/router_impl.cc index 0fa2a904e1e5b..8ab3a80a569b1 100644 --- a/source/extensions/filters/udp/udp_proxy/router/router_impl.cc +++ b/source/extensions/filters/udp/udp_proxy/router/router_impl.cc @@ -15,17 +15,16 @@ namespace UdpFilters { namespace UdpProxy { namespace Router { -Matcher::ActionFactoryCb RouteMatchActionFactory::createActionFactoryCb( - const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) { +Matcher::ActionConstSharedPtr +RouteMatchActionFactory::createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& route_config = MessageUtil::downcastAndValidate< const envoy::extensions::filters::udp::udp_proxy::v3::Route&>(config, validation_visitor); const auto& cluster = route_config.cluster(); // Emplace cluster names to context to get all cluster names. context.cluster_name_.emplace(cluster); - - return [cluster]() { return std::make_unique(cluster); }; + return std::make_shared(cluster); } REGISTER_FACTORY(RouteMatchActionFactory, Matcher::ActionFactory); diff --git a/source/extensions/filters/udp/udp_proxy/router/router_impl.h b/source/extensions/filters/udp/udp_proxy/router/router_impl.h index f506778db6af2..04bb887fde713 100644 --- a/source/extensions/filters/udp/udp_proxy/router/router_impl.h +++ b/source/extensions/filters/udp/udp_proxy/router/router_impl.h @@ -32,9 +32,9 @@ class RouteMatchAction class RouteMatchActionFactory : public Matcher::ActionFactory { public: - Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, RouteActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, RouteActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; std::string name() const override { return "route"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); diff --git a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc index d0074a95cb4d3..f5c3322021cdf 100644 --- a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc +++ b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc @@ -225,7 +225,6 @@ UdpProxyFilter::ClusterInfo::ClusterInfo(UdpProxyFilter& filter, host_to_sessions_.erase(host_sessions_it); } } - return absl::OkStatus(); })) {} UdpProxyFilter::ClusterInfo::~ClusterInfo() { @@ -343,6 +342,7 @@ UdpProxyFilter::ActiveSession::~ActiveSession() { } void UdpProxyFilter::ActiveSession::onSessionComplete() { + ENVOY_BUG(!on_session_complete_called_, "onSessionComplete() called twice"); ENVOY_LOG(debug, "deleting the session: downstream={} local={} upstream={}", addresses_.peer_->asStringView(), addresses_.local_->asStringView(), host_ != nullptr ? host_->address()->asStringView() : "unknown"); @@ -385,7 +385,7 @@ UdpProxyFilter::ActiveSession::createDownstreamConnectionInfoProvider() { } void UdpProxyFilter::ActiveSession::fillSessionStreamInfo() { - ProtobufWkt::Struct stats_obj; + Protobuf::Struct stats_obj; auto& fields_map = *stats_obj.mutable_fields(); if (cluster_ != nullptr) { fields_map["cluster_name"] = ValueUtil::stringValue(cluster_->cluster_info_->name()); @@ -402,7 +402,7 @@ void UdpProxyFilter::ActiveSession::fillSessionStreamInfo() { } void UdpProxyFilter::fillProxyStreamInfo() { - ProtobufWkt::Struct stats_obj; + Protobuf::Struct stats_obj; auto& fields_map = *stats_obj.mutable_fields(); fields_map["bytes_sent"] = ValueUtil::numberValue(config_->stats().downstream_sess_tx_bytes_.value()); @@ -892,21 +892,23 @@ const std::string HttpUpstreamImpl::resolveTargetTunnelPath() { return absl::StrCat("/.well-known/masque/udp/", target_host, "/", target_port, "/"); } -HttpUpstreamImpl::~HttpUpstreamImpl() { resetEncoder(Network::ConnectionEvent::LocalClose); } +HttpUpstreamImpl::~HttpUpstreamImpl() { + resetEncoder(Network::ConnectionEvent::LocalClose, /*by_local_close=*/true); +} -void HttpUpstreamImpl::resetEncoder(Network::ConnectionEvent event, bool by_downstream) { +void HttpUpstreamImpl::resetEncoder(Network::ConnectionEvent event, bool by_local_close) { if (!request_encoder_) { return; } request_encoder_->getStream().removeCallbacks(*this); - if (by_downstream) { + if (by_local_close) { request_encoder_->getStream().resetStream(Http::StreamResetReason::LocalReset); } request_encoder_ = nullptr; - if (!by_downstream) { + if (!by_local_close) { // If we did not receive a valid CONNECT response yet we treat this as a pool // failure, otherwise we forward the event downstream. if (tunnel_creation_callbacks_.has_value()) { @@ -1070,20 +1072,14 @@ void UdpProxyFilter::TunnelingActiveSession::onStreamFailure( break; case ConnectionPool::PoolFailureReason::Timeout: udp_session_info_.setResponseFlag(StreamInfo::CoreResponseFlag::UpstreamConnectionFailure); - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.enable_udp_proxy_outlier_detection")) { - host.outlierDetector().putResult(Upstream::Outlier::Result::LocalOriginTimeout); - } + host.outlierDetector().putResult(Upstream::Outlier::Result::LocalOriginTimeout); onUpstreamEvent(Network::ConnectionEvent::RemoteClose); break; case ConnectionPool::PoolFailureReason::RemoteConnectionFailure: if (connecting_) { udp_session_info_.setResponseFlag(StreamInfo::CoreResponseFlag::UpstreamConnectionFailure); } - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.enable_udp_proxy_outlier_detection")) { - host.outlierDetector().putResult(Upstream::Outlier::Result::LocalOriginConnectFailed); - } + host.outlierDetector().putResult(Upstream::Outlier::Result::LocalOriginConnectFailed); onUpstreamEvent(Network::ConnectionEvent::RemoteClose); break; } @@ -1105,10 +1101,7 @@ void UdpProxyFilter::TunnelingActiveSession::onStreamReady(StreamInfo::StreamInf connecting_ = false; can_send_upstream_ = true; cluster_->cluster_stats_.sess_tunnel_success_.inc(); - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.enable_udp_proxy_outlier_detection")) { - host.outlierDetector().putResult(Upstream::Outlier::Result::LocalOriginConnectSuccessFinal); - } + host.outlierDetector().putResult(Upstream::Outlier::Result::LocalOriginConnectSuccessFinal); if (filter_.config_->flushAccessLogOnTunnelConnected()) { fillSessionStreamInfo(); diff --git a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h index 850d6832b5be7..f2b6f64606fa9 100644 --- a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h +++ b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h @@ -293,7 +293,7 @@ class HttpUpstreamImpl : public HttpUpstream, protected Http::StreamCallbacks { void onDownstreamEvent(Network::ConnectionEvent event) override { if (event == Network::ConnectionEvent::LocalClose || event == Network::ConnectionEvent::RemoteClose) { - resetEncoder(event, /*by_downstream=*/true); + resetEncoder(event, /*by_local_close=*/true); } }; @@ -360,7 +360,14 @@ class HttpUpstreamImpl : public HttpUpstream, protected Http::StreamCallbacks { }; const std::string resolveTargetTunnelPath(); - void resetEncoder(Network::ConnectionEvent event, bool by_downstream = false); + + /** + * Resets the encoder for the upstream connection. + * @param event the event that caused the reset. + * @param by_local_close whether the reset was initiated by a local close (e.g. session idle + * timeout, envoy termination, etc.) or by upstream close. + */ + void resetEncoder(Network::ConnectionEvent event, bool by_local_close = false); ResponseDecoder response_decoder_; Http::RequestEncoder* request_encoder_{}; diff --git a/source/extensions/formatter/cel/BUILD b/source/extensions/formatter/cel/BUILD index 2483f1de9d457..f6386177a8ef0 100644 --- a/source/extensions/formatter/cel/BUILD +++ b/source/extensions/formatter/cel/BUILD @@ -27,6 +27,7 @@ envoy_cc_library( { "//bazel:windows_x86_64": [], "//conditions:default": [ + "@com_google_cel_cpp//eval/public:value_export_util", "@com_google_cel_cpp//parser", ], }, diff --git a/source/extensions/formatter/cel/cel.cc b/source/extensions/formatter/cel/cel.cc index ac896db2c6f97..7cdc10fcd866a 100644 --- a/source/extensions/formatter/cel/cel.cc +++ b/source/extensions/formatter/cel/cel.cc @@ -7,6 +7,7 @@ #if defined(USE_CEL_PARSER) #include "parser/parser.h" +#include "eval/public/value_export_util.h" #endif namespace Envoy { @@ -16,21 +17,26 @@ namespace Formatter { namespace Expr = Filters::Common::Expr; CELFormatter::CELFormatter(const ::Envoy::LocalInfo::LocalInfo& local_info, - Expr::BuilderInstanceSharedPtr expr_builder, - const google::api::expr::v1alpha1::Expr& input_expr, - absl::optional& max_length) - : local_info_(local_info), expr_builder_(expr_builder), parsed_expr_(input_expr), - max_length_(max_length) { - compiled_expr_ = Expr::createExpression(expr_builder_->builder(), parsed_expr_); -} + Expr::BuilderInstanceSharedConstPtr expr_builder, + const cel::expr::Expr& input_expr, absl::optional& max_length, + bool typed) + : local_info_(local_info), max_length_(max_length), compiled_expr_([&]() { + auto compiled_expr = Expr::CompiledExpression::Create(expr_builder, input_expr); + if (!compiled_expr.ok()) { + throw EnvoyException( + absl::StrCat("failed to create an expression: ", compiled_expr.status().message())); + } + return std::move(compiled_expr.value()); + }()), + typed_(typed) {} absl::optional CELFormatter::formatWithContext(const Envoy::Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { Protobuf::Arena arena; auto eval_status = - Expr::evaluate(*compiled_expr_, arena, &local_info_, stream_info, &context.requestHeaders(), - &context.responseHeaders(), &context.responseTrailers()); + compiled_expr_.evaluate(arena, &local_info_, stream_info, &context.requestHeaders(), + &context.responseHeaders(), &context.responseTrailers()); if (!eval_status.has_value() || eval_status.value().IsError()) { return absl::nullopt; } @@ -42,33 +48,50 @@ CELFormatter::formatWithContext(const Envoy::Formatter::HttpFormatterContext& co return result; } -ProtobufWkt::Value +Protobuf::Value CELFormatter::formatValueWithContext(const Envoy::Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { - auto result = formatWithContext(context, stream_info); - if (!result.has_value()) { - return ValueUtil::nullValue(); + if (typed_) { + Protobuf::Arena arena; + auto eval_status = + compiled_expr_.evaluate(arena, &local_info_, stream_info, &context.requestHeaders(), + &context.responseHeaders(), &context.responseTrailers()); + if (!eval_status.has_value() || eval_status.value().IsError()) { + return ValueUtil::nullValue(); + } + + Protobuf::Value proto_value; + if (!ExportAsProtoValue(eval_status.value(), &proto_value).ok()) { + return ValueUtil::nullValue(); + } + + if (max_length_ && proto_value.kind_case() == Protobuf::Value::kStringValue) { + proto_value.set_string_value(proto_value.string_value().substr(0, max_length_.value())); + } + return proto_value; + } else { + auto result = formatWithContext(context, stream_info); + if (!result.has_value()) { + return ValueUtil::nullValue(); + } + return ValueUtil::stringValue(result.value()); } - return ValueUtil::stringValue(result.value()); } ::Envoy::Formatter::FormatterProviderPtr CELFormatterCommandParser::parse(absl::string_view command, absl::string_view subcommand, absl::optional max_length) const { #if defined(USE_CEL_PARSER) - if (command == "CEL") { + if (command == "CEL" || command == "TYPED_CEL") { auto parse_status = google::api::expr::parser::Parse(subcommand); if (!parse_status.ok()) { - throw EnvoyException("Not able to parse filter expression: " + - parse_status.status().ToString()); + throw EnvoyException("Not able to parse expression: " + parse_status.status().ToString()); } - Server::Configuration::ServerFactoryContext& context = Server::Configuration::ServerFactoryContextInstance::get(); - - return std::make_unique(context.localInfo(), - Extensions::Filters::Common::Expr::getBuilder(context), - parse_status.value().expr(), max_length); + return std::make_unique( + context.localInfo(), Extensions::Filters::Common::Expr::getBuilder(context), + parse_status.value().expr(), max_length, command == "TYPED_CEL"); } return nullptr; diff --git a/source/extensions/formatter/cel/cel.h b/source/extensions/formatter/cel/cel.h index c217c8375b00e..6a5fb8398f2ef 100644 --- a/source/extensions/formatter/cel/cel.h +++ b/source/extensions/formatter/cel/cel.h @@ -15,21 +15,20 @@ namespace Formatter { class CELFormatter : public ::Envoy::Formatter::FormatterProvider { public: CELFormatter(const ::Envoy::LocalInfo::LocalInfo& local_info, - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr, - const google::api::expr::v1alpha1::Expr&, absl::optional&); + Extensions::Filters::Common::Expr::BuilderInstanceSharedConstPtr expr_builder, + const cel::expr::Expr& input_expr, absl::optional& max_length, bool typed); absl::optional formatWithContext(const Envoy::Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValueWithContext(const Envoy::Formatter::HttpFormatterContext& context, - const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValueWithContext(const Envoy::Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo&) const override; private: const ::Envoy::LocalInfo::LocalInfo& local_info_; - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr expr_builder_; - const google::api::expr::v1alpha1::Expr parsed_expr_; const absl::optional max_length_; - Extensions::Filters::Common::Expr::ExpressionPtr compiled_expr_; + const Extensions::Filters::Common::Expr::CompiledExpression compiled_expr_; + const bool typed_; }; class CELFormatterCommandParser : public ::Envoy::Formatter::CommandParser { diff --git a/source/extensions/formatter/req_without_query/req_without_query.cc b/source/extensions/formatter/req_without_query/req_without_query.cc index cf5e07eaac604..c3916b7dfe1e5 100644 --- a/source/extensions/formatter/req_without_query/req_without_query.cc +++ b/source/extensions/formatter/req_without_query/req_without_query.cc @@ -40,7 +40,7 @@ ReqWithoutQuery::formatWithContext(const Envoy::Formatter::HttpFormatterContext& return val; } -ProtobufWkt::Value +Protobuf::Value ReqWithoutQuery::formatValueWithContext(const Envoy::Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo&) const { const Http::HeaderEntry* header = findHeader(context.requestHeaders()); diff --git a/source/extensions/formatter/req_without_query/req_without_query.h b/source/extensions/formatter/req_without_query/req_without_query.h index a4c1be0df82e4..cdc67f30ca427 100644 --- a/source/extensions/formatter/req_without_query/req_without_query.h +++ b/source/extensions/formatter/req_without_query/req_without_query.h @@ -20,8 +20,8 @@ class ReqWithoutQuery : public ::Envoy::Formatter::FormatterProvider { absl::optional formatWithContext(const Envoy::Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo&) const override; - ProtobufWkt::Value formatValueWithContext(const Envoy::Formatter::HttpFormatterContext& context, - const StreamInfo::StreamInfo&) const override; + Protobuf::Value formatValueWithContext(const Envoy::Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo&) const override; private: const Http::HeaderEntry* findHeader(const Http::HeaderMap& headers) const; diff --git a/source/extensions/formatter/xfcc_value/BUILD b/source/extensions/formatter/xfcc_value/BUILD new file mode 100644 index 0000000000000..3e8ddcff3d13c --- /dev/null +++ b/source/extensions/formatter/xfcc_value/BUILD @@ -0,0 +1,20 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["xfcc_value.cc"], + hdrs = ["xfcc_value.h"], + deps = [ + "//source/common/formatter:formatter_extension_lib", + "//source/common/formatter:substitution_formatter_lib", + "//source/common/protobuf:utility_lib", + ], +) diff --git a/source/extensions/formatter/xfcc_value/xfcc_value.cc b/source/extensions/formatter/xfcc_value/xfcc_value.cc new file mode 100644 index 0000000000000..56659cb4a7104 --- /dev/null +++ b/source/extensions/formatter/xfcc_value/xfcc_value.cc @@ -0,0 +1,245 @@ +#include "source/extensions/formatter/xfcc_value/xfcc_value.h" + +#include +#include + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +namespace { + +const absl::flat_hash_set& supportedKeys() { + // The keys are case-insensitive, so we store them in lower case. + CONSTRUCT_ON_FIRST_USE(absl::flat_hash_set, { + "by", + "hash", + "cert", + "chain", + "subject", + "uri", + "dns", + }); +} + +size_t countBackslashes(absl::string_view str) { + size_t count = 0; + // Search from end to start for first not '\' + for (char r : std::ranges::reverse_view(str)) { + if (r == '\\') { + ++count; + } else { + break; + } + } + return count; +} + +// The XFCC header value is a comma (`,`) separated string. Each substring is an XFCC element, +// which holds information added by a single proxy. A proxy can append the current client +// certificate information as an XFCC element, to the end of the request’s XFCC header after a +// comma. +// +// Each XFCC element is a semicolon (`;`) separated string. Each substring is a key-value pair, +// grouped together by an equals (`=`) sign. The keys are case-insensitive, the values are +// case-sensitive. If `,`, `;` or `=` appear in a value, the value should be double-quoted. +// Double-quotes in the value should be replaced by backslash-double-quote (`\"`). +// +// There maybe multiple XFCC elements and the oldest/leftest one with the key will be used by +// default because Envoy assumes the oldest XFCC element come from the original client certificate. +// So scan the header left-to-right. + +// Handles a single key/value pair within an XFCC element. +absl::optional parseKeyValuePair(absl::string_view pair, absl::string_view target) { + // Find '=' not in quotes. Because key will always be the first part and won't be double + // quoted or contain `=`, we can safely use `absl::StrSplit`. + std::pair key_value = + absl::StrSplit(pair, absl::MaxSplits('=', 1)); + absl::string_view raw_key = absl::StripAsciiWhitespace(key_value.first); + if (!absl::EqualsIgnoreCase(raw_key, target)) { + return absl::nullopt; + } + + absl::string_view raw_value = absl::StripAsciiWhitespace(key_value.second); + // If value is double quoted, remove quotes. + if (raw_value.size() >= 2 && raw_value.front() == '"' && raw_value.back() == '"') { + raw_value = raw_value.substr(1, raw_value.size() - 2); + } + + // Quick path to avoid handle unescaping if not needed. + if (raw_value.find('\\') == absl::string_view::npos) { + return std::string(raw_value); + } + + // Handle unescaping. + + // If the raw value only contains a single backslash then return it as is. + if (raw_value.size() < 2) { + return std::string(raw_value); + } + + // Unescape double quotes and backslashes. + std::string unescaped; + unescaped.reserve(raw_value.size()); + size_t i = 0; + for (; i < raw_value.size() - 1; ++i) { + if (raw_value[i] == '\\') { + if (raw_value[i + 1] == '"' || raw_value[i + 1] == '\\') { + unescaped.push_back(raw_value[i + 1]); + ++i; + continue; + } + } + unescaped.push_back(raw_value[i]); + } + // Handle the last character. + if (i < raw_value.size()) { + unescaped.push_back(raw_value[i]); + } + + return unescaped; +} + +// Handles a single XFCC element (semicolon-separated key/value pairs). +absl::optional parseElementForKey(absl::string_view element, + absl::string_view target) { + + // Scan key-value pairs in this element (by semicolon not in quotes). + bool in_quotes = false; + size_t start = 0; + const size_t element_size = element.size(); + for (size_t i = 0; i <= element_size; ++i) { + // Check for end of key-value pair. + if (i == element_size || element[i] == ';') { + // If not in quotes then we found the end of a key-value pair. + if (!in_quotes) { + auto value = parseKeyValuePair(element.substr(start, i - start), target); + if (value.has_value()) { + return value; + } + start = i + 1; + } + continue; + } + + // Switch quote state if we encounter a quote character. + if (element[i] == '"') { + if (countBackslashes(element.substr(0, i)) % 2 == 0) { + in_quotes = !in_quotes; + } + } + } + + // Note, we should never encounter unmatched quotes here because if there is + // an unmatched quote, it should be handled in the parseValueFromXfccByKey() + // and will not enter this function. + ASSERT(!in_quotes); + return absl::nullopt; +} + +// Extracts the key from the XFCC header. +absl::StatusOr parseValueFromXfccByKey(const Http::RequestHeaderMap& headers, + absl::string_view target) { + absl::string_view value = headers.getForwardedClientCertValue(); + if (value.empty()) { + return absl::InvalidArgumentError("XFCC header is not present"); + } + + // Scan elements in the XFCC header (by comma not in quotes). + bool in_quotes = false; + size_t start = 0; + const size_t value_size = value.size(); + for (size_t i = 0; i <= value_size; ++i) { + // Check for end of element. + if (i == value_size || value[i] == ',') { + // If not in quotes then we found the end of an element. + if (!in_quotes) { + auto result = parseElementForKey(value.substr(start, i - start), target); + if (result.has_value()) { + return result.value(); + } + start = i + 1; + } + continue; + } + + // Switch quote state if we encounter a quote character. + if (value[i] == '"') { + if (countBackslashes(value.substr(0, i)) % 2 == 0) { + in_quotes = !in_quotes; + } + } + } + + if (in_quotes) { + return absl::InvalidArgumentError("Invalid XFCC header: unmatched quotes"); + } + + return absl::InvalidArgumentError("XFCC header does not contain target key"); +} + +} // namespace + +class XfccValueFormatterProvider : public ::Envoy::Formatter::FormatterProvider, + Logger::Loggable { +public: + XfccValueFormatterProvider(Http::LowerCaseString&& key) : key_(key) {} + + absl::optional formatWithContext(const Envoy::Formatter::Context& context, + const StreamInfo::StreamInfo&) const override { + auto status_or = parseValueFromXfccByKey(context.requestHeaders(), key_); + if (!status_or.ok()) { + ENVOY_LOG(debug, "XFCC value extraction failure: {}", status_or.status().message()); + return absl::nullopt; + } + return std::move(status_or.value()); + } + + Protobuf::Value formatValueWithContext(const Envoy::Formatter::Context& context, + const StreamInfo::StreamInfo& stream_info) const override { + absl::optional value = formatWithContext(context, stream_info); + if (!value.has_value()) { + return ValueUtil::nullValue(); + } + Protobuf::Value result; + result.set_string_value(std::move(value.value())); + return result; + } + +private: + Http::LowerCaseString key_; +}; + +Envoy::Formatter::FormatterProviderPtr +XfccValueFormatterCommandParser::parse(absl::string_view command, absl::string_view subcommand, + absl::optional) const { + // Implementation for parsing the XFCC_VALUE() command. + if (command != "XFCC_VALUE") { + return nullptr; + } + + Http::LowerCaseString lower_subcommand(subcommand); + if (subcommand.empty()) { + throw EnvoyException("XFCC_VALUE command requires a subcommand"); + } + if (!supportedKeys().contains(lower_subcommand.get())) { + throw EnvoyException( + absl::StrCat("XFCC_VALUE command does not support subcommand: ", lower_subcommand.get())); + } + return std::make_unique(std::move(lower_subcommand)); +} + +class XfccValueCommandParserFactory : public Envoy::Formatter::BuiltInCommandParserFactory { +public: + XfccValueCommandParserFactory() = default; + Envoy::Formatter::CommandParserPtr createCommandParser() const override { + return std::make_unique(); + } + std::string name() const override { return "envoy.built_in_formatters.xfcc_value"; } +}; + +REGISTER_FACTORY(XfccValueCommandParserFactory, Envoy::Formatter::BuiltInCommandParserFactory); + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/formatter/xfcc_value/xfcc_value.h b/source/extensions/formatter/xfcc_value/xfcc_value.h new file mode 100644 index 0000000000000..43a977edfb0ba --- /dev/null +++ b/source/extensions/formatter/xfcc_value/xfcc_value.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "envoy/config/typed_config.h" +#include "envoy/registry/registry.h" + +#include "source/common/formatter/stream_info_formatter.h" +#include "source/common/formatter/substitution_formatter.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { + +// Access log handler for XFCC_VALUE() command. +class XfccValueFormatterCommandParser : public ::Envoy::Formatter::CommandParser { +public: + XfccValueFormatterCommandParser() = default; + Envoy::Formatter::FormatterProviderPtr parse(absl::string_view command, + absl::string_view subcommand, + absl::optional max_length) const override; +}; + +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/geoip_providers/maxmind/geoip_provider.cc b/source/extensions/geoip_providers/maxmind/geoip_provider.cc index 4bc0f8fb2e7d8..1e5fea39bda30 100644 --- a/source/extensions/geoip_providers/maxmind/geoip_provider.cc +++ b/source/extensions/geoip_providers/maxmind/geoip_provider.cc @@ -72,7 +72,7 @@ GeoipProviderConfig::GeoipProviderConfig( : absl::nullopt; isp_header_ = !geo_headers_to_add.isp().empty() ? absl::make_optional(geo_headers_to_add.isp()) : absl::nullopt; - apple_private_relay_header_ = !geo_headers_to_add.isp().empty() + apple_private_relay_header_ = !geo_headers_to_add.apple_private_relay().empty() ? absl::make_optional(geo_headers_to_add.apple_private_relay()) : absl::nullopt; if (!city_db_path_ && !anon_db_path_ && !asn_db_path_ && !isp_db_path_) { @@ -99,6 +99,7 @@ void GeoipProviderConfig::registerGeoDbStats(const absl::string_view& db_type) { stat_name_set_->rememberBuiltin(absl::StrCat(db_type, ".lookup_error")); stat_name_set_->rememberBuiltin(absl::StrCat(db_type, ".db_reload_error")); stat_name_set_->rememberBuiltin(absl::StrCat(db_type, ".db_reload_success")); + stat_name_set_->rememberBuiltin(absl::StrCat(db_type, ".db_build_epoch")); } bool GeoipProviderConfig::isLookupEnabledForHeader(const absl::optional& header) { @@ -109,6 +110,10 @@ void GeoipProviderConfig::incCounter(Stats::StatName name) { stats_scope_->counterFromStatName(name).inc(); } +void GeoipProviderConfig::setGuage(Stats::StatName name, const uint64_t value) { + stats_scope_->gaugeFromStatName(name, Stats::Gauge::ImportMode::Accumulate).set(value); +} + GeoipProvider::GeoipProvider(Event::Dispatcher& dispatcher, Api::Api& api, Singleton::InstanceSharedPtr owner, GeoipProviderConfigSharedPtr config) @@ -126,31 +131,27 @@ GeoipProvider::GeoipProvider(Event::Dispatcher& dispatcher, Api::Api& api, mmdb_reload_thread_ = api.threadFactory().createThread( [this]() -> void { ENVOY_LOG_MISC(debug, "Started mmdb_reload_routine"); - if (config_->cityDbPath() && - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.mmdb_files_reload_enabled")) { + if (config_->cityDbPath()) { THROW_IF_NOT_OK(mmdb_watcher_->addWatch( config_->cityDbPath().value(), Filesystem::Watcher::Events::MovedTo, [this](uint32_t) { return onMaxmindDbUpdate(config_->cityDbPath().value(), CITY_DB_TYPE); })); } - if (config_->ispDbPath() && - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.mmdb_files_reload_enabled")) { + if (config_->ispDbPath()) { THROW_IF_NOT_OK(mmdb_watcher_->addWatch( config_->ispDbPath().value(), Filesystem::Watcher::Events::MovedTo, [this](uint32_t) { return onMaxmindDbUpdate(config_->ispDbPath().value(), ISP_DB_TYPE); })); } - if (config_->anonDbPath() && - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.mmdb_files_reload_enabled")) { + if (config_->anonDbPath()) { THROW_IF_NOT_OK(mmdb_watcher_->addWatch( config_->anonDbPath().value(), Filesystem::Watcher::Events::MovedTo, [this](uint32_t) { return onMaxmindDbUpdate(config_->anonDbPath().value(), ANON_DB_TYPE); })); } - if (config_->asnDbPath() && - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.mmdb_files_reload_enabled")) { + if (config_->asnDbPath()) { THROW_IF_NOT_OK(mmdb_watcher_->addWatch( config_->asnDbPath().value(), Filesystem::Watcher::Events::MovedTo, [this](uint32_t) { return onMaxmindDbUpdate(config_->asnDbPath().value(), ASN_DB_TYPE); @@ -202,7 +203,7 @@ void GeoipProvider::lookupInCityDb( city_db->mmdb(), reinterpret_cast(remote_address->sockAddr()), &mmdb_error); const uint32_t n_prev_hits = lookup_result.size(); - if (!mmdb_error) { + if (!mmdb_error && mmdb_lookup_result.found_entry) { MMDB_entry_data_list_s* entry_data_list; int status = MMDB_get_entry_data_list(&mmdb_lookup_result.entry, &entry_data_list); if (status == MMDB_SUCCESS) { @@ -255,7 +256,7 @@ void GeoipProvider::lookupInAsnDb( asn_db_ptr->mmdb(), reinterpret_cast(remote_address->sockAddr()), &mmdb_error); const uint32_t n_prev_hits = lookup_result.size(); - if (!mmdb_error) { + if (!mmdb_error && mmdb_lookup_result.found_entry) { MMDB_entry_data_list_s* entry_data_list; int status = MMDB_get_entry_data_list(&mmdb_lookup_result.entry, &entry_data_list); if (status == MMDB_SUCCESS) { @@ -293,7 +294,7 @@ void GeoipProvider::lookupInAnonDb( anon_db->mmdb(), reinterpret_cast(remote_address->sockAddr()), &mmdb_error); const uint32_t n_prev_hits = lookup_result.size(); - if (!mmdb_error) { + if (!mmdb_error && mmdb_lookup_result.found_entry) { MMDB_entry_data_list_s* entry_data_list; int status = MMDB_get_entry_data_list(&mmdb_lookup_result.entry, &entry_data_list); if (status == MMDB_SUCCESS) { @@ -347,7 +348,7 @@ void GeoipProvider::lookupInIspDb( MMDB_lookup_result_s mmdb_lookup_result = MMDB_lookup_sockaddr( isp_db->mmdb(), reinterpret_cast(remote_address->sockAddr()), &mmdb_error); const uint32_t n_prev_hits = lookup_result.size(); - if (!mmdb_error) { + if (!mmdb_error && mmdb_lookup_result.found_entry) { MMDB_entry_data_list_s* entry_data_list; int status = MMDB_get_entry_data_list(&mmdb_lookup_result.entry, &entry_data_list); if (status == MMDB_SUCCESS) { @@ -400,6 +401,8 @@ MaxmindDbSharedPtr GeoipProvider::initMaxmindDb(const std::string& db_path, return nullptr; } + config_->setDbBuildEpoch(db_type, maxmind_db.metadata.build_epoch); + ENVOY_LOG(info, "Succeeded to reload Maxmind database {} from file {}.", db_type, db_path); return std::make_shared(std::move(maxmind_db)); } diff --git a/source/extensions/geoip_providers/maxmind/geoip_provider.h b/source/extensions/geoip_providers/maxmind/geoip_provider.h index 77e7e4523e8fa..91731fdf4407a 100644 --- a/source/extensions/geoip_providers/maxmind/geoip_provider.h +++ b/source/extensions/geoip_providers/maxmind/geoip_provider.h @@ -67,6 +67,12 @@ class GeoipProviderConfig { unknown_hit_)); } + void setDbBuildEpoch(absl::string_view maxmind_db_type, const uint64_t value) { + setGuage( + stat_name_set_->getBuiltin(absl::StrCat(maxmind_db_type, ".db_build_epoch"), unknown_hit_), + value); + } + void registerGeoDbStats(const absl::string_view& db_type); Stats::Scope& getStatsScopeForTest() const { return *stats_scope_; } @@ -95,6 +101,7 @@ class GeoipProviderConfig { Stats::StatNameSetPtr stat_name_set_; const Stats::StatName unknown_hit_; void incCounter(Stats::StatName name); + void setGuage(Stats::StatName name, const uint64_t value); }; using GeoipProviderConfigSharedPtr = std::shared_ptr; diff --git a/source/extensions/health_check/event_sinks/file/file_sink_impl.cc b/source/extensions/health_check/event_sinks/file/file_sink_impl.cc index 61f0113c75c47..8ee9cba7a58c4 100644 --- a/source/extensions/health_check/event_sinks/file/file_sink_impl.cc +++ b/source/extensions/health_check/event_sinks/file/file_sink_impl.cc @@ -18,7 +18,7 @@ void HealthCheckEventFileSink::log(envoy::data::core::v3::HealthCheckEvent event }; HealthCheckEventSinkPtr HealthCheckEventFileSinkFactory::createHealthCheckEventSink( - const ProtobufWkt::Any& config, Server::Configuration::HealthCheckerFactoryContext& context) { + const Protobuf::Any& config, Server::Configuration::HealthCheckerFactoryContext& context) { const auto& validator_config = Envoy::MessageUtil::anyConvertAndValidate< envoy::extensions::health_check::event_sinks::file::v3::HealthCheckEventFileSink>( config, context.messageValidationVisitor()); diff --git a/source/extensions/health_check/event_sinks/file/file_sink_impl.h b/source/extensions/health_check/event_sinks/file/file_sink_impl.h index 712e0fe2964fc..f4f8f25b3d3d4 100644 --- a/source/extensions/health_check/event_sinks/file/file_sink_impl.h +++ b/source/extensions/health_check/event_sinks/file/file_sink_impl.h @@ -31,7 +31,7 @@ class HealthCheckEventFileSinkFactory : public HealthCheckEventSinkFactory { HealthCheckEventFileSinkFactory() = default; HealthCheckEventSinkPtr - createHealthCheckEventSink(const ProtobufWkt::Any& config, + createHealthCheckEventSink(const Protobuf::Any& config, Server::Configuration::HealthCheckerFactoryContext& context) override; std::string name() const override { return "envoy.health_check.event_sink.file"; } diff --git a/source/extensions/health_checkers/common/health_checker_base_impl.cc b/source/extensions/health_checkers/common/health_checker_base_impl.cc index 04eb57f7b6a20..3923e1eaba570 100644 --- a/source/extensions/health_checkers/common/health_checker_base_impl.cc +++ b/source/extensions/health_checkers/common/health_checker_base_impl.cc @@ -40,9 +40,8 @@ HealthCheckerImplBase::HealthCheckerImplBase(const Cluster& cluster, transport_socket_options_(initTransportSocketOptions(config)), transport_socket_match_metadata_(initTransportSocketMatchMetadata(config)), member_update_cb_{cluster_.prioritySet().addMemberUpdateCb( - [this](const HostVector& hosts_added, const HostVector& hosts_removed) -> absl::Status { + [this](const HostVector& hosts_added, const HostVector& hosts_removed) { onClusterMemberUpdate(hosts_added, hosts_removed); - return absl::OkStatus(); })} {} std::shared_ptr diff --git a/source/extensions/health_checkers/http/health_checker_impl.cc b/source/extensions/health_checkers/http/health_checker_impl.cc index bcbed8bd861e8..5c08c5e0d4083 100644 --- a/source/extensions/health_checkers/http/health_checker_impl.cc +++ b/source/extensions/health_checkers/http/health_checker_impl.cc @@ -75,6 +75,31 @@ HttpHealthCheckerImpl::HttpHealthCheckerImpl( random_generator_(context.api().randomGenerator()) { // TODO(boteng): introduce additional validation for the authority and path headers // based on the default UHV when it is available. + + // Process send payload. + if (config.http_health_check().has_send()) { + // Validate that the method supports a request body when payload is specified. + // Use the same logic as HeaderUtility::requestShouldHaveNoBody(), except CONNECT is already + // disallowed by proto validation. + if (method_ == envoy::config::core::v3::GET || method_ == envoy::config::core::v3::HEAD || + method_ == envoy::config::core::v3::DELETE || method_ == envoy::config::core::v3::TRACE) { + throw EnvoyException( + fmt::format("HTTP health check cannot specify a request payload with method '{}'. " + "Only methods that support a request body (POST, PUT, PATCH, OPTIONS) can be " + "used with payload.", + envoy::config::core::v3::RequestMethod_Name(method_))); + } + + // Process the payload and store it in the buffer once during construction. + auto send_bytes_or_error = PayloadMatcher::loadProtoBytes(config.http_health_check().send()); + THROW_IF_NOT_OK_REF(send_bytes_or_error.status()); + + // Copy the processed payload into the buffer once. + for (const auto& segment : send_bytes_or_error.value()) { + request_payload_.add(segment.data(), segment.size()); + } + } + auto bytes_or_error = PayloadMatcher::loadProtoBytes(config.http_health_check().receive()); THROW_IF_NOT_OK_REF(bytes_or_error.status()); receive_bytes_ = bytes_or_error.value(); @@ -275,9 +300,25 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onInterval() { stream_info.setUpstreamInfo(std::make_shared()); stream_info.upstreamInfo()->setUpstreamHost(host_); parent_.request_headers_parser_->evaluateHeaders(*request_headers, stream_info); - auto status = request_encoder->encodeHeaders(*request_headers, true); + + // Check if we have a payload to send. + const bool has_payload = parent_.request_payload_.length() > 0; + if (has_payload) { + // Set Content-Length header for the payload. + request_headers->setContentLength(parent_.request_payload_.length()); + } + + auto status = request_encoder->encodeHeaders(*request_headers, !has_payload); // Encoding will only fail if required request headers are missing. ASSERT(status.ok()); + + // Send the payload as request body if specified. + if (has_payload) { + // Copy the payload buffer to send (we need to preserve the original for reuse). + Buffer::OwnedImpl payload_copy; + payload_copy.add(parent_.request_payload_); + request_encoder->encodeData(payload_copy, true); + } } void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onResetStream(Http::StreamResetReason, diff --git a/source/extensions/health_checkers/http/health_checker_impl.h b/source/extensions/health_checkers/http/health_checker_impl.h index a92226471a7d6..81fae53f73330 100644 --- a/source/extensions/health_checkers/http/health_checker_impl.h +++ b/source/extensions/health_checkers/http/health_checker_impl.h @@ -166,6 +166,7 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase { const std::string path_; const std::string host_value_; + Buffer::OwnedImpl request_payload_; PayloadMatcher::MatchSegments receive_bytes_; const envoy::config::core::v3::RequestMethod method_; uint64_t response_buffer_size_; diff --git a/source/extensions/health_checkers/tcp/health_checker_impl.cc b/source/extensions/health_checkers/tcp/health_checker_impl.cc index d1c75e45531b1..a0365718b7349 100644 --- a/source/extensions/health_checkers/tcp/health_checker_impl.cc +++ b/source/extensions/health_checkers/tcp/health_checker_impl.cc @@ -50,13 +50,12 @@ TcpHealthCheckerImpl::TcpHealthCheckerImpl(const Cluster& cluster, HealthCheckEventLoggerPtr&& event_logger) : HealthCheckerImplBase(cluster, config, dispatcher, runtime, random, std::move(event_logger)), send_bytes_([&config] { - Protobuf::RepeatedPtrField send_repeated; if (!config.tcp_health_check().send().text().empty()) { - send_repeated.Add()->CopyFrom(config.tcp_health_check().send()); + auto bytes_or_error = PayloadMatcher::loadProtoBytes(config.tcp_health_check().send()); + THROW_IF_NOT_OK_REF(bytes_or_error.status()); + return bytes_or_error.value(); } - auto bytes_or_error = PayloadMatcher::loadProtoBytes(send_repeated); - THROW_IF_NOT_OK_REF(bytes_or_error.status()); - return bytes_or_error.value(); + return PayloadMatcher::MatchSegments{}; }()), proxy_protocol_config_(config.tcp_health_check().has_proxy_protocol_config() ? std::make_unique( diff --git a/source/extensions/http/early_header_mutation/header_mutation/config.cc b/source/extensions/http/early_header_mutation/header_mutation/config.cc index 7dddb6547eb25..8079804092ba1 100644 --- a/source/extensions/http/early_header_mutation/header_mutation/config.cc +++ b/source/extensions/http/early_header_mutation/header_mutation/config.cc @@ -12,12 +12,12 @@ Envoy::Http::EarlyHeaderMutationPtr Factory::createExtension(const Protobuf::Message& message, Server::Configuration::FactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - *Envoy::Protobuf::DynamicCastMessage(&message), + *Envoy::Protobuf::DynamicCastMessage(&message), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::http::early_header_mutation::header_mutation::v3::HeaderMutation&>( *mptr, context.messageValidationVisitor()); - return std::make_unique(proto_config); + return std::make_unique(proto_config, context.serverFactoryContext()); } REGISTER_FACTORY(Factory, Envoy::Http::EarlyHeaderMutationFactory); diff --git a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc index 3605b3f0523da..c1d0d5c368ec4 100644 --- a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc +++ b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc @@ -10,9 +10,11 @@ namespace Http { namespace EarlyHeaderMutation { namespace HeaderMutation { -HeaderMutation::HeaderMutation(const ProtoHeaderMutation& mutations) - : mutations_(THROW_OR_RETURN_VALUE(Envoy::Http::HeaderMutations::create(mutations.mutations()), - std::unique_ptr)) {} +HeaderMutation::HeaderMutation(const ProtoHeaderMutation& mutations, + Server::Configuration::ServerFactoryContext& context) + : mutations_(THROW_OR_RETURN_VALUE( + Envoy::Http::HeaderMutations::create(mutations.mutations(), context), + std::unique_ptr)) {} bool HeaderMutation::mutate(Envoy::Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& stream_info) const { diff --git a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h index e6ca0f52ac510..d2f02f6cf75d2 100644 --- a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h +++ b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h @@ -19,7 +19,8 @@ using ProtoHeaderMutation = class HeaderMutation : public Envoy::Http::EarlyHeaderMutation { public: - HeaderMutation(const ProtoHeaderMutation& mutations); + HeaderMutation(const ProtoHeaderMutation& mutations, + Server::Configuration::ServerFactoryContext& context); bool mutate(Envoy::Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& stream_info) const override; diff --git a/source/extensions/http/header_validators/envoy_default/config.cc b/source/extensions/http/header_validators/envoy_default/config.cc index 545645ab7eb34..45a47cbd35d01 100644 --- a/source/extensions/http/header_validators/envoy_default/config.cc +++ b/source/extensions/http/header_validators/envoy_default/config.cc @@ -15,7 +15,7 @@ namespace EnvoyDefault { ::Envoy::Http::HeaderValidatorFactoryPtr HeaderValidatorFactoryConfig::createFromProto( const Protobuf::Message& message, Server::Configuration::ServerFactoryContext& server_context) { auto mptr = ::Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(message), server_context.messageValidationVisitor(), + dynamic_cast(message), server_context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate CustomHeaderIPDetectionFactory::createExtension(const Protobuf::Message& message, Server::Configuration::FactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - *Envoy::Protobuf::DynamicCastMessage(&message), + *Envoy::Protobuf::DynamicCastMessage(&message), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig&>( diff --git a/source/extensions/http/original_ip_detection/xff/config.cc b/source/extensions/http/original_ip_detection/xff/config.cc index 3ebfa8deecef5..1ae2b55c5fdeb 100644 --- a/source/extensions/http/original_ip_detection/xff/config.cc +++ b/source/extensions/http/original_ip_detection/xff/config.cc @@ -17,7 +17,7 @@ absl::StatusOr XffIPDetectionFactory::createExtension(const Protobuf::Message& message, Server::Configuration::FactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(message), context.messageValidationVisitor(), *this); + dynamic_cast(message), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::http::original_ip_detection::xff::v3::XffConfig&>( *mptr, context.messageValidationVisitor()); diff --git a/source/extensions/io_socket/user_space/io_handle.h b/source/extensions/io_socket/user_space/io_handle.h index 8266c9e0b465d..20c9e24f1eb21 100644 --- a/source/extensions/io_socket/user_space/io_handle.h +++ b/source/extensions/io_socket/user_space/io_handle.h @@ -32,6 +32,7 @@ class PassthroughState { }; using PassthroughStateSharedPtr = std::shared_ptr; +using PassthroughStatePtr = std::unique_ptr; /** * The interface for the peer as a writer and supplied read status query. diff --git a/source/extensions/io_socket/user_space/io_handle_impl.cc b/source/extensions/io_socket/user_space/io_handle_impl.cc index 8ca2088cb4d31..c08347967fc1f 100644 --- a/source/extensions/io_socket/user_space/io_handle_impl.cc +++ b/source/extensions/io_socket/user_space/io_handle_impl.cc @@ -392,6 +392,41 @@ void PassthroughStateImpl::mergeInto(envoy::config::core::v3::Metadata& metadata filter_state_objects_.clear(); state_ = State::Done; } + +std::pair +IoHandleFactory::createIoHandlePair(PassthroughStatePtr state) { + PassthroughStateSharedPtr shared_state; + if (state != nullptr) { + shared_state = std::move(state); + } else { + shared_state = std::make_shared(); + } + auto p = std::pair{new IoHandleImpl(shared_state), + new IoHandleImpl(shared_state)}; + p.first->setPeerHandle(p.second.get()); + p.second->setPeerHandle(p.first.get()); + return p; +} + +std::pair +IoHandleFactory::createBufferLimitedIoHandlePair(uint32_t buffer_size, PassthroughStatePtr state) { + PassthroughStateSharedPtr shared_state; + if (state != nullptr) { + shared_state = std::move(state); + } else { + shared_state = std::make_shared(); + } + auto p = std::pair{new IoHandleImpl(shared_state), + new IoHandleImpl(shared_state)}; + // This buffer watermark setting emulates the OS socket buffer parameter + // `/proc/sys/net/ipv4/tcp_{r,w}mem`. + p.first->setWatermarks(buffer_size); + p.second->setWatermarks(buffer_size); + p.first->setPeerHandle(p.second.get()); + p.second->setPeerHandle(p.first.get()); + return p; +} + } // namespace UserSpace } // namespace IoSocket } // namespace Extensions diff --git a/source/extensions/io_socket/user_space/io_handle_impl.h b/source/extensions/io_socket/user_space/io_handle_impl.h index 0c75dfda4d8f1..0b2e67553d0f8 100644 --- a/source/extensions/io_socket/user_space/io_handle_impl.h +++ b/source/extensions/io_socket/user_space/io_handle_impl.h @@ -193,7 +193,7 @@ class PassthroughStateImpl : public PassthroughState, public Logger::Loggable metadata_; @@ -203,27 +203,22 @@ class PassthroughStateImpl : public PassthroughState, public Logger::Loggable; class IoHandleFactory { public: - static std::pair createIoHandlePair() { - auto state = std::make_shared(); - auto p = std::pair{new IoHandleImpl(state), - new IoHandleImpl(state)}; - p.first->setPeerHandle(p.second.get()); - p.second->setPeerHandle(p.first.get()); - return p; - } + /** + * @return a pair of connected IoHandleImpl instances. + * @param state optional existing value to use as the shared PassthroughState. If omitted, a + * newly constructed PassthroughStateImpl will be used. + */ static std::pair - createBufferLimitedIoHandlePair(uint32_t buffer_size) { - auto state = std::make_shared(); - auto p = std::pair{new IoHandleImpl(state), - new IoHandleImpl(state)}; - // This buffer watermark setting emulates the OS socket buffer parameter - // `/proc/sys/net/ipv4/tcp_{r,w}mem`. - p.first->setWatermarks(buffer_size); - p.second->setWatermarks(buffer_size); - p.first->setPeerHandle(p.second.get()); - p.second->setPeerHandle(p.first.get()); - return p; - } + createIoHandlePair(PassthroughStatePtr state = nullptr); + + /** + * @return a pair of connected IoHandleImpl instances with pre-configured watermarks. + * @param buffer_size buffer watermark size in bytes + * @param state optional existing value to use as the shared PassthroughState. If omitted, a + * newly constructed PassthroughStateImpl will be used. + */ + static std::pair + createBufferLimitedIoHandlePair(uint32_t buffer_size, PassthroughStatePtr state = nullptr); }; } // namespace UserSpace } // namespace IoSocket diff --git a/source/extensions/listener_managers/validation_listener_manager/validation_listener_manager.h b/source/extensions/listener_managers/validation_listener_manager/validation_listener_manager.h index c6d1961598547..cae99807e919f 100644 --- a/source/extensions/listener_managers/validation_listener_manager/validation_listener_manager.h +++ b/source/extensions/listener_managers/validation_listener_manager/validation_listener_manager.h @@ -14,8 +14,8 @@ class ValidationListenerComponentFactory : public ListenerComponentFactory { LdsApiPtr createLdsApi(const envoy::config::core::v3::ConfigSource& lds_config, const xds::core::v3::ResourceLocator* lds_resources_locator) override { return std::make_unique( - lds_config, lds_resources_locator, parent_.clusterManager(), parent_.initManager(), - *parent_.stats().rootScope(), parent_.listenerManager(), + lds_config, lds_resources_locator, parent_.xdsManager(), parent_.clusterManager(), + parent_.initManager(), *parent_.stats().rootScope(), parent_.listenerManager(), parent_.messageValidationContext().dynamicValidationVisitor()); } absl::StatusOr createNetworkFilterFactoryList( diff --git a/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.cc b/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.cc index 6d46954015e2a..44d8f7ed51379 100644 --- a/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.cc +++ b/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.cc @@ -63,10 +63,8 @@ ClientSideWeightedRoundRobinLoadBalancer::WorkerLocalLb::WorkerLocalLb( common_config, healthy_panic_threshold, 100, 50), getRoundRobinConfig(common_config), time_source) { if (tls_shim.has_value()) { - apply_weights_cb_handle_ = tls_shim->apply_weights_cb_helper_.add([this](uint32_t priority) { - refresh(priority); - return absl::OkStatus(); - }); + apply_weights_cb_handle_ = + tls_shim->apply_weights_cb_helper_.add([this](uint32_t priority) { refresh(priority); }); } } @@ -258,7 +256,7 @@ void ClientSideWeightedRoundRobinLoadBalancer::WorkerLocalLbFactory::applyWeight uint32_t priority) { tls_->runOnAllThreads([priority](OptRef tls_shim) -> void { if (tls_shim.has_value()) { - auto status = tls_shim->apply_weights_cb_helper_.runCallbacks(priority); + tls_shim->apply_weights_cb_helper_.runCallbacks(priority); } }); } @@ -295,10 +293,9 @@ absl::Status ClientSideWeightedRoundRobinLoadBalancer::initialize() { // Setup a callback to receive priority set updates. priority_update_cb_ = priority_set_.addPriorityUpdateCb( - [this](uint32_t, const HostVector& hosts_added, const HostVector&) -> absl::Status { + [this](uint32_t, const HostVector& hosts_added, const HostVector&) { addClientSideLbPolicyDataToHosts(hosts_added); updateWeightsOnMainThread(); - return absl::OkStatus(); }); weight_calculation_timer_->enableTimer(weight_update_period_); diff --git a/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.h b/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.h index b9652c0771bd5..2e65e71c519a8 100644 --- a/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.h +++ b/source/extensions/load_balancing_policies/client_side_weighted_round_robin/client_side_weighted_round_robin_lb.h @@ -138,7 +138,7 @@ class ClientSideWeightedRoundRobinLoadBalancer : public Upstream::ThreadAwareLoa // Thread local shim to store callbacks for weight updates of worker local lb. class ThreadLocalShim : public Envoy::ThreadLocal::ThreadLocalObject { public: - Common::CallbackManager apply_weights_cb_helper_; + Common::CallbackManager apply_weights_cb_helper_; }; // This class is used to handle the load balancing on the worker thread. diff --git a/source/extensions/load_balancing_policies/common/BUILD b/source/extensions/load_balancing_policies/common/BUILD index 21d7e317e6a3d..630c58ed6015a 100644 --- a/source/extensions/load_balancing_policies/common/BUILD +++ b/source/extensions/load_balancing_policies/common/BUILD @@ -34,6 +34,16 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "locality_wrr_lib", + srcs = ["locality_wrr.cc"], + hdrs = ["locality_wrr.h"], + deps = [ + "//envoy/upstream:upstream_interface", + "//source/common/upstream:scheduler_lib", + ], +) + envoy_cc_library( name = "load_balancer_lib", srcs = ["load_balancer_impl.cc"], @@ -41,6 +51,7 @@ envoy_cc_library( # previously considered core code and used by mobile. visibility = ["//visibility:public"], deps = [ + ":locality_wrr_lib", "//envoy/common:random_generator_interface", "//envoy/runtime:runtime_interface", "//envoy/stats:stats_interface", diff --git a/source/extensions/load_balancing_policies/common/load_balancer_impl.cc b/source/extensions/load_balancing_policies/common/load_balancer_impl.cc index c7463e08cab18..11e8138df33aa 100644 --- a/source/extensions/load_balancing_policies/common/load_balancer_impl.cc +++ b/source/extensions/load_balancing_policies/common/load_balancer_impl.cc @@ -108,13 +108,12 @@ LoadBalancerBase::LoadBalancerBase(const PrioritySet& priority_set, ClusterLbSta recalculatePerPriorityPanic(); priority_update_cb_ = priority_set_.addPriorityUpdateCb( - [this](uint32_t priority, const HostVector&, const HostVector&) -> absl::Status { + [this](uint32_t priority, const HostVector&, const HostVector&) { recalculatePerPriorityState(priority, priority_set_, per_priority_load_, per_priority_health_, per_priority_degraded_, total_healthy_hosts_); recalculatePerPriorityPanic(); stashed_random_.clear(); - return absl::OkStatus(); }); } @@ -417,6 +416,9 @@ ZoneAwareLoadBalancerBase::ZoneAwareLoadBalancerBase( ? PROTOBUF_PERCENT_TO_ROUNDED_INTEGER_OR_DEFAULT( locality_config->zone_aware_lb_config(), routing_enabled, 100, 100) : 100), + locality_basis_(locality_config.has_value() + ? locality_config->zone_aware_lb_config().locality_basis() + : LocalityLbConfig::ZoneAwareLbConfig::HEALTHY_HOSTS_NUM), fail_traffic_on_panic_(locality_config.has_value() ? locality_config->zone_aware_lb_config().fail_traffic_on_panic() : false), @@ -424,8 +426,14 @@ ZoneAwareLoadBalancerBase::ZoneAwareLoadBalancerBase( locality_config->has_locality_weighted_lb_config()) { ASSERT(!priority_set.hostSetsPerPriority().empty()); resizePerPriorityState(); + if (locality_weighted_balancing_) { + for (uint32_t priority = 0; priority < priority_set_.hostSetsPerPriority().size(); ++priority) { + rebuildLocalityWrrForPriority(priority); + } + } + priority_update_cb_ = priority_set_.addPriorityUpdateCb( - [this](uint32_t priority, const HostVector&, const HostVector&) -> absl::Status { + [this](uint32_t priority, const HostVector&, const HostVector&) { // Make sure per_priority_state_ is as large as priority_set_.hostSetsPerPriority() resizePerPriorityState(); // If P=0 changes, regenerate locality routing structures. Locality based routing is @@ -433,7 +441,10 @@ ZoneAwareLoadBalancerBase::ZoneAwareLoadBalancerBase( if (local_priority_set_ && priority == 0) { regenerateLocalityRoutingStructures(); } - return absl::OkStatus(); + + if (locality_weighted_balancing_) { + rebuildLocalityWrrForPriority(priority); + } }); if (local_priority_set_) { // Multiple priorities are unsupported for local priority sets. @@ -442,16 +453,22 @@ ZoneAwareLoadBalancerBase::ZoneAwareLoadBalancerBase( // the locality routing structure. ASSERT(local_priority_set_->hostSetsPerPriority().size() == 1); local_priority_set_member_update_cb_handle_ = local_priority_set_->addPriorityUpdateCb( - [this](uint32_t priority, const HostVector&, const HostVector&) -> absl::Status { + [this](uint32_t priority, const HostVector&, const HostVector&) { ASSERT(priority == 0); // If the set of local Envoys changes, regenerate routing for P=0 as it does priority // based routing. regenerateLocalityRoutingStructures(); - return absl::OkStatus(); }); } } +void ZoneAwareLoadBalancerBase::rebuildLocalityWrrForPriority(uint32_t priority) { + ASSERT(priority < priority_set_.hostSetsPerPriority().size()); + auto& host_set = *priority_set_.hostSetsPerPriority()[priority]; + per_priority_state_[priority]->locality_wrr_ = + std::make_unique(host_set, random_.random()); +} + void ZoneAwareLoadBalancerBase::regenerateLocalityRoutingStructures() { ASSERT(local_priority_set_); stats_.lb_recalculate_zone_structures_.inc(); @@ -637,18 +654,57 @@ absl::FixedArray ZoneAwareLoadBalancerBase::calculateLocalityPercentages( const HostsPerLocality& local_hosts_per_locality, const HostsPerLocality& upstream_hosts_per_locality) { - uint64_t total_local_hosts = 0; - std::map local_counts; + absl::flat_hash_map + local_weights; + absl::flat_hash_map + upstream_weights; + uint64_t total_local_weight = 0; for (const auto& locality_hosts : local_hosts_per_locality.get()) { - total_local_hosts += locality_hosts.size(); + uint64_t locality_weight = 0; + switch (locality_basis_) { + // If locality_basis_ is set to HEALTHY_HOSTS_WEIGHT, it uses the host's weight to calculate the + // locality percentage. + case LocalityLbConfig::ZoneAwareLbConfig::HEALTHY_HOSTS_WEIGHT: + for (const auto& host : locality_hosts) { + locality_weight += host->weight(); + } + break; + // By default it uses the number of healthy hosts in the locality. + case LocalityLbConfig::ZoneAwareLbConfig::HEALTHY_HOSTS_NUM: + locality_weight = locality_hosts.size(); + break; + default: + PANIC_DUE_TO_CORRUPT_ENUM; + } + total_local_weight += locality_weight; // If there is no entry in the map for a given locality, it is assumed to have 0 hosts. if (!locality_hosts.empty()) { - local_counts.insert(std::make_pair(locality_hosts[0]->locality(), locality_hosts.size())); + local_weights.emplace(locality_hosts[0]->locality(), locality_weight); } } - uint64_t total_upstream_hosts = 0; + uint64_t total_upstream_weight = 0; for (const auto& locality_hosts : upstream_hosts_per_locality.get()) { - total_upstream_hosts += locality_hosts.size(); + uint64_t locality_weight = 0; + switch (locality_basis_) { + // If locality_basis_ is set to HEALTHY_HOSTS_WEIGHT, it uses the host's weight to calculate the + // locality percentage. + case LocalityLbConfig::ZoneAwareLbConfig::HEALTHY_HOSTS_WEIGHT: + for (const auto& host : locality_hosts) { + locality_weight += host->weight(); + } + break; + // By default it uses the number of healthy hosts in the locality. + case LocalityLbConfig::ZoneAwareLbConfig::HEALTHY_HOSTS_NUM: + locality_weight = locality_hosts.size(); + break; + default: + PANIC_DUE_TO_CORRUPT_ENUM; + } + total_upstream_weight += locality_weight; + // If there is no entry in the map for a given locality, it is assumed to have 0 hosts. + if (!locality_hosts.empty()) { + upstream_weights.emplace(locality_hosts[0]->locality(), locality_weight); + } } absl::FixedArray percentages(upstream_hosts_per_locality.get().size()); @@ -664,13 +720,17 @@ ZoneAwareLoadBalancerBase::calculateLocalityPercentages( } const auto& locality = upstream_hosts[0]->locality(); - const auto& local_count_it = local_counts.find(locality); - const uint64_t local_count = local_count_it == local_counts.end() ? 0 : local_count_it->second; + const auto local_weight_it = local_weights.find(locality); + const uint64_t local_weight = + local_weight_it == local_weights.end() ? 0 : local_weight_it->second; + const auto upstream_weight_it = upstream_weights.find(locality); + const uint64_t upstream_weight = + upstream_weight_it == upstream_weights.end() ? 0 : upstream_weight_it->second; const uint64_t local_percentage = - total_local_hosts > 0 ? 10000ULL * local_count / total_local_hosts : 0; + total_local_weight > 0 ? 10000ULL * local_weight / total_local_weight : 0; const uint64_t upstream_percentage = - total_upstream_hosts > 0 ? 10000ULL * upstream_hosts.size() / total_upstream_hosts : 0; + total_upstream_weight > 0 ? 10000ULL * upstream_weight / total_upstream_weight : 0; percentages[i] = LocalityPercentages{local_percentage, upstream_percentage}; } @@ -765,9 +825,9 @@ ZoneAwareLoadBalancerBase::hostSourceToUse(LoadBalancerContext* context, uint64_ if (locality_weighted_balancing_) { absl::optional locality; if (host_availability == HostAvailability::Degraded) { - locality = host_set.chooseDegradedLocality(); + locality = chooseDegradedLocality(host_set); } else { - locality = host_set.chooseHealthyLocality(); + locality = chooseHealthyLocality(host_set); } if (locality.has_value()) { @@ -873,16 +933,12 @@ EdfLoadBalancerBase::EdfLoadBalancerBase( // so we will need to do better at delta tracking to scale (see // https://github.com/envoyproxy/envoy/issues/2874). priority_update_cb_ = priority_set.addPriorityUpdateCb( - [this](uint32_t priority, const HostVector&, const HostVector&) { - refresh(priority); - return absl::OkStatus(); - }); - member_update_cb_ = priority_set.addMemberUpdateCb( - [this](const HostVector& hosts_added, const HostVector&) -> absl::Status { + [this](uint32_t priority, const HostVector&, const HostVector&) { refresh(priority); }); + member_update_cb_ = + priority_set.addMemberUpdateCb([this](const HostVector& hosts_added, const HostVector&) { if (isSlowStartEnabled()) { recalculateHostsInSlowStart(hosts_added); } - return absl::OkStatus(); }); } diff --git a/source/extensions/load_balancing_policies/common/load_balancer_impl.h b/source/extensions/load_balancing_policies/common/load_balancer_impl.h index 1f2fd332481d1..296ad5b6fb456 100644 --- a/source/extensions/load_balancing_policies/common/load_balancer_impl.h +++ b/source/extensions/load_balancing_policies/common/load_balancer_impl.h @@ -20,9 +20,11 @@ #include "envoy/upstream/upstream.h" #include "source/common/protobuf/utility.h" +#include "source/common/runtime/runtime_features.h" #include "source/common/runtime/runtime_protos.h" #include "source/common/upstream/edf_scheduler.h" #include "source/common/upstream/load_balancer_context_base.h" +#include "source/extensions/load_balancing_policies/common/locality_wrr.h" namespace Envoy { namespace Upstream { @@ -435,6 +437,16 @@ class ZoneAwareLoadBalancerBase : public LoadBalancerBase { return absl::nullopt; } + absl::optional chooseHealthyLocality(HostSet& host_set) const { + ASSERT(per_priority_state_[host_set.priority()]->locality_wrr_); + return per_priority_state_[host_set.priority()]->locality_wrr_->chooseHealthyLocality(); + }; + + absl::optional chooseDegradedLocality(HostSet& host_set) const { + ASSERT(per_priority_state_[host_set.priority()]->locality_wrr_); + return per_priority_state_[host_set.priority()]->locality_wrr_->chooseDegradedLocality(); + }; + // The set of local Envoy instances which are load balancing across priority_set_. const PrioritySet* local_priority_set_; @@ -447,8 +459,14 @@ class ZoneAwareLoadBalancerBase : public LoadBalancerBase { // for each of the non-local localities to determine what traffic should be // routed where. std::vector residual_capacity_; + + // Locality Weighted Round Robin config. + std::unique_ptr locality_wrr_; }; using PerPriorityStatePtr = std::unique_ptr; + + void rebuildLocalityWrrForPriority(uint32_t priority); + // Routing state broken out for each priority level in priority_set_. std::vector per_priority_state_; Common::CallbackHandlePtr priority_update_cb_; @@ -459,6 +477,7 @@ class ZoneAwareLoadBalancerBase : public LoadBalancerBase { const absl::optional force_local_zone_min_size_; // Keep small members (bools and enums) at the end of class, to reduce alignment overhead. const uint32_t routing_enabled_; + const LocalityLbConfig::ZoneAwareLbConfig::LocalityBasis locality_basis_; const bool fail_traffic_on_panic_ : 1; // If locality weight aware routing is enabled. diff --git a/source/extensions/load_balancing_policies/common/locality_wrr.cc b/source/extensions/load_balancing_policies/common/locality_wrr.cc new file mode 100644 index 0000000000000..1efe8a3feb718 --- /dev/null +++ b/source/extensions/load_balancing_policies/common/locality_wrr.cc @@ -0,0 +1,108 @@ +#include "source/extensions/load_balancing_policies/common/locality_wrr.h" + +namespace Envoy { +namespace Upstream { + +LocalityWrr::LocalityWrr(const HostSet& host_set, uint64_t seed) { + rebuildLocalityScheduler(healthy_locality_scheduler_, healthy_locality_entries_, + host_set.healthyHostsPerLocality(), host_set.healthyHosts(), + host_set.hostsPerLocalityPtr(), host_set.excludedHostsPerLocalityPtr(), + host_set.localityWeights(), host_set.overprovisioningFactor(), seed); + rebuildLocalityScheduler(degraded_locality_scheduler_, degraded_locality_entries_, + host_set.degradedHostsPerLocality(), host_set.degradedHosts(), + host_set.hostsPerLocalityPtr(), host_set.excludedHostsPerLocalityPtr(), + host_set.localityWeights(), host_set.overprovisioningFactor(), seed); +} + +absl::optional LocalityWrr::chooseHealthyLocality() { + return chooseLocality(healthy_locality_scheduler_.get()); +} + +absl::optional LocalityWrr::chooseDegradedLocality() { + return chooseLocality(degraded_locality_scheduler_.get()); +} + +void LocalityWrr::rebuildLocalityScheduler( + std::unique_ptr>& locality_scheduler, + std::vector>& locality_entries, + const HostsPerLocality& eligible_hosts_per_locality, const HostVector& eligible_hosts, + HostsPerLocalityConstSharedPtr all_hosts_per_locality, + HostsPerLocalityConstSharedPtr excluded_hosts_per_locality, + LocalityWeightsConstSharedPtr locality_weights, uint32_t overprovisioning_factor, + uint64_t seed) { + // Rebuild the locality scheduler by computing the effective weight of each + // locality in this priority. The scheduler is reset by default, and is rebuilt only if we have + // locality weights (i.e. using EDS) and there is at least one eligible host in this priority. + // + // We omit building a scheduler when there are zero eligible hosts in the priority as + // all the localities will have zero effective weight. At selection time, we'll either select + // from a different scheduler or there will be no available hosts in the priority. At that point + // we'll rely on other mechanisms such as panic mode to select a host, none of which rely on the + // scheduler. + // + // TODO(htuch): if the underlying locality index -> + // envoy::config::core::v3::Locality hasn't changed in hosts_/healthy_hosts_/degraded_hosts_, we + // could just update locality_weight_ without rebuilding. Similar to how host + // level WRR works, we would age out the existing entries via picks and lazily + // apply the new weights. + locality_scheduler = nullptr; + if (all_hosts_per_locality != nullptr && locality_weights != nullptr && + !locality_weights->empty() && !eligible_hosts.empty()) { + locality_entries.clear(); + for (uint32_t i = 0; i < all_hosts_per_locality->get().size(); ++i) { + const double effective_weight = effectiveLocalityWeight( + i, eligible_hosts_per_locality, *excluded_hosts_per_locality, *all_hosts_per_locality, + *locality_weights, overprovisioning_factor); + if (effective_weight > 0) { + locality_entries.emplace_back(std::make_shared(i, effective_weight)); + } + } + // If not all effective weights were zero, create the scheduler. + if (!locality_entries.empty()) { + locality_scheduler = std::make_unique>( + EdfScheduler::createWithPicks( + locality_entries, [](const LocalityEntry& entry) { return entry.effective_weight_; }, + seed)); + } + } +} + +absl::optional +LocalityWrr::chooseLocality(EdfScheduler* locality_scheduler) { + if (locality_scheduler == nullptr) { + return {}; + } + const std::shared_ptr locality = locality_scheduler->pickAndAdd( + [](const LocalityEntry& locality) { return locality.effective_weight_; }); + // We don't build a schedule if there are no weighted localities, so we should always succeed. + ASSERT(locality != nullptr); + // If we picked it before, its weight must have been positive. + ASSERT(locality->effective_weight_ > 0); + return locality->index_; +} + +double LocalityWrr::effectiveLocalityWeight(uint32_t index, + const HostsPerLocality& eligible_hosts_per_locality, + const HostsPerLocality& excluded_hosts_per_locality, + const HostsPerLocality& all_hosts_per_locality, + const LocalityWeights& locality_weights, + uint32_t overprovisioning_factor) { + const auto& locality_eligible_hosts = eligible_hosts_per_locality.get()[index]; + const uint32_t excluded_count = excluded_hosts_per_locality.get().size() > index + ? excluded_hosts_per_locality.get()[index].size() + : 0; + const auto host_count = all_hosts_per_locality.get()[index].size() - excluded_count; + if (host_count == 0) { + return 0.0; + } + const double locality_availability_ratio = 1.0 * locality_eligible_hosts.size() / host_count; + const uint32_t weight = locality_weights[index]; + // Availability ranges from 0-1.0, and is the ratio of eligible hosts to total hosts, modified + // by the overprovisioning factor. + const double effective_locality_availability_ratio = + std::min(1.0, (overprovisioning_factor / 100.0) * locality_availability_ratio); + return weight * effective_locality_availability_ratio; +} + +} // namespace Upstream +} // namespace Envoy diff --git a/source/extensions/load_balancing_policies/common/locality_wrr.h b/source/extensions/load_balancing_policies/common/locality_wrr.h new file mode 100644 index 0000000000000..cb0e9546550ba --- /dev/null +++ b/source/extensions/load_balancing_policies/common/locality_wrr.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include "envoy/upstream/upstream.h" + +#include "source/common/upstream/edf_scheduler.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Upstream { + +class LocalityWrr { +public: + explicit LocalityWrr(const HostSet& host_set, uint64_t seed); + + absl::optional chooseHealthyLocality(); + absl::optional chooseDegradedLocality(); + +private: + struct LocalityEntry { + LocalityEntry(uint32_t index, double effective_weight) + : index_(index), effective_weight_(effective_weight) {} + const uint32_t index_; + const double effective_weight_; + }; + + // Rebuilds the provided locality scheduler with locality entries based on the locality weights + // and eligible hosts. + // + // @param locality_scheduler the locality scheduler to rebuild. Will be set to nullptr if no + // localities are eligible. + // @param locality_entries the vector that holds locality entries. Will be reset and populated + // with entries corresponding to the new scheduler. + // @param eligible_hosts_per_locality eligible hosts for this scheduler grouped by locality. + // @param eligible_hosts all eligible hosts for this scheduler. + // @param all_hosts_per_locality all hosts for this HostSet grouped by locality. + // @param locality_weights the weighting of each locality. + // @param overprovisioning_factor the overprovisioning factor to use when computing the effective + // weight of a locality. + // @param seed a random number of initial picks to "invoke" on the locality scheduler. This + // allows to distribute the load between different localities across worker threads and a fleet + // of Envoys. + static void + rebuildLocalityScheduler(std::unique_ptr>& locality_scheduler, + std::vector>& locality_entries, + const HostsPerLocality& eligible_hosts_per_locality, + const HostVector& eligible_hosts, + HostsPerLocalityConstSharedPtr all_hosts_per_locality, + HostsPerLocalityConstSharedPtr excluded_hosts_per_locality, + LocalityWeightsConstSharedPtr locality_weights, + uint32_t overprovisioning_factor, uint64_t seed); + // Weight for a locality taking into account health status using the provided eligible hosts per + // locality. + static double effectiveLocalityWeight(uint32_t index, + const HostsPerLocality& eligible_hosts_per_locality, + const HostsPerLocality& excluded_hosts_per_locality, + const HostsPerLocality& all_hosts_per_locality, + const LocalityWeights& locality_weights, + uint32_t overprovisioning_factor); + + static absl::optional chooseLocality(EdfScheduler* locality_scheduler); + + std::vector> healthy_locality_entries_; + std::unique_ptr> healthy_locality_scheduler_; + std::vector> degraded_locality_entries_; + std::unique_ptr> degraded_locality_scheduler_; +}; + +} // namespace Upstream +} // namespace Envoy diff --git a/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.cc b/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.cc index 4056b0aae5d82..01a3e2dd21841 100644 --- a/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.cc +++ b/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.cc @@ -14,18 +14,17 @@ namespace Upstream { // HostSetImpl::effectiveLocalityWeight. namespace { -absl::Status normalizeHostWeights(const HostVector& hosts, double normalized_locality_weight, - NormalizedHostWeightVector& normalized_host_weights, - double& min_normalized_weight, double& max_normalized_weight) { +void normalizeHostWeights(const HostVector& hosts, double normalized_locality_weight, + NormalizedHostWeightVector& normalized_host_weights, + double& min_normalized_weight, double& max_normalized_weight) { // sum should be at most uint32_t max value, so we can validate it by accumulating into unit64_t // and making sure there was no overflow uint64_t sum = 0; for (const auto& host : hosts) { sum += host->weight(); if (sum > std::numeric_limits::max()) { - return absl::InvalidArgumentError( - fmt::format("The sum of weights of all upstream hosts in a locality exceeds {}", - std::numeric_limits::max())); + IS_ENVOY_BUG("weights should have been previously validated in validateEndpoints()"); + return; } } @@ -35,14 +34,12 @@ absl::Status normalizeHostWeights(const HostVector& hosts, double normalized_loc min_normalized_weight = std::min(min_normalized_weight, weight); max_normalized_weight = std::max(max_normalized_weight, weight); } - return absl::OkStatus(); } -absl::Status normalizeLocalityWeights(const HostsPerLocality& hosts_per_locality, - const LocalityWeights& locality_weights, - NormalizedHostWeightVector& normalized_host_weights, - double& min_normalized_weight, - double& max_normalized_weight) { +void normalizeLocalityWeights(const HostsPerLocality& hosts_per_locality, + const LocalityWeights& locality_weights, + NormalizedHostWeightVector& normalized_host_weights, + double& min_normalized_weight, double& max_normalized_weight) { ASSERT(locality_weights.size() == hosts_per_locality.get().size()); // sum should be at most uint32_t max value, so we can validate it by accumulating into unit64_t @@ -51,15 +48,13 @@ absl::Status normalizeLocalityWeights(const HostsPerLocality& hosts_per_locality for (const auto weight : locality_weights) { sum += weight; if (sum > std::numeric_limits::max()) { - return absl::InvalidArgumentError( - fmt::format("The sum of weights of all localities at the same priority exceeds {}", - std::numeric_limits::max())); + IS_ENVOY_BUG("locality weights should have been validated in validateEndpoints"); } } // Locality weights (unlike host weights) may be 0. If _all_ locality weights were 0, bail out. if (sum == 0) { - return absl::OkStatus(); + return; } // Compute normalized weights for all hosts in each locality. If a locality was assigned zero @@ -68,33 +63,29 @@ absl::Status normalizeLocalityWeights(const HostsPerLocality& hosts_per_locality if (locality_weights[i] != 0) { const HostVector& hosts = hosts_per_locality.get()[i]; const double normalized_locality_weight = static_cast(locality_weights[i]) / sum; - RETURN_IF_NOT_OK(normalizeHostWeights(hosts, normalized_locality_weight, - normalized_host_weights, min_normalized_weight, - max_normalized_weight)); + normalizeHostWeights(hosts, normalized_locality_weight, normalized_host_weights, + min_normalized_weight, max_normalized_weight); } } - return absl::OkStatus(); } -absl::Status normalizeWeights(const HostSet& host_set, bool in_panic, - NormalizedHostWeightVector& normalized_host_weights, - double& min_normalized_weight, double& max_normalized_weight, - bool locality_weighted_balancing) { +void normalizeWeights(const HostSet& host_set, bool in_panic, + NormalizedHostWeightVector& normalized_host_weights, + double& min_normalized_weight, double& max_normalized_weight, + bool locality_weighted_balancing) { if (!locality_weighted_balancing || host_set.localityWeights() == nullptr || host_set.localityWeights()->empty()) { // If we're not dealing with locality weights, just normalize weights for the flat set of hosts. const auto& hosts = in_panic ? host_set.hosts() : host_set.healthyHosts(); - RETURN_IF_NOT_OK(normalizeHostWeights(hosts, 1.0, normalized_host_weights, - min_normalized_weight, max_normalized_weight)); + normalizeHostWeights(hosts, 1.0, normalized_host_weights, min_normalized_weight, + max_normalized_weight); } else { // Otherwise, normalize weights across all localities. const auto& hosts_per_locality = in_panic ? host_set.hostsPerLocality() : host_set.healthyHostsPerLocality(); - RETURN_IF_NOT_OK(normalizeLocalityWeights(hosts_per_locality, *(host_set.localityWeights()), - normalized_host_weights, min_normalized_weight, - max_normalized_weight)); + normalizeLocalityWeights(hosts_per_locality, *(host_set.localityWeights()), + normalized_host_weights, min_normalized_weight, max_normalized_weight); } - return absl::OkStatus(); } std::string generateCookie(LoadBalancerContext* context, absl::string_view name, @@ -136,12 +127,13 @@ absl::Status ThreadAwareLoadBalancerBase::initialize() { // complicated initialization as the load balancer would need its own initialized callback. I // think the synchronous/asynchronous split is probably the best option. priority_update_cb_ = priority_set_.addPriorityUpdateCb( - [this](uint32_t, const HostVector&, const HostVector&) -> absl::Status { return refresh(); }); + [this](uint32_t, const HostVector&, const HostVector&) { refresh(); }); - return refresh(); + refresh(); + return absl::OkStatus(); } -absl::Status ThreadAwareLoadBalancerBase::refresh() { +void ThreadAwareLoadBalancerBase::refresh() { auto per_priority_state_vector = std::make_shared>( priority_set_.hostSetsPerPriority().size()); auto healthy_per_priority_load = @@ -161,10 +153,8 @@ absl::Status ThreadAwareLoadBalancerBase::refresh() { NormalizedHostWeightVector normalized_host_weights; double min_normalized_weight = 1.0; double max_normalized_weight = 0.0; - absl::Status status = normalizeWeights(*host_set, per_priority_state->global_panic_, - normalized_host_weights, min_normalized_weight, - max_normalized_weight, locality_weighted_balancing_); - RETURN_IF_NOT_OK(status); + normalizeWeights(*host_set, per_priority_state->global_panic_, normalized_host_weights, + min_normalized_weight, max_normalized_weight, locality_weighted_balancing_); per_priority_state->current_lb_ = createLoadBalancer( std::move(normalized_host_weights), min_normalized_weight, max_normalized_weight); } @@ -175,7 +165,6 @@ absl::Status ThreadAwareLoadBalancerBase::refresh() { factory_->degraded_per_priority_load_ = degraded_per_priority_load; factory_->per_priority_state_ = per_priority_state_vector; } - return absl::OkStatus(); } HostSelectionResponse @@ -370,5 +359,34 @@ TypedHashLbConfigBase::TypedHashLbConfigBase(absl::Spanweight(); + if (host_sum > std::numeric_limits::max()) { + return absl::InvalidArgumentError( + fmt::format("The sum of weights of all upstream hosts in a locality exceeds {}", + std::numeric_limits::max())); + } + } + + uint64_t locality_sum = 0; + for (const auto& [_, weight] : locality_weights_map) { + locality_sum += weight; + if (locality_sum > std::numeric_limits::max()) { + return absl::InvalidArgumentError( + fmt::format("The sum of weights of all localities at the same priority exceeds {}", + std::numeric_limits::max())); + } + } + } + + return absl::OkStatus(); +} + } // namespace Upstream } // namespace Envoy diff --git a/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.h b/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.h index 13ecbe805870c..9db1c2247a7df 100644 --- a/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.h +++ b/source/extensions/load_balancing_policies/common/thread_aware_lb_impl.h @@ -38,7 +38,7 @@ class ThreadAwareLoadBalancerBase : public LoadBalancerBase, public ThreadAwareL virtual ~HashingLoadBalancer() = default; virtual HostSelectionResponse chooseHost(uint64_t hash, uint32_t attempt) const PURE; const absl::string_view hashKey(HostConstSharedPtr host, bool use_hostname) const { - const ProtobufWkt::Value& val = Config::Metadata::metadataValue( + const Protobuf::Value& val = Config::Metadata::metadataValue( host->metadata().get(), Config::MetadataFilters::get().ENVOY_LB, Config::MetadataEnvoyLbKeys::get().HASH_KEY); if (val.kind_case() != val.kStringValue && val.kind_case() != val.KIND_NOT_SET) { @@ -176,7 +176,7 @@ class ThreadAwareLoadBalancerBase : public LoadBalancerBase, public ThreadAwareL virtual HashingLoadBalancerSharedPtr createLoadBalancer(const NormalizedHostWeightVector& normalized_host_weights, double min_normalized_weight, double max_normalized_weight) PURE; - absl::Status refresh(); + void refresh(); std::shared_ptr factory_; const bool locality_weighted_balancing_{}; @@ -189,6 +189,8 @@ class TypedHashLbConfigBase : public LoadBalancerConfig { TypedHashLbConfigBase(absl::Span hash_policy, Regex::Engine& regex_engine, absl::Status& creation_status); + absl::Status validateEndpoints(const PriorityState& priorities) const override; + HashPolicySharedPtr hash_policy_; }; diff --git a/source/extensions/load_balancing_policies/override_host/load_balancer.cc b/source/extensions/load_balancing_policies/override_host/load_balancer.cc index 889a254b407ad..30f2ee662421a 100644 --- a/source/extensions/load_balancing_policies/override_host/load_balancer.cc +++ b/source/extensions/load_balancing_policies/override_host/load_balancer.cc @@ -186,8 +186,7 @@ OverrideHostLoadBalancer::LoadBalancerImpl::chooseHost(LoadBalancerContext* cont absl::optional OverrideHostLoadBalancer::LoadBalancerImpl::getSelectedHostsFromMetadata( const ::envoy::config::core::v3::Metadata& metadata, const Config::MetadataKey& metadata_key) { - const ProtobufWkt::Value& metadata_value = - Config::Metadata::metadataValue(&metadata, metadata_key); + const Protobuf::Value& metadata_value = Config::Metadata::metadataValue(&metadata, metadata_key); // TODO(yanavlasov): make it distinguish between not-present and invalid metadata. if (metadata_value.has_string_value()) { return absl::string_view{metadata_value.string_value()}; diff --git a/source/extensions/load_balancing_policies/subset/subset_lb.cc b/source/extensions/load_balancing_policies/subset/subset_lb.cc index f963d02a85356..5fca592be3c8a 100644 --- a/source/extensions/load_balancing_policies/subset/subset_lb.cc +++ b/source/extensions/load_balancing_policies/subset/subset_lb.cc @@ -92,7 +92,6 @@ SubsetLoadBalancer::SubsetLoadBalancer(const SubsetLoadBalancerConfig& lb_config [this](uint32_t priority, const HostVector&, const HostVector&) { refreshSubsets(priority); purgeEmptySubsets(subsets_); - return absl::OkStatus(); }); } @@ -188,7 +187,7 @@ HostSelectionResponse SubsetLoadBalancer::chooseHost(LoadBalancerContext* contex Cluster_LbSubsetConfig_LbSubsetMetadataFallbackPolicy_FALLBACK_LIST) { return chooseHostIteration(context); } - const ProtobufWkt::Value* metadata_fallbacks = getMetadataFallbackList(context); + const Protobuf::Value* metadata_fallbacks = getMetadataFallbackList(context); if (metadata_fallbacks == nullptr) { return chooseHostIteration(context); } @@ -231,7 +230,7 @@ SubsetLoadBalancer::removeMetadataFallbackList(LoadBalancerContext* context) { return {context, to_preserve}; } -const ProtobufWkt::Value* +const Protobuf::Value* SubsetLoadBalancer::getMetadataFallbackList(LoadBalancerContext* context) const { if (context == nullptr) { return nullptr; @@ -423,24 +422,23 @@ SubsetLoadBalancer::LbSubsetEntryPtr SubsetLoadBalancer::findSubset( } void SubsetLoadBalancer::updateFallbackSubset(uint32_t priority, const HostVector& all_hosts) { - auto update_func = [priority, &all_hosts](LbSubsetPtr& subset, const HostPredicate& predicate, - uint64_t seed) { + auto update_func = [priority, &all_hosts](LbSubsetPtr& subset, const HostPredicate& predicate) { for (const auto& host : all_hosts) { if (predicate(*host)) { subset->pushHost(priority, host); } } - subset->finalize(priority, seed); + subset->finalize(priority); }; if (subset_any_ != nullptr) { - update_func(subset_any_->lb_subset_, [](const Host&) { return true; }, random_.random()); + update_func(subset_any_->lb_subset_, [](const Host&) { return true; }); } if (subset_default_ != nullptr) { HostPredicate predicate = std::bind(&SubsetLoadBalancer::hostMatches, this, default_subset_metadata_, std::placeholders::_1); - update_func(subset_default_->lb_subset_, predicate, random_.random()); + update_func(subset_default_->lb_subset_, predicate); } if (fallback_subset_ == nullptr) { @@ -515,9 +513,9 @@ void SubsetLoadBalancer::processSubsets(uint32_t priority, const HostVector& all single_duplicate_stat_->set(collision_count_of_single_host_entries); // Finalize updates after all the hosts are evaluated. - forEachSubset(subsets_, [priority, this](LbSubsetEntryPtr entry) { + forEachSubset(subsets_, [priority](LbSubsetEntryPtr entry) { if (entry->initialized()) { - entry->lb_subset_->finalize(priority, random_.random()); + entry->lb_subset_->finalize(priority); } }); } @@ -557,7 +555,7 @@ SubsetLoadBalancer::extractSubsetMetadata(const std::set& subset_ke break; } - if (list_as_any_ && it->second.kind_case() == ProtobufWkt::Value::kListValue) { + if (list_as_any_ && it->second.kind_case() == Protobuf::Value::kListValue) { // If the list of kvs is empty, we initialize one kvs for each value in the list. // Otherwise, we branch the list of kvs by generating one new kvs per old kvs per // new value. @@ -611,7 +609,7 @@ std::string SubsetLoadBalancer::describeMetadata(const SubsetLoadBalancer::Subse first = false; } - const ProtobufWkt::Value& value = it.second; + const Protobuf::Value& value = it.second; buf << it.first << "=" << MessageUtil::getJsonStringFromMessageOrError(value); } return buf.str(); @@ -625,7 +623,7 @@ SubsetLoadBalancer::findOrCreateLbSubsetEntry(LbSubsetMap& subsets, const Subset ASSERT(idx < kvs.size()); const std::string& name = kvs[idx].first; - const ProtobufWkt::Value& pb_value = kvs[idx].second; + const Protobuf::Value& pb_value = kvs[idx].second; const HashedValue value(pb_value); LbSubsetEntryPtr entry; @@ -732,7 +730,7 @@ SubsetLoadBalancer::PrioritySubsetImpl::PrioritySubsetImpl(const SubsetLoadBalan // hosts that belong in this subset. void SubsetLoadBalancer::HostSubsetImpl::update(const HostHashSet& matching_hosts, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed) { + const HostVector& hosts_removed) { auto cached_predicate = [&matching_hosts](const auto& host) { return matching_hosts.count(&host) == 1; }; @@ -793,7 +791,7 @@ void SubsetLoadBalancer::HostSubsetImpl::update(const HostHashSet& matching_host HostSetImpl::updateHostsParams( hosts, hosts_per_locality, healthy_hosts, healthy_hosts_per_locality, degraded_hosts, degraded_hosts_per_locality, excluded_hosts, excluded_hosts_per_locality), - determineLocalityWeights(*hosts_per_locality), hosts_added, hosts_removed, seed, + determineLocalityWeights(*hosts_per_locality), hosts_added, hosts_removed, original_host_set_.weightedPriorityHealth(), original_host_set_.overprovisioningFactor()); } @@ -850,10 +848,9 @@ HostSetImplPtr SubsetLoadBalancer::PrioritySubsetImpl::createHostSet( void SubsetLoadBalancer::PrioritySubsetImpl::update(uint32_t priority, const HostHashSet& matching_hosts, const HostVector& hosts_added, - const HostVector& hosts_removed, - uint64_t seed) { + const HostVector& hosts_removed) { const auto& host_subset = getOrCreateHostSet(priority); - updateSubset(priority, matching_hosts, hosts_added, hosts_removed, seed); + updateSubset(priority, matching_hosts, hosts_added, hosts_removed); if (host_subset.hosts().empty() != empty_) { empty_ = true; @@ -870,7 +867,7 @@ void SubsetLoadBalancer::PrioritySubsetImpl::update(uint32_t priority, } } -void SubsetLoadBalancer::PriorityLbSubset::finalize(uint32_t priority, uint64_t seed) { +void SubsetLoadBalancer::PriorityLbSubset::finalize(uint32_t priority) { while (host_sets_.size() <= priority) { host_sets_.push_back({HostHashSet(), HostHashSet()}); } @@ -891,7 +888,7 @@ void SubsetLoadBalancer::PriorityLbSubset::finalize(uint32_t priority, uint64_t } } - subset_.update(priority, new_hosts, added, removed, seed); + subset_.update(priority, new_hosts, added, removed); old_hosts.swap(new_hosts); new_hosts.clear(); @@ -908,7 +905,7 @@ SubsetLoadBalancer::LoadBalancerContextWrapper::LoadBalancerContextWrapper( } SubsetLoadBalancer::LoadBalancerContextWrapper::LoadBalancerContextWrapper( - LoadBalancerContext* wrapped, const ProtobufWkt::Struct& metadata_match_criteria_override) + LoadBalancerContext* wrapped, const Protobuf::Struct& metadata_match_criteria_override) : wrapped_(wrapped) { ASSERT(wrapped->metadataMatchCriteria()); metadata_match_ = diff --git a/source/extensions/load_balancing_policies/subset/subset_lb.h b/source/extensions/load_balancing_policies/subset/subset_lb.h index 55c896b97dfa0..aa886c14fb0d9 100644 --- a/source/extensions/load_balancing_policies/subset/subset_lb.h +++ b/source/extensions/load_balancing_policies/subset/subset_lb.h @@ -58,7 +58,7 @@ class SubsetLoadBalancer : public LoadBalancer, Logger::Loggable>; + using SubsetMetadata = std::vector>; static std::string describeMetadata(const SubsetMetadata& kvs); private: @@ -86,7 +86,7 @@ class SubsetLoadBalancer : public LoadBalancer, Logger::Loggable(host_sets_[priority].get()) - ->update(matching_hosts, hosts_added, hosts_removed, seed); - THROW_IF_NOT_OK(runUpdateCallbacks(hosts_added, hosts_removed)); + ->update(matching_hosts, hosts_added, hosts_removed); + runUpdateCallbacks(hosts_added, hosts_removed); } // Thread aware LB if applicable. @@ -149,7 +148,7 @@ class SubsetLoadBalancer : public LoadBalancer, Logger::Loggable; using LbSubsetMap = absl::node_hash_map; using SubsetSelectorFallbackParamsRef = std::reference_wrapper; - using MetadataFallbacks = ProtobufWkt::RepeatedPtrField; + using MetadataFallbacks = Protobuf::RepeatedPtrField; public: class LoadBalancerContextWrapper : public LoadBalancerContext { @@ -162,7 +161,7 @@ class SubsetLoadBalancer : public LoadBalancer, Logger::Loggable computeHashKey() override { return wrapped_->computeHashKey(); } const Router::MetadataMatchCriteria* metadataMatchCriteria() override { @@ -226,7 +225,7 @@ class SubsetLoadBalancer : public LoadBalancer, Logger::Loggable; @@ -249,7 +248,7 @@ class SubsetLoadBalancer : public LoadBalancer, Logger::Loggable>& a vector of @@ -181,7 +181,7 @@ class LoadBalancerSubsetInfoImpl : public LoadBalancerSubsetInfo { MetadataFallbackPolicy metadataFallbackPolicy() const override { return metadata_fallback_policy_; } - const ProtobufWkt::Struct& defaultSubset() const override { return default_subset_; } + const Protobuf::Struct& defaultSubset() const override { return default_subset_; } const std::vector& subsetSelectors() const override { return subset_selectors_; } @@ -192,7 +192,7 @@ class LoadBalancerSubsetInfoImpl : public LoadBalancerSubsetInfo { bool allowRedundantKeys() const override { return allow_redundant_keys_; } private: - const ProtobufWkt::Struct default_subset_; + const Protobuf::Struct default_subset_; std::vector subset_selectors_; // Keep small members (bools and enums) at the end of class, to reduce alignment overhead. const FallbackPolicy fallback_policy_; @@ -217,6 +217,13 @@ class SubsetLoadBalancerConfig : public Upstream::LoadBalancerConfig { TypedLoadBalancerFactory* child_factory, LoadBalancerConfigPtr child_config); + absl::Status validateEndpoints(const PriorityState& priorities) const override { + if (child_lb_config_ != nullptr) { + return child_lb_config_->validateEndpoints(priorities); + } + return absl::OkStatus(); + } + Upstream::ThreadAwareLoadBalancerPtr createLoadBalancer(const Upstream::ClusterInfo& cluster_info, const Upstream::PrioritySet& child_priority_set, Runtime::Loader& runtime, diff --git a/source/extensions/matching/actions/format_string/config.cc b/source/extensions/matching/actions/format_string/config.cc index 962082481b1f3..9476c572ecee7 100644 --- a/source/extensions/matching/actions/format_string/config.cc +++ b/source/extensions/matching/actions/format_string/config.cc @@ -23,10 +23,10 @@ ActionImpl::get(const Server::Configuration::FilterChainsByName& filter_chains_b return nullptr; } -Matcher::ActionFactoryCb -ActionFactory::createActionFactoryCb(const Protobuf::Message& proto_config, - FilterChainActionFactoryContext& context, - ProtobufMessage::ValidationVisitor& validator) { +Matcher::ActionConstSharedPtr +ActionFactory::createAction(const Protobuf::Message& proto_config, + FilterChainActionFactoryContext& context, + ProtobufMessage::ValidationVisitor& validator) { const auto& config = MessageUtil::downcastAndValidate( proto_config, validator); @@ -35,7 +35,7 @@ ActionFactory::createActionFactoryCb(const Protobuf::Message& proto_config, Formatter::FormatterConstSharedPtr formatter = THROW_OR_RETURN_VALUE( Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config, generic_context), Formatter::FormatterPtr); - return [formatter]() { return std::make_unique(formatter); }; + return std::make_shared(std::move(formatter)); } REGISTER_FACTORY(ActionFactory, Matcher::ActionFactory); diff --git a/source/extensions/matching/actions/format_string/config.h b/source/extensions/matching/actions/format_string/config.h index 359490c375238..0b893e2903031 100644 --- a/source/extensions/matching/actions/format_string/config.h +++ b/source/extensions/matching/actions/format_string/config.h @@ -17,7 +17,7 @@ namespace FormatString { class ActionImpl : public Matcher::ActionBase { public: - ActionImpl(const Formatter::FormatterConstSharedPtr& formatter) : formatter_(formatter) {} + ActionImpl(Formatter::FormatterConstSharedPtr formatter) : formatter_(std::move(formatter)) {} const Network::FilterChain* get(const Server::Configuration::FilterChainsByName& filter_chains_by_name, const StreamInfo::StreamInfo& info) const override; @@ -30,10 +30,9 @@ using FilterChainActionFactoryContext = Server::Configuration::ServerFactoryCont class ActionFactory : public Matcher::ActionFactory { public: std::string name() const override { return "envoy.matching.actions.format_string"; } - Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& proto_config, - FilterChainActionFactoryContext& context, - ProtobufMessage::ValidationVisitor& validator) override; + Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& proto_config, FilterChainActionFactoryContext& context, + ProtobufMessage::ValidationVisitor& validator) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); } diff --git a/source/extensions/matching/http/metadata_input/meta_input.h b/source/extensions/matching/http/metadata_input/meta_input.h index cec7b0253052b..b112e31d37eac 100644 --- a/source/extensions/matching/http/metadata_input/meta_input.h +++ b/source/extensions/matching/http/metadata_input/meta_input.h @@ -15,8 +15,8 @@ namespace MetadataInput { class MetadataMatchData : public ::Envoy::Matcher::CustomMatchData { public: - explicit MetadataMatchData(const ProtobufWkt::Value& value) : value_(value) {} - const ProtobufWkt::Value& value_; + explicit MetadataMatchData(const Protobuf::Value& value) : value_(value) {} + const Protobuf::Value& value_; }; template diff --git a/source/extensions/matching/input_matchers/cel_matcher/matcher.cc b/source/extensions/matching/input_matchers/cel_matcher/matcher.cc index 5ffe47eb7937e..fdcf42a9608bc 100644 --- a/source/extensions/matching/input_matchers/cel_matcher/matcher.cc +++ b/source/extensions/matching/input_matchers/cel_matcher/matcher.cc @@ -10,43 +10,27 @@ using ::Envoy::Extensions::Matching::Http::CelInput::CelMatchData; using ::xds::type::v3::CelExpression; CelInputMatcher::CelInputMatcher(CelMatcherSharedPtr cel_matcher, - Filters::Common::Expr::BuilderInstanceSharedPtr builder) - : builder_(builder), cel_matcher_(std::move(cel_matcher)) { - const CelExpression& input_expr = cel_matcher_->expr_match(); - - auto expr = Filters::Common::Expr::getExpr(input_expr); - if (expr.has_value()) { - compiled_expr_ = Filters::Common::Expr::createExpression(builder_->builder(), expr.value()); - return; - } - - switch (input_expr.expr_specifier_case()) { - case CelExpression::ExprSpecifierCase::kParsedExpr: - compiled_expr_ = Filters::Common::Expr::createExpression(builder_->builder(), - input_expr.parsed_expr().expr()); - return; - case CelExpression::ExprSpecifierCase::kCheckedExpr: - compiled_expr_ = Filters::Common::Expr::createExpression(builder_->builder(), - input_expr.checked_expr().expr()); - return; - case CelExpression::ExprSpecifierCase::EXPR_SPECIFIER_NOT_SET: - PANIC_DUE_TO_PROTO_UNSET; - } - PANIC_DUE_TO_CORRUPT_ENUM; -} + Filters::Common::Expr::BuilderInstanceSharedConstPtr builder) + : compiled_expr_([&]() { + auto compiled_expr = + Filters::Common::Expr::CompiledExpression::Create(builder, cel_matcher->expr_match()); + if (!compiled_expr.ok()) { + throw EnvoyException( + absl::StrCat("failed to create an expression: ", compiled_expr.status().message())); + } + return std::move(compiled_expr.value()); + }()) {} bool CelInputMatcher::match(const MatchingDataType& input) { Protobuf::Arena arena; if (auto* ptr = absl::get_if>(&input); ptr != nullptr) { CelMatchData* cel_data = dynamic_cast((*ptr).get()); - // Compiled expression should not be nullptr at this point because the program should have - // encountered a panic in the constructor earlier if any such error cases occurred. CEL matching - // data should also not be nullptr since any errors should have been thrown by the CEL library - // already. - ASSERT(compiled_expr_ != nullptr && cel_data != nullptr); + // CEL matching data should also not be nullptr since any errors should + // have been thrown by the CEL library already. + ASSERT(cel_data != nullptr); - auto eval_result = compiled_expr_->Evaluate(*cel_data->activation_, &arena); + auto eval_result = compiled_expr_.evaluate(*cel_data->activation_, &arena); if (eval_result.ok() && eval_result.value().IsBool()) { return eval_result.value().BoolOrDie(); } diff --git a/source/extensions/matching/input_matchers/cel_matcher/matcher.h b/source/extensions/matching/input_matchers/cel_matcher/matcher.h index 57e15a02c6c78..0404526341361 100644 --- a/source/extensions/matching/input_matchers/cel_matcher/matcher.h +++ b/source/extensions/matching/input_matchers/cel_matcher/matcher.h @@ -24,14 +24,13 @@ using ::Envoy::Matcher::InputMatcherFactoryCb; using ::Envoy::Matcher::MatchingDataType; using CelMatcher = ::xds::type::matcher::v3::CelMatcher; -using CompiledExpressionPtr = std::unique_ptr; using BaseActivationPtr = std::unique_ptr; using CelMatcherSharedPtr = std::shared_ptr<::xds::type::matcher::v3::CelMatcher>; class CelInputMatcher : public InputMatcher, public Logger::Loggable { public: CelInputMatcher(CelMatcherSharedPtr cel_matcher, - Filters::Common::Expr::BuilderInstanceSharedPtr builder); + Filters::Common::Expr::BuilderInstanceSharedConstPtr builder); bool match(const MatchingDataType& input) override; @@ -41,10 +40,7 @@ class CelInputMatcher : public InputMatcher, public Logger::Loggable); REGISTER_FACTORY(HttpFilterStateInputFactory, Matcher::DataInputFactory); +class NetworkNamespaceInputFactory : public NetworkNamespaceInputBaseFactory {}; +class UdpNetworkNamespaceInputFactory : public NetworkNamespaceInputBaseFactory {}; +class HttpNetworkNamespaceInputFactory + : public NetworkNamespaceInputBaseFactory {}; +REGISTER_FACTORY(NetworkNamespaceInputFactory, Matcher::DataInputFactory); +REGISTER_FACTORY(UdpNetworkNamespaceInputFactory, Matcher::DataInputFactory); +REGISTER_FACTORY(HttpNetworkNamespaceInputFactory, + Matcher::DataInputFactory); + } // namespace Matching } // namespace Network } // namespace Envoy diff --git a/source/extensions/matching/network/common/inputs.h b/source/extensions/matching/network/common/inputs.h index 46297c8e2b685..6ede244482c6e 100644 --- a/source/extensions/matching/network/common/inputs.h +++ b/source/extensions/matching/network/common/inputs.h @@ -6,6 +6,7 @@ #include "envoy/network/filter.h" #include "envoy/registry/registry.h" +#include "source/common/common/logger.h" #include "source/common/network/utility.h" namespace Envoy { @@ -308,6 +309,47 @@ class FilterStateInputBaseFactory : public Matcher::DataInputFactory +class NetworkNamespaceInput : public Matcher::DataInput, + public Logger::Loggable { +public: + Matcher::DataInputGetResult get(const MatchingDataType& data) const override { + const auto& address = data.localAddress(); + const auto network_namespace = address.networkNamespace(); + + if (network_namespace.has_value() && !network_namespace->empty()) { + ENVOY_LOG(debug, "NetworkNamespaceInput: local_address={} network_namespace='{}'", + address.asString(), network_namespace.value()); + return {Matcher::DataInputGetResult::DataAvailability::AllDataAvailable, + network_namespace.value()}; + } + + ENVOY_LOG(debug, "NetworkNamespaceInput: no network namespace for local_address={}", + address.asString()); + return {Matcher::DataInputGetResult::DataAvailability::AllDataAvailable, absl::monostate()}; + } +}; + +template +class NetworkNamespaceInputBaseFactory + : public BaseFactory< + NetworkNamespaceInput, + envoy::extensions::matching::common_inputs::network::v3::NetworkNamespaceInput, + MatchingDataType> { +public: + NetworkNamespaceInputBaseFactory() + : BaseFactory, + envoy::extensions::matching::common_inputs::network::v3::NetworkNamespaceInput, + MatchingDataType>("network_namespace") {} + + // Provide a literal factory name for static discovery by the extensions checker. + std::string name() const override { return "envoy.matching.inputs.network_namespace"; } +}; + +DECLARE_FACTORY(NetworkNamespaceInputFactory); +DECLARE_FACTORY(UdpNetworkNamespaceInputFactory); +DECLARE_FACTORY(HttpNetworkNamespaceInputFactory); + } // namespace Matching } // namespace Network } // namespace Envoy diff --git a/source/extensions/network/dns_resolver/apple/apple_dns_impl.cc b/source/extensions/network/dns_resolver/apple/apple_dns_impl.cc index 36f70e778a7bb..b98ff078b713c 100644 --- a/source/extensions/network/dns_resolver/apple/apple_dns_impl.cc +++ b/source/extensions/network/dns_resolver/apple/apple_dns_impl.cc @@ -230,15 +230,10 @@ std::list& AppleDnsResolverImpl::PendingResolution::finalAddressLis pending_response_.all_responses_.insert(pending_response_.all_responses_.end(), pending_response_.v4_responses_.begin(), pending_response_.v4_responses_.end()); - if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.prefer_ipv6_dns_on_macos")) { - pending_response_.all_responses_.insert(pending_response_.all_responses_.end(), - pending_response_.v6_responses_.begin(), - pending_response_.v6_responses_.end()); - } else { - pending_response_.all_responses_.insert(pending_response_.all_responses_.begin(), - pending_response_.v6_responses_.begin(), - pending_response_.v6_responses_.end()); - } + // Prefer IPv6 addresses by inserting them at the beginning of the response list + pending_response_.all_responses_.insert(pending_response_.all_responses_.begin(), + pending_response_.v6_responses_.begin(), + pending_response_.v6_responses_.end()); return pending_response_.all_responses_; } IS_ENVOY_BUG("unexpected DnsLookupFamily enum"); diff --git a/source/extensions/network/dns_resolver/cares/dns_impl.cc b/source/extensions/network/dns_resolver/cares/dns_impl.cc index 69588e9897067..2840b1f522e03 100644 --- a/source/extensions/network/dns_resolver/cares/dns_impl.cc +++ b/source/extensions/network/dns_resolver/cares/dns_impl.cc @@ -19,6 +19,8 @@ #include "source/common/network/address_impl.h" #include "source/common/network/resolver_impl.h" #include "source/common/network/utility.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" #include "source/common/runtime/runtime_features.h" #include "absl/strings/str_join.h" @@ -51,15 +53,32 @@ DnsResolverImpl::DnsResolverImpl( config, query_timeout_seconds, DEFAULT_QUERY_TIMEOUT_SECONDS))), query_tries_(static_cast( PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, query_tries, DEFAULT_QUERY_TRIES))), - rotate_nameservers_(config.rotate_nameservers()), resolvers_csv_(resolvers_csv), + rotate_nameservers_(config.rotate_nameservers()), + edns0_max_payload_size_(static_cast(PROTOBUF_GET_WRAPPED_OR_DEFAULT( + config, edns0_max_payload_size, 0))), // 0 means use c-ares default EDNS0 + max_udp_channel_duration_( + config.has_max_udp_channel_duration() + ? std::chrono::milliseconds(Protobuf::util::TimeUtil::DurationToMilliseconds( + config.max_udp_channel_duration())) + : std::chrono::milliseconds::zero()), + resolvers_csv_(resolvers_csv), filter_unroutable_families_(config.filter_unroutable_families()), scope_(root_scope.createScope("dns.cares.")), stats_(generateCaresDnsResolverStats(*scope_)) { AresOptions options = defaultAresOptions(); initializeChannel(&options.options_, options.optmask_); + + // Initialize the periodic UDP channel refresh timer if configured. + if (max_udp_channel_duration_ > std::chrono::milliseconds::zero()) { + udp_channel_refresh_timer_ = dispatcher.createTimer([this] { onUdpChannelRefreshTimer(); }); + udp_channel_refresh_timer_->enableTimer(max_udp_channel_duration_); + } } DnsResolverImpl::~DnsResolverImpl() { timer_->disableTimer(); + if (udp_channel_refresh_timer_) { + udp_channel_refresh_timer_->disableTimer(); + } ares_destroy(channel_); } @@ -126,6 +145,12 @@ DnsResolverImpl::AresOptions DnsResolverImpl::defaultAresOptions() { options.optmask_ |= ARES_OPT_NOROTATE; } + // Configure EDNS0 payload size if specified + if (edns0_max_payload_size_ > 0) { + options.optmask_ |= ARES_OPT_EDNSPSZ; + options.options_.ednspsz = edns0_max_payload_size_; + } + // Disable query cache by default. options.optmask_ |= ARES_OPT_QUERY_CACHE; options.options_.qcache_max_ttl = 0; @@ -437,6 +462,20 @@ void DnsResolverImpl::reinitializeChannel() { } } +void DnsResolverImpl::onUdpChannelRefreshTimer() { + ENVOY_LOG_EVENT(debug, "cares_udp_channel_periodic_refresh", + "Performing periodic UDP channel refresh after {} ms", + max_udp_channel_duration_.count()); + + // Reinitialize the channel to refresh UDP sockets. + reinitializeChannel(); + + // Re-enable the timer for the next periodic refresh. + if (udp_channel_refresh_timer_) { + udp_channel_refresh_timer_->enableTimer(max_udp_channel_duration_); + } +} + ActiveDnsQuery* DnsResolverImpl::resolve(const std::string& dns_name, DnsLookupFamily dns_lookup_family, ResolveCb callback) { ENVOY_LOG_EVENT(trace, "cares_dns_resolution_start", "dns resolution for {} started", dns_name); diff --git a/source/extensions/network/dns_resolver/cares/dns_impl.h b/source/extensions/network/dns_resolver/cares/dns_impl.h index 20b772853e399..7552fd958cf7e 100644 --- a/source/extensions/network/dns_resolver/cares/dns_impl.h +++ b/source/extensions/network/dns_resolver/cares/dns_impl.h @@ -185,6 +185,8 @@ class DnsResolverImpl : public DnsResolver, protected Logger::Loggable resolvers_csv_; const bool filter_unroutable_families_; Stats::ScopeSharedPtr scope_; diff --git a/source/extensions/network/dns_resolver/getaddrinfo/BUILD b/source/extensions/network/dns_resolver/getaddrinfo/BUILD index 05b119daa59cd..d66bbe96dc81a 100644 --- a/source/extensions/network/dns_resolver/getaddrinfo/BUILD +++ b/source/extensions/network/dns_resolver/getaddrinfo/BUILD @@ -12,6 +12,7 @@ envoy_cc_extension( name = "config", srcs = ["getaddrinfo.cc"], hdrs = ["getaddrinfo.h"], + extra_visibility = ["//test:__subpackages__"], deps = [ "//envoy/network:dns_resolver_interface", "//envoy/registry", diff --git a/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc b/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc index 4cad7867adc9c..6e2c29a52dc5e 100644 --- a/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc +++ b/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc @@ -41,7 +41,7 @@ GetAddrInfoDnsResolver::GetAddrInfoDnsResolver( GetAddrInfoDnsResolver::~GetAddrInfoDnsResolver() { { - absl::MutexLock guard(&mutex_); + absl::MutexLock guard(mutex_); shutting_down_ = true; pending_queries_.clear(); } @@ -60,9 +60,8 @@ ActiveDnsQuery* GetAddrInfoDnsResolver::resolve(const std::string& dns_name, new_query->addTrace(static_cast(GetAddrInfoTrace::NotStarted)); ActiveDnsQuery* active_query = new_query.get(); { - absl::MutexLock guard(&mutex_); - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.getaddrinfo_num_retries") && - config_.has_num_retries()) { + absl::MutexLock guard(mutex_); + if (config_.has_num_retries()) { // + 1 to include the initial query. pending_queries_.push_back({std::move(new_query), config_.num_retries().value() + 1}); } else { @@ -145,7 +144,7 @@ void GetAddrInfoDnsResolver::resolveThreadRoutine() { std::unique_ptr next_query; absl::optional num_retries; { - absl::MutexLock guard(&mutex_); + absl::MutexLock guard(mutex_); auto condition = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) { return shutting_down_ || !pending_queries_.empty(); }; @@ -193,7 +192,7 @@ void GetAddrInfoDnsResolver::resolveThreadRoutine() { ENVOY_LOG(trace, "retrying query [{}]", next_query->dns_name_); next_query->addTrace(static_cast(GetAddrInfoTrace::Retrying)); { - absl::MutexLock guard(&mutex_); + absl::MutexLock guard(mutex_); pending_queries_.push_back({std::move(next_query), absl::nullopt}); } continue; @@ -204,7 +203,7 @@ void GetAddrInfoDnsResolver::resolveThreadRoutine() { *num_retries); next_query->addTrace(static_cast(GetAddrInfoTrace::Retrying)); { - absl::MutexLock guard(&mutex_); + absl::MutexLock guard(mutex_); pending_queries_.push_back({std::move(next_query), *num_retries}); } continue; diff --git a/source/extensions/quic/connection_id_generator/quic_lb/BUILD b/source/extensions/quic/connection_id_generator/quic_lb/BUILD index b5ee28656d9bd..0c185fedefa51 100644 --- a/source/extensions/quic/connection_id_generator/quic_lb/BUILD +++ b/source/extensions/quic/connection_id_generator/quic_lb/BUILD @@ -19,6 +19,7 @@ envoy_cc_library( srcs = envoy_select_enable_http3(["quic_lb.cc"]), hdrs = envoy_select_enable_http3(["quic_lb.h"]), deps = envoy_select_enable_http3([ + "//source/common/common:base64_lib", "//source/common/config:datasource_lib", "//source/common/quic:envoy_quic_connection_id_generator_factory_interface", "//source/common/quic:envoy_quic_utils_lib", diff --git a/source/extensions/quic/connection_id_generator/quic_lb/quic_lb.cc b/source/extensions/quic/connection_id_generator/quic_lb/quic_lb.cc index 7cdff55e7cd19..f9dc0949eeaa4 100644 --- a/source/extensions/quic/connection_id_generator/quic_lb/quic_lb.cc +++ b/source/extensions/quic/connection_id_generator/quic_lb/quic_lb.cc @@ -2,6 +2,7 @@ #include "envoy/server/transport_socket_config.h" +#include "source/common/common/base64.h" #include "source/common/config/datasource.h" #include "source/common/network/socket_option_impl.h" #include "source/common/quic/envoy_quic_utils.h" @@ -209,6 +210,10 @@ Factory::create(const envoy::extensions::quic::connection_id_generator::quic_lb: std::string server_id = server_id_or_result.value(); + if (config.server_id_base64_encoded()) { + server_id = Base64::decodeWithoutPadding(server_id); + } + if (config.expected_server_id_length() > 0 && config.expected_server_id_length() != server_id.size()) { return absl::InvalidArgumentError( diff --git a/source/extensions/rate_limit_descriptors/expr/config.cc b/source/extensions/rate_limit_descriptors/expr/config.cc index e4a0cd8371d98..87b3b3ca758cd 100644 --- a/source/extensions/rate_limit_descriptors/expr/config.cc +++ b/source/extensions/rate_limit_descriptors/expr/config.cc @@ -23,21 +23,16 @@ class ExpressionDescriptor : public RateLimit::DescriptorProducer { public: ExpressionDescriptor( const envoy::extensions::rate_limit_descriptors::expr::v3::Descriptor& config, - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr& builder, - const google::api::expr::v1alpha1::Expr& input_expr) - : builder_(builder), input_expr_(input_expr), descriptor_key_(config.descriptor_key()), - skip_if_error_(config.skip_if_error()) { - compiled_expr_ = - Extensions::Filters::Common::Expr::createExpression(builder_->builder(), input_expr_); - } + Extensions::Filters::Common::Expr::CompiledExpression&& compiled_expr) + : descriptor_key_(config.descriptor_key()), skip_if_error_(config.skip_if_error()), + compiled_expr_(std::move(compiled_expr)) {} // Ratelimit::DescriptorProducer bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override { - ProtobufWkt::Arena arena; - const auto result = Filters::Common::Expr::evaluate(*compiled_expr_.get(), arena, nullptr, info, - &headers, nullptr, nullptr); + Protobuf::Arena arena; + const auto result = compiled_expr_.evaluate(arena, nullptr, info, &headers, nullptr, nullptr); if (!result.has_value() || result.value().IsError()) { // If result is an error and if skip_if_error is true skip this descriptor, // while calling rate limiting service. If skip_if_error is false, do not call rate limiting @@ -49,11 +44,9 @@ class ExpressionDescriptor : public RateLimit::DescriptorProducer { } private: - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder_; - const google::api::expr::v1alpha1::Expr input_expr_; const std::string descriptor_key_; const bool skip_if_error_; - Extensions::Filters::Common::Expr::ExpressionPtr compiled_expr_; + const Extensions::Filters::Common::Expr::CompiledExpression compiled_expr_; }; } // namespace @@ -79,11 +72,24 @@ ExprDescriptorFactory::createDescriptorProducerFromProto( return absl::InvalidArgumentError(absl::StrCat("Unable to parse descriptor expression: ", parse_status.status().ToString())); } - return std::make_unique(config, builder, parse_status.value().expr()); + auto compiled_expr = Extensions::Filters::Common::Expr::CompiledExpression::Create( + builder, parse_status.value().expr()); + if (!compiled_expr.ok()) { + return absl::InvalidArgumentError( + absl::StrCat("failed to create an expression: ", compiled_expr.status().message())); + } + return std::make_unique(config, std::move(compiled_expr.value())); } #endif - case envoy::extensions::rate_limit_descriptors::expr::v3::Descriptor::kParsed: - return std::make_unique(config, builder, config.parsed()); + case envoy::extensions::rate_limit_descriptors::expr::v3::Descriptor::kParsed: { + auto compiled_expr = + Extensions::Filters::Common::Expr::CompiledExpression::Create(builder, config.parsed()); + if (!compiled_expr.ok()) { + return absl::InvalidArgumentError( + absl::StrCat("failed to create an expression: ", compiled_expr.status().message())); + } + return std::make_unique(config, std::move(compiled_expr.value())); + } default: return absl::InvalidArgumentError( "Rate limit descriptor extension failed: expression specifier is not set"); diff --git a/source/extensions/retry/host/omit_host_metadata/omit_host_metadata.h b/source/extensions/retry/host/omit_host_metadata/omit_host_metadata.h index e7f3a04c2230d..8a1fb2051f1b6 100644 --- a/source/extensions/retry/host/omit_host_metadata/omit_host_metadata.h +++ b/source/extensions/retry/host/omit_host_metadata/omit_host_metadata.h @@ -29,7 +29,7 @@ class OmitHostsRetryPredicate : public Upstream::RetryHostPredicate { private: const envoy::config::core::v3::Metadata metadata_match_criteria_; - std::vector> label_set_; + std::vector> label_set_; }; } // namespace Host diff --git a/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.cc b/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.cc index 70d35193631e2..8553f75f0cde9 100644 --- a/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.cc +++ b/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.cc @@ -10,14 +10,12 @@ namespace Extensions { namespace Router { namespace Matcher { -Envoy::Matcher::ActionFactoryCb ClusterActionFactory::createActionFactoryCb( - const Protobuf::Message& config, ClusterActionContext&, - ProtobufMessage::ValidationVisitor& validation_visitor) { +Envoy::Matcher::ActionConstSharedPtr +ClusterActionFactory::createAction(const Protobuf::Message& config, ClusterActionContext&, + ProtobufMessage::ValidationVisitor& validation_visitor) { const auto& proto_config = MessageUtil::downcastAndValidate(config, validation_visitor); - auto cluster = std::make_shared(proto_config.cluster()); - - return [cluster]() { return std::make_unique(cluster); }; + return std::make_shared(proto_config.cluster()); } REGISTER_FACTORY(ClusterActionFactory, Envoy::Matcher::ActionFactory); @@ -43,9 +41,7 @@ class MatcherRouteEntry : public Envoy::Router::DelegatingRouteEntry { if (!match_result.isMatch()) { return; } - - const Envoy::Matcher::ActionPtr result = match_result.action(); - cluster_name_.emplace(result->getTyped().cluster()); + cluster_name_.emplace(match_result.action()->getTyped().cluster()); } private: diff --git a/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.h b/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.h index 52562d728cb0f..88e7746cea02b 100644 --- a/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.h +++ b/source/extensions/router/cluster_specifiers/matcher/matcher_cluster_specifier.h @@ -26,21 +26,21 @@ struct ClusterActionContext {}; */ class ClusterAction : public Envoy::Matcher::ActionBase { public: - explicit ClusterAction(std::shared_ptr cluster) : cluster_(cluster) {} + explicit ClusterAction(absl::string_view cluster) : cluster_(cluster) {} - const std::string& cluster() const { return *cluster_; } + const std::string& cluster() const { return cluster_; } private: - const std::shared_ptr cluster_; + const std::string cluster_; }; // Registered factory for ClusterAction. This factory will be used to load proto configuration // from opaque config. class ClusterActionFactory : public Envoy::Matcher::ActionFactory { public: - Envoy::Matcher::ActionFactoryCb - createActionFactoryCb(const Protobuf::Message& config, ClusterActionContext& context, - ProtobufMessage::ValidationVisitor& validation_visitor) override; + Envoy::Matcher::ActionConstSharedPtr + createAction(const Protobuf::Message& config, ClusterActionContext& context, + ProtobufMessage::ValidationVisitor& validation_visitor) override; std::string name() const override { return "cluster"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); diff --git a/source/extensions/stat_sinks/open_telemetry/BUILD b/source/extensions/stat_sinks/open_telemetry/BUILD index 4a87743eff39e..e1770f4603fb0 100644 --- a/source/extensions/stat_sinks/open_telemetry/BUILD +++ b/source/extensions/stat_sinks/open_telemetry/BUILD @@ -17,6 +17,7 @@ envoy_cc_library( "//envoy/grpc:async_client_interface", "//envoy/singleton:instance_interface", "//source/common/grpc:async_client_lib", + "//source/extensions/tracers/opentelemetry/resource_detectors:resource_detector_lib", "@envoy_api//envoy/extensions/stat_sinks/open_telemetry/v3:pkg_cc_proto", "@opentelemetry_proto//:metrics_proto_cc", "@opentelemetry_proto//:metrics_service_proto_cc", diff --git a/source/extensions/stat_sinks/open_telemetry/config.cc b/source/extensions/stat_sinks/open_telemetry/config.cc index 391f87e387177..d8dbe0cab1ed6 100644 --- a/source/extensions/stat_sinks/open_telemetry/config.cc +++ b/source/extensions/stat_sinks/open_telemetry/config.cc @@ -4,6 +4,7 @@ #include "source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.h" #include "source/extensions/stat_sinks/open_telemetry/open_telemetry_proto_descriptors.h" +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h" namespace Envoy { namespace Extensions { @@ -18,7 +19,11 @@ OpenTelemetrySinkFactory::createStatsSink(const Protobuf::Message& config, const auto& sink_config = MessageUtil::downcastAndValidate( config, server.messageValidationContext().staticValidationVisitor()); - auto otlp_options = std::make_shared(sink_config); + Tracers::OpenTelemetry::ResourceProviderPtr resource_provider = + std::make_unique(); + auto otlp_options = std::make_shared( + sink_config, resource_provider->getResource(sink_config.resource_detectors(), server, + /*service_name=*/"")); std::shared_ptr otlp_metrics_flusher = std::make_shared(otlp_options); diff --git a/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.cc b/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.cc index e50a6a85de91f..ef105b0057e17 100644 --- a/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.cc +++ b/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.cc @@ -7,14 +7,27 @@ namespace Extensions { namespace StatSinks { namespace OpenTelemetry { -OtlpOptions::OtlpOptions(const SinkConfig& sink_config) +Protobuf::RepeatedPtrField +generateResourceAttributes(const Tracers::OpenTelemetry::Resource& resource) { + Protobuf::RepeatedPtrField resource_attributes; + for (const auto& attr : resource.attributes_) { + auto* attribute = resource_attributes.Add(); + attribute->set_key(attr.first); + attribute->mutable_value()->set_string_value(attr.second); + } + return resource_attributes; +} + +OtlpOptions::OtlpOptions(const SinkConfig& sink_config, + const Tracers::OpenTelemetry::Resource& resource) : report_counters_as_deltas_(sink_config.report_counters_as_deltas()), report_histograms_as_deltas_(sink_config.report_histograms_as_deltas()), emit_tags_as_attributes_( PROTOBUF_GET_WRAPPED_OR_DEFAULT(sink_config, emit_tags_as_attributes, true)), use_tag_extracted_name_( PROTOBUF_GET_WRAPPED_OR_DEFAULT(sink_config, use_tag_extracted_name, true)), - stat_prefix_(!sink_config.prefix().empty() ? sink_config.prefix() + "." : "") {} + stat_prefix_(!sink_config.prefix().empty() ? sink_config.prefix() + "." : ""), + resource_attributes_(generateResourceAttributes(resource)) {} OpenTelemetryGrpcMetricsExporterImpl::OpenTelemetryGrpcMetricsExporterImpl( const OtlpOptionsSharedPtr config, Grpc::RawAsyncClientSharedPtr raw_async_client) @@ -46,7 +59,8 @@ MetricsExportRequestPtr OtlpMetricsFlusherImpl::flush(Stats::MetricSnapshot& sna auto request = std::make_unique(); auto* resource_metrics = request->add_resource_metrics(); auto* scope_metrics = resource_metrics->add_scope_metrics(); - + resource_metrics->mutable_resource()->mutable_attributes()->CopyFrom( + config_->resource_attributes()); int64_t snapshot_time_ns = std::chrono::duration_cast( snapshot.snapshotTime().time_since_epoch()) .count(); diff --git a/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.h b/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.h index 92ce4fe58d2e1..58a46b7983226 100644 --- a/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.h +++ b/source/extensions/stat_sinks/open_telemetry/open_telemetry_impl.h @@ -12,6 +12,7 @@ #include "envoy/stats/stats.h" #include "source/common/grpc/typed_async_client.h" +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h" #include "opentelemetry/proto/collector/metrics/v1/metrics_service.pb.h" #include "opentelemetry/proto/common/v1/common.pb.h" @@ -35,13 +36,17 @@ using SinkConfig = envoy::extensions::stat_sinks::open_telemetry::v3::SinkConfig class OtlpOptions { public: - OtlpOptions(const SinkConfig& sink_config); + OtlpOptions(const SinkConfig& sink_config, const Tracers::OpenTelemetry::Resource& resource); bool reportCountersAsDeltas() { return report_counters_as_deltas_; } bool reportHistogramsAsDeltas() { return report_histograms_as_deltas_; } bool emitTagsAsAttributes() { return emit_tags_as_attributes_; } bool useTagExtractedName() { return use_tag_extracted_name_; } const std::string& statPrefix() { return stat_prefix_; } + const Protobuf::RepeatedPtrField& + resource_attributes() const { + return resource_attributes_; + } private: const bool report_counters_as_deltas_; @@ -49,6 +54,7 @@ class OtlpOptions { const bool emit_tags_as_attributes_; const bool use_tag_extracted_name_; const std::string stat_prefix_; + const Protobuf::RepeatedPtrField resource_attributes_; }; using OtlpOptionsSharedPtr = std::shared_ptr; diff --git a/source/extensions/tracers/datadog/dict_util.cc b/source/extensions/tracers/datadog/dict_util.cc index 7932e7e8779f0..b8c42bcd76491 100644 --- a/source/extensions/tracers/datadog/dict_util.cc +++ b/source/extensions/tracers/datadog/dict_util.cc @@ -70,8 +70,7 @@ void TraceContextReader::visit( visitor) const { context_.forEach([&](absl::string_view key, absl::string_view value) { visitor(key, value); - const bool continue_iterating = true; - return continue_iterating; + return true; }); } diff --git a/source/extensions/tracers/datadog/span.cc b/source/extensions/tracers/datadog/span.cc index 6aba88f937b23..d3617d233d788 100644 --- a/source/extensions/tracers/datadog/span.cc +++ b/source/extensions/tracers/datadog/span.cc @@ -33,7 +33,8 @@ class TraceContextWriter : public datadog::tracing::DictWriter { } // namespace -Span::Span(datadog::tracing::Span&& span) : span_(std::move(span)) {} +Span::Span(datadog::tracing::Span&& span, bool use_local_decision) + : span_(std::move(span)), use_local_decision_(use_local_decision) {} const datadog::tracing::Optional& Span::impl() const { return span_; } diff --git a/source/extensions/tracers/datadog/span.h b/source/extensions/tracers/datadog/span.h index 0e41272ef49bc..38c3acbb8a151 100644 --- a/source/extensions/tracers/datadog/span.h +++ b/source/extensions/tracers/datadog/span.h @@ -31,7 +31,7 @@ namespace Datadog { */ class Span : public Tracing::Span { public: - explicit Span(datadog::tracing::Span&& span); + explicit Span(datadog::tracing::Span&& span, bool use_local_decision = false); const datadog::tracing::Optional& impl() const; @@ -45,6 +45,7 @@ class Span : public Tracing::Span { Tracing::SpanPtr spawnChild(const Tracing::Config& config, const std::string& name, SystemTime start_time) override; void setSampled(bool sampled) override; + bool useLocalDecision() const override { return use_local_decision_; } std::string getBaggage(absl::string_view key) override; void setBaggage(absl::string_view key, absl::string_view value) override; std::string getTraceId() const override; @@ -52,6 +53,7 @@ class Span : public Tracing::Span { private: datadog::tracing::Optional span_; + const bool use_local_decision_{false}; }; } // namespace Datadog diff --git a/source/extensions/tracers/datadog/tracer.cc b/source/extensions/tracers/datadog/tracer.cc index ae5b510f9c0f7..4b8efc3480fa1 100644 --- a/source/extensions/tracers/datadog/tracer.cc +++ b/source/extensions/tracers/datadog/tracer.cc @@ -107,12 +107,15 @@ Tracing::SpanPtr Tracer::startSpan(const Tracing::Config&, Tracing::TraceContext // // If Envoy is telling us to keep the trace, then we leave it up to the // tracer's internal sampler (which might decide to drop the trace anyway). - if (!span.trace_segment().sampling_decision().has_value() && !tracing_decision.traced) { + const bool use_local_decision = !span.trace_segment().sampling_decision().has_value(); + if (use_local_decision && !tracing_decision.traced) { + // TODO(wbpcode): use USER_KEEP to indicate that the trace should be kept if the + // Envoy is telling us to keep the trace. span.trace_segment().override_sampling_priority( int(datadog::tracing::SamplingPriority::USER_DROP)); } - return std::make_unique(std::move(span)); + return std::make_unique(std::move(span), use_local_decision); } datadog::tracing::Span Tracer::extractOrCreateSpan(datadog::tracing::Tracer& tracer, diff --git a/source/extensions/tracers/fluentd/fluentd_tracer_impl.cc b/source/extensions/tracers/fluentd/fluentd_tracer_impl.cc index d4513c22cad9a..edfa97a20cf7e 100644 --- a/source/extensions/tracers/fluentd/fluentd_tracer_impl.cc +++ b/source/extensions/tracers/fluentd/fluentd_tracer_impl.cc @@ -89,21 +89,11 @@ absl::StatusOr SpanContextExtractor::extractSpanContext() { // it is invalid and MUST be discarded. Because we're already checking for the // traceparent header above, we don't need to check here. // See https://www.w3.org/TR/trace-context/#processing-model-for-working-with-trace-context - absl::string_view tracestate_key = FluentdConstants::get().TRACE_STATE.key(); - std::vector tracestate_values; - // Multiple tracestate header fields MUST be handled as specified by RFC7230 Section 3.2.2 Field - // Order. - trace_context_.forEach( - [&tracestate_key, &tracestate_values](absl::string_view key, absl::string_view value) { - if (key == tracestate_key) { - tracestate_values.push_back(std::string{value}); - } - return true; - }); - std::string tracestate = absl::StrJoin(tracestate_values, ","); - - SpanContext span_context(version, trace_id, parent_id, sampled, tracestate); - return span_context; + const auto tracestate_values = FluentdConstants::get().TRACE_STATE.getAll(trace_context_); + + SpanContext parent_context(version, trace_id, parent_id, sampled, + absl::StrJoin(tracestate_values, ",")); + return parent_context; } // Define default version and trace context construction// Define default version and trace context @@ -160,17 +150,12 @@ Tracing::SpanPtr Driver::startSpan(const Tracing::Config& /*config*/, SpanContextExtractor extractor(trace_context); if (!extractor.propagationHeaderPresent()) { // No propagation header, so we can create a fresh span with the given decision. - - return tracer.startSpan(trace_context, stream_info.startTime(), operation_name, - tracing_decision); + return tracer.startSpan(stream_info.startTime(), operation_name, tracing_decision); } else { // Try to extract the span context. If we can't, just return a null span. absl::StatusOr span_context = extractor.extractSpanContext(); if (span_context.ok()) { - - return tracer.startSpan(trace_context, stream_info.startTime(), operation_name, - tracing_decision, span_context.value()); - + return tracer.startSpan(stream_info.startTime(), operation_name, span_context.value()); } else { ENVOY_LOG(trace, "Unable to extract span context: ", span_context.status()); return std::make_unique(); @@ -196,12 +181,11 @@ FluentdTracerImpl::FluentdTracerImpl(Upstream::ThreadLocalCluster& cluster, time_source_(dispatcher.timeSource()) {} // Initialize a span object -Span::Span(Tracing::TraceContext& trace_context, SystemTime start_time, - const std::string& operation_name, Tracing::Decision tracing_decision, - FluentdTracerSharedPtr tracer, const SpanContext& span_context, TimeSource& time_source) - : trace_context_(trace_context), start_time_(start_time), operation_(operation_name), - tracing_decision_(tracing_decision), tracer_(tracer), span_context_(span_context), - time_source_(time_source) {} +Span::Span(SystemTime start_time, const std::string& operation_name, FluentdTracerSharedPtr tracer, + SpanContext&& span_context, TimeSource& time_source, bool use_local_decision) + : start_time_(start_time), operation_(operation_name), tracer_(std::move(tracer)), + span_context_(std::move(span_context)), time_source_(time_source), + use_local_decision_(use_local_decision) {} // Set the operation name for the span void Span::setOperation(absl::string_view operation) { operation_ = std::string(operation); } @@ -230,19 +214,14 @@ void Span::finishSpan() { .count(); // Make the record map - std::map record_map; + std::map record_map = std::move(tags_); record_map["operation"] = operation_; record_map["trace_id"] = span_context_.traceId(); - record_map["span_id"] = span_context_.parentId(); + record_map["span_id"] = span_context_.spanId(); record_map["start_time"] = std::to_string( std::chrono::duration_cast(start_time_.time_since_epoch()).count()); record_map["end_time"] = std::to_string(time); - // Add the tags to the record map - for (const auto& tag : tags_) { - record_map[tag.first] = tag.second; - } - EntryPtr entry = std::make_unique(time, std::move(record_map)); tracer_->log(std::move(entry)); @@ -253,30 +232,26 @@ void Span::injectContext(Tracing::TraceContext& trace_context, const Tracing::UpstreamContext& /*upstream*/) { std::string trace_id_hex = span_context_.traceId(); - std::string parent_id_hex = span_context_.parentId(); + std::string span_id_hex = span_context_.spanId(); std::vector trace_flags_vec{sampled()}; std::string trace_flags_hex = Hex::encode(trace_flags_vec); std::string traceparent_header_value = - absl::StrCat(kDefaultVersion, "-", trace_id_hex, "-", parent_id_hex, "-", trace_flags_hex); + absl::StrCat(kDefaultVersion, "-", trace_id_hex, "-", span_id_hex, "-", trace_flags_hex); // Set the traceparent in the trace_context. traceParentHeader().setRefKey(trace_context, traceparent_header_value); - // Also set the tracestate. - traceStateHeader().setRefKey(trace_context, span_context_.tracestate()); + if (!span_context_.tracestate().empty()) { + // Also set the tracestate. + traceStateHeader().setRefKey(trace_context, span_context_.tracestate()); + } } // Spawns a child span Tracing::SpanPtr Span::spawnChild(const Tracing::Config&, const std::string& name, SystemTime start_time) { - SpanContext span_context = - SpanContext(kDefaultVersion, span_context_.traceId(), span_context_.parentId(), sampled(), - span_context_.tracestate()); - return tracer_->startSpan(trace_context_, start_time, name, tracing_decision_, span_context); + return tracer_->startSpan(start_time, name, span_context_); } -// Set the sampled flag for the span -void Span::setSampled(bool sampled) { sampled_ = sampled; } - std::string Span::getBaggage(absl::string_view /*key*/) { // not implemented return EMPTY_STRING; @@ -288,11 +263,10 @@ void Span::setBaggage(absl::string_view /*key*/, absl::string_view /*value*/) { std::string Span::getTraceId() const { return span_context_.traceId(); } -std::string Span::getSpanId() const { return span_context_.parentId(); } +std::string Span::getSpanId() const { return span_context_.spanId(); } // Start a new span with no parent context -Tracing::SpanPtr FluentdTracerImpl::startSpan(Tracing::TraceContext& trace_context, - SystemTime start_time, +Tracing::SpanPtr FluentdTracerImpl::startSpan(SystemTime start_time, const std::string& operation_name, Tracing::Decision tracing_decision) { // make a new span context @@ -300,34 +274,24 @@ Tracing::SpanPtr FluentdTracerImpl::startSpan(Tracing::TraceContext& trace_conte uint64_t trace_id = random_.random(); uint64_t span_id = random_.random(); - SpanContext span_context = SpanContext( + SpanContext span_context( kDefaultVersion, absl::StrCat(Hex::uint64ToHex(trace_id_high), Hex::uint64ToHex(trace_id)), Hex::uint64ToHex(span_id), tracing_decision.traced, ""); - Span new_span(trace_context, start_time, operation_name, tracing_decision, shared_from_this(), - span_context, time_source_); - - new_span.setSampled(tracing_decision.traced); - - return std::make_unique(new_span); + return std::make_unique(start_time, operation_name, shared_from_this(), + std::move(span_context), time_source_, true); } // Start a new span with a parent context -Tracing::SpanPtr FluentdTracerImpl::startSpan(Tracing::TraceContext& trace_context, - SystemTime start_time, +Tracing::SpanPtr FluentdTracerImpl::startSpan(SystemTime start_time, const std::string& operation_name, - Tracing::Decision tracing_decision, - const SpanContext& previous_span_context) { - SpanContext span_context = SpanContext( - kDefaultVersion, previous_span_context.traceId(), Hex::uint64ToHex(random_.random()), - previous_span_context.sampled(), previous_span_context.tracestate()); - - Span new_span(trace_context, start_time, operation_name, tracing_decision, shared_from_this(), - span_context, time_source_); - - new_span.setSampled(previous_span_context.sampled()); - - return std::make_unique(new_span); + const SpanContext& parent_context) { + // Generate a new span context with new span id based on the parent context. + SpanContext span_context(kDefaultVersion, parent_context.traceId(), + Hex::uint64ToHex(random_.random()), parent_context.sampled(), + parent_context.tracestate()); + return std::make_unique(start_time, operation_name, shared_from_this(), + std::move(span_context), time_source_, false); } void FluentdTracerImpl::packMessage(MessagePackPacker& packer) { diff --git a/source/extensions/tracers/fluentd/fluentd_tracer_impl.h b/source/extensions/tracers/fluentd/fluentd_tracer_impl.h index 70800414188c7..040878679b3a2 100644 --- a/source/extensions/tracers/fluentd/fluentd_tracer_impl.h +++ b/source/extensions/tracers/fluentd/fluentd_tracer_impl.h @@ -29,27 +29,28 @@ using FluentdConfigSharedPtr = std::shared_ptr; class SpanContext { public: SpanContext() = default; - SpanContext(absl::string_view version, absl::string_view trace_id, absl::string_view parent_id, - bool sampled, absl::string_view tracestate) - : version_(version), trace_id_(trace_id), parent_id_(parent_id), sampled_(sampled), - tracestate_(tracestate) {} + SpanContext(absl::string_view version, absl::string_view trace_id, absl::string_view span_id, + bool sampled, std::string tracestate) + : version_(version), trace_id_(trace_id), span_id_(span_id), sampled_(sampled), + tracestate_(std::move(tracestate)) {} const std::string& version() const { return version_; } const std::string& traceId() const { return trace_id_; } - const std::string& parentId() const { return parent_id_; } + const std::string& spanId() const { return span_id_; } bool sampled() const { return sampled_; } + void setSampled(bool sampled) { sampled_ = sampled; } const std::string& tracestate() const { return tracestate_; } private: - const std::string version_; - const std::string trace_id_; - const std::string parent_id_; - const bool sampled_{false}; - const std::string tracestate_; + std::string version_; + std::string trace_id_; + std::string span_id_; + bool sampled_{false}; + std::string tracestate_; }; // Trace context definitions @@ -82,14 +83,13 @@ class FluentdTracerImpl : public FluentdBase, BackOffStrategyPtr backoff_strategy, Stats::Scope& parent_scope, Random::RandomGenerator& random); - Tracing::SpanPtr startSpan(Tracing::TraceContext& trace_context, SystemTime start_time, - const std::string& operation_name, Tracing::Decision tracing_decision); + Tracing::SpanPtr startSpan(SystemTime start_time, const std::string& operation_name, + Tracing::Decision tracing_decision); - Tracing::SpanPtr startSpan(Tracing::TraceContext& trace_context, SystemTime start_time, - const std::string& operation_name, Tracing::Decision tracing_decision, - const SpanContext& previous_span_context); + Tracing::SpanPtr startSpan(SystemTime start_time, const std::string& operation_name, + const SpanContext& parent_context); - void packMessage(MessagePackPacker& packer); + void packMessage(MessagePackPacker& packer) override; private: std::map option_; @@ -157,9 +157,8 @@ class Driver : Logger::Loggable, public Tracing::Driver { // Span holds the span context and handles span operations class Span : public Tracing::Span { public: - Span(Tracing::TraceContext& trace_context, SystemTime start_time, - const std::string& operation_name, Tracing::Decision tracing_decision, - FluentdTracerSharedPtr tracer, const SpanContext& span_context, TimeSource& time_source); + Span(SystemTime start_time, const std::string& operation_name, FluentdTracerSharedPtr tracer, + SpanContext&& span_context, TimeSource& time_source, bool use_local_decision); // Tracing::Span void setOperation(absl::string_view operation) override; @@ -170,8 +169,10 @@ class Span : public Tracing::Span { const Tracing::UpstreamContext& upstream) override; Tracing::SpanPtr spawnChild(const Tracing::Config& config, const std::string& name, SystemTime start_time) override; - void setSampled(bool sampled) override; - bool sampled() const { return sampled_; } + void setSampled(bool sampled) override { span_context_.setSampled(sampled); } + bool sampled() const { return span_context_.sampled(); } + bool useLocalDecision() const override { return use_local_decision_; } + std::string getBaggage(absl::string_view key) override; void setBaggage(absl::string_view key, absl::string_view value) override; std::string getTraceId() const override; @@ -179,16 +180,14 @@ class Span : public Tracing::Span { private: // config - Tracing::TraceContext& trace_context_; SystemTime start_time_; std::string operation_; - Tracing::Decision tracing_decision_; FluentdTracerSharedPtr tracer_; SpanContext span_context_; std::map tags_; - bool sampled_; Envoy::TimeSource& time_source_; + const bool use_local_decision_{false}; }; } // namespace Fluentd diff --git a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc index 956eb4759acc9..dd758787dd9f1 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc @@ -75,7 +75,10 @@ Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetr POOL_COUNTER_PREFIX(context.serverFactoryContext().scope(), "tracing.opentelemetry"))} { auto& factory_context = context.serverFactoryContext(); - Resource resource = resource_provider.getResource(opentelemetry_config, context); + Resource resource = resource_provider.getResource( + opentelemetry_config.resource_detectors(), context.serverFactoryContext(), + opentelemetry_config.service_name().empty() ? kDefaultServiceName + : opentelemetry_config.service_name()); ResourceConstSharedPtr resource_ptr = std::make_shared(std::move(resource)); if (opentelemetry_config.has_grpc_service() && opentelemetry_config.has_http_service()) { diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.cc b/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.cc index 564c2cf291cdb..e8eeed9642927 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.cc +++ b/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.cc @@ -13,10 +13,10 @@ namespace Tracers { namespace OpenTelemetry { ResourceDetectorPtr DynatraceResourceDetectorFactory::createResourceDetector( - const Protobuf::Message& message, Server::Configuration::TracerFactoryContext& context) { + const Protobuf::Message& message, Server::Configuration::ServerFactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(message), context.messageValidationVisitor(), *this); + dynamic_cast(message), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.h b/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.h index f3063484aeb9b..3b955656ece67 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.h +++ b/source/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config.h @@ -28,7 +28,7 @@ class DynatraceResourceDetectorFactory : public ResourceDetectorFactory { */ ResourceDetectorPtr createResourceDetector(const Protobuf::Message& message, - Server::Configuration::TracerFactoryContext& context) override; + Server::Configuration::ServerFactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(message), context.messageValidationVisitor(), *this); + dynamic_cast(message), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h b/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h index a2bf1f72025fd..d09a6bd41c1c1 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h +++ b/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h @@ -26,7 +26,7 @@ class EnvironmentResourceDetectorFactory : public ResourceDetectorFactory { */ ResourceDetectorPtr createResourceDetector(const Protobuf::Message& message, - Server::Configuration::TracerFactoryContext& context) override; + Server::Configuration::ServerFactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique; +using ResourceAttributes = absl::flat_hash_map; /** * @brief A Resource represents the entity producing telemetry as Attributes. @@ -67,7 +67,7 @@ class ResourceDetectorFactory : public Envoy::Config::TypedFactory { */ virtual ResourceDetectorPtr createResourceDetector(const Protobuf::Message& message, - Server::Configuration::TracerFactoryContext& context) PURE; + Server::Configuration::ServerFactoryContext& context) PURE; std::string category() const override { return "envoy.tracers.opentelemetry.resource_detectors"; } }; diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc index 123994576a4c1..b6e04cb1333fb 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc +++ b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc @@ -14,23 +14,18 @@ namespace OpenTelemetry { namespace { bool isEmptyResource(const Resource& resource) { return resource.attributes_.empty(); } -Resource createInitialResource(const std::string& service_name) { +Resource createInitialResource(absl::string_view service_name) { Resource resource{}; // Creates initial resource with the static service.name and telemetry.sdk.* attributes. - resource.attributes_[std::string(kServiceNameKey.data(), kServiceNameKey.size())] = - service_name.empty() ? std::string{kDefaultServiceName} : service_name; - - resource - .attributes_[std::string(kTelemetrySdkLanguageKey.data(), kTelemetrySdkLanguageKey.size())] = - kDefaultTelemetrySdkLanguage; + if (!service_name.empty()) { + resource.attributes_[kServiceNameKey] = service_name; + } + resource.attributes_[kTelemetrySdkLanguageKey] = kDefaultTelemetrySdkLanguage; - resource.attributes_[std::string(kTelemetrySdkNameKey.data(), kTelemetrySdkNameKey.size())] = - kDefaultTelemetrySdkName; + resource.attributes_[kTelemetrySdkNameKey] = kDefaultTelemetrySdkName; - resource - .attributes_[std::string(kTelemetrySdkVersionKey.data(), kTelemetrySdkVersionKey.size())] = - Envoy::VersionInfo::version(); + resource.attributes_[kTelemetrySdkVersionKey] = Envoy::VersionInfo::version(); return resource; } @@ -88,14 +83,14 @@ void mergeResource(Resource& old_resource, const Resource& updating_resource) { } // namespace Resource ResourceProviderImpl::getResource( - const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, - Server::Configuration::TracerFactoryContext& context) const { - - Resource resource = createInitialResource(opentelemetry_config.service_name()); + const Protobuf::RepeatedPtrField& + resource_detectors, + Envoy::Server::Configuration::ServerFactoryContext& context, + absl::string_view service_name) const { - const auto& detectors_configs = opentelemetry_config.resource_detectors(); + Resource resource = createInitialResource(service_name); - for (const auto& detector_config : detectors_configs) { + for (const auto& detector_config : resource_detectors) { ResourceDetectorPtr detector; auto* factory = Envoy::Config::Utility::getFactory(detector_config); diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h index 8a2e0739a97b7..a9c56e18d6fbb 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h +++ b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h @@ -36,14 +36,20 @@ class ResourceProvider : public Logger::Loggable { * @return Resource const The merged resource. */ virtual Resource - getResource(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, - Server::Configuration::TracerFactoryContext& context) const PURE; + getResource(const Protobuf::RepeatedPtrField& + resource_detectors, + Server::Configuration::ServerFactoryContext& context, + absl::string_view service_name) const PURE; }; +using ResourceProviderPtr = std::shared_ptr; class ResourceProviderImpl : public ResourceProvider { public: - Resource getResource(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, - Server::Configuration::TracerFactoryContext& context) const override; + Resource + getResource(const Protobuf::RepeatedPtrField& + resource_detectors, + Server::Configuration::ServerFactoryContext& context, + absl::string_view service_name) const override; }; } // namespace OpenTelemetry diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/static/config.cc b/source/extensions/tracers/opentelemetry/resource_detectors/static/config.cc index 5476816af27ca..b25c875c2c1d6 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/static/config.cc +++ b/source/extensions/tracers/opentelemetry/resource_detectors/static/config.cc @@ -12,10 +12,10 @@ namespace Tracers { namespace OpenTelemetry { ResourceDetectorPtr StaticConfigResourceDetectorFactory::createResourceDetector( - const Protobuf::Message& message, Server::Configuration::TracerFactoryContext& context) { + const Protobuf::Message& message, Server::Configuration::ServerFactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(message), context.messageValidationVisitor(), *this); + dynamic_cast(message), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/static/config.h b/source/extensions/tracers/opentelemetry/resource_detectors/static/config.h index 4554622ecee0e..55566f0f6f5c7 100644 --- a/source/extensions/tracers/opentelemetry/resource_detectors/static/config.h +++ b/source/extensions/tracers/opentelemetry/resource_detectors/static/config.h @@ -25,7 +25,7 @@ class StaticConfigResourceDetectorFactory : public ResourceDetectorFactory { */ ResourceDetectorPtr createResourceDetector(const Protobuf::Message& message, - Server::Configuration::TracerFactoryContext& context) override; + Server::Configuration::ServerFactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_uniquebuilder(), parsed_expr_); -} + Expr::BuilderInstanceSharedConstPtr builder, + const xds::type::v3::CelExpression& expr) + : local_info_(local_info), compiled_expr_([&]() { + auto compiled_expr = Expr::CompiledExpression::Create(builder, expr); + if (!compiled_expr.ok()) { + throw EnvoyException( + absl::StrCat("failed to create an expression: ", compiled_expr.status().message())); + } + return std::move(compiled_expr.value()); + }()) {} SamplingResult CELSampler::shouldSample(const StreamInfo::StreamInfo& stream_info, const absl::optional parent_context, @@ -32,8 +37,8 @@ SamplingResult CELSampler::shouldSample(const StreamInfo::StreamInfo& stream_inf request_headers = trace_context->requestHeaders().ptr(); } - auto eval_status = Expr::evaluate( - *compiled_expr_, arena, &local_info_, stream_info, request_headers /* request_headers */, + auto eval_status = compiled_expr_.evaluate( + arena, &local_info_, stream_info, request_headers /* request_headers */, nullptr /* response_headers */, nullptr /* response_trailers */); SamplingResult result; if (!eval_status.has_value() || eval_status.value().IsError()) { @@ -54,8 +59,6 @@ SamplingResult CELSampler::shouldSample(const StreamInfo::StreamInfo& stream_inf return result; } -std::string CELSampler::getDescription() const { return "CELSampler"; } - } // namespace OpenTelemetry } // namespace Tracers } // namespace Extensions diff --git a/source/extensions/tracers/opentelemetry/samplers/cel/cel_sampler.h b/source/extensions/tracers/opentelemetry/samplers/cel/cel_sampler.h index 26a77c46fddaa..ac9611fcfb4b0 100644 --- a/source/extensions/tracers/opentelemetry/samplers/cel/cel_sampler.h +++ b/source/extensions/tracers/opentelemetry/samplers/cel/cel_sampler.h @@ -25,20 +25,20 @@ namespace Expr = Envoy::Extensions::Filters::Common::Expr; class CELSampler : public Sampler, Logger::Loggable { public: CELSampler(const ::Envoy::LocalInfo::LocalInfo& local_info, - Expr::BuilderInstanceSharedPtr builder, const google::api::expr::v1alpha1::Expr& expr); + Expr::BuilderInstanceSharedConstPtr builder, const xds::type::v3::CelExpression& expr); + SamplingResult shouldSample(const StreamInfo::StreamInfo& stream_info, const absl::optional parent_context, const std::string& trace_id, const std::string& name, OTelSpanKind spankind, OptRef trace_context, const std::vector& links) override; - std::string getDescription() const override; + + std::string getDescription() const override { return "CELSampler"; } private: const ::Envoy::LocalInfo::LocalInfo& local_info_; - Extensions::Filters::Common::Expr::BuilderInstanceSharedPtr builder_; - const google::api::expr::v1alpha1::Expr parsed_expr_; - Extensions::Filters::Common::Expr::ExpressionPtr compiled_expr_; + const Extensions::Filters::Common::Expr::CompiledExpression compiled_expr_; }; } // namespace OpenTelemetry diff --git a/source/extensions/tracers/opentelemetry/samplers/cel/config.cc b/source/extensions/tracers/opentelemetry/samplers/cel/config.cc index 44e59fdd7826a..f14741a889265 100644 --- a/source/extensions/tracers/opentelemetry/samplers/cel/config.cc +++ b/source/extensions/tracers/opentelemetry/samplers/cel/config.cc @@ -17,20 +17,16 @@ SamplerSharedPtr CELSamplerFactory::createSampler(const Protobuf::Message& config, Server::Configuration::TracerFactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(config), context.messageValidationVisitor(), *this); + dynamic_cast(config), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::tracers::opentelemetry::samplers::v3::CELSamplerConfig&>( *mptr, context.messageValidationVisitor()); - auto expr = Expr::getExpr(proto_config.expression()); - if (!expr.has_value()) { - throw EnvoyException("CEL expression not set"); - } - return std::make_unique( context.serverFactoryContext().localInfo(), - Extensions::Filters::Common::Expr::getBuilder(context.serverFactoryContext()), expr.value()); + Extensions::Filters::Common::Expr::getBuilder(context.serverFactoryContext()), + proto_config.expression()); } /** diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc b/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc index bfcfb2d382f93..11f9e9cb1d2df 100644 --- a/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc @@ -17,7 +17,7 @@ SamplerSharedPtr DynatraceSamplerFactory::createSampler(const Protobuf::Message& config, Server::Configuration::TracerFactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(config), context.messageValidationVisitor(), *this); + dynamic_cast(config), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::tracers::opentelemetry::samplers::v3::DynatraceSamplerConfig&>( diff --git a/source/extensions/tracers/opentelemetry/samplers/parent_based/config.cc b/source/extensions/tracers/opentelemetry/samplers/parent_based/config.cc index 87ca75f763de0..0b4170343c77d 100644 --- a/source/extensions/tracers/opentelemetry/samplers/parent_based/config.cc +++ b/source/extensions/tracers/opentelemetry/samplers/parent_based/config.cc @@ -16,7 +16,7 @@ SamplerSharedPtr ParentBasedSamplerFactory::createSampler(const Protobuf::Message& config, Server::Configuration::TracerFactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(config), context.messageValidationVisitor(), *this); + dynamic_cast(config), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate< const envoy::extensions::tracers::opentelemetry::samplers::v3::ParentBasedSamplerConfig&>( *mptr, context.messageValidationVisitor()); diff --git a/source/extensions/tracers/opentelemetry/samplers/trace_id_ratio_based/config.cc b/source/extensions/tracers/opentelemetry/samplers/trace_id_ratio_based/config.cc index cbe15acf95d32..17f40da90e170 100644 --- a/source/extensions/tracers/opentelemetry/samplers/trace_id_ratio_based/config.cc +++ b/source/extensions/tracers/opentelemetry/samplers/trace_id_ratio_based/config.cc @@ -15,7 +15,7 @@ namespace OpenTelemetry { SamplerSharedPtr TraceIdRatioBasedSamplerFactory::createSampler( const Protobuf::Message& config, Server::Configuration::TracerFactoryContext& context) { auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(config), context.messageValidationVisitor(), *this); + dynamic_cast(config), context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate SpanContextExtractor::extractSpanContext() { } absl::string_view version = propagation_header_components[0]; absl::string_view trace_id = propagation_header_components[1]; - absl::string_view parent_id = propagation_header_components[2]; + absl::string_view span_id = propagation_header_components[2]; absl::string_view trace_flags = propagation_header_components[3]; if (version.size() != kVersionHexSize || trace_id.size() != kTraceIdHexSize || - parent_id.size() != kParentIdHexSize || trace_flags.size() != kTraceFlagsHexSize) { + span_id.size() != kParentIdHexSize || trace_flags.size() != kTraceFlagsHexSize) { return absl::InvalidArgumentError("Invalid traceparent field sizes"); } - if (!isValidHex(version) || !isValidHex(trace_id) || !isValidHex(parent_id) || + if (!isValidHex(version) || !isValidHex(trace_id) || !isValidHex(span_id) || !isValidHex(trace_flags)) { return absl::InvalidArgumentError("Invalid header hex"); } @@ -76,7 +76,7 @@ absl::StatusOr SpanContextExtractor::extractSpanContext() { if (isAllZeros(trace_id)) { return absl::InvalidArgumentError("Invalid trace id"); } - if (isAllZeros(parent_id)) { + if (isAllZeros(span_id)) { return absl::InvalidArgumentError("Invalid parent id"); } @@ -89,21 +89,11 @@ absl::StatusOr SpanContextExtractor::extractSpanContext() { // it is invalid and MUST be discarded. Because we're already checking for the // traceparent header above, we don't need to check here. // See https://www.w3.org/TR/trace-context/#processing-model-for-working-with-trace-context - absl::string_view tracestate_key = OpenTelemetryConstants::get().TRACE_STATE.key(); - std::vector tracestate_values; - // Multiple tracestate header fields MUST be handled as specified by RFC7230 Section 3.2.2 Field - // Order. - trace_context_.forEach( - [&tracestate_key, &tracestate_values](absl::string_view key, absl::string_view value) { - if (key == tracestate_key) { - tracestate_values.push_back(std::string{value}); - } - return true; - }); - std::string tracestate = absl::StrJoin(tracestate_values, ","); - - SpanContext span_context(version, trace_id, parent_id, sampled, tracestate); - return span_context; + const auto tracestate_values = OpenTelemetryConstants::get().TRACE_STATE.getAll(trace_context_); + + SpanContext parent_context(version, trace_id, span_id, sampled, + absl::StrJoin(tracestate_values, ",")); + return parent_context; } } // namespace OpenTelemetry diff --git a/source/extensions/tracers/opentelemetry/tracer.cc b/source/extensions/tracers/opentelemetry/tracer.cc index 9dd7e9a05dd40..9017f360e2fbe 100644 --- a/source/extensions/tracers/opentelemetry/tracer.cc +++ b/source/extensions/tracers/opentelemetry/tracer.cc @@ -60,8 +60,9 @@ void callSampler(SamplerSharedPtr sampler, const StreamInfo::StreamInfo& stream_ Span::Span(const std::string& name, const StreamInfo::StreamInfo& stream_info, SystemTime start_time, Envoy::TimeSource& time_source, Tracer& parent_tracer, - OTelSpanKind span_kind) - : stream_info_(stream_info), parent_tracer_(parent_tracer), time_source_(time_source) { + OTelSpanKind span_kind, bool use_local_decision) + : stream_info_(stream_info), parent_tracer_(parent_tracer), time_source_(time_source), + use_local_decision_(use_local_decision) { span_ = ::opentelemetry::proto::trace::v1::Span(); span_.set_kind(span_kind); @@ -73,7 +74,8 @@ Span::Span(const std::string& name, const StreamInfo::StreamInfo& stream_info, Tracing::SpanPtr Span::spawnChild(const Tracing::Config&, const std::string& name, SystemTime start_time) { // Build span_context from the current span, then generate the child span from that context. - SpanContext span_context(kDefaultVersion, getTraceId(), spanId(), sampled(), tracestate()); + SpanContext span_context(kDefaultVersion, getTraceId(), spanId(), sampled(), + std::string(tracestate())); return parent_tracer_.startSpan(name, stream_info_, start_time, span_context, {}, ::opentelemetry::proto::trace::v1::Span::SPAN_KIND_CLIENT); } @@ -269,47 +271,56 @@ Tracing::SpanPtr Tracer::startSpan(const std::string& operation_name, Tracing::Decision tracing_decision, OptRef trace_context, OTelSpanKind span_kind) { + // If reached here, then this is main span for request and there is no previous span context. + // If the custom sampler is set, then the Envoy tracing decision is ignored and the custom sampler + // should make a sampling decision, otherwise the local Envoy tracing decision is used. + const bool use_local_decision = sampler_ == nullptr; + // Create an Tracers::OpenTelemetry::Span class that will contain the OTel span. - Span new_span(operation_name, stream_info, start_time, time_source_, *this, span_kind); + auto new_span = std::make_unique(operation_name, stream_info, start_time, time_source_, + *this, span_kind, use_local_decision); uint64_t trace_id_high = random_.random(); uint64_t trace_id = random_.random(); - new_span.setTraceId(absl::StrCat(Hex::uint64ToHex(trace_id_high), Hex::uint64ToHex(trace_id))); + new_span->setTraceId(absl::StrCat(Hex::uint64ToHex(trace_id_high), Hex::uint64ToHex(trace_id))); uint64_t span_id = random_.random(); - new_span.setId(Hex::uint64ToHex(span_id)); + new_span->setId(Hex::uint64ToHex(span_id)); if (sampler_) { - callSampler(sampler_, stream_info, absl::nullopt, new_span, operation_name, trace_context); + callSampler(sampler_, stream_info, absl::nullopt, *new_span, operation_name, trace_context); } else { - new_span.setSampled(tracing_decision.traced); + new_span->setSampled(tracing_decision.traced); } - return std::make_unique(new_span); + return new_span; } Tracing::SpanPtr Tracer::startSpan(const std::string& operation_name, const StreamInfo::StreamInfo& stream_info, SystemTime start_time, - const SpanContext& previous_span_context, + const SpanContext& parent_context, OptRef trace_context, OTelSpanKind span_kind) { + // If reached here, then this is main span for request with a parent context or this is + // subsequent spans. Ignore the Envoy tracing decision anyway. + // Create a new span and populate details from the span context. - Span new_span(operation_name, stream_info, start_time, time_source_, *this, span_kind); - new_span.setTraceId(previous_span_context.traceId()); - if (!previous_span_context.parentId().empty()) { - new_span.setParentId(previous_span_context.parentId()); + auto new_span = std::make_unique(operation_name, stream_info, start_time, time_source_, + *this, span_kind, false); + new_span->setTraceId(parent_context.traceId()); + if (!parent_context.spanId().empty()) { + new_span->setParentId(parent_context.spanId()); } // Generate a new identifier for the span id. uint64_t span_id = random_.random(); - new_span.setId(Hex::uint64ToHex(span_id)); + new_span->setId(Hex::uint64ToHex(span_id)); if (sampler_) { // Sampler should make a sampling decision and set tracestate - callSampler(sampler_, stream_info, previous_span_context, new_span, operation_name, - trace_context); + callSampler(sampler_, stream_info, parent_context, *new_span, operation_name, trace_context); } else { // Respect the previous span's sampled flag. - new_span.setSampled(previous_span_context.sampled()); - if (!previous_span_context.tracestate().empty()) { - new_span.setTracestate(std::string{previous_span_context.tracestate()}); + new_span->setSampled(parent_context.sampled()); + if (!parent_context.tracestate().empty()) { + new_span->setTracestate(parent_context.tracestate()); } } - return std::make_unique(new_span); + return new_span; } } // namespace OpenTelemetry diff --git a/source/extensions/tracers/opentelemetry/tracer.h b/source/extensions/tracers/opentelemetry/tracer.h index 11ae574f9e32b..1f6e10ccf63ba 100644 --- a/source/extensions/tracers/opentelemetry/tracer.h +++ b/source/extensions/tracers/opentelemetry/tracer.h @@ -85,7 +85,8 @@ class Tracer : Logger::Loggable { class Span : Logger::Loggable, public Tracing::Span { public: Span(const std::string& name, const StreamInfo::StreamInfo& stream_info, SystemTime start_time, - Envoy::TimeSource& time_source, Tracer& parent_tracer, OTelSpanKind span_kind); + Envoy::TimeSource& time_source, Tracer& parent_tracer, OTelSpanKind span_kind, + bool use_local_decision = false); // Tracing::Span functions void setOperation(absl::string_view /*operation*/) override; @@ -103,9 +104,13 @@ class Span : Logger::Loggable, public Tracing::Span { void setSampled(bool sampled) override { sampled_ = sampled; }; /** - * @return whether or not the sampled attribute is set + * @return whether the local tracing decision is used by the span. */ + bool useLocalDecision() const override { return use_local_decision_; } + /** + * @return whether or not the sampled attribute is set + */ bool sampled() const { return sampled_; } std::string getBaggage(absl::string_view /*key*/) override { return EMPTY_STRING; }; @@ -116,7 +121,7 @@ class Span : Logger::Loggable, public Tracing::Span { /** * Sets the span's trace id attribute. */ - void setTraceId(const absl::string_view& trace_id_hex) { + void setTraceId(absl::string_view trace_id_hex) { span_.set_trace_id(absl::HexStringToBytes(trace_id_hex)); } @@ -129,12 +134,12 @@ class Span : Logger::Loggable, public Tracing::Span { /** * @return the operation name set on the span */ - std::string name() const { return span_.name(); } + absl::string_view name() const { return span_.name(); } /** * Sets the span's id. */ - void setId(const absl::string_view& span_id_hex) { + void setId(absl::string_view span_id_hex) { span_.set_span_id(absl::HexStringToBytes(span_id_hex)); } @@ -143,16 +148,16 @@ class Span : Logger::Loggable, public Tracing::Span { /** * Sets the span's parent id. */ - void setParentId(const absl::string_view& parent_span_id_hex) { + void setParentId(absl::string_view parent_span_id_hex) { span_.set_parent_span_id(absl::HexStringToBytes(parent_span_id_hex)); } - std::string tracestate() const { return span_.trace_state(); } + absl::string_view tracestate() const { return span_.trace_state(); } /** * Sets the span's tracestate. */ - void setTracestate(const absl::string_view& tracestate) { + void setTracestate(absl::string_view tracestate) { span_.set_trace_state(std::string{tracestate}); } @@ -172,6 +177,7 @@ class Span : Logger::Loggable, public Tracing::Span { Tracer& parent_tracer_; Envoy::TimeSource& time_source_; bool sampled_; + const bool use_local_decision_{false}; }; using TracerPtr = std::unique_ptr; diff --git a/source/extensions/tracers/skywalking/tracer.h b/source/extensions/tracers/skywalking/tracer.h index 32b948f4eabe5..ec046f9d386f0 100644 --- a/source/extensions/tracers/skywalking/tracer.h +++ b/source/extensions/tracers/skywalking/tracer.h @@ -86,6 +86,10 @@ class Span : public Tracing::Span { Tracing::SpanPtr spawnChild(const Tracing::Config& config, const std::string& name, SystemTime start_time) override; void setSampled(bool do_sample) override; + // TODO(wbpcode): The SkyWalking tracer may create NullSpanImpl if the tracing decision is not to + // trace. That make it is impossible to update the sampling decision. So, the useLocalDecision() + // always return false now. This should be resolved in the future. + bool useLocalDecision() const override { return false; } std::string getBaggage(absl::string_view) override { return EMPTY_STRING; } void setBaggage(absl::string_view, absl::string_view) override {} std::string getTraceId() const override { return tracing_context_->traceId(); } diff --git a/source/extensions/tracers/xray/config.cc b/source/extensions/tracers/xray/config.cc index 805a900a1f3d6..93746a158d292 100644 --- a/source/extensions/tracers/xray/config.cc +++ b/source/extensions/tracers/xray/config.cc @@ -44,7 +44,7 @@ XRayTracerFactory::createTracerDriverTyped(const envoy::config::trace::v3::XRayC const std::string endpoint = fmt::format("{}:{}", proto_config.daemon_endpoint().address(), proto_config.daemon_endpoint().port_value()); - auto aws = absl::flat_hash_map{}; + auto aws = absl::flat_hash_map{}; for (const auto& field : proto_config.segment_fields().aws().fields()) { aws.emplace(field.first, field.second); } diff --git a/source/extensions/tracers/xray/localized_sampling.cc b/source/extensions/tracers/xray/localized_sampling.cc index 5126545b1736a..a9b5ec7b93d5a 100644 --- a/source/extensions/tracers/xray/localized_sampling.cc +++ b/source/extensions/tracers/xray/localized_sampling.cc @@ -34,8 +34,8 @@ void fail(absl::string_view msg) { bool isValidRate(double n) { return n >= 0 && n <= 1.0; } bool isValidFixedTarget(double n) { return n >= 0 && static_cast(n) == n; } -bool validateRule(const ProtobufWkt::Struct& rule) { - using ProtobufWkt::Value; +bool validateRule(const Protobuf::Struct& rule) { + using Protobuf::Value; const auto host_it = rule.fields().find(HostJsonKey); if (host_it != rule.fields().end() && @@ -93,7 +93,7 @@ LocalizedSamplingManifest::LocalizedSamplingManifest(const std::string& rule_jso return; } - ProtobufWkt::Struct document; + Protobuf::Struct document; TRY_NEEDS_AUDIT { MessageUtil::loadFromJson(rule_json, document); } END_TRY catch (EnvoyException& e) { fail("invalid JSON format"); @@ -106,7 +106,7 @@ LocalizedSamplingManifest::LocalizedSamplingManifest(const std::string& rule_jso return; } - if (version_it->second.kind_case() != ProtobufWkt::Value::KindCase::kNumberValue || + if (version_it->second.kind_case() != Protobuf::Value::KindCase::kNumberValue || version_it->second.number_value() != SamplingFileVersion) { fail("wrong version number"); return; @@ -114,7 +114,7 @@ LocalizedSamplingManifest::LocalizedSamplingManifest(const std::string& rule_jso const auto default_rule_it = document.fields().find(DefaultRuleJsonKey); if (default_rule_it == document.fields().end() || - default_rule_it->second.kind_case() != ProtobufWkt::Value::KindCase::kStructValue) { + default_rule_it->second.kind_case() != Protobuf::Value::KindCase::kStructValue) { fail("missing default rule"); return; } @@ -134,13 +134,13 @@ LocalizedSamplingManifest::LocalizedSamplingManifest(const std::string& rule_jso return; } - if (custom_rules_it->second.kind_case() != ProtobufWkt::Value::KindCase::kListValue) { + if (custom_rules_it->second.kind_case() != Protobuf::Value::KindCase::kListValue) { fail("rules must be JSON array"); return; } for (auto& el : custom_rules_it->second.list_value().values()) { - if (el.kind_case() != ProtobufWkt::Value::KindCase::kStructValue) { + if (el.kind_case() != Protobuf::Value::KindCase::kStructValue) { fail("rules array must be objects"); return; } diff --git a/source/extensions/tracers/xray/tracer.h b/source/extensions/tracers/xray/tracer.h index 6d21b5999a94c..5708ab98b59f0 100644 --- a/source/extensions/tracers/xray/tracer.h +++ b/source/extensions/tracers/xray/tracer.h @@ -115,14 +115,14 @@ class Span : public Tracing::Span, Logger::Loggable { /** * Sets the aws metadata field of the Span. */ - void setAwsMetadata(const absl::flat_hash_map& aws_metadata) { + void setAwsMetadata(const absl::flat_hash_map& aws_metadata) { aws_metadata_ = aws_metadata; } /* * Adds to the http request annotation field of the Span. */ - void addToHttpRequestAnnotations(absl::string_view key, const ProtobufWkt::Value& value) { + void addToHttpRequestAnnotations(absl::string_view key, const Protobuf::Value& value) { http_request_annotations_.emplace(std::string(key), value); } @@ -136,7 +136,7 @@ class Span : public Tracing::Span, Logger::Loggable { /* * Adds to the http response annotation field of the Span. */ - void addToHttpResponseAnnotations(absl::string_view key, const ProtobufWkt::Value& value) { + void addToHttpResponseAnnotations(absl::string_view key, const Protobuf::Value& value) { http_response_annotations_.emplace(std::string(key), value); } @@ -153,6 +153,9 @@ class Span : public Tracing::Span, Logger::Loggable { */ void setSampled(bool sampled) override { sampled_ = sampled; }; + // X-Ray tracer does not use the sampling decision from Envoy anyway. + bool useLocalDecision() const override { return false; } + /** * Sets the server error as true for the traced operation/request. */ @@ -257,9 +260,9 @@ class Span : public Tracing::Span, Logger::Loggable { std::string name_; std::string origin_; std::string type_; - absl::flat_hash_map aws_metadata_; - absl::flat_hash_map http_request_annotations_; - absl::flat_hash_map http_response_annotations_; + absl::flat_hash_map aws_metadata_; + absl::flat_hash_map http_request_annotations_; + absl::flat_hash_map http_response_annotations_; absl::flat_hash_map custom_annotations_; bool server_error_{false}; uint64_t response_status_code_{0}; @@ -271,7 +274,7 @@ using SpanPtr = std::unique_ptr; class Tracer { public: Tracer(absl::string_view segment_name, absl::string_view origin, - const absl::flat_hash_map& aws_metadata, + const absl::flat_hash_map& aws_metadata, DaemonBrokerPtr daemon_broker, TimeSource& time_source, Random::RandomGenerator& random) : segment_name_(segment_name), origin_(origin), aws_metadata_(aws_metadata), daemon_broker_(std::move(daemon_broker)), time_source_(time_source), random_(random) {} @@ -293,7 +296,7 @@ class Tracer { private: const std::string segment_name_; const std::string origin_; - const absl::flat_hash_map aws_metadata_; + const absl::flat_hash_map aws_metadata_; const DaemonBrokerPtr daemon_broker_; Envoy::TimeSource& time_source_; Random::RandomGenerator& random_; diff --git a/source/extensions/tracers/xray/xray_configuration.h b/source/extensions/tracers/xray/xray_configuration.h index d5a9837f5dc9d..70e855fce0a27 100644 --- a/source/extensions/tracers/xray/xray_configuration.h +++ b/source/extensions/tracers/xray/xray_configuration.h @@ -18,7 +18,7 @@ struct XRayConfiguration { const std::string segment_name_; const std::string sampling_rules_; const std::string origin_; - const absl::flat_hash_map aws_metadata_; + const absl::flat_hash_map aws_metadata_; }; enum class SamplingDecision { diff --git a/source/extensions/tracers/zipkin/BUILD b/source/extensions/tracers/zipkin/BUILD index 171080ecb766c..8604a416d1466 100644 --- a/source/extensions/tracers/zipkin/BUILD +++ b/source/extensions/tracers/zipkin/BUILD @@ -15,7 +15,6 @@ envoy_cc_library( name = "zipkin_lib", srcs = [ "span_buffer.cc", - "span_context.cc", "span_context_extractor.cc", "tracer.cc", "util.cc", @@ -55,6 +54,7 @@ envoy_cc_library( "//source/common/singleton:const_singleton", "//source/common/tracing:http_tracer_lib", "//source/common/upstream:cluster_update_tracker_lib", + "//source/extensions/tracers/opentelemetry:opentelemetry_tracer_lib", "@com_github_openzipkin_zipkinapi//:zipkin_cc_proto", "@com_google_absl//absl/types:optional", "@envoy_api//envoy/config/trace/v3:pkg_cc_proto", diff --git a/source/extensions/tracers/zipkin/config.cc b/source/extensions/tracers/zipkin/config.cc index fda262d91f1bb..26493c3670234 100644 --- a/source/extensions/tracers/zipkin/config.cc +++ b/source/extensions/tracers/zipkin/config.cc @@ -17,12 +17,7 @@ ZipkinTracerFactory::ZipkinTracerFactory() : FactoryBase("envoy.tracers.zipkin") Tracing::DriverSharedPtr ZipkinTracerFactory::createTracerDriverTyped( const envoy::config::trace::v3::ZipkinConfig& proto_config, Server::Configuration::TracerFactoryContext& context) { - return std::make_shared( - proto_config, context.serverFactoryContext().clusterManager(), - context.serverFactoryContext().scope(), context.serverFactoryContext().threadLocal(), - context.serverFactoryContext().runtime(), context.serverFactoryContext().localInfo(), - context.serverFactoryContext().api().randomGenerator(), - context.serverFactoryContext().timeSource()); + return std::make_shared(proto_config, context.serverFactoryContext()); } /** diff --git a/source/extensions/tracers/zipkin/span_buffer.cc b/source/extensions/tracers/zipkin/span_buffer.cc index 818ba0681908c..91ca412aaae72 100644 --- a/source/extensions/tracers/zipkin/span_buffer.cc +++ b/source/extensions/tracers/zipkin/span_buffer.cc @@ -74,7 +74,7 @@ std::string JsonV2Serializer::serialize(const std::vector& zipkin_spans) { absl::StrAppend( out, absl::StrJoin( toListOfSpans(zipkin_span, replacements), ",", - [&replacement_values](std::string* element, const ProtobufWkt::Struct& span) { + [&replacement_values](std::string* element, const Protobuf::Struct& span) { absl::StatusOr json_or_error = MessageUtil::getJsonStringFromMessage(span, false, true); ENVOY_BUG(json_or_error.ok(), "Failed to parse json"); @@ -108,16 +108,16 @@ std::string JsonV2Serializer::serialize(const std::vector& zipkin_spans) { return absl::StrCat("[", serialized_elements, "]"); } -const std::vector +const std::vector JsonV2Serializer::toListOfSpans(const Span& zipkin_span, Util::Replacements& replacements) const { - std::vector spans; + std::vector spans; spans.reserve(zipkin_span.annotations().size()); // This holds the annotation entries from logs. - std::vector annotation_entries; + std::vector annotation_entries; for (const auto& annotation : zipkin_span.annotations()) { - ProtobufWkt::Struct span; + Protobuf::Struct span; auto* fields = span.mutable_fields(); if (annotation.value() == CLIENT_SEND) { (*fields)[SPAN_KIND] = ValueUtil::stringValue(KIND_CLIENT); @@ -127,7 +127,7 @@ JsonV2Serializer::toListOfSpans(const Span& zipkin_span, Util::Replacements& rep } (*fields)[SPAN_KIND] = ValueUtil::stringValue(KIND_SERVER); } else { - ProtobufWkt::Struct annotation_entry; + Protobuf::Struct annotation_entry; auto* annotation_entry_fields = annotation_entry.mutable_fields(); (*annotation_entry_fields)[ANNOTATION_VALUE] = ValueUtil::stringValue(annotation.value()); (*annotation_entry_fields)[ANNOTATION_TIMESTAMP] = @@ -137,7 +137,7 @@ JsonV2Serializer::toListOfSpans(const Span& zipkin_span, Util::Replacements& rep } if (annotation.isSetEndpoint()) { - // Usually we store number to a ProtobufWkt::Struct object via ValueUtil::numberValue. + // Usually we store number to a Protobuf::Struct object via ValueUtil::numberValue. // However, due to the possibility of rendering that to a number with scientific notation, we // chose to store it as a string and keeping track the corresponding replacement. For example, // we have 1584324295476870 if we stored it as a double value, MessageToJsonString gives @@ -171,7 +171,7 @@ JsonV2Serializer::toListOfSpans(const Span& zipkin_span, Util::Replacements& rep const auto& binary_annotations = zipkin_span.binaryAnnotations(); if (!binary_annotations.empty()) { - ProtobufWkt::Struct tags; + Protobuf::Struct tags; auto* tag_fields = tags.mutable_fields(); for (const auto& binary_annotation : binary_annotations) { (*tag_fields)[binary_annotation.key()] = ValueUtil::stringValue(binary_annotation.value()); @@ -193,8 +193,8 @@ JsonV2Serializer::toListOfSpans(const Span& zipkin_span, Util::Replacements& rep return spans; } -const ProtobufWkt::Struct JsonV2Serializer::toProtoEndpoint(const Endpoint& zipkin_endpoint) const { - ProtobufWkt::Struct endpoint; +const Protobuf::Struct JsonV2Serializer::toProtoEndpoint(const Endpoint& zipkin_endpoint) const { + Protobuf::Struct endpoint; auto* fields = endpoint.mutable_fields(); Network::Address::InstanceConstSharedPtr address = zipkin_endpoint.address(); diff --git a/source/extensions/tracers/zipkin/span_buffer.h b/source/extensions/tracers/zipkin/span_buffer.h index df1536044367f..1304b7cd457cd 100644 --- a/source/extensions/tracers/zipkin/span_buffer.h +++ b/source/extensions/tracers/zipkin/span_buffer.h @@ -3,7 +3,6 @@ #include "envoy/config/trace/v3/zipkin.pb.h" #include "source/common/protobuf/protobuf.h" -#include "source/extensions/tracers/zipkin/tracer_interface.h" #include "source/extensions/tracers/zipkin/zipkin_core_types.h" #include "zipkin.pb.h" @@ -107,9 +106,9 @@ class JsonV2Serializer : public Serializer { std::string serialize(const std::vector& pending_spans) override; private: - const std::vector toListOfSpans(const Span& zipkin_span, - Util::Replacements& replacements) const; - const ProtobufWkt::Struct toProtoEndpoint(const Endpoint& zipkin_endpoint) const; + const std::vector toListOfSpans(const Span& zipkin_span, + Util::Replacements& replacements) const; + const Protobuf::Struct toProtoEndpoint(const Endpoint& zipkin_endpoint) const; const bool shared_span_context_; }; diff --git a/source/extensions/tracers/zipkin/span_context.cc b/source/extensions/tracers/zipkin/span_context.cc deleted file mode 100644 index ecbd14b5c8bb2..0000000000000 --- a/source/extensions/tracers/zipkin/span_context.cc +++ /dev/null @@ -1,20 +0,0 @@ -#include "source/extensions/tracers/zipkin/span_context.h" - -#include "source/common/common/macros.h" -#include "source/common/common/utility.h" -#include "source/extensions/tracers/zipkin/zipkin_core_constants.h" - -namespace Envoy { -namespace Extensions { -namespace Tracers { -namespace Zipkin { - -SpanContext::SpanContext(const Span& span, bool inner_context) - : trace_id_high_(span.isSetTraceIdHigh() ? span.traceIdHigh() : 0), trace_id_(span.traceId()), - id_(span.id()), parent_id_(span.isSetParentId() ? span.parentId() : 0), - sampled_(span.sampled()), inner_context_(inner_context) {} - -} // namespace Zipkin -} // namespace Tracers -} // namespace Extensions -} // namespace Envoy diff --git a/source/extensions/tracers/zipkin/span_context.h b/source/extensions/tracers/zipkin/span_context.h index b1dfa3168743a..e4098e334cf63 100644 --- a/source/extensions/tracers/zipkin/span_context.h +++ b/source/extensions/tracers/zipkin/span_context.h @@ -1,10 +1,7 @@ #pragma once -#include - -#include "source/extensions/tracers/zipkin/util.h" -#include "source/extensions/tracers/zipkin/zipkin_core_constants.h" -#include "source/extensions/tracers/zipkin/zipkin_core_types.h" +#include +#include namespace Envoy { namespace Extensions { @@ -29,7 +26,7 @@ class SpanContext { * @param trace_id_high The high 64 bits of the trace id. * @param trace_id The low 64 bits of the trace id. * @param id The span id. - * @param parent_id The parent id. + * @param parent_id The parent span id. * @param sampled The sampled flag. * @param inner_context If this context is created base on the inner span. */ @@ -38,14 +35,6 @@ class SpanContext { : trace_id_high_(trace_id_high), trace_id_(trace_id), id_(id), parent_id_(parent_id), sampled_(sampled), inner_context_(inner_context) {} - /** - * Constructor that creates a context object from the given Zipkin span object. - * - * @param span The Zipkin span used to initialize a SpanContext object. - * @param inner_context If this context is created base on the inner span. - */ - SpanContext(const Span& span, bool inner_context = true); - /** * @return the span id as an integer */ @@ -80,6 +69,7 @@ class SpanContext { * @return the inner context flag. True if this context is created base on the inner span. */ bool innerContext() const { return inner_context_; } + void setInnerContextForTest(bool inner_context) { inner_context_ = inner_context; } private: const uint64_t trace_id_high_{0}; @@ -87,7 +77,7 @@ class SpanContext { const uint64_t id_{0}; const uint64_t parent_id_{0}; const bool sampled_{false}; - const bool inner_context_{false}; + bool inner_context_{false}; }; } // namespace Zipkin diff --git a/source/extensions/tracers/zipkin/span_context_extractor.cc b/source/extensions/tracers/zipkin/span_context_extractor.cc index 1e6c1f154258f..c02ec29cc11e2 100644 --- a/source/extensions/tracers/zipkin/span_context_extractor.cc +++ b/source/extensions/tracers/zipkin/span_context_extractor.cc @@ -1,5 +1,7 @@ #include "source/extensions/tracers/zipkin/span_context_extractor.h" +#include + #include "source/common/common/assert.h" #include "source/common/common/utility.h" #include "source/extensions/tracers/zipkin/span_context.h" @@ -11,6 +13,7 @@ namespace Tracers { namespace Zipkin { namespace { constexpr int FormatMaxLength = 32 + 1 + 16 + 3 + 16; // traceid128-spanid-1-parentid + bool validSamplingFlags(char c) { if (c == '1' || c == '0' || c == 'd') { return true; @@ -18,23 +21,33 @@ bool validSamplingFlags(char c) { return false; } -bool getSamplingFlags(char c, const Tracing::Decision tracing_decision) { +absl::optional getSamplingFlags(char c) { if (validSamplingFlags(c)) { return c == '0' ? false : true; } else { - return tracing_decision.traced; + return absl::nullopt; } } +// Helper function to parse hex string_view to uint64_t using std::from_chars +bool parseHexStringView(absl::string_view hex_str, uint64_t& result) { + const char* begin = hex_str.data(); + const char* end = begin + hex_str.size(); + auto [ptr, ec] = std::from_chars(begin, end, result, 16); + return ec == std::errc{} && ptr == end; +} + } // namespace -SpanContextExtractor::SpanContextExtractor(Tracing::TraceContext& trace_context) - : trace_context_(trace_context) {} +SpanContextExtractor::SpanContextExtractor(Tracing::TraceContext& trace_context, + bool w3c_fallback_enabled) + : trace_context_(trace_context), w3c_fallback_enabled_(w3c_fallback_enabled) {} SpanContextExtractor::~SpanContextExtractor() = default; -bool SpanContextExtractor::extractSampled(const Tracing::Decision tracing_decision) { +absl::optional SpanContextExtractor::extractSampled() { bool sampled(false); + // Try B3 single format first. auto b3_header_entry = ZipkinCoreConstants::get().B3.get(trace_context_); if (b3_header_entry.has_value()) { // This is an implicitly untrusted header, so only the first value is used. @@ -56,35 +69,53 @@ bool SpanContextExtractor::extractSampled(const Tracing::Decision tracing_decisi sampled_pos = 50; break; default: - return tracing_decision.traced; + return absl::nullopt; // invalid length } - return getSamplingFlags(b3[sampled_pos], tracing_decision); + return getSamplingFlags(b3[sampled_pos]); } + // Try individual B3 sampled header. auto x_b3_sampled_entry = ZipkinCoreConstants::get().X_B3_SAMPLED.get(trace_context_); - if (!x_b3_sampled_entry.has_value()) { - return tracing_decision.traced; + + if (x_b3_sampled_entry.has_value()) { + // Checking if sampled flag has been specified. Also checking for 'true' value, as some old + // zipkin tracers may still use that value, although should be 0 or 1. + // This is an implicitly untrusted header, so only the first value is used. + absl::string_view xb3_sampled = x_b3_sampled_entry.value(); + sampled = xb3_sampled == SAMPLED || xb3_sampled == "true"; + return sampled; } - // Checking if sampled flag has been specified. Also checking for 'true' value, as some old - // zipkin tracers may still use that value, although should be 0 or 1. - // This is an implicitly untrusted header, so only the first value is used. - absl::string_view xb3_sampled = x_b3_sampled_entry.value(); - sampled = xb3_sampled == SAMPLED || xb3_sampled == "true"; - return sampled; + + // Try W3C Trace Context format as fallback only if enabled. + if (w3c_fallback_enabled_) { + Extensions::Tracers::OpenTelemetry::SpanContextExtractor w3c_extractor( + const_cast(trace_context_)); + if (w3c_extractor.propagationHeaderPresent()) { + auto w3c_span_context = w3c_extractor.extractSpanContext(); + if (w3c_span_context.ok()) { + return w3c_span_context.value().sampled(); + } + } + } + + return absl::nullopt; } std::pair SpanContextExtractor::extractSpanContext(bool is_sampled) { + // Try B3 single format first. if (ZipkinCoreConstants::get().B3.get(trace_context_).has_value()) { return extractSpanContextFromB3SingleFormat(is_sampled); } - uint64_t trace_id(0); - uint64_t trace_id_high(0); - uint64_t span_id(0); - uint64_t parent_id(0); + // Try individual B3 headers. auto b3_trace_id_entry = ZipkinCoreConstants::get().X_B3_TRACE_ID.get(trace_context_); auto b3_span_id_entry = ZipkinCoreConstants::get().X_B3_SPAN_ID.get(trace_context_); if (b3_span_id_entry.has_value() && b3_trace_id_entry.has_value()) { + uint64_t trace_id(0); + uint64_t trace_id_high(0); + uint64_t span_id(0); + uint64_t parent_id(0); + // Extract trace id - which can either be 128 or 64 bit. For 128 bit, // it needs to be divided into two 64 bit numbers (high and low). // This is an implicitly untrusted header, so only the first value is used. @@ -115,11 +146,23 @@ std::pair SpanContextExtractor::extractSpanContext(bool is_sa throw ExtractorException(absl::StrCat("Invalid parent span id ", pspid.c_str())); } } - } else { - return {SpanContext(), false}; + + return {SpanContext(trace_id_high, trace_id, span_id, parent_id, is_sampled), true}; } - return {SpanContext(trace_id_high, trace_id, span_id, parent_id, is_sampled), true}; + // Try W3C Trace Context format as fallback only if enabled. + if (w3c_fallback_enabled_) { + Extensions::Tracers::OpenTelemetry::SpanContextExtractor w3c_extractor( + const_cast(trace_context_)); + if (w3c_extractor.propagationHeaderPresent()) { + auto w3c_span_context = w3c_extractor.extractSpanContext(); + if (w3c_span_context.ok()) { + return convertW3CToZipkin(w3c_span_context.value(), is_sampled); + } + } + } + + return {SpanContext(), false}; } std::pair @@ -207,7 +250,7 @@ SpanContextExtractor::extractSpanContextFromB3SingleFormat(bool is_sampled) { } if (b3.length() > pos) { - // If we are at this point, we should have a parent ID, encoded as "-[0-9a-f]{16}" + // If we are at this point, we should have a parent ID, encoded as "-[0-9a-f]{16}". if (b3.length() != pos + 17) { throw ExtractorException("Invalid input: truncated"); } @@ -226,6 +269,47 @@ SpanContextExtractor::extractSpanContextFromB3SingleFormat(bool is_sampled) { return {SpanContext(trace_id_high, trace_id, span_id, parent_id, is_sampled), true}; } +std::pair SpanContextExtractor::convertW3CToZipkin( + const Extensions::Tracers::OpenTelemetry::SpanContext& w3c_context, bool fallback_sampled) { + // Convert W3C 128-bit trace ID (32 hex chars) to Zipkin format. + const absl::string_view trace_id_str = w3c_context.traceId(); + + if (trace_id_str.length() != 32) { + throw ExtractorException(fmt::format("Invalid W3C trace ID length: {}", trace_id_str.length())); + } + + // Split 128-bit trace ID into high and low 64-bit parts for Zipkin. + const absl::string_view trace_id_high_str = absl::string_view(trace_id_str).substr(0, 16); + const absl::string_view trace_id_low_str = absl::string_view(trace_id_str).substr(16, 16); + + uint64_t trace_id_high(0); + uint64_t trace_id(0); + if (!parseHexStringView(trace_id_high_str, trace_id_high) || + !parseHexStringView(trace_id_low_str, trace_id)) { + throw ExtractorException(fmt::format("Invalid W3C trace ID: {}", trace_id_str)); + } + + // Convert W3C span ID (16 hex chars) to Zipkin span ID. + const absl::string_view span_id_str = w3c_context.spanId(); + if (span_id_str.length() != 16) { + throw ExtractorException(fmt::format("Invalid W3C span ID length: {}", span_id_str.length())); + } + + uint64_t span_id(0); + if (!parseHexStringView(span_id_str, span_id)) { + throw ExtractorException(fmt::format("Invalid W3C span ID: {}", span_id_str)); + } + + // W3C doesn't have a direct parent span concept like B3 + // The W3C span-id becomes our span-id, and we don't set a parent. + uint64_t parent_id(0); + + // Use W3C sampling decision, or fallback if not specified. + bool sampled = w3c_context.sampled() || fallback_sampled; + + return {SpanContext(trace_id_high, trace_id, span_id, parent_id, sampled), true}; +} + } // namespace Zipkin } // namespace Tracers } // namespace Extensions diff --git a/source/extensions/tracers/zipkin/span_context_extractor.h b/source/extensions/tracers/zipkin/span_context_extractor.h index 1cc1f2fe1fd9d..a1df976fac505 100644 --- a/source/extensions/tracers/zipkin/span_context_extractor.h +++ b/source/extensions/tracers/zipkin/span_context_extractor.h @@ -4,6 +4,7 @@ #include "envoy/tracing/tracer.h" #include "source/common/http/header_map_impl.h" +#include "source/extensions/tracers/opentelemetry/span_context_extractor.h" namespace Envoy { namespace Extensions { @@ -21,9 +22,9 @@ struct ExtractorException : public EnvoyException { */ class SpanContextExtractor { public: - SpanContextExtractor(Tracing::TraceContext& trace_context); + SpanContextExtractor(Tracing::TraceContext& trace_context, bool w3c_fallback_enabled = false); ~SpanContextExtractor(); - bool extractSampled(const Tracing::Decision tracing_decision); + absl::optional extractSampled(); std::pair extractSpanContext(bool is_sampled); private: @@ -33,8 +34,17 @@ class SpanContextExtractor { * See: "https://github.com/openzipkin/b3-propagation */ std::pair extractSpanContextFromB3SingleFormat(bool is_sampled); + + /* + * Convert W3C span context to Zipkin span context format + */ + std::pair + convertW3CToZipkin(const Extensions::Tracers::OpenTelemetry::SpanContext& w3c_context, + bool fallback_sampled); + bool tryExtractSampledFromB3SingleFormat(); const Tracing::TraceContext& trace_context_; + bool w3c_fallback_enabled_; }; } // namespace Zipkin diff --git a/source/extensions/tracers/zipkin/tracer.cc b/source/extensions/tracers/zipkin/tracer.cc index 0ab5b01273bc7..e50283fc66a1a 100644 --- a/source/extensions/tracers/zipkin/tracer.cc +++ b/source/extensions/tracers/zipkin/tracer.cc @@ -6,6 +6,7 @@ #include "source/common/tracing/http_tracer_impl.h" #include "source/extensions/tracers/zipkin/util.h" #include "source/extensions/tracers/zipkin/zipkin_core_constants.h" +#include "source/extensions/tracers/zipkin/zipkin_json_field_names.h" namespace Envoy { namespace Extensions { @@ -53,7 +54,7 @@ SpanPtr Tracer::startSpan(const Tracing::Config& config, const std::string& span cs.setEndpoint(std::move(ep)); // Create an all-new span, with no parent id - SpanPtr span_ptr = std::make_unique(time_source_); + SpanPtr span_ptr = std::make_unique(time_source_, *this); span_ptr->setName(span_name); uint64_t random_number = random_generator_.random(); span_ptr->setId(random_number); @@ -75,14 +76,12 @@ SpanPtr Tracer::startSpan(const Tracing::Config& config, const std::string& span // Add CS annotation to the span span_ptr->addAnnotation(std::move(cs)); - span_ptr->setTracer(this); - return span_ptr; } SpanPtr Tracer::startSpan(const Tracing::Config& config, const std::string& span_name, SystemTime timestamp, const SpanContext& previous_context) { - SpanPtr span_ptr = std::make_unique(time_source_); + SpanPtr span_ptr = std::make_unique(time_source_, *this); // If the previous context is inner context then this span is span for upstream request. Annotation annotation = getAnnotation(split_spans_for_request_ || config.spawnUpstreamSpan(), previous_context.innerContext(), config.operationName()); @@ -140,8 +139,6 @@ SpanPtr Tracer::startSpan(const Tracing::Config& config, const std::string& span .count(); span_ptr->setStartTime(start_time_micro); - span_ptr->setTracer(this); - return span_ptr; } diff --git a/source/extensions/tracers/zipkin/tracer.h b/source/extensions/tracers/zipkin/tracer.h index 6b8fa780014dd..6d038df9f84a3 100644 --- a/source/extensions/tracers/zipkin/tracer.h +++ b/source/extensions/tracers/zipkin/tracer.h @@ -1,13 +1,10 @@ #pragma once -#include "envoy/common/pure.h" #include "envoy/common/random_generator.h" #include "envoy/common/time.h" -#include "envoy/tracing/tracer.h" +#include "envoy/config/trace/v3/zipkin.pb.h" #include "source/extensions/tracers/zipkin/span_context.h" -#include "source/extensions/tracers/zipkin/tracer_interface.h" -#include "source/extensions/tracers/zipkin/zipkin_core_constants.h" #include "source/extensions/tracers/zipkin/zipkin_core_types.h" namespace Envoy { @@ -15,29 +12,6 @@ namespace Extensions { namespace Tracers { namespace Zipkin { -/** - * Abstract class that delegates to users of the Tracer class the responsibility - * of "reporting" a Zipkin span that has ended its life cycle. "Reporting" can mean that the - * span will be sent to out to Zipkin, or buffered so that it can be sent out later. - */ -class Reporter { -public: - /** - * Destructor. - */ - virtual ~Reporter() = default; - - /** - * Method that a concrete Reporter class must implement to handle finished spans. - * For example, a span-buffer management policy could be implemented. - * - * @param span The span that needs action. - */ - virtual void reportSpan(Span&& span) PURE; -}; - -using ReporterPtr = std::unique_ptr; - /** * This class implements the Zipkin tracer. It has methods to create the appropriate Zipkin span * type, i.e., root span, child span, or shared-context span. @@ -69,32 +43,24 @@ class Tracer : public TracerInterface { split_spans_for_request_(split_spans_for_request) {} /** - * Creates a "root" Zipkin span. - * - * @param config The tracing configuration - * @param span_name Name of the new span. - * @param start_time The time indicating the beginning of the span. - * @return SpanPtr The root span. + * Sets the trace context option for header injection behavior. + * @param trace_context_option The trace context option from ZipkinConfig. */ - SpanPtr startSpan(const Tracing::Config&, const std::string& span_name, SystemTime timestamp); + void setTraceContextOption(TraceContextOption trace_context_option) { + trace_context_option_ = trace_context_option; + } /** - * Depending on the given context, creates either a "child" or a "shared-context" Zipkin span. - * - * @param config The tracing configuration - * @param span_name Name of the new span. - * @param start_time The time indicating the beginning of the span. - * @param previous_context The context of the span preceding the one to be created. - * @return SpanPtr The child span. + * Gets the current trace context option. + * @return The current trace context option. */ - SpanPtr startSpan(const Tracing::Config&, const std::string& span_name, SystemTime timestamp, - const SpanContext& previous_context); + TraceContextOption traceContextOption() const override { return trace_context_option_; } - /** - * TracerInterface::reportSpan. - * - * @param span The span to be reported. - */ + // TracerInterface + SpanPtr startSpan(const Tracing::Config&, const std::string& span_name, + SystemTime timestamp) override; + SpanPtr startSpan(const Tracing::Config&, const std::string& span_name, SystemTime timestamp, + const SpanContext& previous_context) override; void reportSpan(Span&& span) override; /** @@ -113,6 +79,7 @@ class Tracer : public TracerInterface { const bool shared_span_context_; TimeSource& time_source_; const bool split_spans_for_request_{}; + TraceContextOption trace_context_option_{envoy::config::trace::v3::ZipkinConfig::USE_B3}; }; using TracerPtr = std::unique_ptr; diff --git a/source/extensions/tracers/zipkin/tracer_interface.h b/source/extensions/tracers/zipkin/tracer_interface.h index c56e130e2868d..fc8ad623c985b 100644 --- a/source/extensions/tracers/zipkin/tracer_interface.h +++ b/source/extensions/tracers/zipkin/tracer_interface.h @@ -5,28 +5,36 @@ #include #include "envoy/common/pure.h" +#include "envoy/config/trace/v3/zipkin.pb.h" +#include "envoy/tracing/trace_config.h" + +#include "source/extensions/tracers/zipkin/span_context.h" namespace Envoy { namespace Extensions { namespace Tracers { namespace Zipkin { +using TraceContextOption = envoy::config::trace::v3::ZipkinConfig::TraceContextOption; + class Span; +using SpanPtr = std::unique_ptr; /** - * This interface must be observed by a Zipkin tracer. + * Abstract class that delegates to users of the Tracer class the responsibility + * of "reporting" a Zipkin span that has ended its life cycle. "Reporting" can mean that the + * span will be sent to out to Zipkin, or buffered so that it can be sent out later. */ -class TracerInterface { +class Reporter { public: /** * Destructor. */ - virtual ~TracerInterface() = default; + virtual ~Reporter() = default; /** - * A Zipkin tracer must implement this method. Its implementation must perform whatever - * actions are required when the given span is considered finished. An implementation - * will typically buffer the given span so that it can be flushed later. + * Method that a concrete Reporter class must implement to handle finished spans. + * For example, a span-buffer management policy could be implemented. * * This method is invoked by the Span object when its finish() method is called. * @@ -35,6 +43,43 @@ class TracerInterface { virtual void reportSpan(Span&& span) PURE; }; +using ReporterPtr = std::unique_ptr; + +/** + * This interface must be observed by a Zipkin tracer. + */ +class TracerInterface : public Reporter { +public: + /** + * Creates a "root" Zipkin span. + * + * @param config The tracing configuration + * @param span_name Name of the new span. + * @param start_time The time indicating the beginning of the span. + * @return SpanPtr The root span. + */ + virtual SpanPtr startSpan(const Tracing::Config&, const std::string& span_name, + SystemTime timestamp) PURE; + + /** + * Depending on the given context, creates either a "child" or a "shared-context" Zipkin span. + * + * @param config The tracing configuration + * @param span_name Name of the new span. + * @param start_time The time indicating the beginning of the span. + * @param previous_context The context of the span preceding the one to be created. + * @return SpanPtr The child span. + */ + virtual SpanPtr startSpan(const Tracing::Config&, const std::string& span_name, + SystemTime timestamp, const SpanContext& previous_context) PURE; + + /** + * Gets the current trace context option for header injection behavior. + * @return The current trace context option. + */ + virtual TraceContextOption traceContextOption() const PURE; +}; + /** * Buffered pending spans serializer. */ diff --git a/source/extensions/tracers/zipkin/util.cc b/source/extensions/tracers/zipkin/util.cc index 53763443576f6..05d23514b98dd 100644 --- a/source/extensions/tracers/zipkin/util.cc +++ b/source/extensions/tracers/zipkin/util.cc @@ -23,8 +23,8 @@ uint64_t Util::generateRandom64(TimeSource& time_source) { return rand_64(); } -ProtobufWkt::Value Util::uint64Value(uint64_t value, absl::string_view name, - Replacements& replacements) { +Protobuf::Value Util::uint64Value(uint64_t value, absl::string_view name, + Replacements& replacements) { const std::string string_value = std::to_string(value); replacements.push_back({absl::StrCat("\"", name, "\":\"", string_value, "\""), absl::StrCat("\"", name, "\":", string_value)}); diff --git a/source/extensions/tracers/zipkin/util.h b/source/extensions/tracers/zipkin/util.h index 6f21924052855..ded339112024e 100644 --- a/source/extensions/tracers/zipkin/util.h +++ b/source/extensions/tracers/zipkin/util.h @@ -55,10 +55,10 @@ class Util { * @param value unt64_t number that will be represented in string. * @param name std::string that is the key for the value being replaced. * @param replacements a container to hold the required replacements when serializing this value. - * @return ProtobufWkt::Value wrapped uint64_t as a string. + * @return Protobuf::Value wrapped uint64_t as a string. */ - static ProtobufWkt::Value uint64Value(uint64_t value, absl::string_view name, - Replacements& replacements); + static Protobuf::Value uint64Value(uint64_t value, absl::string_view name, + Replacements& replacements); }; } // namespace Zipkin diff --git a/source/extensions/tracers/zipkin/zipkin_core_constants.h b/source/extensions/tracers/zipkin/zipkin_core_constants.h index 3685849259f4e..06aad0c4ebc15 100644 --- a/source/extensions/tracers/zipkin/zipkin_core_constants.h +++ b/source/extensions/tracers/zipkin/zipkin_core_constants.h @@ -53,6 +53,10 @@ class ZipkinCoreConstantValues { // Zipkin b3 single header const Tracing::TraceContextHandler B3{"b3"}; + + // W3C trace context headers + const Tracing::TraceContextHandler TRACE_PARENT{"traceparent"}; + const Tracing::TraceContextHandler TRACE_STATE{"tracestate"}; }; using ZipkinCoreConstants = ConstSingleton; diff --git a/source/extensions/tracers/zipkin/zipkin_core_types.cc b/source/extensions/tracers/zipkin/zipkin_core_types.cc index eb6c17c8382a2..040c430adff72 100644 --- a/source/extensions/tracers/zipkin/zipkin_core_types.cc +++ b/source/extensions/tracers/zipkin/zipkin_core_types.cc @@ -2,12 +2,14 @@ #include -#include "source/common/common/utility.h" #include "source/extensions/tracers/zipkin/span_context.h" #include "source/extensions/tracers/zipkin/util.h" #include "source/extensions/tracers/zipkin/zipkin_core_constants.h" #include "source/extensions/tracers/zipkin/zipkin_json_field_names.h" +#include "absl/strings/str_cat.h" +#include "fmt/format.h" + namespace Envoy { namespace Extensions { namespace Tracers { @@ -24,8 +26,8 @@ Endpoint& Endpoint::operator=(const Endpoint& ep) { return *this; } -const ProtobufWkt::Struct Endpoint::toStruct(Util::Replacements&) const { - ProtobufWkt::Struct endpoint; +const Protobuf::Struct Endpoint::toStruct(Util::Replacements&) const { + Protobuf::Struct endpoint; auto* fields = endpoint.mutable_fields(); if (!address_) { (*fields)[ENDPOINT_IPV4] = ValueUtil::stringValue(""); @@ -65,8 +67,8 @@ void Annotation::changeEndpointServiceName(const std::string& service_name) { } } -const ProtobufWkt::Struct Annotation::toStruct(Util::Replacements& replacements) const { - ProtobufWkt::Struct annotation; +const Protobuf::Struct Annotation::toStruct(Util::Replacements& replacements) const { + Protobuf::Struct annotation; auto* fields = annotation.mutable_fields(); (*fields)[ANNOTATION_TIMESTAMP] = Util::uint64Value(timestamp_, SPAN_TIMESTAMP, replacements); (*fields)[ANNOTATION_VALUE] = ValueUtil::stringValue(value_); @@ -97,8 +99,8 @@ BinaryAnnotation& BinaryAnnotation::operator=(const BinaryAnnotation& ann) { return *this; } -const ProtobufWkt::Struct BinaryAnnotation::toStruct(Util::Replacements& replacements) const { - ProtobufWkt::Struct binary_annotation; +const Protobuf::Struct BinaryAnnotation::toStruct(Util::Replacements& replacements) const { + Protobuf::Struct binary_annotation; auto* fields = binary_annotation.mutable_fields(); (*fields)[BINARY_ANNOTATION_KEY] = ValueUtil::stringValue(key_); (*fields)[BINARY_ANNOTATION_VALUE] = ValueUtil::stringValue(value_); @@ -113,38 +115,14 @@ const ProtobufWkt::Struct BinaryAnnotation::toStruct(Util::Replacements& replace const std::string Span::EMPTY_HEX_STRING_ = "0000000000000000"; -Span::Span(const Span& span) : time_source_(span.time_source_) { - trace_id_ = span.traceId(); - if (span.isSetTraceIdHigh()) { - trace_id_high_ = span.traceIdHigh(); - } - name_ = span.name(); - id_ = span.id(); - if (span.isSetParentId()) { - parent_id_ = span.parentId(); - } - debug_ = span.debug(); - sampled_ = span.sampled(); - annotations_ = span.annotations(); - binary_annotations_ = span.binaryAnnotations(); - if (span.isSetTimestamp()) { - timestamp_ = span.timestamp(); - } - if (span.isSetDuration()) { - duration_ = span.duration(); - } - monotonic_start_time_ = span.startTime(); - tracer_ = span.tracer(); -} - void Span::setServiceName(const std::string& service_name) { for (auto& annotation : annotations_) { annotation.changeEndpointServiceName(service_name); } } -const ProtobufWkt::Struct Span::toStruct(Util::Replacements& replacements) const { - ProtobufWkt::Struct span; +const Protobuf::Struct Span::toStruct(Util::Replacements& replacements) const { + Protobuf::Struct span; auto* fields = span.mutable_fields(); (*fields)[SPAN_TRACE_ID] = ValueUtil::stringValue(traceIdAsHexString()); (*fields)[SPAN_NAME] = ValueUtil::stringValue(name_); @@ -155,7 +133,7 @@ const ProtobufWkt::Struct Span::toStruct(Util::Replacements& replacements) const } if (timestamp_.has_value()) { - // Usually we store number to a ProtobufWkt::Struct object via ValueUtil::numberValue. + // Usually we store number to a Protobuf::Struct object via ValueUtil::numberValue. // However, due to the possibility of rendering that to a number with scientific notation, we // chose to store it as a string and keeping track the corresponding replacement. (*fields)[SPAN_TIMESTAMP] = Util::uint64Value(timestamp_.value(), SPAN_TIMESTAMP, replacements); @@ -168,7 +146,8 @@ const ProtobufWkt::Struct Span::toStruct(Util::Replacements& replacements) const } if (!annotations_.empty()) { - std::vector annotation_list; + std::vector annotation_list; + annotation_list.reserve(annotations_.size()); for (auto& annotation : annotations_) { annotation_list.push_back(ValueUtil::structValue(annotation.toStruct(replacements))); } @@ -176,7 +155,8 @@ const ProtobufWkt::Struct Span::toStruct(Util::Replacements& replacements) const } if (!binary_annotations_.empty()) { - std::vector binary_annotation_list; + std::vector binary_annotation_list; + binary_annotation_list.reserve(binary_annotations_.size()); for (auto& binary_annotation : binary_annotations_) { binary_annotation_list.push_back( ValueUtil::structValue(binary_annotation.toStruct(replacements))); @@ -187,9 +167,8 @@ const ProtobufWkt::Struct Span::toStruct(Util::Replacements& replacements) const return span; } -void Span::finish() { +void Span::finishSpan() { // Assumption: Span will have only one annotation when this method is called. - SpanContext context(*this); if (annotations_[0].value() == SERVER_RECV) { // Need to set the SS annotation Annotation ss; @@ -218,9 +197,7 @@ void Span::finish() { setDuration(monotonic_stop_time - monotonic_start_time_); } - if (auto t = tracer()) { - t->reportSpan(std::move(*this)); - } + tracer_.reportSpan(std::move(*this)); } void Span::setTag(absl::string_view name, absl::string_view value) { @@ -237,6 +214,63 @@ void Span::log(SystemTime timestamp, const std::string& event) { addAnnotation(std::move(annotation)); } +void Span::injectContext(Tracing::TraceContext& trace_context, const Tracing::UpstreamContext&) { + auto trace_context_option = tracer_.traceContextOption(); + + // Always inject B3 headers + ZipkinCoreConstants::get().X_B3_TRACE_ID.setRefKey(trace_context, traceIdAsHexString()); + ZipkinCoreConstants::get().X_B3_SPAN_ID.setRefKey(trace_context, idAsHexString()); + + // Set the parent-span header properly, based on the newly-created span structure. + if (isSetParentId()) { + ZipkinCoreConstants::get().X_B3_PARENT_SPAN_ID.setRefKey(trace_context, parentIdAsHexString()); + } + + // Set the sampled header. + ZipkinCoreConstants::get().X_B3_SAMPLED.setRefKey(trace_context, + sampled() ? SAMPLED : NOT_SAMPLED); + + // Additionally inject W3C headers if dual propagation is enabled + if (trace_context_option == envoy::config::trace::v3::ZipkinConfig::USE_B3_WITH_W3C_PROPAGATION) { + injectW3CContext(trace_context); + } +} +Tracing::SpanPtr Span::spawnChild(const Tracing::Config& config, const std::string& name, + SystemTime start_time) { + return tracer_.startSpan(config, name, start_time, spanContext()); +} + +SpanContext Span::spanContext() const { + // The inner_context is set to true because this SpanContext is context of Envoy created span + // rather than the one that extracted from the downstream request headers. + return {trace_id_high_.value_or(0), trace_id_, id_, parent_id_.value_or(0), sampled_, true}; +} + +void Span::injectW3CContext(Tracing::TraceContext& trace_context) { + // Convert Zipkin span context to W3C traceparent format + // W3C traceparent format: 00-{trace-id}-{span-id}-{trace-flags} + + // Construct the 128-bit trace ID (32 hex chars) + std::string trace_id_str; + if (trace_id_high_.has_value() && trace_id_high_.value() != 0) { + // We have a 128-bit trace ID, use both high and low parts + trace_id_str = absl::StrCat(fmt::format("{:016x}", trace_id_high_.value()), + fmt::format("{:016x}", trace_id_)); + } else { + // We have a 64-bit trace ID, pad with zeros for the high part + trace_id_str = absl::StrCat("0000000000000000", fmt::format("{:016x}", trace_id_)); + } + + // Construct the traceparent header value in W3C format: version-traceid-spanid-flags + std::string traceparent_value = + fmt::format("00-{}-{:016x}-{}", trace_id_str, id_, sampled() ? "01" : "00"); + + // Set the W3C traceparent header + ZipkinCoreConstants::get().TRACE_PARENT.setRefKey(trace_context, traceparent_value); + + // For now, we don't set tracestate as it's optional and we don't have vendor-specific data +} + } // namespace Zipkin } // namespace Tracers } // namespace Extensions diff --git a/source/extensions/tracers/zipkin/zipkin_core_types.h b/source/extensions/tracers/zipkin/zipkin_core_types.h index 26ee5c187c1a8..52c8d3806e2ab 100644 --- a/source/extensions/tracers/zipkin/zipkin_core_types.h +++ b/source/extensions/tracers/zipkin/zipkin_core_types.h @@ -35,11 +35,11 @@ class ZipkinBase { /** * All classes defining Zipkin abstractions need to implement this method to convert - * the corresponding abstraction to a ProtobufWkt::Struct. + * the corresponding abstraction to a Protobuf::Struct. * @param replacements A container that is used to hold the required replacements when this object * is serialized. */ - virtual const ProtobufWkt::Struct toStruct(Util::Replacements& replacements) const PURE; + virtual const Protobuf::Struct toStruct(Util::Replacements& replacements) const PURE; /** * Serializes the a type as a Zipkin-compliant JSON representation as a string. @@ -110,7 +110,7 @@ class Endpoint : public ZipkinBase { * * @return a protobuf struct. */ - const ProtobufWkt::Struct toStruct(Util::Replacements& replacements) const override; + const Protobuf::Struct toStruct(Util::Replacements& replacements) const override; private: std::string service_name_; @@ -202,7 +202,7 @@ class Annotation : public ZipkinBase { * * @return a protobuf struct. */ - const ProtobufWkt::Struct toStruct(Util::Replacements& replacements) const override; + const Protobuf::Struct toStruct(Util::Replacements& replacements) const override; private: uint64_t timestamp_{0}; @@ -290,7 +290,7 @@ class BinaryAnnotation : public ZipkinBase { * @param replacements Used to hold the required replacements on serialization step. * @return a protobuf struct. */ - const ProtobufWkt::Struct toStruct(Util::Replacements& replacements) const override; + const Protobuf::Struct toStruct(Util::Replacements& replacements) const override; private: std::string key_; @@ -299,22 +299,16 @@ class BinaryAnnotation : public ZipkinBase { AnnotationType annotation_type_{}; }; -using SpanPtr = std::unique_ptr; - /** * Represents a Zipkin span. This class is based on Zipkin's Thrift definition of a span. */ -class Span : public ZipkinBase { +class Span : public ZipkinBase, public Tracing::Span { public: - /** - * Copy constructor. - */ - Span(const Span&); - /** * Default constructor. Creates an empty span. */ - explicit Span(TimeSource& time_source) : time_source_(time_source) {} + explicit Span(TimeSource& time_source, TracerInterface& tracer) + : time_source_(time_source), tracer_(tracer) {} /** * Sets the span's trace id attribute. @@ -324,7 +318,7 @@ class Span : public ZipkinBase { /** * Sets the span's name attribute. */ - void setName(const std::string& val) { name_ = val; } + void setName(absl::string_view val) { name_ = std::string(val); } /** * Sets the span's id. @@ -341,11 +335,6 @@ class Span : public ZipkinBase { */ bool isSetParentId() const { return parent_id_.has_value(); } - /** - * Set the span's sampled flag. - */ - void setSampled(bool val) { sampled_ = val; } - /** * @return a vector with all annotations added to the span. */ @@ -544,21 +533,19 @@ class Span : public ZipkinBase { * * @return a protobuf struct. */ - const ProtobufWkt::Struct toStruct(Util::Replacements& replacements) const override; + const Protobuf::Struct toStruct(Util::Replacements& replacements) const override; /** - * Associates a Tracer object with the span. The tracer's reportSpan() method is invoked - * by the span's finish() method so that the tracer can decide what to do with the span - * when it is finished. + * @return the span's context. * - * @param tracer Represents the Tracer object to be associated with the span. + * This method returns a SpanContext object that contains the span's trace id, span id, parent id, + * and sampled attributes. */ - void setTracer(TracerInterface* tracer) { tracer_ = tracer; } + SpanContext spanContext() const; - /** - * @return the Tracer object associated with the span. - */ - TracerInterface* tracer() const { return tracer_; } + void setUseLocalDecision(bool use_local_decision) { use_local_decision_ = use_local_decision; } + + // Tracing::Span /** * Marks a successful end of the span. This method will: @@ -568,25 +555,28 @@ class Span : public ZipkinBase { * (2) compute and set the span's duration; and * (3) invoke the tracer's reportSpan() method if a tracer has been associated with the span. */ - void finish(); + void finishSpan() override; + void setTag(absl::string_view name, absl::string_view value) override; + void log(SystemTime timestamp, const std::string& event) override; + void setSampled(bool val) override { sampled_ = val; } + bool useLocalDecision() const override { return use_local_decision_; } + void setOperation(absl::string_view operation) override { setName(std::string(operation)); } + void injectContext(Tracing::TraceContext& trace_context, + const Tracing::UpstreamContext&) override; + Tracing::SpanPtr spawnChild(const Tracing::Config&, const std::string& name, + SystemTime start_time) override; + + void setBaggage(absl::string_view, absl::string_view) override {} + std::string getBaggage(absl::string_view) override { return EMPTY_STRING; } + std::string getSpanId() const override { return EMPTY_STRING; }; + std::string getTraceId() const override { return traceIdAsHexString(); }; +private: /** - * Adds a binary annotation to the span. - * - * @param name The binary annotation's key. - * @param value The binary annotation's value. + * Injects W3C trace context headers based on this span's context. + * @param trace_context The trace context to inject headers into. */ - void setTag(absl::string_view name, absl::string_view value); - - /** - * Adds an annotation to the span - * - * @param timestamp The annotation's timestamp. - * @param event The annotation's value. - */ - void log(SystemTime timestamp, const std::string& event); - -private: + void injectW3CContext(Tracing::TraceContext& trace_context); static const std::string EMPTY_HEX_STRING_; uint64_t trace_id_{0}; std::string name_; @@ -600,10 +590,13 @@ class Span : public ZipkinBase { absl::optional duration_; absl::optional trace_id_high_; int64_t monotonic_start_time_{0}; - TracerInterface* tracer_{nullptr}; TimeSource& time_source_; + TracerInterface& tracer_; + bool use_local_decision_{false}; }; +using SpanPtr = std::unique_ptr; + } // namespace Zipkin } // namespace Tracers } // namespace Extensions diff --git a/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc b/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc index 6da1581176028..98ee263007f8b 100644 --- a/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc +++ b/source/extensions/tracers/zipkin/zipkin_tracer_impl.cc @@ -1,113 +1,128 @@ #include "source/extensions/tracers/zipkin/zipkin_tracer_impl.h" +#include + #include "envoy/config/trace/v3/zipkin.pb.h" -#include "source/common/common/empty_string.h" #include "source/common/common/enum_to_int.h" -#include "source/common/common/fmt.h" -#include "source/common/common/utility.h" #include "source/common/config/utility.h" #include "source/common/http/headers.h" #include "source/common/http/message_impl.h" #include "source/common/http/utility.h" -#include "source/common/tracing/http_tracer_impl.h" #include "source/extensions/tracers/zipkin/span_context_extractor.h" #include "source/extensions/tracers/zipkin/zipkin_core_constants.h" +#include "absl/strings/string_view.h" + namespace Envoy { namespace Extensions { namespace Tracers { namespace Zipkin { -ZipkinSpan::ZipkinSpan(Zipkin::Span& span, Zipkin::Tracer& tracer) : span_(span), tracer_(tracer) {} +namespace { +// Helper function to parse URI and extract hostname and path +std::pair parseUri(absl::string_view uri) { + // Find the scheme separator + size_t scheme_pos = uri.find("://"); + if (scheme_pos == std::string::npos) { + // No scheme, treat as path only + return {"", std::string(uri)}; + } -void ZipkinSpan::finishSpan() { span_.finish(); } + // Skip past the scheme + size_t host_start = scheme_pos + 3; -void ZipkinSpan::setOperation(absl::string_view operation) { - span_.setName(std::string(operation)); -} + // Find the path separator + size_t path_pos = uri.find('/', host_start); + if (path_pos == std::string::npos) { + // No path, hostname only + return {std::string(uri.substr(host_start)), "/"}; + } -void ZipkinSpan::setTag(absl::string_view name, absl::string_view value) { - span_.setTag(name, value); -} + std::string hostname = std::string(uri.substr(host_start, path_pos - host_start)); + std::string path = std::string(uri.substr(path_pos)); -void ZipkinSpan::log(SystemTime timestamp, const std::string& event) { - span_.log(timestamp, event); + return {hostname, path}; } +} // namespace -// TODO(#11622): Implement baggage storage for zipkin spans -void ZipkinSpan::setBaggage(absl::string_view, absl::string_view) {} -std::string ZipkinSpan::getBaggage(absl::string_view) { return EMPTY_STRING; } - -void ZipkinSpan::injectContext(Tracing::TraceContext& trace_context, - const Tracing::UpstreamContext&) { - // Set the trace-id and span-id headers properly, based on the newly-created span structure. - ZipkinCoreConstants::get().X_B3_TRACE_ID.setRefKey(trace_context, span_.traceIdAsHexString()); - ZipkinCoreConstants::get().X_B3_SPAN_ID.setRefKey(trace_context, span_.idAsHexString()); - - // Set the parent-span header properly, based on the newly-created span structure. - if (span_.isSetParentId()) { - ZipkinCoreConstants::get().X_B3_PARENT_SPAN_ID.setRefKey(trace_context, - span_.parentIdAsHexString()); - } - - // Set the sampled header. - ZipkinCoreConstants::get().X_B3_SAMPLED.setRefKey(trace_context, - span_.sampled() ? SAMPLED : NOT_SAMPLED); -} +Driver::Driver(const envoy::config::trace::v3::ZipkinConfig& zipkin_config, + Server::Configuration::ServerFactoryContext& context) + : collector_(std::make_shared()), tls_(context.threadLocal().allocateSlot()), + trace_context_option_(zipkin_config.trace_context_option()) { + + // Check if HttpService is configured (preferred over legacy fields) + if (zipkin_config.has_collector_service()) { + const auto& http_service = zipkin_config.collector_service(); + // Extract cluster and endpoint from HttpService + const auto& http_uri = http_service.http_uri(); + + collector_->cluster_ = http_uri.cluster(); + + // Parse the URI to extract hostname and path + if (auto [hostname, path] = parseUri(http_uri.uri()); !hostname.empty()) { + // Use the hostname from the URI + collector_->hostname_ = hostname; + collector_->endpoint_ = path; + } else { + // Fallback to cluster name if no hostname in URI + collector_->hostname_ = collector_->cluster_; + collector_->endpoint_ = path; + } -void ZipkinSpan::setSampled(bool sampled) { span_.setSampled(sampled); } + // Parse headers from HttpService + for (const auto& header_option : http_service.request_headers_to_add()) { + const auto& header_value = header_option.header(); + collector_->request_headers_.emplace_back(header_value.key(), header_value.value()); + } + } else { + if (zipkin_config.collector_cluster().empty() || zipkin_config.collector_endpoint().empty()) { + throw EnvoyException( + "collector_cluster and collector_endpoint must be specified when not using " + "collector_service"); + } -Tracing::SpanPtr ZipkinSpan::spawnChild(const Tracing::Config& config, const std::string& name, - SystemTime start_time) { - SpanContext previous_context(span_); - return std::make_unique( - *tracer_.startSpan(config, name, start_time, previous_context), tracer_); -} + collector_->cluster_ = zipkin_config.collector_cluster(); + collector_->hostname_ = !zipkin_config.collector_hostname().empty() + ? zipkin_config.collector_hostname() + : zipkin_config.collector_cluster(); + collector_->endpoint_ = zipkin_config.collector_endpoint(); -Driver::TlsTracer::TlsTracer(TracerPtr&& tracer, Driver& driver) - : tracer_(std::move(tracer)), driver_(driver) {} + // Legacy configuration has no custom headers support + // Custom headers are only available through HttpService. + } -Driver::Driver(const envoy::config::trace::v3::ZipkinConfig& zipkin_config, - Upstream::ClusterManager& cluster_manager, Stats::Scope& scope, - ThreadLocal::SlotAllocator& tls, Runtime::Loader& runtime, - const LocalInfo::LocalInfo& local_info, Random::RandomGenerator& random_generator, - TimeSource& time_source) - : cm_(cluster_manager), - tracer_stats_{ZIPKIN_TRACER_STATS(POOL_COUNTER_PREFIX(scope, "tracing.zipkin."))}, - tls_(tls.allocateSlot()), runtime_(runtime), local_info_(local_info), - time_source_(time_source) { - THROW_IF_NOT_OK_REF(Config::Utility::checkCluster("envoy.tracers.zipkin", - zipkin_config.collector_cluster(), cm_, + // Validate cluster exists + THROW_IF_NOT_OK_REF(Config::Utility::checkCluster("envoy.tracers.zipkin", collector_->cluster_, + context.clusterManager(), /* allow_added_via_api */ true) .status()); - cluster_ = zipkin_config.collector_cluster(); - hostname_ = !zipkin_config.collector_hostname().empty() ? zipkin_config.collector_hostname() - : zipkin_config.collector_cluster(); - CollectorInfo collector; - if (!zipkin_config.collector_endpoint().empty()) { - collector.endpoint_ = zipkin_config.collector_endpoint(); - } // The current default version of collector_endpoint_version is HTTP_JSON. - collector.version_ = zipkin_config.collector_endpoint_version(); + collector_->version_ = zipkin_config.collector_endpoint_version(); const bool trace_id_128bit = zipkin_config.trace_id_128bit(); const bool shared_span_context = PROTOBUF_GET_WRAPPED_OR_DEFAULT( zipkin_config, shared_span_context, DEFAULT_SHARED_SPAN_CONTEXT); - collector.shared_span_context_ = shared_span_context; + collector_->shared_span_context_ = shared_span_context; const bool split_spans_for_request = zipkin_config.split_spans_for_request(); - tls_->set([this, collector, &random_generator, trace_id_128bit, shared_span_context, + auto stats = std::make_shared(ZipkinTracerStats{ + ZIPKIN_TRACER_STATS(POOL_COUNTER_PREFIX(context.scope(), "tracing.zipkin."))}); + + tls_->set([&context, c = collector_, t = trace_context_option_, stats, trace_id_128bit, split_spans_for_request]( Event::Dispatcher& dispatcher) -> ThreadLocal::ThreadLocalObjectSharedPtr { TracerPtr tracer = std::make_unique( - local_info_.clusterName(), local_info_.address(), random_generator, trace_id_128bit, - shared_span_context, time_source_, split_spans_for_request); - tracer->setReporter( - ReporterImpl::newInstance(std::ref(*this), std::ref(dispatcher), collector)); - return std::make_shared(std::move(tracer), *this); + context.localInfo().clusterName(), context.localInfo().address(), + context.api().randomGenerator(), trace_id_128bit, c->shared_span_context_, + context.timeSource(), split_spans_for_request); + tracer->setTraceContextOption(t); + auto reporter = std::make_unique(dispatcher, context.clusterManager(), + context.runtime(), stats, c); + tracer->setReporter(std::move(reporter)); + return std::make_shared(std::move(tracer)); }); } @@ -117,55 +132,56 @@ Tracing::SpanPtr Driver::startSpan(const Tracing::Config& config, Tracing::Decision tracing_decision) { Tracer& tracer = *tls_->getTyped().tracer_; SpanPtr new_zipkin_span; - SpanContextExtractor extractor(trace_context); - bool sampled{extractor.extractSampled(tracing_decision)}; + + // W3C fallback extraction is only enabled when USE_B3_WITH_W3C_PROPAGATION is configured + SpanContextExtractor extractor(trace_context, w3cFallbackEnabled()); + const absl::optional sampled = extractor.extractSampled(); + bool use_local_decision = !sampled.has_value(); TRY_NEEDS_AUDIT { - auto ret_span_context = extractor.extractSpanContext(sampled); + auto ret_span_context = extractor.extractSpanContext(sampled.value_or(tracing_decision.traced)); if (!ret_span_context.second) { // Create a root Zipkin span. No context was found in the headers. new_zipkin_span = tracer.startSpan(config, std::string(trace_context.host()), stream_info.startTime()); - new_zipkin_span->setSampled(sampled); + new_zipkin_span->setSampled(sampled.value_or(tracing_decision.traced)); } else { + use_local_decision = false; new_zipkin_span = tracer.startSpan(config, std::string(trace_context.host()), stream_info.startTime(), ret_span_context.first); } } END_TRY catch (const ExtractorException& e) { return std::make_unique(); } + new_zipkin_span->setUseLocalDecision(use_local_decision); // Return the active Zipkin span. - return std::make_unique(*new_zipkin_span, tracer); + return new_zipkin_span; } -ReporterImpl::ReporterImpl(Driver& driver, Event::Dispatcher& dispatcher, - const CollectorInfo& collector) - : driver_(driver), collector_(collector), +ReporterImpl::ReporterImpl(Event::Dispatcher& dispatcher, Upstream::ClusterManager& cm, + Runtime::Loader& runtime, ZipkinTracerStatsSharedPtr tracer_stats, + CollectorInfoConstSharedPtr collector) + : runtime_(runtime), tracer_stats_(std::move(tracer_stats)), collector_(std::move(collector)), span_buffer_{ - std::make_unique(collector.version_, collector.shared_span_context_)}, - collector_cluster_(driver_.clusterManager(), driver_.cluster()) { + std::make_unique(collector_->version_, collector_->shared_span_context_)}, + collector_cluster_(cm, collector_->cluster_) { flush_timer_ = dispatcher.createTimer([this]() -> void { - driver_.tracerStats().timer_flushed_.inc(); + tracer_stats_->timer_flushed_.inc(); flushSpans(); enableTimer(); }); const uint64_t min_flush_spans = - driver_.runtime().snapshot().getInteger("tracing.zipkin.min_flush_spans", 5U); + runtime_.snapshot().getInteger("tracing.zipkin.min_flush_spans", 5U); span_buffer_->allocateBuffer(min_flush_spans); enableTimer(); } -ReporterPtr ReporterImpl::newInstance(Driver& driver, Event::Dispatcher& dispatcher, - const CollectorInfo& collector) { - return std::make_unique(driver, dispatcher, collector); -} - void ReporterImpl::reportSpan(Span&& span) { span_buffer_->addSpan(std::move(span)); const uint64_t min_flush_spans = - driver_.runtime().snapshot().getInteger("tracing.zipkin.min_flush_spans", 5U); + runtime_.snapshot().getInteger("tracing.zipkin.min_flush_spans", 5U); if (span_buffer_->pendingSpans() == min_flush_spans) { flushSpans(); @@ -174,27 +190,35 @@ void ReporterImpl::reportSpan(Span&& span) { void ReporterImpl::enableTimer() { const uint64_t flush_interval = - driver_.runtime().snapshot().getInteger("tracing.zipkin.flush_interval_ms", 5000U); + runtime_.snapshot().getInteger("tracing.zipkin.flush_interval_ms", 5000U); flush_timer_->enableTimer(std::chrono::milliseconds(flush_interval)); } void ReporterImpl::flushSpans() { if (span_buffer_->pendingSpans()) { - driver_.tracerStats().spans_sent_.add(span_buffer_->pendingSpans()); + tracer_stats_->spans_sent_.add(span_buffer_->pendingSpans()); const std::string request_body = span_buffer_->serialize(); Http::RequestMessagePtr message = std::make_unique(); message->headers().setReferenceMethod(Http::Headers::get().MethodValues.Post); - message->headers().setPath(collector_.endpoint_); - message->headers().setHost(driver_.hostname()); + // Set path and hostname - both are stored in collector_ + message->headers().setPath(collector_->endpoint_); + message->headers().setHost(collector_->hostname_); + message->headers().setReferenceContentType( - collector_.version_ == envoy::config::trace::v3::ZipkinConfig::HTTP_PROTO + collector_->version_ == envoy::config::trace::v3::ZipkinConfig::HTTP_PROTO ? Http::Headers::get().ContentTypeValues.Protobuf : Http::Headers::get().ContentTypeValues.Json); + // Add custom headers from collector configuration + for (const auto& header : collector_->request_headers_) { + // Replace any existing header with the configured value + message->headers().setCopy(header.first, header.second); + } + message->body().add(request_body); const uint64_t timeout = - driver_.runtime().snapshot().getInteger("tracing.zipkin.request_timeout", 5000U); + runtime_.snapshot().getInteger("tracing.zipkin.request_timeout", 5000U); if (collector_cluster_.threadLocalCluster().has_value()) { Http::AsyncClient::Request* request = @@ -205,8 +229,8 @@ void ReporterImpl::flushSpans() { active_requests_.add(*request); } } else { - ENVOY_LOG(debug, "collector cluster '{}' does not exist", driver_.cluster()); - driver_.tracerStats().reports_skipped_no_cluster_.inc(); + ENVOY_LOG(debug, "collector cluster '{}' does not exist", collector_->cluster_); + tracer_stats_->reports_skipped_no_cluster_.inc(); } span_buffer_->clear(); @@ -216,7 +240,7 @@ void ReporterImpl::flushSpans() { void ReporterImpl::onFailure(const Http::AsyncClient::Request& request, Http::AsyncClient::FailureReason) { active_requests_.remove(request); - driver_.tracerStats().reports_failed_.inc(); + tracer_stats_->reports_failed_.inc(); } void ReporterImpl::onSuccess(const Http::AsyncClient::Request& request, @@ -224,9 +248,9 @@ void ReporterImpl::onSuccess(const Http::AsyncClient::Request& request, active_requests_.remove(request); if (Http::Utility::getResponseStatus(http_response->headers()) != enumToInt(Http::Code::Accepted)) { - driver_.tracerStats().reports_dropped_.inc(); + tracer_stats_->reports_dropped_.inc(); } else { - driver_.tracerStats().reports_sent_.inc(); + tracer_stats_->reports_sent_.inc(); } } diff --git a/source/extensions/tracers/zipkin/zipkin_tracer_impl.h b/source/extensions/tracers/zipkin/zipkin_tracer_impl.h index 2739bc61010bf..90435368fcd2e 100644 --- a/source/extensions/tracers/zipkin/zipkin_tracer_impl.h +++ b/source/extensions/tracers/zipkin/zipkin_tracer_impl.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/common/random_generator.h" #include "envoy/config/trace/v3/zipkin.pb.h" #include "envoy/local_info/local_info.h" @@ -8,15 +10,11 @@ #include "envoy/tracing/trace_driver.h" #include "envoy/upstream/cluster_manager.h" -#include "source/common/common/empty_string.h" #include "source/common/http/async_client_utility.h" -#include "source/common/http/header_map_impl.h" -#include "source/common/json/json_loader.h" -#include "source/common/tracing/common_values.h" -#include "source/common/tracing/null_span_impl.h" #include "source/common/upstream/cluster_update_tracker.h" #include "source/extensions/tracers/zipkin/span_buffer.h" #include "source/extensions/tracers/zipkin/tracer.h" +#include "source/extensions/tracers/zipkin/tracer_interface.h" #include "source/extensions/tracers/zipkin/zipkin_core_constants.h" namespace Envoy { @@ -36,84 +34,58 @@ struct ZipkinTracerStats { ZIPKIN_TRACER_STATS(GENERATE_COUNTER_STRUCT) }; +using ZipkinTracerStatsSharedPtr = std::shared_ptr; + /** - * Class for Zipkin spans, wrapping a Zipkin::Span object. + * Information about the Zipkin collector. */ -class ZipkinSpan : public Tracing::Span { -public: - /** - * Constructor. Wraps a Zipkin::Span object. - * - * @param span to be wrapped. - */ - ZipkinSpan(Zipkin::Span& span, Zipkin::Tracer& tracer); - - /** - * Calls Zipkin::Span::finishSpan() to perform all actions needed to finalize the span. - * This function is called by Tracing::HttpTracerUtility::finalizeSpan(). - */ - void finishSpan() override; - - /** - * This method sets the operation name on the span. - * @param operation the operation name - */ - void setOperation(absl::string_view operation) override; - - /** - * This function adds a Zipkin "string" binary annotation to this span. - * In Zipkin, binary annotations of the type "string" allow arbitrary key-value pairs - * to be associated with a span. - * - * Note that Tracing::HttpTracerUtility::finalizeSpan() makes several calls to this function, - * associating several key-value pairs with this span. - */ - void setTag(absl::string_view name, absl::string_view value) override; - - void log(SystemTime timestamp, const std::string& event) override; - - void injectContext(Tracing::TraceContext& trace_context, - const Tracing::UpstreamContext&) override; - Tracing::SpanPtr spawnChild(const Tracing::Config&, const std::string& name, - SystemTime start_time) override; - - void setSampled(bool sampled) override; +struct CollectorInfo { + std::string cluster_; // The cluster to use to reach the collector. - // TODO(#11622): Implement baggage storage for zipkin spans - void setBaggage(absl::string_view, absl::string_view) override; - std::string getBaggage(absl::string_view) override; + // The Zipkin collector endpoint/path to receive the collected trace data. + // For legacy configuration: from collector_endpoint field. + // For HttpService configuration: from http_service_.http_uri().uri(). + std::string endpoint_; - std::string getTraceId() const override { return span_.traceIdAsHexString(); }; + // The hostname to use when sending spans to the collector. + // For legacy configuration: from collector_hostname field or cluster name. + // For HttpService configuration: cluster name. + std::string hostname_; - // TODO(#34412): This method is unimplemented for Zipkin. - std::string getSpanId() const override { return EMPTY_STRING; }; + // The version of the collector. This is related to endpoint's supported payload specification and + // transport. + envoy::config::trace::v3::ZipkinConfig::CollectorEndpointVersion version_{ + envoy::config::trace::v3::ZipkinConfig::HTTP_JSON}; - /** - * @return a reference to the Zipkin::Span object. - */ - Zipkin::Span& span() { return span_; } + bool shared_span_context_{DEFAULT_SHARED_SPAN_CONTEXT}; -private: - Zipkin::Span span_; - Zipkin::Tracer& tracer_; + // Additional custom headers to include in requests to the Zipkin collector. + // Only available when using HttpService configuration via request_headers_to_add. + // Legacy configuration does not support custom headers. + std::vector> request_headers_; }; -using ZipkinSpanPtr = std::unique_ptr; +using CollectorInfoConstSharedPtr = std::shared_ptr; /** * Class for a Zipkin-specific Driver. */ class Driver : public Tracing::Driver { public: + /** + * Thread-local store containing ZipkinDriver and Zipkin::Tracer objects. + */ + struct TlsTracer : ThreadLocal::ThreadLocalObject { + TlsTracer(TracerPtr tracer) : tracer_(std::move(tracer)) {} + TracerPtr tracer_; + }; + /** * Constructor. It adds itself and a newly-created Zipkin::Tracer object to a thread-local store. * Also, it associates the given random-number generator to the Zipkin::Tracer object it creates. */ Driver(const envoy::config::trace::v3::ZipkinConfig& zipkin_config, - Upstream::ClusterManager& cluster_manager, Stats::Scope& scope, - ThreadLocal::SlotAllocator& tls, Runtime::Loader& runtime, - const LocalInfo::LocalInfo& localinfo, Random::RandomGenerator& random_generator, - TimeSource& time_source); + Server::Configuration::ServerFactoryContext& context); /** * This function is inherited from the abstract Driver class. @@ -130,46 +102,18 @@ class Driver : public Tracing::Driver { const std::string& operation_name, Tracing::Decision tracing_decision) override; - // Getters to return the ZipkinDriver's key members. - Upstream::ClusterManager& clusterManager() { return cm_; } - const std::string& cluster() { return cluster_; } - const std::string& hostname() { return hostname_; } - Runtime::Loader& runtime() { return runtime_; } - ZipkinTracerStats& tracerStats() { return tracer_stats_; } + bool w3cFallbackEnabled() const { + return trace_context_option_ == + envoy::config::trace::v3::ZipkinConfig::USE_B3_WITH_W3C_PROPAGATION; + } + TraceContextOption traceContextOption() const { return trace_context_option_; } -private: - /** - * Thread-local store containing ZipkinDriver and Zipkin::Tracer objects. - */ - struct TlsTracer : ThreadLocal::ThreadLocalObject { - TlsTracer(TracerPtr&& tracer, Driver& driver); - - TracerPtr tracer_; - Driver& driver_; - }; + const std::string& hostnameForTest() { return collector_->hostname_; } - Upstream::ClusterManager& cm_; - std::string cluster_; - std::string hostname_; - ZipkinTracerStats tracer_stats_; +private: + std::shared_ptr collector_; ThreadLocal::SlotPtr tls_; - Runtime::Loader& runtime_; - const LocalInfo::LocalInfo& local_info_; - TimeSource& time_source_; -}; - -/** - * Information about the Zipkin collector. - */ -struct CollectorInfo { - // The Zipkin collector endpoint/path to receive the collected trace data. - std::string endpoint_; - - // The version of the collector. This is related to endpoint's supported payload specification and - // transport. - envoy::config::trace::v3::ZipkinConfig::CollectorEndpointVersion version_; - - bool shared_span_context_{DEFAULT_SHARED_SPAN_CONTEXT}; + TraceContextOption trace_context_option_; }; /** @@ -193,13 +137,19 @@ class ReporterImpl : Logger::Loggable, /** * Constructor. * - * @param driver ZipkinDriver to be associated with the reporter. * @param dispatcher Controls the timer used to flush buffered spans. + * @param cm Reference to the cluster manager. This is used to get a handle + * to the cluster that contains the Zipkin collector. + * @param runtime Reference to the runtime. This is used to get the values + * of the runtime parameters that control the span-buffering/flushing behavior. + * @param tracer_stats Reference to the structure used to record Zipkin-related stats. * @param collector holds the endpoint version and path information. * when making HTTP POST requests carrying spans. This value comes from the * Zipkin-related tracing configuration. */ - ReporterImpl(Driver& driver, Event::Dispatcher& dispatcher, const CollectorInfo& collector); + ReporterImpl(Event::Dispatcher& dispatcher, Upstream::ClusterManager& cm, + Runtime::Loader& runtime, ZipkinTracerStatsSharedPtr tracer_stats, + CollectorInfoConstSharedPtr collector); /** * Implementation of Zipkin::Reporter::reportSpan(). @@ -216,20 +166,6 @@ class ReporterImpl : Logger::Loggable, void onFailure(const Http::AsyncClient::Request&, Http::AsyncClient::FailureReason) override; void onBeforeFinalizeUpstreamSpan(Tracing::Span&, const Http::ResponseHeaderMap*) override {} - /** - * Creates a heap-allocated ZipkinReporter. - * - * @param driver ZipkinDriver to be associated with the reporter. - * @param dispatcher Controls the timer used to flush buffered spans. - * @param collector holds the endpoint version and path information. - * when making HTTP POST requests carrying spans. This value comes from the - * Zipkin-related tracing configuration. - * - * @return Pointer to the newly-created ZipkinReporter. - */ - static ReporterPtr newInstance(Driver& driver, Event::Dispatcher& dispatcher, - const CollectorInfo& collector); - private: /** * Enables the span-flushing timer. @@ -241,9 +177,11 @@ class ReporterImpl : Logger::Loggable, */ void flushSpans(); - Driver& driver_; + Runtime::Loader& runtime_; + ZipkinTracerStatsSharedPtr tracer_stats_; + CollectorInfoConstSharedPtr collector_; + Event::TimerPtr flush_timer_; - const CollectorInfo collector_; SpanBufferPtr span_buffer_; Upstream::ClusterUpdateTracker collector_cluster_; // Track active HTTP requests to be able to cancel them on destruction. diff --git a/source/extensions/transport_sockets/alts/alts_channel_pool.cc b/source/extensions/transport_sockets/alts/alts_channel_pool.cc index 75bb7fd32b31f..8af118ba23569 100644 --- a/source/extensions/transport_sockets/alts/alts_channel_pool.cc +++ b/source/extensions/transport_sockets/alts/alts_channel_pool.cc @@ -54,7 +54,7 @@ AltsChannelPool::AltsChannelPool(const std::vector AltsChannelPool::getChannel() { std::shared_ptr channel; { - absl::MutexLock lock(&mu_); + absl::MutexLock lock(mu_); channel = channel_pool_[index_]; index_ = (index_ + 1) % channel_pool_.size(); } diff --git a/source/extensions/transport_sockets/alts/tsi_socket.cc b/source/extensions/transport_sockets/alts/tsi_socket.cc index 37d87347d8614..cf343e66082dd 100644 --- a/source/extensions/transport_sockets/alts/tsi_socket.cc +++ b/source/extensions/transport_sockets/alts/tsi_socket.cc @@ -118,8 +118,8 @@ Network::PostIoAction TsiSocket::doHandshakeNextDone(NextResultPtr&& next_result err); return Network::PostIoAction::Close; } - ProtobufWkt::Struct dynamic_metadata; - ProtobufWkt::Value val; + Protobuf::Struct dynamic_metadata; + Protobuf::Value val; val.set_string_value(tsi_info.peer_identity_); dynamic_metadata.mutable_fields()->insert({std::string("peer_identity"), val}); callbacks_->connection().streamInfo().setDynamicMetadata( diff --git a/source/extensions/transport_sockets/http_11_proxy/connect.cc b/source/extensions/transport_sockets/http_11_proxy/connect.cc index 69bd12ba02e98..9fc84f678bd7c 100644 --- a/source/extensions/transport_sockets/http_11_proxy/connect.cc +++ b/source/extensions/transport_sockets/http_11_proxy/connect.cc @@ -38,15 +38,10 @@ UpstreamHttp11ConnectSocket::UpstreamHttp11ConnectSocket( // options, we want to maintain the original behavior of this transport socket. if (options_ && options_->http11ProxyInfo()) { if (transport_socket_->ssl()) { - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.proxy_ssl_port")) { - header_buffer_.add(absl::StrCat( - "CONNECT ", options_->http11ProxyInfo()->hostname, - Http::HeaderUtility::hostHasPort(options_->http11ProxyInfo()->hostname) ? "" : ":443", - " HTTP/1.1\r\n\r\n")); - } else { - header_buffer_.add(absl::StrCat("CONNECT ", options_->http11ProxyInfo()->hostname, - ":443 HTTP/1.1\r\n\r\n")); - } + header_buffer_.add(absl::StrCat( + "CONNECT ", options_->http11ProxyInfo()->hostname, + Http::HeaderUtility::hostHasPort(options_->http11ProxyInfo()->hostname) ? "" : ":443", + " HTTP/1.1\r\n\r\n")); need_to_strip_connect_response_ = true; } return; diff --git a/source/extensions/transport_sockets/tap/tap_config_impl.cc b/source/extensions/transport_sockets/tap/tap_config_impl.cc index e7421e51c33a0..1e9f1bc019a08 100644 --- a/source/extensions/transport_sockets/tap/tap_config_impl.cc +++ b/source/extensions/transport_sockets/tap/tap_config_impl.cc @@ -55,9 +55,7 @@ void PerSocketTapperImpl::closeSocket(Network::ConnectionEvent) { initStreamingEvent(event); event.mutable_closed(); // submit directly and don't check current_streamed_rx_tx_bytes_ any more - sink_handle_->submitTrace(std::move(streamed_trace_)); - buffered_trace_.reset(); - current_streamed_rx_tx_bytes_ = 0; + submitStreamedDataPerConfiguredSize(); } else { TapCommon::TraceWrapperPtr trace = makeTraceSegment(); auto& event = *trace->mutable_socket_streamed_trace_segment()->mutable_event(); @@ -106,6 +104,31 @@ bool PerSocketTapperImpl::shouldSendStreamedMsgByConfiguredSize() const { return config_->minStreamedSentBytes() > 0; } +void PerSocketTapperImpl::submitStreamedDataPerConfiguredSize() { + sink_handle_->submitTrace(std::move(streamed_trace_)); + streamed_trace_.reset(); + current_streamed_rx_tx_bytes_ = 0; +} + +bool PerSocketTapperImpl::shouldSubmitStreamedDataPerConfiguredSizeByAgedDuration() const { + if (streamed_trace_ == nullptr) { + return false; + } + const envoy::data::tap::v3::SocketEvents& streamed_events = + streamed_trace_->socket_streamed_trace_segment().events(); + auto& repeated_streamed_events = streamed_events.events(); + if (repeated_streamed_events.size() < 2) { + // Only one event. + return false; + } + + const Protobuf::Timestamp& first_event_ts = repeated_streamed_events[0].timestamp(); + const Protobuf::Timestamp& last_event_ts = + repeated_streamed_events[repeated_streamed_events.size() - 1].timestamp(); + return (last_event_ts.seconds() - first_event_ts.seconds()) >= + static_cast(DefaultBufferedAgedDuration); +} + void PerSocketTapperImpl::handleSendingStreamTappedMsgPerConfigSize(const Buffer::Instance& data, const uint32_t total_bytes, const bool is_read, @@ -127,10 +150,9 @@ void PerSocketTapperImpl::handleSendingStreamTappedMsgPerConfigSize(const Buffer current_streamed_rx_tx_bytes_ += event.write().data().as_bytes().size(); } - if (current_streamed_rx_tx_bytes_ >= config_->minStreamedSentBytes()) { - sink_handle_->submitTrace(std::move(streamed_trace_)); - streamed_trace_.reset(); - current_streamed_rx_tx_bytes_ = 0; + if (current_streamed_rx_tx_bytes_ >= config_->minStreamedSentBytes() || + shouldSubmitStreamedDataPerConfiguredSizeByAgedDuration()) { + submitStreamedDataPerConfiguredSize(); pegSubmitCounter(true); } } diff --git a/source/extensions/transport_sockets/tap/tap_config_impl.h b/source/extensions/transport_sockets/tap/tap_config_impl.h index 2c47b432a5565..19c2798cfc2c7 100644 --- a/source/extensions/transport_sockets/tap/tap_config_impl.h +++ b/source/extensions/transport_sockets/tap/tap_config_impl.h @@ -48,6 +48,8 @@ class PerSocketTapperImpl : public PerSocketTapper { } void pegSubmitCounter(const bool is_streaming); bool shouldSendStreamedMsgByConfiguredSize() const; + bool shouldSubmitStreamedDataPerConfiguredSizeByAgedDuration() const; + void submitStreamedDataPerConfiguredSize(); void handleSendingStreamTappedMsgPerConfigSize(const Buffer::Instance& data, const uint32_t total_bytes, const bool is_read, const bool is_end_stream); @@ -55,6 +57,10 @@ class PerSocketTapperImpl : public PerSocketTapper { // (This means that per transport socket buffer trace, the minimum amount // which triggering to send the tapped messages size is 9 bytes). static constexpr uint32_t DefaultMinBufferedBytes = 9; + // It isn't easy to meet data submit threshold when the configured byte size is too large + // and the tapped data volume is low, therefore, set below buffer aged duration (seconds) + // to make sure that the tapped data is submitted in time. + static constexpr uint32_t DefaultBufferedAgedDuration = 15; SocketTapConfigSharedPtr config_; Extensions::Common::Tap::PerTapSinkHandleManagerPtr sink_handle_; const Network::Connection& connection_; diff --git a/source/server/BUILD b/source/server/BUILD index b0abef48337ef..8c7a660d22170 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -400,6 +400,7 @@ envoy_cc_library( "//source/common/common:cleanup_lib", "//source/common/common:logger_lib", "//source/common/common:mutex_tracer_lib", + "//source/common/common:notification_lib", "//source/common/common:perf_tracing_lib", "//source/common/common:utility_lib", "//source/common/config:utility_lib", diff --git a/source/server/admin/admin.cc b/source/server/admin/admin.cc index c3ad262d82ef2..d4b308c760d18 100644 --- a/source/server/admin/admin.cc +++ b/source/server/admin/admin.cc @@ -136,7 +136,7 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, {Admin::ParamDescriptor::Type::String, "mask", "The mask to apply. When both resource and mask are specified, " "the mask is applied to every element in the desired repeated field so that only a " - "subset of fields are returned. The mask is parsed as a ProtobufWkt::FieldMask"}, + "subset of fields are returned. The mask is parsed as a Protobuf::FieldMask"}, {Admin::ParamDescriptor::Type::String, "name_regex", "Dump only the currently loaded configurations whose names match the specified " "regex. Can be used with both resource and mask query parameters."}, @@ -147,7 +147,7 @@ AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server, MAKE_ADMIN_HANDLER(init_dump_handler_.handlerInitDump), false, false, {{Admin::ParamDescriptor::Type::String, "mask", "The desired component to dump unready targets. The mask is parsed as " - "a ProtobufWkt::FieldMask. For example, get the unready targets of " + "a Protobuf::FieldMask. For example, get the unready targets of " "all listeners with /init_dump?mask=listener`"}}), makeHandler("/contention", "dump current Envoy mutex contention stats (if enabled)", MAKE_ADMIN_HANDLER(stats_handler_.handlerContention), false, false), diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index 2bf3706e07100..25af385aff56c 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -152,6 +152,9 @@ class AdminImpl : public Admin, uint32_t maxRequestHeadersKb() const override { return max_request_headers_kb_; } uint32_t maxRequestHeadersCount() const override { return max_request_headers_count_; } std::chrono::milliseconds streamIdleTimeout() const override { return {}; } + absl::optional streamFlushTimeout() const override { + return std::nullopt; + } std::chrono::milliseconds requestTimeout() const override { return {}; } std::chrono::milliseconds requestHeadersTimeout() const override { return {}; } std::chrono::milliseconds delayedCloseTimeout() const override { return {}; } diff --git a/source/server/admin/config_dump_handler.cc b/source/server/admin/config_dump_handler.cc index 6526155fff86a..3fbd9c476f936 100644 --- a/source/server/admin/config_dump_handler.cc +++ b/source/server/admin/config_dump_handler.cc @@ -95,7 +95,7 @@ bool trimResourceMessage(const Protobuf::FieldMask& field_mask, Protobuf::Messag if (reflection->HasField(message, any_field)) { ASSERT(any_field != nullptr); // Unpack to a DynamicMessage. - ProtobufWkt::Any any_message; + Protobuf::Any any_message; any_message.MergeFrom(reflection->GetMessage(message, any_field)); Protobuf::DynamicMessageFactory dmf; const absl::string_view inner_type_name = diff --git a/source/server/admin/runtime_handler.cc b/source/server/admin/runtime_handler.cc index 456146cb8e9c5..80408cfb6001d 100644 --- a/source/server/admin/runtime_handler.cc +++ b/source/server/admin/runtime_handler.cc @@ -23,7 +23,7 @@ Http::Code RuntimeHandler::handlerRuntime(Http::ResponseHeaderMap& response_head // TODO(jsedgwick): Use proto to structure this output instead of arbitrary JSON. const auto& layers = server_.runtime().snapshot().getLayers(); - std::vector layer_names; + std::vector layer_names; layer_names.reserve(layers.size()); std::map> entries; for (const auto& layer : layers) { @@ -45,10 +45,10 @@ Http::Code RuntimeHandler::handlerRuntime(Http::ResponseHeaderMap& response_head } } - ProtobufWkt::Struct layer_entries; + Protobuf::Struct layer_entries; auto* layer_entry_fields = layer_entries.mutable_fields(); for (const auto& entry : entries) { - std::vector layer_entry_values; + std::vector layer_entry_values; layer_entry_values.reserve(entry.second.size()); std::string final_value; for (const auto& value : entry.second) { @@ -58,7 +58,7 @@ Http::Code RuntimeHandler::handlerRuntime(Http::ResponseHeaderMap& response_head layer_entry_values.push_back(ValueUtil::stringValue(value)); } - ProtobufWkt::Struct layer_entry_value; + Protobuf::Struct layer_entry_value; auto* layer_entry_value_fields = layer_entry_value.mutable_fields(); (*layer_entry_value_fields)["final_value"] = ValueUtil::stringValue(final_value); @@ -66,7 +66,7 @@ Http::Code RuntimeHandler::handlerRuntime(Http::ResponseHeaderMap& response_head (*layer_entry_fields)[entry.first] = ValueUtil::structValue(layer_entry_value); } - ProtobufWkt::Struct runtime; + Protobuf::Struct runtime; auto* fields = runtime.mutable_fields(); (*fields)["layers"] = ValueUtil::listValue(layer_names); diff --git a/source/server/backtrace.cc b/source/server/backtrace.cc index 7a1254c1f8b6c..b21d9a0411264 100644 --- a/source/server/backtrace.cc +++ b/source/server/backtrace.cc @@ -8,8 +8,8 @@ namespace Envoy { bool BackwardsTrace::log_to_stderr_ = false; -const std::string& BackwardsTrace::addrMapping(bool setup) { - CONSTRUCT_ON_FIRST_USE(std::string, [setup]() -> std::string { +absl::string_view BackwardsTrace::addrMapping(bool setup) { + static absl::string_view value = [setup]() -> absl::string_view { if (!setup) { return ""; } @@ -23,12 +23,14 @@ const std::string& BackwardsTrace::addrMapping(bool setup) { while (std::getline(maps, line)) { std::vector parts = absl::StrSplit(line, ' '); if (parts[1] == "r-xp") { - return absl::StrCat(parts[0], " ", parts.back()); + static std::string result = absl::StrCat(parts[0], " ", parts.back()); + return result; } } #endif return ""; - }()); + }(); + return value; } void BackwardsTrace::setLogToStderr(bool log_to_stderr) { log_to_stderr_ = log_to_stderr; } diff --git a/source/server/backtrace.h b/source/server/backtrace.h index fcd41f59df80c..b8c27cf8b00db 100644 --- a/source/server/backtrace.h +++ b/source/server/backtrace.h @@ -58,7 +58,7 @@ class BackwardsTrace : Logger::Loggable { * e.g. * `7d34c0e28000-7d34c1e0d000 /build/foo/bar/source/exe/envoy-static` */ - static const std::string& addrMapping(bool setup = false); + static absl::string_view addrMapping(bool setup = false); /** * Directs the output of logTrace() to directly stderr rather than the diff --git a/source/server/config_validation/cluster_manager.cc b/source/server/config_validation/cluster_manager.cc index 199ccb8a634b4..90636c9165a7a 100644 --- a/source/server/config_validation/cluster_manager.cc +++ b/source/server/config_validation/cluster_manager.cc @@ -11,21 +11,19 @@ namespace Upstream { absl::StatusOr ValidationClusterManagerFactory::clusterManagerFromProto( const envoy::config::bootstrap::v3::Bootstrap& bootstrap) { absl::Status creation_status = absl::OkStatus(); - auto cluster_manager = std::unique_ptr{new ValidationClusterManager( - bootstrap, *this, context_, stats_, tls_, context_.runtime(), context_.localInfo(), - context_.accessLogManager(), context_.mainThreadDispatcher(), context_.admin(), - context_.api(), http_context_, context_.grpcContext(), context_.routerContext(), server_, - context_.xdsManager(), creation_status)}; + auto cluster_manager = std::unique_ptr{ + new ValidationClusterManager(bootstrap, *this, context_, creation_status)}; RETURN_IF_NOT_OK(creation_status); return cluster_manager; } absl::StatusOr ValidationClusterManagerFactory::createCds( const envoy::config::core::v3::ConfigSource& cds_config, - const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm) { + const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm, + bool support_multi_ads_sources) { // Create the CdsApiImpl... - auto cluster_or_error = - ProdClusterManagerFactory::createCds(cds_config, cds_resources_locator, cm); + auto cluster_or_error = ProdClusterManagerFactory::createCds(cds_config, cds_resources_locator, + cm, support_multi_ads_sources); RETURN_IF_NOT_OK_REF(cluster_or_error.status()); // ... and then throw it away, so that we don't actually connect to it. return nullptr; diff --git a/source/server/config_validation/cluster_manager.h b/source/server/config_validation/cluster_manager.h index edc12ec3488db..2821f87413dca 100644 --- a/source/server/config_validation/cluster_manager.h +++ b/source/server/config_validation/cluster_manager.h @@ -21,12 +21,9 @@ class ValidationClusterManagerFactory : public ProdClusterManagerFactory { using ProdClusterManagerFactory::ProdClusterManagerFactory; explicit ValidationClusterManagerFactory( - Server::Configuration::ServerFactoryContext& server_context, Stats::Store& stats, - ThreadLocal::Instance& tls, Http::Context& http_context, - LazyCreateDnsResolver dns_resolver_fn, Ssl::ContextManager& ssl_context_manager, - Quic::QuicStatNames& quic_stat_names, Server::Instance& server) - : ProdClusterManagerFactory(server_context, stats, tls, http_context, dns_resolver_fn, - ssl_context_manager, quic_stat_names, server) {} + Server::Configuration::ServerFactoryContext& server_context, + LazyCreateDnsResolver dns_resolver_fn, Quic::QuicStatNames& quic_stat_names) + : ProdClusterManagerFactory(server_context, dns_resolver_fn, quic_stat_names) {} absl::StatusOr clusterManagerFromProto(const envoy::config::bootstrap::v3::Bootstrap& bootstrap) override; @@ -35,7 +32,7 @@ class ValidationClusterManagerFactory : public ProdClusterManagerFactory { // unconditionally. absl::StatusOr createCds(const envoy::config::core::v3::ConfigSource& cds_config, const xds::core::v3::ResourceLocator* cds_resources_locator, - ClusterManager& cm) override; + ClusterManager& cm, bool support_multi_ads_sources) override; }; /** diff --git a/source/server/config_validation/server.cc b/source/server/config_validation/server.cc index df5c8d643ce83..92c1101ef8a0d 100644 --- a/source/server/config_validation/server.cc +++ b/source/server/config_validation/server.cc @@ -161,9 +161,8 @@ void ValidationInstance::initialize(const Options& options, *local_info_, validation_context_, *this); cluster_manager_factory_ = std::make_unique( - server_contexts_, stats(), threadLocal(), http_context_, - [this]() -> Network::DnsResolverSharedPtr { return this->dnsResolver(); }, - sslContextManager(), quic_stat_names_, *this); + server_contexts_, [this]() -> Network::DnsResolverSharedPtr { return this->dnsResolver(); }, + quic_stat_names_); THROW_IF_NOT_OK(config_.initialize(bootstrap_, *this, *cluster_manager_factory_)); THROW_IF_NOT_OK(runtime().initialize(clusterManager())); clusterManager().setInitializedCb([this]() -> void { init_manager_.initialize(init_watcher_); }); diff --git a/source/server/configuration_impl.cc b/source/server/configuration_impl.cc index a1c9a438604cd..f341803f78c4e 100644 --- a/source/server/configuration_impl.cc +++ b/source/server/configuration_impl.cc @@ -99,6 +99,14 @@ StatsConfigImpl::StatsConfigImpl(const envoy::config::bootstrap::v3::Bootstrap& if (bootstrap.stats_flush_case() == envoy::config::bootstrap::v3::Bootstrap::kStatsFlushOnAdmin) { flush_on_admin_ = bootstrap.stats_flush_on_admin(); } + + const auto evict_interval_ms = PROTOBUF_GET_MS_OR_DEFAULT(bootstrap, stats_eviction_interval, 0); + if (evict_interval_ms % flush_interval_.count() != 0) { + status = absl::InvalidArgumentError( + "stats_eviction_interval must be a multiple of stats_flush_interval"); + return; + } + evict_on_flush_ = evict_interval_ms / flush_interval_.count(); } absl::Status MainImpl::initialize(const envoy::config::bootstrap::v3::Bootstrap& bootstrap, diff --git a/source/server/configuration_impl.h b/source/server/configuration_impl.h index 2165239456f1b..932ffab806b24 100644 --- a/source/server/configuration_impl.h +++ b/source/server/configuration_impl.h @@ -56,6 +56,7 @@ class StatsConfigImpl : public StatsConfig { const std::list& sinks() const override { return sinks_; } std::chrono::milliseconds flushInterval() const override { return flush_interval_; } bool flushOnAdmin() const override { return flush_on_admin_; } + uint32_t evictOnFlush() const override { return evict_on_flush_; } void addSink(Stats::SinkPtr sink) { sinks_.emplace_back(std::move(sink)); } bool enableDeferredCreationStats() const override { @@ -67,6 +68,7 @@ class StatsConfigImpl : public StatsConfig { std::chrono::milliseconds flush_interval_; bool flush_on_admin_{false}; const envoy::config::bootstrap::v3::Bootstrap::DeferredStatOptions deferred_stat_options_; + uint32_t evict_on_flush_{0}; }; /** diff --git a/source/server/drain_manager_impl.h b/source/server/drain_manager_impl.h index 1fb01e8146679..143c4ea73220d 100644 --- a/source/server/drain_manager_impl.h +++ b/source/server/drain_manager_impl.h @@ -62,7 +62,7 @@ class DrainManagerImpl : Logger::Loggable, public DrainManager std::map drain_deadlines_ = { {Network::DrainDirection::InboundOnly, MonotonicTime()}, {Network::DrainDirection::All, MonotonicTime()}}; - mutable Common::CallbackManager cbs_{}; + mutable Common::CallbackManager cbs_{}; std::vector> drain_complete_cbs_; // Callbacks called by startDrainSequence to cascade/proxy to children diff --git a/source/server/overload_manager_impl.cc b/source/server/overload_manager_impl.cc index 524e2592f2da9..84e1803daac9a 100644 --- a/source/server/overload_manager_impl.cc +++ b/source/server/overload_manager_impl.cc @@ -140,6 +140,8 @@ absl::StatusOr parseTimerType( return Event::ScaledTimerType::TransportSocketConnectTimeout; case Config::HTTP_DOWNSTREAM_CONNECTION_MAX: return Event::ScaledTimerType::HttpDownstreamMaxConnectionTimeout; + case Config::HTTP_DOWNSTREAM_STREAM_FLUSH: + return Event::ScaledTimerType::HttpDownstreamStreamFlush; default: return absl::InvalidArgumentError( fmt::format("Unknown timer type {}", static_cast(config_timer_type))); @@ -147,7 +149,7 @@ absl::StatusOr parseTimerType( } absl::StatusOr -parseTimerMinimums(const ProtobufWkt::Any& typed_config, +parseTimerMinimums(const Protobuf::Any& typed_config, ProtobufMessage::ValidationVisitor& validation_visitor) { using Config = envoy::config::overload::v3::ScaleTimersOverloadActionConfig; const Config action_config = diff --git a/source/server/server.cc b/source/server/server.cc index c4ed0c43d7e76..cd89ef64c6a9c 100644 --- a/source/server/server.cc +++ b/source/server/server.cc @@ -28,6 +28,7 @@ #include "source/common/api/os_sys_calls_impl.h" #include "source/common/common/enum_to_int.h" #include "source/common/common/mutex_tracer_impl.h" +#include "source/common/common/notification.h" #include "source/common/common/utility.h" #include "source/common/config/utility.h" #include "source/common/config/well_known_names.h" @@ -291,6 +292,12 @@ void InstanceBase::flushStatsInternal() { auto& stats_config = config_.statsConfig(); InstanceUtil::flushMetricsToSinks(stats_config.sinks(), stats_store_, clusterManager(), timeSource()); + if (const auto evict_on_flush = stats_config.evictOnFlush(); evict_on_flush > 0) { + stats_eviction_counter_ = (stats_eviction_counter_ + 1) % evict_on_flush; + if (stats_eviction_counter_ == 0) { + stats_store_.evictUnused(); + } + } // TODO(ramaraochavali): consider adding different flush interval for histograms. if (stat_flush_timer_ != nullptr) { stat_flush_timer_->enableTimer(stats_config.flushInterval()); @@ -752,6 +759,8 @@ absl::Status InstanceBase::initializeOrThrow(Network::Address::InstanceConstShar [this](const char*) { server_stats_->debug_assertion_failures_.inc(); }); envoy_bug_action_registration_ = Assert::addEnvoyBugFailureRecordAction( [this](const char*) { server_stats_->envoy_bug_failures_.inc(); }); + envoy_notification_registration_ = Notification::addEnvoyNotificationRecordAction( + [this](absl::string_view) { server_stats_->envoy_notifications_.inc(); }); } if (initial_config.admin().address()) { @@ -794,9 +803,9 @@ absl::Status InstanceBase::initializeOrThrow(Network::Address::InstanceConstShar *local_info_, validation_context_, *this); cluster_manager_factory_ = std::make_unique( - serverFactoryContext(), stats_store_, thread_local_, http_context_, + serverFactoryContext(), [this]() -> Network::DnsResolverSharedPtr { return this->getOrCreateDnsResolver(); }, - *ssl_context_manager_, quic_stat_names_, *this); + quic_stat_names_); // Now that the worker thread are initialized, notify the bootstrap extensions. for (auto&& bootstrap_extension : bootstrap_extensions_) { @@ -952,9 +961,10 @@ void InstanceBase::loadServerFlags(const absl::optional& flags_path } RunHelper::RunHelper(Instance& instance, const Options& options, Event::Dispatcher& dispatcher, - Upstream::ClusterManager& cm, AccessLog::AccessLogManager& access_log_manager, - Init::Manager& init_manager, OverloadManager& overload_manager, - OverloadManager& null_overload_manager, std::function post_init_cb) + Config::XdsManager& xds_manager, Upstream::ClusterManager& cm, + AccessLog::AccessLogManager& access_log_manager, Init::Manager& init_manager, + OverloadManager& overload_manager, OverloadManager& null_overload_manager, + std::function post_init_cb) : init_watcher_("RunHelper", [&instance, post_init_cb]() { if (!instance.isShutdown()) { post_init_cb(); @@ -1006,7 +1016,7 @@ RunHelper::RunHelper(Instance& instance, const Options& options, Event::Dispatch // this can fire immediately if all clusters have already initialized. Also note that we need // to guard against shutdown at two different levels since SIGTERM can come in once the run loop // starts. - cm.setInitializedCb([&instance, &init_manager, &cm, this]() { + cm.setInitializedCb([&instance, &init_manager, &xds_manager, this]() { if (instance.isShutdown()) { return; } @@ -1015,10 +1025,7 @@ RunHelper::RunHelper(Instance& instance, const Options& options, Event::Dispatch // Pause RDS to ensure that we don't send any requests until we've // subscribed to all the RDS resources. The subscriptions happen in the init callbacks, // so we pause RDS until we've completed all the callbacks. - Config::ScopedResume maybe_resume_rds; - if (cm.adsMux()) { - maybe_resume_rds = cm.adsMux()->pause(type_url); - } + Config::ScopedResume resume_rds = xds_manager.pause(type_url); ENVOY_LOG(info, "all clusters initialized. initializing init manager"); init_manager.initialize(init_watcher_); @@ -1033,8 +1040,8 @@ void InstanceBase::run() { // RunHelper exists primarily to facilitate testing of how we respond to early shutdown during // startup (see RunHelperTest in server_test.cc). const auto run_helper = - RunHelper(*this, options_, *dispatcher_, clusterManager(), access_log_manager_, init_manager_, - overloadManager(), nullOverloadManager(), [this] { + RunHelper(*this, options_, *dispatcher_, xdsManager(), clusterManager(), access_log_manager_, + init_manager_, overloadManager(), nullOverloadManager(), [this] { notifyCallbacksForStage(Stage::PostInit); startWorkers(); }); diff --git a/source/server/server.h b/source/server/server.h index 2df13ded91d63..b59fce228f034 100644 --- a/source/server/server.h +++ b/source/server/server.h @@ -72,6 +72,7 @@ struct ServerCompilationSettingsStats { #define ALL_SERVER_STATS(COUNTER, GAUGE, HISTOGRAM) \ COUNTER(debug_assertion_failures) \ COUNTER(envoy_bug_failures) \ + COUNTER(envoy_notifications) \ COUNTER(dynamic_unknown_fields) \ COUNTER(static_unknown_fields) \ COUNTER(wip_protos) \ @@ -160,9 +161,10 @@ class InstanceUtil : Logger::Loggable { class RunHelper : Logger::Loggable { public: RunHelper(Instance& instance, const Options& options, Event::Dispatcher& dispatcher, - Upstream::ClusterManager& cm, AccessLog::AccessLogManager& access_log_manager, - Init::Manager& init_manager, OverloadManager& overload_manager, - OverloadManager& null_overload_manager, std::function workers_start_cb); + Config::XdsManager& xds_manager, Upstream::ClusterManager& cm, + AccessLog::AccessLogManager& access_log_manager, Init::Manager& init_manager, + OverloadManager& overload_manager, OverloadManager& null_overload_manager, + std::function workers_start_cb); private: Init::WatcherImpl init_watcher_; @@ -392,6 +394,7 @@ class InstanceBase : Logger::Loggable, server_compilation_settings_stats_; Assert::ActionRegistrationPtr assert_action_registration_; Assert::ActionRegistrationPtr envoy_bug_action_registration_; + Assert::ActionRegistrationPtr envoy_notification_registration_; ThreadLocal::Instance& thread_local_; Random::RandomGeneratorPtr random_generator_; envoy::config::bootstrap::v3::Bootstrap bootstrap_; @@ -450,6 +453,8 @@ class InstanceBase : Logger::Loggable, : RaiiListElement(callbacks, callback) {} }; + uint32_t stats_eviction_counter_{0}; + #ifdef ENVOY_PERFETTO std::unique_ptr tracing_session_{}; os_fd_t tracing_fd_{INVALID_HANDLE}; @@ -465,6 +470,8 @@ class InstanceBase : Logger::Loggable, // copying and probably be a cleaner API in general. class MetricSnapshotImpl : public Stats::MetricSnapshot { public: + // MetricSnapshotImpl captures a snapshot of metrics by latching the delta usage, and optionally + // marking the stats as used. explicit MetricSnapshotImpl(Stats::Store& store, Upstream::ClusterManager& cluster_manager, TimeSource& time_source); diff --git a/test/README.md b/test/README.md index a2c9298b857b0..32ac508c37da2 100644 --- a/test/README.md +++ b/test/README.md @@ -17,7 +17,7 @@ downstream-Envoy-upstream communication. Envoy includes some custom Google Mock matchers to make test expectation statements simpler to write and easier to understand. -### HeaderValueOf +### ContainsHeader Tests that a HeaderMap argument contains exactly one header with the given key, whose value satisfies the given expectation. The expectation can be a matcher, @@ -26,13 +26,13 @@ or a string that the value should equal. Examples: ```cpp -EXPECT_THAT(response->headers(), HeaderValueOf(Headers::get().Server, "envoy")); +EXPECT_THAT(response->headers(), ContainsHeader(Headers::get().Server, "envoy")); ``` ```cpp using testing::HasSubstr; EXPECT_THAT(request->headers(), - HeaderValueOf(Headers::get().AcceptEncoding, HasSubstr("gzip"))); + ContainsHeader(Headers::get().AcceptEncoding, HasSubstr("gzip"))); ``` ### HttpStatusIs diff --git a/test/common/access_log/access_log_impl_test.cc b/test/common/access_log/access_log_impl_test.cc index 9fbc073609582..de134b55488a2 100644 --- a/test/common/access_log/access_log_impl_test.cc +++ b/test/common/access_log/access_log_impl_test.cc @@ -1494,7 +1494,7 @@ name: accesslog )EOF"; TestStreamInfo stream_info(time_source_); - ProtobufWkt::Struct metadata_val; + Protobuf::Struct metadata_val; auto& fields_a = *metadata_val.mutable_fields(); auto& struct_b = *fields_a["a"].mutable_struct_value(); auto& fields_b = *struct_b.mutable_fields(); @@ -1530,7 +1530,7 @@ name: accesslog )EOF"; TestStreamInfo stream_info(time_source_); - ProtobufWkt::Struct metadata_val; + Protobuf::Struct metadata_val; stream_info.setDynamicMetadata("some.namespace", metadata_val); const InstanceSharedPtr log = @@ -1577,7 +1577,7 @@ name: accesslog )EOF"; TestStreamInfo stream_info(time_source_); - ProtobufWkt::Struct metadata_val; + Protobuf::Struct metadata_val; auto& fields_a = *metadata_val.mutable_fields(); auto& struct_b = *fields_a["a"].mutable_struct_value(); auto& fields_b = *struct_b.mutable_fields(); @@ -1603,7 +1603,7 @@ class TestHeaderFilterFactory : public ExtensionFilterFactory { ~TestHeaderFilterFactory() override = default; FilterPtr createFilter(const envoy::config::accesslog::v3::ExtensionFilter& config, - Server::Configuration::FactoryContext& context) override { + Server::Configuration::GenericFactoryContext& context) override { auto factory_config = Config::Utility::translateToFactoryConfig( config, context.messageValidationVisitor(), *this); const auto& header_config = @@ -1699,18 +1699,17 @@ class SampleExtensionFilterFactory : public ExtensionFilterFactory { ~SampleExtensionFilterFactory() override = default; FilterPtr createFilter(const envoy::config::accesslog::v3::ExtensionFilter& config, - Server::Configuration::FactoryContext& context) override { + Server::Configuration::GenericFactoryContext& context) override { auto factory_config = Config::Utility::translateToFactoryConfig( config, context.messageValidationVisitor(), *this); - ProtobufWkt::Struct struct_config = - *dynamic_cast(factory_config.get()); + Protobuf::Struct struct_config = *dynamic_cast(factory_config.get()); return std::make_unique( static_cast(struct_config.fields().at("rate").number_value())); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "sample_extension_filter"; } @@ -1846,6 +1845,24 @@ name: accesslog EXPECT_THROW_WITH_REGEX(AccessLogFactory::fromProto(parseAccessLogFromV3Yaml(yaml), context_), EnvoyException, "Not able to parse filter expression: .*"); } + +TEST_F(AccessLogImplTest, CelExtensionFilterExpressionUncompilable) { + const std::string yaml = R"EOF( +name: accesslog +filter: + extension_filter: + name: cel_extension_filter + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.filters.cel.v3.ExpressionFilter + expression: "f()" +typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/null + )EOF"; + + EXPECT_THROW_WITH_REGEX(AccessLogFactory::fromProto(parseAccessLogFromV3Yaml(yaml), context_), + EnvoyException, "failed to create an expression: .*"); +} #endif // USE_CEL_PARSER } // namespace diff --git a/test/common/buffer/buffer_fuzz.cc b/test/common/buffer/buffer_fuzz.cc index bb6903d9c0894..3377befecc515 100644 --- a/test/common/buffer/buffer_fuzz.cc +++ b/test/common/buffer/buffer_fuzz.cc @@ -216,14 +216,14 @@ class StringBuffer : public Buffer::Instance { return total_size_to_write; } - void setWatermarks(uint32_t, uint32_t) override { + void setWatermarks(uint64_t, uint32_t) override { // Not implemented. // TODO(antoniovicente) Implement and add fuzz coverage as we merge the Buffer::OwnedImpl and // WatermarkBuffer implementations. ASSERT(false); } - uint32_t highWatermark() const override { return 0; } + uint64_t highWatermark() const override { return 0; } bool highWatermarkTriggered() const override { return false; } absl::string_view asStringView() const { return {start(), size_}; } diff --git a/test/common/buffer/watermark_buffer_test.cc b/test/common/buffer/watermark_buffer_test.cc index 46585eefab5d3..3473db20de37d 100644 --- a/test/common/buffer/watermark_buffer_test.cc +++ b/test/common/buffer/watermark_buffer_test.cc @@ -442,47 +442,27 @@ TEST_F(WatermarkBufferTest, OverflowWatermarkDisabled) { EXPECT_EQ(21, buffer1.length()); } +class TestWatermarkBuffer : public Buffer::WatermarkBuffer { +public: + using WatermarkBuffer::overflowWatermarkForTestOnly; + using WatermarkBuffer::WatermarkBuffer; +}; + TEST_F(WatermarkBufferTest, OverflowWatermarkDisabledOnVeryHighValue) { -// Disabling execution with TSAN as it causes the test to use too much memory -// and time, making the test fail in some settings (such as CI) -#if defined(__has_feature) && (__has_feature(thread_sanitizer) || __has_feature(memory_sanitizer)) - ENVOY_LOG_MISC(critical, "WatermarkBufferTest::OverflowWatermarkDisabledOnVeryHighValue not " - "supported by this compiler configuration"); -#else // Verifies that the overflow watermark is disabled when its value is higher - // than uint32_t max value - int high_watermark_buffer1 = 0; - int overflow_watermark_buffer1 = 0; - Buffer::WatermarkBuffer buffer1{[&]() -> void {}, [&]() -> void { ++high_watermark_buffer1; }, - [&]() -> void { ++overflow_watermark_buffer1; }}; + // than uint64_t max value + TestWatermarkBuffer buffer1{[&]() -> void {}, [&]() -> void {}, [&]() -> void {}}; // Make sure the overflow threshold will be above std::numeric_limits::max() const uint64_t overflow_multiplier = 3; - const uint32_t high_watermark_threshold = - (std::numeric_limits::max() / overflow_multiplier) + 1; + uint64_t high_watermark_threshold = + (std::numeric_limits::max() / overflow_multiplier) + 1; buffer1.setWatermarks(high_watermark_threshold, overflow_multiplier); + EXPECT_EQ(buffer1.overflowWatermarkForTestOnly(), 0); - // Add many segments instead of full uint32_t::max to get around std::bad_alloc exception - const uint32_t segment_denominator = 128; - const uint32_t big_segment_len = std::numeric_limits::max() / segment_denominator + 1; - for (uint32_t i = 0; i < segment_denominator; ++i) { - auto reservation = buffer1.reserveSingleSlice(big_segment_len); - reservation.commit(big_segment_len); - } - EXPECT_GT(buffer1.length(), std::numeric_limits::max()); - EXPECT_LT(buffer1.length(), high_watermark_threshold * overflow_multiplier); - EXPECT_EQ(1, high_watermark_buffer1); - EXPECT_EQ(0, overflow_watermark_buffer1); - - // Reserve and commit additional space on the buffer beyond the expected - // high_watermark_threshold * overflow_multiplier threshold. - const uint64_t size = high_watermark_threshold * overflow_multiplier - buffer1.length() + 1; - auto reservation = buffer1.reserveSingleSlice(size); - reservation.commit(size); - EXPECT_EQ(buffer1.length(), high_watermark_threshold * overflow_multiplier + 1); - EXPECT_EQ(1, high_watermark_buffer1); - EXPECT_EQ(0, overflow_watermark_buffer1); -#endif + high_watermark_threshold = (std::numeric_limits::max() / overflow_multiplier) - 1; + buffer1.setWatermarks(high_watermark_threshold, overflow_multiplier); + EXPECT_EQ(buffer1.overflowWatermarkForTestOnly(), high_watermark_threshold * overflow_multiplier); } TEST_F(WatermarkBufferTest, OverflowWatermarkEqualHighWatermark) { diff --git a/test/common/common/BUILD b/test/common/common/BUILD index 07ca8a1b96919..eb3c8f6a32acd 100644 --- a/test/common/common/BUILD +++ b/test/common/common/BUILD @@ -307,10 +307,10 @@ envoy_cc_test( ) envoy_cc_test( - name = "trie_lookup_table_test", - srcs = ["trie_lookup_table_test.cc"], + name = "radix_tree_test", + srcs = ["radix_tree_test.cc"], rbe_pool = "6gig", - deps = ["//source/common/common:trie_lookup_table_lib"], + deps = ["//source/common/common:radix_tree_lib"], ) envoy_cc_test( @@ -448,19 +448,51 @@ envoy_benchmark_test( ) envoy_cc_benchmark_binary( - name = "trie_lookup_table_speed_test", - srcs = ["trie_lookup_table_speed_test.cc"], + name = "radix_tree_speed_test", + srcs = ["radix_tree_speed_test.cc"], rbe_pool = "6gig", deps = [ - "//source/common/common:trie_lookup_table_lib", + "//source/common/common:radix_tree_lib", "@com_github_google_benchmark//:benchmark", "@com_google_absl//absl/strings", ], ) envoy_benchmark_test( - name = "trie_lookup_table_speed_test_benchmark_test", - benchmark_binary = "trie_lookup_table_speed_test", + name = "radix_tree_speed_test_benchmark_test", + benchmark_binary = "radix_tree_speed_test", +) + +envoy_cc_benchmark_binary( + name = "prefix_matching_benchmark", + srcs = ["prefix_matching_benchmark.cc"], + rbe_pool = "6gig", + deps = [ + "//source/common/common:radix_tree_lib", + "//source/common/http:headers_lib", + "@com_github_google_benchmark//:benchmark", + "@com_google_absl//absl/strings", + ], +) + +envoy_benchmark_test( + name = "prefix_matching_benchmark_test", + benchmark_binary = "prefix_matching_benchmark", +) + +envoy_cc_benchmark_binary( + name = "shared_pointer_speed_test", + srcs = ["shared_pointer_speed_test.cc"], + rbe_pool = "6gig", + deps = [ + "//source/common/common:assert_lib", + "@com_github_google_benchmark//:benchmark", + ], +) + +envoy_benchmark_test( + name = "shared_pointer_benchmark_test", + benchmark_binary = "shared_pointer_speed_test", ) envoy_cc_test( @@ -620,3 +652,13 @@ envoy_cc_test( "//test/mocks/stream_info:stream_info_mocks", ], ) + +envoy_cc_test( + name = "notification_test", + srcs = ["notification_test.cc"], + rbe_pool = "6gig", + deps = [ + "//source/common/common:notification_lib", + "//test/test_common:logging_lib", + ], +) diff --git a/test/common/common/callback_impl_test.cc b/test/common/common/callback_impl_test.cc index bf1410fa495da..995fb2b7506c0 100644 --- a/test/common/common/callback_impl_test.cc +++ b/test/common/common/callback_impl_test.cc @@ -21,7 +21,7 @@ class CallbackManagerTest : public testing::Test { TEST_F(CallbackManagerTest, All) { InSequence s; - CallbackManager manager; + CallbackManager manager; auto handle1 = manager.add([this](int arg) { called(arg); return absl::OkStatus(); @@ -55,7 +55,7 @@ TEST_F(CallbackManagerTest, All) { TEST_F(CallbackManagerTest, DestroyManagerBeforeHandle) { CallbackHandlePtr handle; { - CallbackManager manager; + CallbackManager manager; handle = manager.add([this](int arg) { called(arg); return absl::OkStatus(); diff --git a/test/common/common/logger_test.cc b/test/common/common/logger_test.cc index 050d6bb959374..686e1ac0e3752 100644 --- a/test/common/common/logger_test.cc +++ b/test/common/common/logger_test.cc @@ -230,52 +230,6 @@ TEST_F(NamedLogTest, NamedLogsAreSentToSink) { ENVOY_LOG_EVENT_TO_LOGGER(Registry::getLog(Id::misc), debug, "misc_event", "log"); } -struct TlsLogSink : SinkDelegate { - TlsLogSink(DelegatingLogSinkSharedPtr log_sink) : SinkDelegate(log_sink) { setTlsDelegate(); } - ~TlsLogSink() override { restoreTlsDelegate(); } - - MOCK_METHOD(void, log, (absl::string_view, const spdlog::details::log_msg&)); - MOCK_METHOD(void, logWithStableName, - (absl::string_view, absl::string_view, absl::string_view, absl::string_view)); - MOCK_METHOD(void, flush, ()); -}; - -// Verifies that we can register a thread local sink override. -TEST(TlsLoggingOverrideTest, OverrideSink) { - MockLogSink global_sink(Envoy::Logger::Registry::getSink()); - testing::InSequence s; - - { - TlsLogSink tls_sink(Envoy::Logger::Registry::getSink()); - - // Calls on the current thread goes to the TLS sink. - EXPECT_CALL(tls_sink, log(_, _)); - ENVOY_LOG_MISC(info, "hello tls"); - - // Calls on other threads should use the global sink. - std::thread([&]() { - EXPECT_CALL(global_sink, log(_, _)); - ENVOY_LOG_MISC(info, "hello global"); - }).join(); - - // Sanity checking that we're still using the TLS sink. - EXPECT_CALL(tls_sink, log(_, _)); - ENVOY_LOG_MISC(info, "hello tls"); - - // All the logging functions should be delegated to the TLS override. - EXPECT_CALL(tls_sink, flush()); - Registry::getSink()->flush(); - - EXPECT_CALL(tls_sink, logWithStableName(_, _, _, _)); - Registry::getSink()->logWithStableName("foo", "level", "bar", "msg"); - } - - // Now that the TLS sink is out of scope, log calls on this thread should use the global sink - // again. - EXPECT_CALL(global_sink, log(_, _)); - ENVOY_LOG_MISC(info, "hello global 2"); -} - TEST(LoggerTest, LogWithLogDetails) { Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -291,7 +245,7 @@ TEST(LoggerTest, LogWithLogDetails) { } TEST(LoggerTest, TestJsonFormatError) { - ProtobufWkt::Any log_struct; + Protobuf::Any log_struct; log_struct.set_type_url("type.googleapis.com/bad.type.url"); log_struct.set_value("asdf"); @@ -307,9 +261,9 @@ TEST(LoggerTest, TestJsonFormatNonEscapedThrows) { Envoy::Logger::Registry::setLogLevel(spdlog::level::info); { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Message"].set_string_value("%v"); - (*log_struct.mutable_fields())["NullField"].set_null_value(ProtobufWkt::NULL_VALUE); + (*log_struct.mutable_fields())["NullField"].set_null_value(Protobuf::NULL_VALUE); auto status = Envoy::Logger::Registry::setJsonLogFormat(log_struct); EXPECT_FALSE(status.ok()); @@ -319,9 +273,9 @@ TEST(LoggerTest, TestJsonFormatNonEscapedThrows) { } { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Message"].set_string_value("%_"); - (*log_struct.mutable_fields())["NullField"].set_null_value(ProtobufWkt::NULL_VALUE); + (*log_struct.mutable_fields())["NullField"].set_null_value(Protobuf::NULL_VALUE); auto status = Envoy::Logger::Registry::setJsonLogFormat(log_struct); EXPECT_FALSE(status.ok()); @@ -332,7 +286,7 @@ TEST(LoggerTest, TestJsonFormatNonEscapedThrows) { } TEST(LoggerTest, TestJsonFormatEmptyStruct) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; Envoy::Logger::Registry::setLogLevel(spdlog::level::info); EXPECT_TRUE(Envoy::Logger::Registry::setJsonLogFormat(log_struct).ok()); EXPECT_TRUE(Envoy::Logger::Registry::jsonLogFormatSet()); @@ -347,10 +301,10 @@ TEST(LoggerTest, TestJsonFormatEmptyStruct) { } TEST(LoggerTest, TestJsonFormatNullAndFixedField) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Message"].set_string_value("%j"); (*log_struct.mutable_fields())["FixedValue"].set_string_value("Fixed"); - (*log_struct.mutable_fields())["NullField"].set_null_value(ProtobufWkt::NULL_VALUE); + (*log_struct.mutable_fields())["NullField"].set_null_value(Protobuf::NULL_VALUE); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); EXPECT_TRUE(Envoy::Logger::Registry::setJsonLogFormat(log_struct).ok()); EXPECT_TRUE(Envoy::Logger::Registry::jsonLogFormatSet()); @@ -367,7 +321,7 @@ TEST(LoggerTest, TestJsonFormatNullAndFixedField) { } TEST(LoggerTest, TestJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -401,7 +355,7 @@ TEST(LoggerTest, TestJsonFormat) { } TEST(LoggerTest, TestJsonFormatWithNestedJsonMessage) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); (*log_struct.mutable_fields())["FixedValue"].set_string_value("Fixed"); @@ -598,7 +552,7 @@ TEST(TaggedLogTest, TestConnEventLog) { } TEST(TaggedLogTest, TestConnEventLogWithJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -682,7 +636,7 @@ TEST(TaggedLogTest, TestStreamLog) { } TEST(TaggedLogTest, TestTaggedLogWithJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -715,7 +669,7 @@ TEST(TaggedLogTest, TestTaggedLogWithJsonFormat) { } TEST(TaggedLogTest, TestTaggedConnLogWithJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -759,7 +713,7 @@ TEST(TaggedLogTest, TestTaggedConnLogWithJsonFormat) { } TEST(TaggedLogTest, TestConnLogWithJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -784,7 +738,7 @@ TEST(TaggedLogTest, TestConnLogWithJsonFormat) { } TEST(TaggedLogTest, TestTaggedStreamLogWithJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -831,7 +785,7 @@ TEST(TaggedLogTest, TestTaggedStreamLogWithJsonFormat) { } TEST(TaggedLogTest, TestStreamLogWithJsonFormat) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message"].set_string_value("%j"); Envoy::Logger::Registry::setLogLevel(spdlog::level::info); @@ -857,7 +811,7 @@ TEST(TaggedLogTest, TestStreamLogWithJsonFormat) { } TEST(TaggedLogTest, TestTaggedLogWithJsonFormatMultipleJFlags) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Level"].set_string_value("%l"); (*log_struct.mutable_fields())["Message1"].set_string_value("%j"); (*log_struct.mutable_fields())["Message2"].set_string_value("%j"); diff --git a/test/common/common/matchers_test.cc b/test/common/common/matchers_test.cc index 468cf2fcf6719..1736719d836c0 100644 --- a/test/common/common/matchers_test.cc +++ b/test/common/common/matchers_test.cc @@ -30,7 +30,7 @@ TEST_F(MetadataTest, MatchNullValue) { Envoy::Config::Metadata::mutableMetadataValue(metadata, "envoy.filter.a", "label") .set_string_value("test"); Envoy::Config::Metadata::mutableMetadataValue(metadata, "envoy.filter.b", "label") - .set_null_value(ProtobufWkt::NullValue::NULL_VALUE); + .set_null_value(Protobuf::NullValue::NULL_VALUE); envoy::type::matcher::v3::MetadataMatcher matcher; matcher.set_filter("envoy.filter.b"); @@ -223,9 +223,9 @@ listMatchEntry(envoy::type::matcher::v3::MetadataMatcher* matcher) { TEST_F(MetadataTest, MatchStringListValue) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Value& metadataValue = + Protobuf::Value& metadataValue = Envoy::Config::Metadata::mutableMetadataValue(metadata, "envoy.filter.a", "groups"); - ProtobufWkt::ListValue* values = metadataValue.mutable_list_value(); + Protobuf::ListValue* values = metadataValue.mutable_list_value(); values->add_values()->set_string_value("first"); values->add_values()->set_string_value("second"); values->add_values()->set_string_value("third"); @@ -251,9 +251,9 @@ TEST_F(MetadataTest, MatchStringListValue) { TEST_F(MetadataTest, MatchBoolListValue) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Value& metadataValue = + Protobuf::Value& metadataValue = Envoy::Config::Metadata::mutableMetadataValue(metadata, "envoy.filter.a", "groups"); - ProtobufWkt::ListValue* values = metadataValue.mutable_list_value(); + Protobuf::ListValue* values = metadataValue.mutable_list_value(); values->add_values()->set_bool_value(false); values->add_values()->set_bool_value(false); @@ -274,9 +274,9 @@ TEST_F(MetadataTest, MatchBoolListValue) { TEST_F(MetadataTest, MatchDoubleListValue) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Value& metadataValue = + Protobuf::Value& metadataValue = Envoy::Config::Metadata::mutableMetadataValue(metadata, "envoy.filter.a", "groups"); - ProtobufWkt::ListValue* values = metadataValue.mutable_list_value(); + Protobuf::ListValue* values = metadataValue.mutable_list_value(); values->add_values()->set_number_value(10); values->add_values()->set_number_value(23); diff --git a/test/common/common/notification_test.cc b/test/common/common/notification_test.cc new file mode 100644 index 0000000000000..9e0373220187e --- /dev/null +++ b/test/common/common/notification_test.cc @@ -0,0 +1,45 @@ +#include "source/common/common/notification.h" + +#include "test/test_common/logging.h" + +#include "gtest/gtest.h" + +namespace Envoy { + +TEST(EnvoyNotification, CallbackInvoked) { + // Use 2 envoy notification action registrations to verify that action chaining is working + // correctly. + int envoy_notification_count = 0; + int envoy_notification_count2 = 0; + std::string name1; + std::string name2; + auto envoy_notification_action_registration = + Notification::addEnvoyNotificationRecordAction([&](absl::string_view name) { + name1 = name; + envoy_notification_count++; + }); + auto envoy_notification_action_registration2 = + Notification::addEnvoyNotificationRecordAction([&](absl::string_view name) { + name2 = name; + envoy_notification_count2++; + }); + + EXPECT_LOG_CONTAINS("debug", "envoy notification: id1.", { ENVOY_NOTIFICATION("id1", ""); }); + EXPECT_EQ(envoy_notification_count, 1); + EXPECT_EQ(envoy_notification_count2, 1); + EXPECT_EQ(name1, "id1"); + EXPECT_EQ(name2, "id1"); + EXPECT_LOG_CONTAINS("debug", "envoy notification: .", { ENVOY_NOTIFICATION("", ""); }); + EXPECT_EQ(envoy_notification_count, 2); + EXPECT_EQ(envoy_notification_count2, 2); + EXPECT_EQ(name1, ""); + EXPECT_EQ(name2, ""); + EXPECT_LOG_CONTAINS("debug", "envoy notification: id2. Details: with some logs", + { ENVOY_NOTIFICATION("id2", "with some logs"); }); + EXPECT_EQ(envoy_notification_count, 3); + EXPECT_EQ(envoy_notification_count2, 3); + EXPECT_EQ(name1, "id2"); + EXPECT_EQ(name2, "id2"); +} + +} // namespace Envoy diff --git a/test/common/common/prefix_matching_benchmark.cc b/test/common/common/prefix_matching_benchmark.cc new file mode 100644 index 0000000000000..0f80037b2f8cd --- /dev/null +++ b/test/common/common/prefix_matching_benchmark.cc @@ -0,0 +1,123 @@ +// Note: this should be run with --compilation_mode=opt, and would benefit from a +// quiescent system with disabled cstate power management. + +#include + +#include "source/common/common/radix_tree.h" +#include "source/common/http/headers.h" + +#include "benchmark/benchmark.h" + +namespace Envoy { + +// NOLINT(namespace-envoy) + +#define ADD_HEADER_TO_KEYS(name) keys.emplace_back(Http::Headers::get().name); + +// Helper function to generate test data with hierarchical prefixes +std::vector generateHierarchicalKeys(int num_keys, int max_depth) { + std::mt19937 prng(1); // PRNG with a fixed seed, for repeatability + std::uniform_int_distribution char_distribution('a', 'z'); + std::uniform_int_distribution depth_distribution(1, max_depth); + + std::vector keys; + for (int i = 0; i < num_keys; i++) { + int depth = depth_distribution(prng); + std::string key; + for (int j = 0; j < depth; j++) { + for (int k = 0; k < 3; k++) { // Each level has 3 characters + key.push_back(static_cast(char_distribution(prng))); + } + if (j < depth - 1) { + key.push_back('/'); // Use '/' as separator for hierarchical structure + } + } + keys.push_back(key); + } + return keys; +} + +// Helper function to generate search keys with various prefix lengths +std::vector generateSearchKeys(const std::vector& keys, + int num_searches) { + std::mt19937 prng(2); // Different seed for search keys + std::uniform_int_distribution keyindex_distribution(0, keys.size() - 1); + std::uniform_int_distribution length_distribution(1, 20); // Random prefix length + + std::vector search_keys; + for (int i = 0; i < num_searches; i++) { + const std::string& base_key = keys[keyindex_distribution(prng)]; + size_t prefix_len = std::min(length_distribution(prng), base_key.length()); + search_keys.push_back(base_key.substr(0, prefix_len)); + } + return search_keys; +} + +template +static void typedBmPrefixMatching(benchmark::State& state, const std::vector& keys, + const std::vector& search_keys) { + TableType table; + for (const std::string& key : keys) { + table.add(key, nullptr); + } + + size_t search_index = 0; + for (auto _ : state) { + UNREFERENCED_PARAMETER(_); + auto result = table.findMatchingPrefixes(search_keys[search_index++]); + // Reset search_index to 0 whenever it reaches the end + search_index %= search_keys.size(); + benchmark::DoNotOptimize(result); + } +} + +// Range args are: +// 0 - num_keys (number of keys in the tree) +// 1 - max_depth (maximum depth of hierarchical keys) +// 2 - num_searches (number of search operations to perform) +template static void typedBmPrefixMatching(benchmark::State& state) { + int num_keys = state.range(0); + int max_depth = state.range(1); + int num_searches = state.range(2); + + std::vector keys = generateHierarchicalKeys(num_keys, max_depth); + std::vector search_keys = generateSearchKeys(keys, num_searches); + + typedBmPrefixMatching(state, keys, search_keys); +} + +// Benchmark for RadixTree +static void bmRadixTreePrefixMatching(benchmark::State& s) { + typedBmPrefixMatching>(s); +} + +static void bmRadixTreeRequestHeadersPrefixMatching(benchmark::State& s) { + std::vector keys; + INLINE_REQ_HEADERS(ADD_HEADER_TO_KEYS); + + // Generate search keys based on the headers + std::vector search_keys = generateSearchKeys(keys, 1000); + + typedBmPrefixMatching>(s, keys, search_keys); +} + +static void bmRadixTreeResponseHeadersPrefixMatching(benchmark::State& s) { + std::vector keys; + INLINE_RESP_HEADERS(ADD_HEADER_TO_KEYS); + + // Generate search keys based on the headers + std::vector search_keys = generateSearchKeys(keys, 1000); + + typedBmPrefixMatching>(s, keys, search_keys); +} + +BENCHMARK(bmRadixTreePrefixMatching) + ->ArgsProduct({{100, 1000, 10000}, {3, 5, 8}, {1000}}) + ->Name("RadixTree/PrefixMatching"); + +BENCHMARK(bmRadixTreeRequestHeadersPrefixMatching)->Name("RadixTree/RequestHeadersPrefixMatching"); + +BENCHMARK(bmRadixTreeResponseHeadersPrefixMatching) + ->Name("RadixTree/ResponseHeadersPrefixMatching"); + +} // namespace Envoy diff --git a/test/common/common/trie_lookup_table_speed_test.cc b/test/common/common/radix_tree_speed_test.cc similarity index 68% rename from test/common/common/trie_lookup_table_speed_test.cc rename to test/common/common/radix_tree_speed_test.cc index 642ef712173e0..5f0af98ad4a5c 100644 --- a/test/common/common/trie_lookup_table_speed_test.cc +++ b/test/common/common/radix_tree_speed_test.cc @@ -5,7 +5,7 @@ #include "envoy/http/header_map.h" -#include "source/common/common/trie_lookup_table.h" +#include "source/common/common/radix_tree.h" #include "source/common/http/headers.h" #include "benchmark/benchmark.h" @@ -15,12 +15,12 @@ namespace Envoy { // NOLINT(namespace-envoy) template -static void typedBmTrieLookups(benchmark::State& state, std::vector& keys) { +static void typedBmRadixTreeLookups(benchmark::State& state, std::vector& keys) { std::mt19937 prng(1); // PRNG with a fixed seed, for repeatability std::uniform_int_distribution keyindex_distribution(0, keys.size() - 1); - TableType trie; + TableType radixtree; for (const std::string& key : keys) { - trie.add(key, nullptr); + radixtree.add(key, nullptr); } std::vector key_selections; for (size_t i = 0; i < 1024; i++) { @@ -29,12 +29,12 @@ static void typedBmTrieLookups(benchmark::State& state, std::vector // key_index indexes into key_selections which is a pre-selected // random ordering of 1024 indexes into the existing keys. This - // way we read from all over the trie, without spending time during + // way we read from all over the radixtree, without spending time during // the performance test generating these random choices. size_t key_index = 0; for (auto _ : state) { UNREFERENCED_PARAMETER(_); - auto v = trie.find(keys[key_selections[key_index++]]); + auto v = radixtree.find(keys[key_selections[key_index++]]); // Reset key_index to 0 whenever it reaches 1024. key_index &= 1023; benchmark::DoNotOptimize(v); @@ -44,7 +44,7 @@ static void typedBmTrieLookups(benchmark::State& state, std::vector // Range args are: // 0 - num_keys // 1 - key_length (0 is a special case that generates mixed-length keys) -template static void typedBmTrieLookups(benchmark::State& state) { +template static void typedBmRadixTreeLookups(benchmark::State& state) { std::mt19937 prng(1); // PRNG with a fixed seed, for repeatability int num_keys = state.range(0); int key_length = state.range(1); @@ -63,27 +63,27 @@ template static void typedBmTrieLookups(benchmark::State& stat std::string key = make_key(key_length_distribution(prng)); keys.push_back(std::move(key)); } - typedBmTrieLookups(state, keys); + typedBmRadixTreeLookups(state, keys); } -static void bmTrieLookups(benchmark::State& s) { - typedBmTrieLookups>(s); +static void bmRadixTreeLookups(benchmark::State& s) { + typedBmRadixTreeLookups>(s); } #define ADD_HEADER_TO_KEYS(name) keys.emplace_back(Http::Headers::get().name); -static void bmTrieLookupsRequestHeaders(benchmark::State& s) { +static void bmRadixTreeLookupsRequestHeaders(benchmark::State& s) { std::vector keys; INLINE_REQ_HEADERS(ADD_HEADER_TO_KEYS); - typedBmTrieLookups>(s, keys); + typedBmRadixTreeLookups>(s, keys); } -static void bmTrieLookupsResponseHeaders(benchmark::State& s) { +static void bmRadixTreeLookupsResponseHeaders(benchmark::State& s) { std::vector keys; INLINE_RESP_HEADERS(ADD_HEADER_TO_KEYS); - typedBmTrieLookups>(s, keys); + typedBmRadixTreeLookups>(s, keys); } -BENCHMARK(bmTrieLookupsRequestHeaders); -BENCHMARK(bmTrieLookupsResponseHeaders); -BENCHMARK(bmTrieLookups)->ArgsProduct({{10, 100, 1000, 10000}, {0, 8, 128}}); +BENCHMARK(bmRadixTreeLookupsRequestHeaders); +BENCHMARK(bmRadixTreeLookupsResponseHeaders); +BENCHMARK(bmRadixTreeLookups)->ArgsProduct({{10, 100, 1000, 10000}, {0, 8, 128}}); } // namespace Envoy diff --git a/test/common/common/radix_tree_test.cc b/test/common/common/radix_tree_test.cc new file mode 100644 index 0000000000000..521a401bf2a35 --- /dev/null +++ b/test/common/common/radix_tree_test.cc @@ -0,0 +1,439 @@ +#include "source/common/common/radix_tree.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::ElementsAre; + +namespace Envoy { + +TEST(RadixTree, AddItems) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + + EXPECT_TRUE(radixtree.add(std::string("foo"), cstr_a)); + EXPECT_TRUE(radixtree.add(std::string("bar"), cstr_b)); + EXPECT_EQ(cstr_a, radixtree.find("foo")); + EXPECT_EQ(cstr_b, radixtree.find("bar")); + + // overwrite_existing = false + EXPECT_FALSE(radixtree.add(std::string("foo"), cstr_c, false)); + EXPECT_EQ(cstr_a, radixtree.find("foo")); + + // overwrite_existing = true + EXPECT_TRUE(radixtree.add(std::string("foo"), cstr_c)); + EXPECT_EQ(cstr_c, radixtree.find("foo")); +} + +TEST(RadixTree, LongestPrefix) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + const char* cstr_d = "d"; + const char* cstr_e = "e"; + const char* cstr_f = "f"; + + EXPECT_TRUE(radixtree.add(std::string("foo/bar"), cstr_d)); + EXPECT_TRUE(radixtree.add(std::string("foo"), cstr_a)); + // Verify that prepending and appending branches to a node both work. + EXPECT_TRUE(radixtree.add(std::string("barn"), cstr_e)); + EXPECT_TRUE(radixtree.add(std::string("barp"), cstr_f)); + EXPECT_TRUE(radixtree.add(std::string("bar"), cstr_b)); + EXPECT_TRUE(radixtree.add(std::string("baro"), cstr_c)); + + EXPECT_EQ(cstr_a, radixtree.find("foo")); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("foo")); + EXPECT_THAT(radixtree.findMatchingPrefixes("foo"), ElementsAre(cstr_a)); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("foosball")); + EXPECT_THAT(radixtree.findMatchingPrefixes("foosball"), ElementsAre(cstr_a)); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("foo/")); + EXPECT_THAT(radixtree.findMatchingPrefixes("foo/"), ElementsAre(cstr_a)); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("foo/bar")); + EXPECT_THAT(radixtree.findMatchingPrefixes("foo/bar"), ElementsAre(cstr_a, cstr_d)); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("foo/bar/zzz")); + EXPECT_THAT(radixtree.findMatchingPrefixes("foo/bar/zzz"), ElementsAre(cstr_a, cstr_d)); + + EXPECT_EQ(cstr_b, radixtree.find("bar")); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("bar")); + EXPECT_THAT(radixtree.findMatchingPrefixes("bar"), ElementsAre(cstr_b)); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("baritone")); + EXPECT_THAT(radixtree.findMatchingPrefixes("baritone"), ElementsAre(cstr_b)); + EXPECT_EQ(cstr_c, radixtree.findLongestPrefix("barometer")); + EXPECT_THAT(radixtree.findMatchingPrefixes("barometer"), ElementsAre(cstr_b, cstr_c)); + + EXPECT_EQ(cstr_e, radixtree.find("barn")); + EXPECT_EQ(cstr_e, radixtree.findLongestPrefix("barnacle")); + EXPECT_THAT(radixtree.findMatchingPrefixes("barnacle"), ElementsAre(cstr_b, cstr_e)); + + EXPECT_EQ(cstr_f, radixtree.find("barp")); + EXPECT_EQ(cstr_f, radixtree.findLongestPrefix("barpomus")); + EXPECT_THAT(radixtree.findMatchingPrefixes("barpomus"), ElementsAre(cstr_b, cstr_f)); + + EXPECT_EQ(nullptr, radixtree.find("toto")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("toto")); + EXPECT_THAT(radixtree.findMatchingPrefixes("toto"), ElementsAre()); + EXPECT_EQ(nullptr, radixtree.find(" ")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix(" ")); + EXPECT_THAT(radixtree.findMatchingPrefixes(" "), ElementsAre()); +} + +TEST(RadixTree, VeryDeepRadixTreeDoesNotStackOverflowOnDestructor) { + RadixTree radixtree; + const char* cstr_a = "a"; + + std::string key_a(20960, 'a'); + EXPECT_TRUE(radixtree.add(key_a, cstr_a)); + EXPECT_EQ(cstr_a, radixtree.find(key_a)); +} + +TEST(RadixTree, RadixTreeSpecificTests) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + const char* cstr_d = "d"; + + // Test radix tree compression + EXPECT_TRUE(radixtree.add(std::string("test"), cstr_a)); + EXPECT_TRUE(radixtree.add(std::string("testing"), cstr_b)); + EXPECT_TRUE(radixtree.add(std::string("tester"), cstr_c)); + EXPECT_TRUE(radixtree.add(std::string("tested"), cstr_d)); + + EXPECT_EQ(cstr_a, radixtree.find("test")); + EXPECT_EQ(cstr_b, radixtree.find("testing")); + EXPECT_EQ(cstr_c, radixtree.find("tester")); + EXPECT_EQ(cstr_d, radixtree.find("tested")); + + // Test prefix matching + EXPECT_THAT(radixtree.findMatchingPrefixes("test"), ElementsAre(cstr_a)); + EXPECT_THAT(radixtree.findMatchingPrefixes("testing"), ElementsAre(cstr_a, cstr_b)); + EXPECT_THAT(radixtree.findMatchingPrefixes("tester"), ElementsAre(cstr_a, cstr_c)); + EXPECT_THAT(radixtree.findMatchingPrefixes("tested"), ElementsAre(cstr_a, cstr_d)); + + // Test longest prefix + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("test")); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("testing")); + EXPECT_EQ(cstr_c, radixtree.findLongestPrefix("tester")); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("tested")); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("testx")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("tex")); +} + +TEST(RadixTree, EmptyAndSingleNode) { + RadixTree radixtree; + const char* cstr_a = "a"; + + // Test empty radixtree + EXPECT_EQ(nullptr, radixtree.find("anything")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("anything")); + EXPECT_THAT(radixtree.findMatchingPrefixes("anything"), ElementsAre()); + + // Test single node + EXPECT_TRUE(radixtree.add(std::string("a"), cstr_a)); + EXPECT_EQ(cstr_a, radixtree.find("a")); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("a")); + EXPECT_THAT(radixtree.findMatchingPrefixes("a"), ElementsAre(cstr_a)); + EXPECT_EQ(nullptr, radixtree.find("b")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("b")); + EXPECT_THAT(radixtree.findMatchingPrefixes("b"), ElementsAre()); +} + +TEST(RadixTree, InsertAndFindEdgeCases) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + const char* cstr_d = "d"; + + // Test empty string + EXPECT_TRUE(radixtree.add(std::string(""), cstr_a)); + EXPECT_EQ(cstr_a, radixtree.find("")); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("")); + EXPECT_THAT(radixtree.findMatchingPrefixes(""), ElementsAre(cstr_a)); + + // Test single character + EXPECT_TRUE(radixtree.add(std::string("x"), cstr_b)); + EXPECT_EQ(cstr_b, radixtree.find("x")); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("x")); + EXPECT_THAT(radixtree.findMatchingPrefixes("x"), ElementsAre(cstr_a, cstr_b)); + + // Test very long string + std::string long_key(1000, 'a'); + EXPECT_TRUE(radixtree.add(long_key, cstr_c)); + EXPECT_EQ(cstr_c, radixtree.find(long_key)); + EXPECT_EQ(cstr_c, radixtree.findLongestPrefix(long_key)); + EXPECT_THAT(radixtree.findMatchingPrefixes(long_key), ElementsAre(cstr_a, cstr_c)); + + // Test special characters + EXPECT_TRUE(radixtree.add(std::string("test/key"), cstr_d)); + EXPECT_EQ(cstr_d, radixtree.find("test/key")); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("test/key")); + EXPECT_THAT(radixtree.findMatchingPrefixes("test/key"), ElementsAre(cstr_a, cstr_d)); + + // Test non-existent keys + EXPECT_EQ(nullptr, radixtree.find("nonexistent")); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("nonexistent")); + EXPECT_THAT(radixtree.findMatchingPrefixes("nonexistent"), ElementsAre(cstr_a)); +} + +TEST(RadixTree, InsertAndFindComplexScenarios) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + const char* cstr_d = "d"; + const char* cstr_e = "e"; + const char* cstr_f = "f"; + + // Test overlapping prefixes + EXPECT_TRUE(radixtree.add(std::string("test"), cstr_a)); + EXPECT_TRUE(radixtree.add(std::string("testing"), cstr_b)); + EXPECT_TRUE(radixtree.add(std::string("tester"), cstr_c)); + EXPECT_TRUE(radixtree.add(std::string("tested"), cstr_d)); + + // Verify all can be found + EXPECT_EQ(cstr_a, radixtree.find("test")); + EXPECT_EQ(cstr_b, radixtree.find("testing")); + EXPECT_EQ(cstr_c, radixtree.find("tester")); + EXPECT_EQ(cstr_d, radixtree.find("tested")); + + // Test prefix matching + EXPECT_THAT(radixtree.findMatchingPrefixes("test"), ElementsAre(cstr_a)); + EXPECT_THAT(radixtree.findMatchingPrefixes("testing"), ElementsAre(cstr_a, cstr_b)); + EXPECT_THAT(radixtree.findMatchingPrefixes("tester"), ElementsAre(cstr_a, cstr_c)); + EXPECT_THAT(radixtree.findMatchingPrefixes("tested"), ElementsAre(cstr_a, cstr_d)); + + // Test longest prefix + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("test")); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("testing")); + EXPECT_EQ(cstr_c, radixtree.findLongestPrefix("tester")); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("tested")); + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("testx")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("tex")); + + // Test branching scenarios + EXPECT_TRUE(radixtree.add(std::string("hello"), cstr_e)); + EXPECT_TRUE(radixtree.add(std::string("world"), cstr_f)); + + EXPECT_EQ(cstr_e, radixtree.find("hello")); + EXPECT_EQ(cstr_f, radixtree.find("world")); + EXPECT_EQ(cstr_e, radixtree.findLongestPrefix("hello")); + EXPECT_EQ(cstr_f, radixtree.findLongestPrefix("world")); +} + +TEST(RadixTree, InsertAndFindOverwriteBehavior) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + + // Test overwrite_existing = true (default) + EXPECT_TRUE(radixtree.add(std::string("key"), cstr_a)); + EXPECT_EQ(cstr_a, radixtree.find("key")); + + EXPECT_TRUE(radixtree.add(std::string("key"), cstr_b)); + EXPECT_EQ(cstr_b, radixtree.find("key")); + + // Test overwrite_existing = false + EXPECT_FALSE(radixtree.add(std::string("key"), cstr_c, false)); + EXPECT_EQ(cstr_b, radixtree.find("key")); // Should still be cstr_b + + // Test overwrite_existing = true explicitly + EXPECT_TRUE(radixtree.add(std::string("key"), cstr_c, true)); + EXPECT_EQ(cstr_c, radixtree.find("key")); +} + +TEST(RadixTree, InsertAndFindDeepNesting) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + + // Test deep nesting + EXPECT_TRUE(radixtree.add(std::string("a/b/c/d/e/f"), cstr_a)); + EXPECT_TRUE(radixtree.add(std::string("a/b/c/d/e/g"), cstr_b)); + EXPECT_TRUE(radixtree.add(std::string("a/b/c/d/e/h"), cstr_c)); + + EXPECT_EQ(cstr_a, radixtree.find("a/b/c/d/e/f")); + EXPECT_EQ(cstr_b, radixtree.find("a/b/c/d/e/g")); + EXPECT_EQ(cstr_c, radixtree.find("a/b/c/d/e/h")); + + // Test prefix matching on deep paths + EXPECT_THAT(radixtree.findMatchingPrefixes("a/b/c/d/e/f"), ElementsAre(cstr_a)); + EXPECT_THAT(radixtree.findMatchingPrefixes("a/b/c/d/e/g"), ElementsAre(cstr_b)); + EXPECT_THAT(radixtree.findMatchingPrefixes("a/b/c/d/e/h"), ElementsAre(cstr_c)); + + // Test longest prefix on deep paths + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("a/b/c/d/e/f")); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("a/b/c/d/e/g")); + EXPECT_EQ(cstr_c, radixtree.findLongestPrefix("a/b/c/d/e/h")); +} + +TEST(RadixTree, InsertAndFindMixedLengths) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + const char* cstr_d = "d"; + + // Test mixed length keys + EXPECT_TRUE(radixtree.add(std::string("a"), cstr_a)); + EXPECT_TRUE(radixtree.add(std::string("aa"), cstr_b)); + EXPECT_TRUE(radixtree.add(std::string("aaa"), cstr_c)); + EXPECT_TRUE(radixtree.add(std::string("aaaa"), cstr_d)); + + EXPECT_EQ(cstr_a, radixtree.find("a")); + EXPECT_EQ(cstr_b, radixtree.find("aa")); + EXPECT_EQ(cstr_c, radixtree.find("aaa")); + EXPECT_EQ(cstr_d, radixtree.find("aaaa")); + + // Test prefix matching + EXPECT_THAT(radixtree.findMatchingPrefixes("a"), ElementsAre(cstr_a)); + EXPECT_THAT(radixtree.findMatchingPrefixes("aa"), ElementsAre(cstr_a, cstr_b)); + EXPECT_THAT(radixtree.findMatchingPrefixes("aaa"), ElementsAre(cstr_a, cstr_b, cstr_c)); + EXPECT_THAT(radixtree.findMatchingPrefixes("aaaa"), ElementsAre(cstr_a, cstr_b, cstr_c, cstr_d)); + + // Test longest prefix + EXPECT_EQ(cstr_a, radixtree.findLongestPrefix("a")); + EXPECT_EQ(cstr_b, radixtree.findLongestPrefix("aa")); + EXPECT_EQ(cstr_c, radixtree.findLongestPrefix("aaa")); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("aaaa")); + EXPECT_EQ(cstr_d, radixtree.findLongestPrefix("aaaaa")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("b")); +} + +TEST(RadixTree, InsertAndFindSpecialCharacters) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + const char* cstr_c = "c"; + + // Test special characters + EXPECT_TRUE(radixtree.add(std::string("test-key"), cstr_a)); + EXPECT_TRUE(radixtree.add(std::string("test_key"), cstr_b)); + EXPECT_TRUE(radixtree.add(std::string("test.key"), cstr_c)); + + EXPECT_EQ(cstr_a, radixtree.find("test-key")); + EXPECT_EQ(cstr_b, radixtree.find("test_key")); + EXPECT_EQ(cstr_c, radixtree.find("test.key")); + + // Test with spaces + EXPECT_TRUE(radixtree.add(std::string("test key"), cstr_a)); + EXPECT_EQ(cstr_a, radixtree.find("test key")); + + // Test with numbers + EXPECT_TRUE(radixtree.add(std::string("test123"), cstr_b)); + EXPECT_EQ(cstr_b, radixtree.find("test123")); +} + +TEST(RadixTree, InsertAndFindBooleanInterface) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + + // Test boolean find interface + const char* result; + + result = radixtree.find("nonexistent"); + EXPECT_EQ(nullptr, result); + + EXPECT_TRUE(radixtree.add(std::string("key"), cstr_a)); + result = radixtree.find("key"); + EXPECT_EQ(cstr_a, result); + + EXPECT_TRUE(radixtree.add(std::string("key"), cstr_b)); + result = radixtree.find("key"); + EXPECT_EQ(cstr_b, result); + + // Test with empty string + EXPECT_TRUE(radixtree.add(std::string(""), cstr_a)); + result = radixtree.find(""); + EXPECT_EQ(cstr_a, result); +} + +TEST(RadixTree, BasicFunctionality) { + RadixTree radixtree; + const char* cstr_a = "a"; + const char* cstr_b = "b"; + + // Test simple insertion + EXPECT_TRUE(radixtree.add(std::string("test"), cstr_a)); + EXPECT_EQ(cstr_a, radixtree.find("test")); + + // Test second insertion + EXPECT_TRUE(radixtree.add(std::string("hello"), cstr_b)); + EXPECT_EQ(cstr_b, radixtree.find("hello")); + EXPECT_EQ(cstr_a, radixtree.find("test")); // Make sure first one still works +} + +TEST(RadixTree, StringOperations) { + RadixTree radixtree; + const char* value_a = "value_a"; + const char* value_b = "value_b"; + const char* value_c = "value_c"; + const char* value_d = "value_d"; + + // Test string operations with various scenarios. + EXPECT_TRUE(radixtree.add("test", value_a)); + EXPECT_TRUE(radixtree.add("testing", value_b)); + EXPECT_TRUE(radixtree.add("hello", value_c)); + EXPECT_TRUE(radixtree.add("world", value_d)); + + // Verify all insertions work correctly. + EXPECT_EQ(value_a, radixtree.find("test")); + EXPECT_EQ(value_b, radixtree.find("testing")); + EXPECT_EQ(value_c, radixtree.find("hello")); + EXPECT_EQ(value_d, radixtree.find("world")); + + // Test prefix matching. + EXPECT_EQ(value_a, radixtree.findLongestPrefix("test")); + EXPECT_EQ(value_b, radixtree.findLongestPrefix("testing")); + EXPECT_EQ(value_a, radixtree.findLongestPrefix("test_other")); + EXPECT_EQ(nullptr, radixtree.findLongestPrefix("xyz")); + + // Test that prefix matching works correctly. + EXPECT_THAT(radixtree.findMatchingPrefixes("testing"), ElementsAre(value_a, value_b)); + EXPECT_THAT(radixtree.findMatchingPrefixes("test"), ElementsAre(value_a)); +} + +TEST(RadixTree, PerformanceCharacteristics) { + RadixTree radixtree; + + // Test with a larger number of keys to verify performance characteristics. + const size_t num_keys = 100; // Reduced for clearer testing + std::vector keys; + keys.reserve(num_keys); + + // Generate keys with common prefixes to test radix tree compression. + for (size_t i = 0; i < num_keys; ++i) { + keys.push_back("prefix_" + std::to_string(i)); + } + + // Insert all keys. + for (size_t i = 0; i < keys.size(); ++i) { + int value = static_cast(i + 1); + EXPECT_TRUE(radixtree.add(keys[i], value)); + } + + // Verify all keys can be found. + for (size_t i = 0; i < keys.size(); ++i) { + int expected_value = static_cast(i + 1); + EXPECT_EQ(expected_value, radixtree.find(keys[i])); + } + + // Test prefix matching: "prefix_50_extra" should match "prefix_5" and "prefix_50". + auto prefix_matches = radixtree.findMatchingPrefixes("prefix_50_extra"); + EXPECT_GE(prefix_matches.size(), 1); // Should match at least "prefix_50" + + // Test longest prefix match. + int longest_match = radixtree.findLongestPrefix("prefix_50_extra"); + EXPECT_EQ(51, longest_match); // prefix_50 + 1 + + // Test non-matching prefix. + EXPECT_EQ(0, radixtree.findLongestPrefix("different_prefix")); // int default value is 0 +} + +} // namespace Envoy diff --git a/test/common/common/shared_pointer_speed_test.cc b/test/common/common/shared_pointer_speed_test.cc new file mode 100644 index 0000000000000..e219d3abbba4d --- /dev/null +++ b/test/common/common/shared_pointer_speed_test.cc @@ -0,0 +1,76 @@ +// Note: this should be run with --compilation_mode=opt, and would benefit from a +// quiescent system with disabled cstate power management. + +#include + +#include +#include +#include + +#include "source/common/common/assert.h" + +#include "benchmark/benchmark.h" + +namespace Envoy { + +class SimpleDataAccessor { +public: + class SimpleData { + public: + uint64_t inc() { return ++data_; } + + private: + uint64_t data_{}; + }; + + SimpleDataAccessor() : data_(std::make_shared()) {} + + std::shared_ptr dataSharedPtrByCopy() const { return data_; } + std::shared_ptr& dataSharedPtrByRef() { return data_; } + SimpleData& dataRefFromSharedPtr() { return *data_; } + SimpleData& dataRefFromStack() { return stack_data_; } + +private: + std::shared_ptr data_; + SimpleData stack_data_; +}; + +static void bmVerifySharedPtrAccessPerformance(benchmark::State& state) { + SimpleDataAccessor accessor; + + const size_t method = state.range(0); + + if (method == 0) { + for (auto _ : state) { + UNREFERENCED_PARAMETER(_); + for (size_t i = 0; i < 1000; i++) { + benchmark::DoNotOptimize(accessor.dataSharedPtrByCopy()->inc()); + } + } + } else if (method == 1) { + for (auto _ : state) { + UNREFERENCED_PARAMETER(_); + for (size_t i = 0; i < 1000; i++) { + benchmark::DoNotOptimize(accessor.dataSharedPtrByRef()->inc()); + } + } + } else if (method == 2) { + for (auto _ : state) { + UNREFERENCED_PARAMETER(_); + for (size_t i = 0; i < 1000; i++) { + benchmark::DoNotOptimize(accessor.dataRefFromSharedPtr().inc()); + } + } + } else { + ASSERT(method == 3); + for (auto _ : state) { + UNREFERENCED_PARAMETER(_); + for (size_t i = 0; i < 1000; i++) { + benchmark::DoNotOptimize(accessor.dataRefFromStack().inc()); + } + } + } +} +BENCHMARK(bmVerifySharedPtrAccessPerformance)->Args({0})->Args({1})->Args({2})->Args({3}); + +} // namespace Envoy diff --git a/test/common/common/trie_lookup_table_test.cc b/test/common/common/trie_lookup_table_test.cc deleted file mode 100644 index a36a21c486868..0000000000000 --- a/test/common/common/trie_lookup_table_test.cc +++ /dev/null @@ -1,92 +0,0 @@ -#include "source/common/common/trie_lookup_table.h" - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -using testing::ElementsAre; - -namespace Envoy { - -TEST(TrieLookupTable, AddItems) { - TrieLookupTable trie; - const char* cstr_a = "a"; - const char* cstr_b = "b"; - const char* cstr_c = "c"; - - EXPECT_TRUE(trie.add("foo", cstr_a)); - EXPECT_TRUE(trie.add("bar", cstr_b)); - EXPECT_EQ(cstr_a, trie.find("foo")); - EXPECT_EQ(cstr_b, trie.find("bar")); - - // overwrite_existing = false - EXPECT_FALSE(trie.add("foo", cstr_c, false)); - EXPECT_EQ(cstr_a, trie.find("foo")); - - // overwrite_existing = true - EXPECT_TRUE(trie.add("foo", cstr_c)); - EXPECT_EQ(cstr_c, trie.find("foo")); -} - -TEST(TrieLookupTable, LongestPrefix) { - TrieLookupTable trie; - const char* cstr_a = "a"; - const char* cstr_b = "b"; - const char* cstr_c = "c"; - const char* cstr_d = "d"; - const char* cstr_e = "e"; - const char* cstr_f = "f"; - - EXPECT_TRUE(trie.add("foo", cstr_a)); - EXPECT_TRUE(trie.add("bar", cstr_b)); - EXPECT_TRUE(trie.add("baro", cstr_c)); - EXPECT_TRUE(trie.add("foo/bar", cstr_d)); - // Verify that prepending and appending branches to a node both work. - EXPECT_TRUE(trie.add("barn", cstr_e)); - EXPECT_TRUE(trie.add("barp", cstr_f)); - - EXPECT_EQ(cstr_a, trie.find("foo")); - EXPECT_EQ(cstr_a, trie.findLongestPrefix("foo")); - EXPECT_THAT(trie.findMatchingPrefixes("foo"), ElementsAre(cstr_a)); - EXPECT_EQ(cstr_a, trie.findLongestPrefix("foosball")); - EXPECT_THAT(trie.findMatchingPrefixes("foosball"), ElementsAre(cstr_a)); - EXPECT_EQ(cstr_a, trie.findLongestPrefix("foo/")); - EXPECT_THAT(trie.findMatchingPrefixes("foo/"), ElementsAre(cstr_a)); - EXPECT_EQ(cstr_d, trie.findLongestPrefix("foo/bar")); - EXPECT_THAT(trie.findMatchingPrefixes("foo/bar"), ElementsAre(cstr_a, cstr_d)); - EXPECT_EQ(cstr_d, trie.findLongestPrefix("foo/bar/zzz")); - EXPECT_THAT(trie.findMatchingPrefixes("foo/bar/zzz"), ElementsAre(cstr_a, cstr_d)); - - EXPECT_EQ(cstr_b, trie.find("bar")); - EXPECT_EQ(cstr_b, trie.findLongestPrefix("bar")); - EXPECT_THAT(trie.findMatchingPrefixes("bar"), ElementsAre(cstr_b)); - EXPECT_EQ(cstr_b, trie.findLongestPrefix("baritone")); - EXPECT_THAT(trie.findMatchingPrefixes("baritone"), ElementsAre(cstr_b)); - EXPECT_EQ(cstr_c, trie.findLongestPrefix("barometer")); - EXPECT_THAT(trie.findMatchingPrefixes("barometer"), ElementsAre(cstr_b, cstr_c)); - - EXPECT_EQ(cstr_e, trie.find("barn")); - EXPECT_EQ(cstr_e, trie.findLongestPrefix("barnacle")); - EXPECT_THAT(trie.findMatchingPrefixes("barnacle"), ElementsAre(cstr_b, cstr_e)); - - EXPECT_EQ(cstr_f, trie.find("barp")); - EXPECT_EQ(cstr_f, trie.findLongestPrefix("barpomus")); - EXPECT_THAT(trie.findMatchingPrefixes("barpomus"), ElementsAre(cstr_b, cstr_f)); - - EXPECT_EQ(nullptr, trie.find("toto")); - EXPECT_EQ(nullptr, trie.findLongestPrefix("toto")); - EXPECT_THAT(trie.findMatchingPrefixes("toto"), ElementsAre()); - EXPECT_EQ(nullptr, trie.find(" ")); - EXPECT_EQ(nullptr, trie.findLongestPrefix(" ")); - EXPECT_THAT(trie.findMatchingPrefixes(" "), ElementsAre()); -} - -TEST(TrieLookupTable, VeryDeepTrieDoesNotStackOverflowOnDestructor) { - TrieLookupTable trie; - const char* cstr_a = "a"; - - std::string key_a(20960, 'a'); - EXPECT_TRUE(trie.add(key_a, cstr_a)); - EXPECT_EQ(cstr_a, trie.find(key_a)); -} - -} // namespace Envoy diff --git a/test/common/config/custom_config_validators_impl_test.cc b/test/common/config/custom_config_validators_impl_test.cc index f907fedaf2381..0ac2434711d24 100644 --- a/test/common/config/custom_config_validators_impl_test.cc +++ b/test/common/config/custom_config_validators_impl_test.cc @@ -36,15 +36,15 @@ class FakeConfigValidatorFactory : public ConfigValidatorFactory { public: FakeConfigValidatorFactory(bool should_reject) : should_reject_(should_reject) {} - ConfigValidatorPtr createConfigValidator(const ProtobufWkt::Any&, + ConfigValidatorPtr createConfigValidator(const Protobuf::Any&, ProtobufMessage::ValidationVisitor&) override { return std::make_unique(should_reject_); } Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom empty config proto. This is only allowed in tests. - return should_reject_ ? ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()} - : ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Value()}; + return should_reject_ ? ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()} + : ProtobufTypes::MessagePtr{new Envoy::Protobuf::Value()}; } std::string name() const override { diff --git a/test/common/config/datasource_test.cc b/test/common/config/datasource_test.cc index b5bfcbceac5b0..203e6435608c5 100644 --- a/test/common/config/datasource_test.cc +++ b/test/common/config/datasource_test.cc @@ -1,3 +1,5 @@ +#include + #include "envoy/config/core/v3/base.pb.h" #include "envoy/config/core/v3/base.pb.validate.h" diff --git a/test/common/config/decoded_resource_impl_test.cc b/test/common/config/decoded_resource_impl_test.cc index 7ebfe808cb81a..5d1e4ae9837eb 100644 --- a/test/common/config/decoded_resource_impl_test.cc +++ b/test/common/config/decoded_resource_impl_test.cc @@ -14,21 +14,21 @@ namespace { TEST(DecodedResourceImplTest, All) { MockOpaqueResourceDecoder resource_decoder; - ProtobufWkt::Any some_opaque_resource; + Protobuf::Any some_opaque_resource; some_opaque_resource.set_type_url("some_type_url"); { EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(some_opaque_resource))) .WillOnce(InvokeWithoutArgs( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); - EXPECT_CALL(resource_decoder, resourceName(ProtoEq(ProtobufWkt::Empty()))) + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); + EXPECT_CALL(resource_decoder, resourceName(ProtoEq(Protobuf::Empty()))) .WillOnce(Return("some_name")); auto decoded_resource = *DecodedResourceImpl::fromResource(resource_decoder, some_opaque_resource, "foo"); EXPECT_EQ("some_name", decoded_resource->name()); EXPECT_TRUE(decoded_resource->aliases().empty()); EXPECT_EQ("foo", decoded_resource->version()); - EXPECT_THAT(decoded_resource->resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource->resource(), ProtoEq(Protobuf::Empty())); EXPECT_TRUE(decoded_resource->hasResource()); } @@ -41,13 +41,13 @@ TEST(DecodedResourceImplTest, All) { resource_wrapper.set_version("foo"); EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(some_opaque_resource))) .WillOnce(InvokeWithoutArgs( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); - EXPECT_CALL(resource_decoder, resourceName(ProtoEq(ProtobufWkt::Empty()))).Times(0); + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); + EXPECT_CALL(resource_decoder, resourceName(ProtoEq(Protobuf::Empty()))).Times(0); DecodedResourceImpl decoded_resource(resource_decoder, resource_wrapper); EXPECT_EQ("real_name", decoded_resource.name()); EXPECT_EQ((std::vector{"bar", "baz"}), decoded_resource.aliases()); EXPECT_EQ("foo", decoded_resource.version()); - EXPECT_THAT(decoded_resource.resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource.resource(), ProtoEq(Protobuf::Empty())); EXPECT_TRUE(decoded_resource.hasResource()); EXPECT_FALSE(decoded_resource.metadata().has_value()); } @@ -62,18 +62,18 @@ TEST(DecodedResourceImplTest, All) { {"fake_test_domain", MessageUtil::keyValueStruct("fake_test_key", "fake_test_value")}); EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(some_opaque_resource))) .WillOnce(InvokeWithoutArgs( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); - EXPECT_CALL(resource_decoder, resourceName(ProtoEq(ProtobufWkt::Empty()))).Times(0); + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); + EXPECT_CALL(resource_decoder, resourceName(ProtoEq(Protobuf::Empty()))).Times(0); DecodedResourceImpl decoded_resource(resource_decoder, resource_wrapper); EXPECT_EQ("real_name", decoded_resource.name()); - EXPECT_THAT(decoded_resource.resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource.resource(), ProtoEq(Protobuf::Empty())); EXPECT_TRUE(decoded_resource.hasResource()); EXPECT_TRUE(decoded_resource.metadata().has_value()); EXPECT_EQ(metadata->DebugString(), decoded_resource.metadata()->DebugString()); } // To verify the metadata is decoded as expected for the fromResource variant - // with ProtobufWkt::Any& input. + // with Protobuf::Any& input. { envoy::service::discovery::v3::Resource resource_wrapper; resource_wrapper.set_name("real_name"); @@ -81,16 +81,16 @@ TEST(DecodedResourceImplTest, All) { auto metadata = resource_wrapper.mutable_metadata(); metadata->mutable_filter_metadata()->insert( {"fake_test_domain", MessageUtil::keyValueStruct("fake_test_key", "fake_test_value")}); - ProtobufWkt::Any resource_any; + Protobuf::Any resource_any; resource_any.PackFrom(resource_wrapper); EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(some_opaque_resource))) .WillOnce(InvokeWithoutArgs( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); - EXPECT_CALL(resource_decoder, resourceName(ProtoEq(ProtobufWkt::Empty()))).Times(0); + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); + EXPECT_CALL(resource_decoder, resourceName(ProtoEq(Protobuf::Empty()))).Times(0); DecodedResourceImplPtr decoded_resource = *DecodedResourceImpl::fromResource(resource_decoder, resource_any, "1"); EXPECT_EQ("real_name", decoded_resource->name()); - EXPECT_THAT(decoded_resource->resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource->resource(), ProtoEq(Protobuf::Empty())); EXPECT_TRUE(decoded_resource->hasResource()); EXPECT_TRUE(decoded_resource->metadata().has_value()); EXPECT_EQ(metadata->DebugString(), decoded_resource->metadata()->DebugString()); @@ -102,15 +102,15 @@ TEST(DecodedResourceImplTest, All) { resource_wrapper.set_version("foo"); resource_wrapper.add_aliases("bar"); resource_wrapper.add_aliases("baz"); - EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(ProtobufWkt::Any()))) + EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(Protobuf::Any()))) .WillOnce(InvokeWithoutArgs( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); EXPECT_CALL(resource_decoder, resourceName(_)).Times(0); DecodedResourceImpl decoded_resource(resource_decoder, resource_wrapper); EXPECT_EQ("real_name", decoded_resource.name()); EXPECT_EQ((std::vector{"bar", "baz"}), decoded_resource.aliases()); EXPECT_EQ("foo", decoded_resource.version()); - EXPECT_THAT(decoded_resource.resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource.resource(), ProtoEq(Protobuf::Empty())); EXPECT_FALSE(decoded_resource.hasResource()); } @@ -126,26 +126,26 @@ TEST(DecodedResourceImplTest, All) { {"fake_test_domain", MessageUtil::keyValueStruct("fake_test_key", "fake_test_value")}); EXPECT_CALL(resource_decoder, decodeResource(ProtoEq(some_opaque_resource))) .WillOnce(InvokeWithoutArgs( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); - EXPECT_CALL(resource_decoder, resourceName(ProtoEq(ProtobufWkt::Empty()))).Times(0); + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); + EXPECT_CALL(resource_decoder, resourceName(ProtoEq(Protobuf::Empty()))).Times(0); DecodedResourceImplPtr decoded_resource = DecodedResourceImpl::fromResource(resource_decoder, resource_wrapper); EXPECT_EQ("real_name", decoded_resource->name()); EXPECT_EQ((std::vector{"bar", "baz"}), decoded_resource->aliases()); EXPECT_EQ("foo", decoded_resource->version()); - EXPECT_THAT(decoded_resource->resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource->resource(), ProtoEq(Protobuf::Empty())); EXPECT_TRUE(decoded_resource->hasResource()); EXPECT_TRUE(decoded_resource->metadata().has_value()); EXPECT_EQ(metadata->DebugString(), decoded_resource->metadata()->DebugString()); } { - auto message = std::make_unique(); + auto message = std::make_unique(); DecodedResourceImpl decoded_resource(std::move(message), "real_name", {"bar", "baz"}, "foo"); EXPECT_EQ("real_name", decoded_resource.name()); EXPECT_EQ((std::vector{"bar", "baz"}), decoded_resource.aliases()); EXPECT_EQ("foo", decoded_resource.version()); - EXPECT_THAT(decoded_resource.resource(), ProtoEq(ProtobufWkt::Empty())); + EXPECT_THAT(decoded_resource.resource(), ProtoEq(Protobuf::Empty())); EXPECT_TRUE(decoded_resource.hasResource()); } } diff --git a/test/common/config/grpc_subscription_test_harness.h b/test/common/config/grpc_subscription_test_harness.h index 13c02090c0f21..8c265f7e0f9ef 100644 --- a/test/common/config/grpc_subscription_test_harness.h +++ b/test/common/config/grpc_subscription_test_harness.h @@ -80,8 +80,9 @@ class GrpcSubscriptionTestHarness : public SubscriptionTestHarness { mux_ = std::make_shared(grpc_mux_context, true); } subscription_ = std::make_unique( - mux_, callbacks_, resource_decoder_, stats_, Config::TypeUrl::get().ClusterLoadAssignment, - dispatcher_, init_fetch_timeout, false, SubscriptionOptions()); + mux_, callbacks_, resource_decoder_, stats_, + Config::TestTypeUrl::get().ClusterLoadAssignment, dispatcher_, init_fetch_timeout, false, + SubscriptionOptions()); } ~GrpcSubscriptionTestHarness() override { @@ -111,7 +112,7 @@ class GrpcSubscriptionTestHarness : public SubscriptionTestHarness { expected_request.set_version_info(version); } expected_request.set_response_nonce(last_response_nonce_); - expected_request.set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + expected_request.set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); if (error_code != Grpc::Status::WellKnownGrpcStatus::Ok) { ::google::rpc::Status* error_detail = expected_request.mutable_error_detail(); error_detail->set_code(error_code); @@ -159,7 +160,7 @@ class GrpcSubscriptionTestHarness : public SubscriptionTestHarness { response->set_version_info(version); last_response_nonce_ = std::to_string(HashUtil::xxHash64(version)); response->set_nonce(last_response_nonce_); - response->set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + response->set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); response->mutable_control_plane()->set_identifier("ground_control_foo123"); Protobuf::RepeatedPtrField typed_resources; for (const auto& cluster : cluster_names) { diff --git a/test/common/config/metadata_test.cc b/test/common/config/metadata_test.cc index 2b37e9d034a70..65d2223a3007d 100644 --- a/test/common/config/metadata_test.cc +++ b/test/common/config/metadata_test.cc @@ -33,20 +33,20 @@ TEST(MetadataTest, MetadataValuePath) { std::vector path{"test_obj", "inner_key"}; // not found case EXPECT_EQ(Metadata::metadataValue(&metadata, filter, path).kind_case(), - ProtobufWkt::Value::KindCase::KIND_NOT_SET); - ProtobufWkt::Struct& filter_struct = (*metadata.mutable_filter_metadata())[filter]; + Protobuf::Value::KindCase::KIND_NOT_SET); + Protobuf::Struct& filter_struct = (*metadata.mutable_filter_metadata())[filter]; auto obj = MessageUtil::keyValueStruct("inner_key", "inner_value"); - ProtobufWkt::Value val; + Protobuf::Value val; *val.mutable_struct_value() = obj; (*filter_struct.mutable_fields())["test_obj"] = val; EXPECT_EQ(Metadata::metadataValue(&metadata, filter, path).string_value(), "inner_value"); // not found with longer path path.push_back("bad_key"); EXPECT_EQ(Metadata::metadataValue(&metadata, filter, path).kind_case(), - ProtobufWkt::Value::KindCase::KIND_NOT_SET); + Protobuf::Value::KindCase::KIND_NOT_SET); // empty path returns not found EXPECT_EQ(Metadata::metadataValue(&metadata, filter, std::vector{}).kind_case(), - ProtobufWkt::Value::KindCase::KIND_NOT_SET); + Protobuf::Value::KindCase::KIND_NOT_SET); } class TypedMetadataTest : public testing::Test { @@ -65,17 +65,16 @@ class TypedMetadataTest : public testing::Test { class FoobarFactory : public TypedMetadataFactory { public: // Throws EnvoyException (conversion failure) if d is empty. - std::unique_ptr - parse(const ProtobufWkt::Struct& d) const override { + std::unique_ptr parse(const Protobuf::Struct& d) const override { if (d.fields().find("name") != d.fields().end()) { return std::make_unique(d.fields().at("name").string_value()); } throw EnvoyException("Cannot create a Foo when Struct metadata is empty."); } - std::unique_ptr parse(const ProtobufWkt::Any& d) const override { + std::unique_ptr parse(const Protobuf::Any& d) const override { if (!(d.type_url().empty())) { - return std::make_unique(std::string(d.value())); + return std::make_unique(MessageUtil::bytesToString(d.value())); } throw EnvoyException("Cannot create a Foo when Any metadata is empty."); } @@ -97,7 +96,7 @@ class TypedMetadataTest : public testing::Test { std::string name() const override { return "baz"; } using FoobarFactory::parse; // Override Any parse() to just return nullptr. - std::unique_ptr parse(const ProtobufWkt::Any&) const override { + std::unique_ptr parse(const Protobuf::Any&) const override { return nullptr; } }; @@ -126,7 +125,7 @@ TEST_F(TypedMetadataTest, OkTestStruct) { // Tests data parsing and retrieving when only Any field present in the metadata. TEST_F(TypedMetadataTest, OkTestAny) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Any any; + Protobuf::Any any; any.set_type_url("type.googleapis.com/waldo"); any.set_value("fred"); (*metadata.mutable_typed_filter_metadata())[bar_factory_.name()] = any; @@ -139,7 +138,7 @@ TEST_F(TypedMetadataTest, OkTestAny) { // also Any data parsing method just return nullptr. TEST_F(TypedMetadataTest, OkTestAnyParseReturnNullptr) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Any any; + Protobuf::Any any; any.set_type_url("type.googleapis.com/waldo"); any.set_value("fred"); (*metadata.mutable_typed_filter_metadata())[baz_factory_.name()] = any; @@ -153,7 +152,7 @@ TEST_F(TypedMetadataTest, OkTestBothSameFactory) { envoy::config::core::v3::Metadata metadata; (*metadata.mutable_filter_metadata())[foo_factory_.name()] = MessageUtil::keyValueStruct("name", "garply"); - ProtobufWkt::Any any; + Protobuf::Any any; any.set_type_url("type.googleapis.com/waldo"); any.set_value("fred"); (*metadata.mutable_typed_filter_metadata())[foo_factory_.name()] = any; @@ -170,7 +169,7 @@ TEST_F(TypedMetadataTest, OkTestBothDifferentFactory) { envoy::config::core::v3::Metadata metadata; (*metadata.mutable_filter_metadata())[foo_factory_.name()] = MessageUtil::keyValueStruct("name", "garply"); - ProtobufWkt::Any any; + Protobuf::Any any; any.set_type_url("type.googleapis.com/waldo"); any.set_value("fred"); (*metadata.mutable_typed_filter_metadata())[bar_factory_.name()] = any; @@ -192,7 +191,7 @@ TEST_F(TypedMetadataTest, OkTestBothSameFactoryAnyParseReturnNullptr) { envoy::config::core::v3::Metadata metadata; (*metadata.mutable_filter_metadata())[baz_factory_.name()] = MessageUtil::keyValueStruct("name", "garply"); - ProtobufWkt::Any any; + Protobuf::Any any; any.set_type_url("type.googleapis.com/waldo"); any.set_value("fred"); (*metadata.mutable_typed_filter_metadata())[baz_factory_.name()] = any; @@ -237,7 +236,7 @@ TEST_F(TypedMetadataTest, StructMetadataRefreshTest) { // Tests data parsing and retrieving when Any metadata updates. TEST_F(TypedMetadataTest, AnyMetadataRefreshTest) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Any any; + Protobuf::Any any; any.set_type_url("type.googleapis.com/waldo"); any.set_value("fred"); (*metadata.mutable_typed_filter_metadata())[bar_factory_.name()] = any; @@ -262,7 +261,7 @@ TEST_F(TypedMetadataTest, AnyMetadataRefreshTest) { // Tests empty Struct metadata parsing case. TEST_F(TypedMetadataTest, InvalidStructMetadataTest) { envoy::config::core::v3::Metadata metadata; - (*metadata.mutable_filter_metadata())[foo_factory_.name()] = ProtobufWkt::Struct(); + (*metadata.mutable_filter_metadata())[foo_factory_.name()] = Protobuf::Struct(); EXPECT_THROW_WITH_MESSAGE(TypedMetadataImpl typed(metadata), Envoy::EnvoyException, "Cannot create a Foo when Struct metadata is empty."); @@ -271,7 +270,7 @@ TEST_F(TypedMetadataTest, InvalidStructMetadataTest) { // Tests empty Any metadata parsing case. TEST_F(TypedMetadataTest, InvalidAnyMetadataTest) { envoy::config::core::v3::Metadata metadata; - (*metadata.mutable_typed_filter_metadata())[bar_factory_.name()] = ProtobufWkt::Any(); + (*metadata.mutable_typed_filter_metadata())[bar_factory_.name()] = Protobuf::Any(); EXPECT_THROW_WITH_MESSAGE(TypedMetadataImpl typed(metadata), Envoy::EnvoyException, "Cannot create a Foo when Any metadata is empty."); diff --git a/test/common/config/opaque_resource_decoder_impl_test.cc b/test/common/config/opaque_resource_decoder_impl_test.cc index 332d436b32822..4541c155c0258 100644 --- a/test/common/config/opaque_resource_decoder_impl_test.cc +++ b/test/common/config/opaque_resource_decoder_impl_test.cc @@ -16,7 +16,7 @@ class OpaqueResourceDecoderImplTest : public testing::Test { public: std::pair decodeTypedResource(const envoy::config::endpoint::v3::ClusterLoadAssignment& typed_resource) { - ProtobufWkt::Any opaque_resource; + Protobuf::Any opaque_resource; opaque_resource.PackFrom(typed_resource); auto decoded_resource = resource_decoder_.decodeResource(opaque_resource); const std::string name = resource_decoder_.resourceName(*decoded_resource); @@ -30,7 +30,7 @@ class OpaqueResourceDecoderImplTest : public testing::Test { // Negative test for bad type URL in Any. TEST_F(OpaqueResourceDecoderImplTest, WrongType) { - ProtobufWkt::Any opaque_resource; + Protobuf::Any opaque_resource; opaque_resource.set_type_url("huh"); EXPECT_THROW_WITH_REGEX(resource_decoder_.decodeResource(opaque_resource), EnvoyException, "Unable to unpack"); @@ -39,7 +39,7 @@ TEST_F(OpaqueResourceDecoderImplTest, WrongType) { // If the Any is empty (no type set), the default instance of the opaque resource decoder type is // created. TEST_F(OpaqueResourceDecoderImplTest, Empty) { - ProtobufWkt::Any opaque_resource; + Protobuf::Any opaque_resource; const auto decoded_resource = resource_decoder_.decodeResource(opaque_resource); EXPECT_THAT(*decoded_resource, ProtoEq(envoy::config::endpoint::v3::ClusterLoadAssignment())); EXPECT_EQ("", resource_decoder_.resourceName(*decoded_resource)); @@ -61,7 +61,7 @@ TEST_F(OpaqueResourceDecoderImplTest, ValidateIgnored) { auto* unknown = strange_resource.GetReflection()->MutableUnknownFields(&strange_resource); // add a field that doesn't exist in the proto definition: unknown->AddFixed32(1000, 1); - ProtobufWkt::Any opaque_resource; + Protobuf::Any opaque_resource; opaque_resource.PackFrom(strange_resource); const auto decoded_resource = resource_decoder.decodeResource(opaque_resource); EXPECT_THAT(*decoded_resource, ProtoEq(strange_resource)); diff --git a/test/common/config/utility_test.cc b/test/common/config/utility_test.cc index e1a7a4f40faec..ee9d81b292aad 100644 --- a/test/common/config/utility_test.cc +++ b/test/common/config/utility_test.cc @@ -729,11 +729,11 @@ TEST(UtilityTest, PrepareJitteredExponentialBackOffStrategyCustomValues) { // Validate that an opaque config of the wrong type throws during conversion. TEST(UtilityTest, AnyWrongType) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(source_duration); - ProtobufWkt::Timestamp out; + Protobuf::Timestamp out; EXPECT_THAT( Utility::translateOpaqueConfig(typed_config, ProtobufMessage::getStrictValidationVisitor(), out) @@ -743,14 +743,14 @@ TEST(UtilityTest, AnyWrongType) { } TEST(UtilityTest, TranslateAnyWrongToFactoryConfig) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(source_duration); MockTypedFactory factory; EXPECT_CALL(factory, createEmptyConfigProto()).WillOnce(Invoke([]() -> ProtobufTypes::MessagePtr { - return ProtobufTypes::MessagePtr{new ProtobufWkt::Timestamp()}; + return ProtobufTypes::MessagePtr{new Protobuf::Timestamp()}; })); EXPECT_THROW_WITH_REGEX( @@ -761,14 +761,14 @@ TEST(UtilityTest, TranslateAnyWrongToFactoryConfig) { } TEST(UtilityTest, TranslateAnyToFactoryConfig) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(source_duration); MockTypedFactory factory; EXPECT_CALL(factory, createEmptyConfigProto()).WillOnce(Invoke([]() -> ProtobufTypes::MessagePtr { - return ProtobufTypes::MessagePtr{new ProtobufWkt::Duration()}; + return ProtobufTypes::MessagePtr{new Protobuf::Duration()}; })); auto config = Utility::translateAnyToFactoryConfig( @@ -779,8 +779,7 @@ TEST(UtilityTest, TranslateAnyToFactoryConfig) { template class UtilityTypedStructTest : public ::testing::Test { public: - static void packTypedStructIntoAny(ProtobufWkt::Any& typed_config, - const Protobuf::Message& inner) { + static void packTypedStructIntoAny(Protobuf::Any& typed_config, const Protobuf::Message& inner) { T typed_struct; (*typed_struct.mutable_type_url()) = absl::StrCat("type.googleapis.com/", inner.GetDescriptor()->full_name()); @@ -794,12 +793,12 @@ TYPED_TEST_SUITE(UtilityTypedStructTest, TypedStructTypes); // Verify that TypedStruct can be translated into google.protobuf.Struct TYPED_TEST(UtilityTypedStructTest, TypedStructToStruct) { - ProtobufWkt::Any typed_config; - ProtobufWkt::Struct untyped_struct; + Protobuf::Any typed_config; + Protobuf::Struct untyped_struct; (*untyped_struct.mutable_fields())["foo"].set_string_value("bar"); this->packTypedStructIntoAny(typed_config, untyped_struct); - ProtobufWkt::Struct out; + Protobuf::Struct out; EXPECT_TRUE(Utility::translateOpaqueConfig(typed_config, ProtobufMessage::getStrictValidationVisitor(), out) .ok()); @@ -810,7 +809,7 @@ TYPED_TEST(UtilityTypedStructTest, TypedStructToStruct) { // Verify that TypedStruct can be translated into an arbitrary message of correct type // (v2 API, no upgrading). TYPED_TEST(UtilityTypedStructTest, TypedStructToClusterV2) { - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; API_NO_BOOST(envoy::api::v2::Cluster) cluster; const std::string cluster_config_yaml = R"EOF( drain_connections_on_host_removal: true @@ -837,7 +836,7 @@ TYPED_TEST(UtilityTypedStructTest, TypedStructToClusterV2) { // Verify that TypedStruct can be translated into an arbitrary message of correct type // (v3 API, upgrading). TYPED_TEST(UtilityTypedStructTest, TypedStructToClusterV3) { - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; API_NO_BOOST(envoy::config::cluster::v3::Cluster) cluster; const std::string cluster_config_yaml = R"EOF( ignore_health_on_host_removal: true @@ -863,7 +862,7 @@ TYPED_TEST(UtilityTypedStructTest, TypedStructToClusterV3) { // Verify that translation from TypedStruct into message of incorrect type fails TYPED_TEST(UtilityTypedStructTest, TypedStructToInvalidType) { - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; envoy::config::bootstrap::v3::Bootstrap bootstrap; const std::string bootstrap_config_yaml = R"EOF( admin: @@ -879,7 +878,7 @@ TYPED_TEST(UtilityTypedStructTest, TypedStructToInvalidType) { TestUtility::loadFromYaml(bootstrap_config_yaml, bootstrap); this->packTypedStructIntoAny(typed_config, bootstrap); - ProtobufWkt::Any out; + Protobuf::Any out; EXPECT_THROW_WITH_REGEX(Utility::translateOpaqueConfig( typed_config, ProtobufMessage::getStrictValidationVisitor(), out) .IgnoreError(), @@ -889,7 +888,7 @@ TYPED_TEST(UtilityTypedStructTest, TypedStructToInvalidType) { // Verify that Any can be translated into an arbitrary message of correct type // (v2 API, no upgrading). TEST(UtilityTest, AnyToClusterV2) { - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; API_NO_BOOST(envoy::api::v2::Cluster) cluster; const std::string cluster_config_yaml = R"EOF( drain_connections_on_host_removal: true @@ -907,7 +906,7 @@ TEST(UtilityTest, AnyToClusterV2) { // Verify that Any can be translated into an arbitrary message of correct type // (v3 API, upgrading). TEST(UtilityTest, AnyToClusterV3) { - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; API_NO_BOOST(envoy::config::cluster::v3::Cluster) cluster; const std::string cluster_config_yaml = R"EOF( ignore_health_on_host_removal: true @@ -922,10 +921,10 @@ TEST(UtilityTest, AnyToClusterV3) { EXPECT_THAT(out, ProtoEq(cluster)); } -// Verify that ProtobufWkt::Empty can load into a typed factory with an empty config proto +// Verify that Protobuf::Empty can load into a typed factory with an empty config proto TEST(UtilityTest, EmptyToEmptyConfig) { - ProtobufWkt::Any typed_config; - ProtobufWkt::Empty empty_config; + Protobuf::Any typed_config; + Protobuf::Empty empty_config; typed_config.PackFrom(empty_config); envoy::extensions::filters::http::cors::v3::Cors out; diff --git a/test/common/config/xds_manager_impl_test.cc b/test/common/config/xds_manager_impl_test.cc index 2df95a37e7885..cbf01c150b1fb 100644 --- a/test/common/config/xds_manager_impl_test.cc +++ b/test/common/config/xds_manager_impl_test.cc @@ -36,7 +36,7 @@ class MockGrpcMuxFactory : public MuxFactory { MockGrpcMuxFactory(absl::string_view name = "envoy.config_mux.grpc_mux_factory") : name_(name) { ON_CALL(*this, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillByDefault(Invoke( - [](std::unique_ptr&&, std::unique_ptr&&, + [](std::shared_ptr&&, std::shared_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, std::unique_ptr&&, BackOffStrategyPtr&&, @@ -50,7 +50,7 @@ class MockGrpcMuxFactory : public MuxFactory { void shutdownAll() override {} MOCK_METHOD(std::shared_ptr, create, - (std::unique_ptr&&, std::unique_ptr&&, + (std::shared_ptr&&, std::shared_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, std::unique_ptr&&, BackOffStrategyPtr&&, @@ -64,14 +64,14 @@ class FakeConfigValidatorFactory : public Config::ConfigValidatorFactory { public: FakeConfigValidatorFactory() = default; - Config::ConfigValidatorPtr createConfigValidator(const ProtobufWkt::Any&, + Config::ConfigValidatorPtr createConfigValidator(const Protobuf::Any&, ProtobufMessage::ValidationVisitor&) override { return nullptr; } Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Value instead of a custom empty config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Value()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Value()}; } std::string name() const override { return "envoy.fake_validator"; } @@ -173,8 +173,8 @@ TEST_F(XdsManagerImplTest, AdsReplacementPrimaryOnly) { NiceMock& ads_mux(*ads_mux_shared.get()); EXPECT_CALL(factory, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillOnce(Invoke( - [&ads_mux_shared](std::unique_ptr&& primary_async_client, - std::unique_ptr&& failover_async_client, + [&ads_mux_shared](std::shared_ptr&& primary_async_client, + std::shared_ptr&& failover_async_client, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, @@ -237,10 +237,10 @@ TEST_F(XdsManagerImplTest, AdsReplacementPrimaryOnly) { )EOF", new_ads_config); - Grpc::RawAsyncClientPtr failover_client; + Grpc::RawAsyncClientSharedPtr failover_client; EXPECT_CALL(ads_mux, updateMuxSource(_, _, _, _, ProtoEq(new_ads_config))) - .WillOnce(Invoke([](Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope&, + .WillOnce(Invoke([](Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope&, BackOffStrategyPtr&&, const envoy::config::core::v3::ApiConfigSource&) -> absl::Status { EXPECT_NE(primary_async_client, nullptr); @@ -264,8 +264,8 @@ TEST_F(XdsManagerImplTest, AdsReplacementPrimaryAndFailover) { NiceMock& ads_mux(*ads_mux_shared.get()); EXPECT_CALL(factory, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillOnce(Invoke( - [&ads_mux_shared](std::unique_ptr&& primary_async_client, - std::unique_ptr&& failover_async_client, + [&ads_mux_shared](std::shared_ptr&& primary_async_client, + std::shared_ptr&& failover_async_client, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, @@ -332,10 +332,10 @@ TEST_F(XdsManagerImplTest, AdsReplacementPrimaryAndFailover) { )EOF", new_ads_config); - Grpc::RawAsyncClientPtr failover_client; + Grpc::RawAsyncClientSharedPtr failover_client; EXPECT_CALL(ads_mux, updateMuxSource(_, _, _, _, ProtoEq(new_ads_config))) - .WillOnce(Invoke([](Grpc::RawAsyncClientPtr&& primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope&, + .WillOnce(Invoke([](Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope&, BackOffStrategyPtr&&, const envoy::config::core::v3::ApiConfigSource&) -> absl::Status { EXPECT_NE(primary_async_client, nullptr); @@ -810,8 +810,8 @@ class XdsManagerImplXdstpConfigSourcesTest : public testing::Test { if (enable_authority_a) { EXPECT_CALL(grpc_mux_factory_, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillOnce(Invoke( - [&](std::unique_ptr&& primary_async_client, - std::unique_ptr&&, Event::Dispatcher&, + [&](std::shared_ptr&& primary_async_client, + std::shared_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, std::unique_ptr&&, BackOffStrategyPtr&&, @@ -824,8 +824,8 @@ class XdsManagerImplXdstpConfigSourcesTest : public testing::Test { if (enable_authority_b) { EXPECT_CALL(grpc_mux_factory_, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillOnce(Invoke( - [&](std::unique_ptr&& primary_async_client, - std::unique_ptr&&, Event::Dispatcher&, + [&](std::shared_ptr&& primary_async_client, + std::shared_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, std::unique_ptr&&, BackOffStrategyPtr&&, @@ -838,8 +838,8 @@ class XdsManagerImplXdstpConfigSourcesTest : public testing::Test { if (enable_default_authority) { EXPECT_CALL(grpc_mux_factory_, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillOnce(Invoke( - [&](std::unique_ptr&& primary_async_client, - std::unique_ptr&&, Event::Dispatcher&, + [&](std::shared_ptr&& primary_async_client, + std::shared_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, std::unique_ptr&&, BackOffStrategyPtr&&, @@ -1169,8 +1169,8 @@ TEST_F(XdsManagerImplXdstpConfigSourcesTest, NonDefaultConfigSourceDeltaGrpc) { Registry::InjectFactory registry(factory); EXPECT_CALL(factory, create(_, _, _, _, _, _, _, _, _, _, _, _)) .WillOnce(Invoke( - [&](std::unique_ptr&& primary_async_client, - std::unique_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, + [&](std::shared_ptr&& primary_async_client, + std::shared_ptr&&, Event::Dispatcher&, Random::RandomGenerator&, Stats::Scope&, const envoy::config::core::v3::ApiConfigSource&, const LocalInfo::LocalInfo&, std::unique_ptr&&, BackOffStrategyPtr&&, OptRef, @@ -2310,6 +2310,128 @@ TEST_F(XdsManagerImplXdstpConfigSourcesTest, NonXdstpResourceRequiresConfigSourc resource_name))); } +// Validate that the pause-resume works on all gRPC-based ADS mux objects. +TEST_F(XdsManagerImplXdstpConfigSourcesTest, PauseResume) { + testing::InSequence s; + // Have a config-source and default_config_source with authority_2.com in each of them. + initialize(R"EOF( + config_sources: + - authorities: + - name: authority_1.com + api_config_source: + api_type: AGGREGATED_GRPC + set_node_on_first_message_only: true + grpc_services: + envoy_grpc: + cluster_name: config_source1_cluster + default_config_source: + authorities: + - name: authority_2.com + api_config_source: + api_type: AGGREGATED_GRPC + set_node_on_first_message_only: true + grpc_services: + envoy_grpc: + cluster_name: default_config_source_cluster + static_resources: + clusters: + - name: config_source1_cluster + connect_timeout: 0.250s + type: static + lb_policy: round_robin + load_assignment: + cluster_name: config_source1_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 11001 + - name: default_config_source_cluster + connect_timeout: 0.250s + type: static + lb_policy: round_robin + load_assignment: + cluster_name: default_config_source_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 11002 + )EOF", + true, false, true); + + // Validate pause() on a single type. + { + const std::string type_url = "type.googleapis.com/some.Type"; + const std::vector types{type_url}; + bool authority_a_resumed = false; + bool default_authority_resumed = false; + + // Validate that pause() on a single type is invoked on the underlying authorities mux objects, + // and that resume() is invoked when the cleanup object goes out of scope. + { + EXPECT_CALL(*authority_A_mux_, pause(types)) + .WillOnce(testing::Invoke( + [&authority_a_resumed](const std::vector) -> ScopedResume { + return std::make_unique( + [&authority_a_resumed]() { authority_a_resumed = true; }); + })); + EXPECT_CALL(*default_mux_, pause(types)) + .WillOnce(testing::Invoke( + [&default_authority_resumed](const std::vector) -> ScopedResume { + return std::make_unique( + [&default_authority_resumed]() { default_authority_resumed = true; }); + })); + ScopedResume pause_object = xds_manager_impl_.pause(type_url); + + // When the pause object gets out of scope, the resume should be invoked. + EXPECT_FALSE(authority_a_resumed); + EXPECT_FALSE(default_authority_resumed); + } + // The pause object is out of scope, the authorities should be resumed. + EXPECT_TRUE(authority_a_resumed); + EXPECT_TRUE(default_authority_resumed); + } + + // Validate pause() on multiple types. + { + const std::string type_url1 = "type.googleapis.com/some.Type1"; + const std::string type_url2 = "type.googleapis.com/some.Type2"; + const std::vector types{type_url1, type_url2}; + bool authority_a_resumed = false; + bool default_authority_resumed = false; + + // Validate that pause() on multiple types is invoked on the underlying authorities mux objects, + // and that resume() is invoked when the cleanup object goes out of scope. + { + EXPECT_CALL(*authority_A_mux_, pause(types)) + .WillOnce(testing::Invoke( + [&authority_a_resumed](const std::vector) -> ScopedResume { + return std::make_unique( + [&authority_a_resumed]() { authority_a_resumed = true; }); + })); + EXPECT_CALL(*default_mux_, pause(types)) + .WillOnce(testing::Invoke( + [&default_authority_resumed](const std::vector) -> ScopedResume { + return std::make_unique( + [&default_authority_resumed]() { default_authority_resumed = true; }); + })); + ScopedResume pause_object = xds_manager_impl_.pause(types); + + // When the pause object gets out of scope, the resume should be invoked. + EXPECT_FALSE(authority_a_resumed); + EXPECT_FALSE(default_authority_resumed); + } + // The pause object is out of scope, the authorities should be resumed. + EXPECT_TRUE(authority_a_resumed); + EXPECT_TRUE(default_authority_resumed); + } +} + } // namespace } // namespace Config } // namespace Envoy diff --git a/test/common/event/BUILD b/test/common/event/BUILD index 8675587e14e7d..077b821a9db16 100644 --- a/test/common/event/BUILD +++ b/test/common/event/BUILD @@ -19,7 +19,6 @@ envoy_cc_test( "//source/common/event:dispatcher_lib", "//source/common/network:address_lib", "//source/common/stats:isolated_store_lib", - "//test/mocks:common_lib", "//test/mocks/server:watch_dog_mocks", "//test/mocks/stats:stats_mocks", "//test/test_common:simulated_time_system_lib", @@ -37,7 +36,6 @@ envoy_cc_test( "//source/common/event:dispatcher_includes", "//source/common/event:dispatcher_lib", "//source/common/stats:isolated_store_lib", - "//test/mocks:common_lib", "//test/test_common:environment_lib", "//test/test_common:test_runtime_lib", "//test/test_common:utility_lib", diff --git a/test/common/event/dispatcher_impl_test.cc b/test/common/event/dispatcher_impl_test.cc index a0a86c84fc114..a5fc026c631d1 100644 --- a/test/common/event/dispatcher_impl_test.cc +++ b/test/common/event/dispatcher_impl_test.cc @@ -14,7 +14,6 @@ #include "source/common/network/address_impl.h" #include "source/common/stats/isolated_store_impl.h" -#include "test/mocks/common.h" #include "test/mocks/event/mocks.h" #include "test/mocks/server/watch_dog.h" #include "test/mocks/stats/mocks.h" @@ -47,10 +46,10 @@ class RunOnDelete { std::function on_destroy_; }; -void onWatcherReady(evwatch*, const evwatch_prepare_cb_info*, void* arg) { - // `arg` contains the ReadyWatcher passed in from evwatch_prepare_new. - auto watcher = static_cast(arg); - watcher->ready(); +void callPrepareCallback(evwatch*, const evwatch_prepare_cb_info*, void* arg) { + // `arg` contains the MockFunction passed in from evwatch_prepare_new. + auto callback = static_cast*>(arg); + callback->Call(); } class SchedulableCallbackImplTest : public testing::Test { @@ -68,9 +67,9 @@ class SchedulableCallbackImplTest : public testing::Test { }; TEST_F(SchedulableCallbackImplTest, ScheduleCurrentAndCancel) { - ReadyWatcher watcher; + MockFunction callback; - auto cb = dispatcher_->createSchedulableCallback([&]() { watcher.ready(); }); + auto cb = dispatcher_->createSchedulableCallback(callback.AsStdFunction()); // Cancel is a no-op if not scheduled. cb->cancel(); @@ -85,7 +84,7 @@ TEST_F(SchedulableCallbackImplTest, ScheduleCurrentAndCancel) { // Scheduled callback executes. cb->scheduleCallbackCurrentIteration(); - EXPECT_CALL(watcher, ready()); + EXPECT_CALL(callback, Call); dispatcher_->run(Dispatcher::RunType::Block); // Callbacks implicitly cancelled if runner is deleted. @@ -95,9 +94,9 @@ TEST_F(SchedulableCallbackImplTest, ScheduleCurrentAndCancel) { } TEST_F(SchedulableCallbackImplTest, ScheduleNextAndCancel) { - ReadyWatcher watcher; + MockFunction callback; - auto cb = dispatcher_->createSchedulableCallback([&]() { watcher.ready(); }); + auto cb = dispatcher_->createSchedulableCallback(callback.AsStdFunction()); // Cancel is a no-op if not scheduled. cb->cancel(); @@ -112,7 +111,7 @@ TEST_F(SchedulableCallbackImplTest, ScheduleNextAndCancel) { // Scheduled callback executes. cb->scheduleCallbackNextIteration(); - EXPECT_CALL(watcher, ready()); + EXPECT_CALL(callback, Call); dispatcher_->run(Dispatcher::RunType::Block); // Callbacks implicitly cancelled if runner is deleted. @@ -122,12 +121,12 @@ TEST_F(SchedulableCallbackImplTest, ScheduleNextAndCancel) { } TEST_F(SchedulableCallbackImplTest, ScheduleOrder) { - ReadyWatcher watcher0; - createCallback([&]() { watcher0.ready(); }); - ReadyWatcher watcher1; - createCallback([&]() { watcher1.ready(); }); - ReadyWatcher watcher2; - createCallback([&]() { watcher2.ready(); }); + MockFunction callback0; + createCallback(callback0.AsStdFunction()); + MockFunction callback1; + createCallback(callback1.AsStdFunction()); + MockFunction callback2; + createCallback(callback2.AsStdFunction()); // Current iteration callbacks run in the order they are scheduled. Next iteration callbacks run // after current iteration callbacks. @@ -135,73 +134,71 @@ TEST_F(SchedulableCallbackImplTest, ScheduleOrder) { callbacks_[1]->scheduleCallbackCurrentIteration(); callbacks_[2]->scheduleCallbackCurrentIteration(); InSequence s; - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher0, ready()); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback0, Call); dispatcher_->run(Dispatcher::RunType::Block); } TEST_F(SchedulableCallbackImplTest, ScheduleChainingAndCancellation) { DispatcherImpl* dispatcher_impl = static_cast(dispatcher_.get()); - ReadyWatcher prepare_watcher; - evwatch_prepare_new(&dispatcher_impl->base(), onWatcherReady, &prepare_watcher); + MockFunction prepare_callback; + evwatch_prepare_new(&dispatcher_impl->base(), callPrepareCallback, &prepare_callback); - ReadyWatcher watcher0; - createCallback([&]() { - watcher0.ready(); + MockFunction callback0, callback1, callback2, callback3, callback4, callback5; + createCallback(callback0.AsStdFunction()); + createCallback(callback1.AsStdFunction()); + createCallback(callback2.AsStdFunction()); + createCallback(callback3.AsStdFunction()); + createCallback(callback4.AsStdFunction()); + createCallback(callback5.AsStdFunction()); + + // Chained callbacks run in the same event loop iteration, as signaled by a single call to + // prepare_callback. callback3 and callback4 are not invoked because cb2 cancels + // cb3 and deletes cb4 as part of its execution. cb5 runs after a second call to the + // prepare callback since it's scheduled for the next iteration. + callbacks_[0]->scheduleCallbackCurrentIteration(); + InSequence s; + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(callback0, Call).WillOnce([this]() { callbacks_[1]->scheduleCallbackCurrentIteration(); }); - - ReadyWatcher watcher1; - createCallback([&]() { - watcher1.ready(); + EXPECT_CALL(callback1, Call).WillOnce([this]() { callbacks_[2]->scheduleCallbackCurrentIteration(); callbacks_[3]->scheduleCallbackCurrentIteration(); callbacks_[4]->scheduleCallbackCurrentIteration(); callbacks_[5]->scheduleCallbackNextIteration(); }); - - ReadyWatcher watcher2; - createCallback([&]() { - watcher2.ready(); + EXPECT_CALL(callback2, Call).WillOnce([this]() { EXPECT_TRUE(callbacks_[3]->enabled()); callbacks_[3]->cancel(); EXPECT_TRUE(callbacks_[4]->enabled()); callbacks_[4].reset(); }); - - ReadyWatcher watcher3; - createCallback([&]() { watcher3.ready(); }); - - ReadyWatcher watcher4; - createCallback([&]() { watcher4.ready(); }); - - ReadyWatcher watcher5; - createCallback([&]() { watcher5.ready(); }); - - // Chained callbacks run in the same event loop iteration, as signaled by a single call to - // prepare_watcher.ready(). watcher3 and watcher4 are not invoked because cb2 cancels - // cb3 and deletes cb4 as part of its execution. cb5 runs after a second call to the - // prepare callback since it's scheduled for the next iteration. - callbacks_[0]->scheduleCallbackCurrentIteration(); - InSequence s; - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(watcher0, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(watcher5, ready()); + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(callback5, Call); dispatcher_->run(Dispatcher::RunType::Block); } TEST_F(SchedulableCallbackImplTest, RescheduleNext) { DispatcherImpl* dispatcher_impl = static_cast(dispatcher_.get()); - ReadyWatcher prepare_watcher; - evwatch_prepare_new(&dispatcher_impl->base(), onWatcherReady, &prepare_watcher); + MockFunction prepare_callback; + evwatch_prepare_new(&dispatcher_impl->base(), callPrepareCallback, &prepare_callback); - ReadyWatcher watcher0; - createCallback([&]() { - watcher0.ready(); + MockFunction callback0, callback1, callback2, callback3; + createCallback(callback0.AsStdFunction()); + createCallback(callback1.AsStdFunction()); + createCallback(callback2.AsStdFunction()); + createCallback(callback3.AsStdFunction()); + + // Schedule callbacks 0 and 1 outside the loop, both will run in the same iteration of the event + // loop. + callbacks_[0]->scheduleCallbackCurrentIteration(); + callbacks_[1]->scheduleCallbackNextIteration(); + + InSequence s; + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(callback0, Call).WillOnce([this]() { // Callback 1 was scheduled from the previous iteration, expect it to fire in the current // iteration despite the attempt to reschedule. callbacks_[1]->scheduleCallbackNextIteration(); @@ -212,26 +209,10 @@ TEST_F(SchedulableCallbackImplTest, RescheduleNext) { callbacks_[3]->scheduleCallbackNextIteration(); callbacks_[3]->scheduleCallbackCurrentIteration(); }); - - ReadyWatcher watcher1; - createCallback([&]() { watcher1.ready(); }); - ReadyWatcher watcher2; - createCallback([&]() { watcher2.ready(); }); - ReadyWatcher watcher3; - createCallback([&]() { watcher3.ready(); }); - - // Schedule callbacks 0 and 1 outside the loop, both will run in the same iteration of the event - // loop. - callbacks_[0]->scheduleCallbackCurrentIteration(); - callbacks_[1]->scheduleCallbackNextIteration(); - - InSequence s; - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(watcher0, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(watcher3, ready()); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(callback3, Call); dispatcher_->run(Dispatcher::RunType::Block); } @@ -257,30 +238,29 @@ TEST(DeferredDeleteTest, DeferredDelete) { InSequence s; Api::ApiPtr api = Api::createApiForTest(); DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - ReadyWatcher watcher1; + MockFunction callback1; dispatcher->deferredDelete( - DeferredDeletablePtr{new TestDeferredDeletable([&]() -> void { watcher1.ready(); })}); + DeferredDeletablePtr{new TestDeferredDeletable(callback1.AsStdFunction())}); // The first one will get deleted inline. - EXPECT_CALL(watcher1, ready()); + EXPECT_CALL(callback1, Call); dispatcher->clearDeferredDeleteList(); // This one does a nested deferred delete. We should need two clear calls to actually get // rid of it with the vector swapping. We also test that inline clear() call does nothing. - ReadyWatcher watcher2; - ReadyWatcher watcher3; - dispatcher->deferredDelete(DeferredDeletablePtr{new TestDeferredDeletable([&]() -> void { - watcher2.ready(); + MockFunction callback2, callback3; + dispatcher->deferredDelete( + DeferredDeletablePtr{new TestDeferredDeletable(callback2.AsStdFunction())}); + + EXPECT_CALL(callback2, Call).WillOnce([&]() { dispatcher->deferredDelete( - DeferredDeletablePtr{new TestDeferredDeletable([&]() -> void { watcher3.ready(); })}); + DeferredDeletablePtr{new TestDeferredDeletable(callback3.AsStdFunction())}); dispatcher->clearDeferredDeleteList(); - })}); - - EXPECT_CALL(watcher2, ready()); + }); dispatcher->clearDeferredDeleteList(); - EXPECT_CALL(watcher3, ready()); + EXPECT_CALL(callback3, Call); dispatcher->clearDeferredDeleteList(); } @@ -288,20 +268,19 @@ TEST(DeferredTaskTest, DeferredTask) { InSequence s; Api::ApiPtr api = Api::createApiForTest(); DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - ReadyWatcher watcher1; + MockFunction callback1; - DeferredTaskUtil::deferredRun(*dispatcher, [&watcher1]() -> void { watcher1.ready(); }); + DeferredTaskUtil::deferredRun(*dispatcher, callback1.AsStdFunction()); // The first one will get deleted inline. - EXPECT_CALL(watcher1, ready()); + EXPECT_CALL(callback1, Call); dispatcher->clearDeferredDeleteList(); // Deferred task is scheduled FIFO. - ReadyWatcher watcher2; - ReadyWatcher watcher3; - DeferredTaskUtil::deferredRun(*dispatcher, [&watcher2]() -> void { watcher2.ready(); }); - DeferredTaskUtil::deferredRun(*dispatcher, [&watcher3]() -> void { watcher3.ready(); }); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher3, ready()); + MockFunction callback2, callback3; + DeferredTaskUtil::deferredRun(*dispatcher, callback2.AsStdFunction()); + DeferredTaskUtil::deferredRun(*dispatcher, callback3.AsStdFunction()); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback3, Call); dispatcher->clearDeferredDeleteList(); } @@ -310,16 +289,15 @@ TEST(DeferredDeleteTest, DeferredDeleteAndPostOrdering) { Api::ApiPtr api = Api::createApiForTest(); DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - ReadyWatcher post_watcher; - ReadyWatcher delete_watcher; + MockFunction post_callback, delete_callback; // DeferredDelete should always run before post callbacks. - EXPECT_CALL(delete_watcher, ready()); - EXPECT_CALL(post_watcher, ready()); + EXPECT_CALL(delete_callback, Call); + EXPECT_CALL(post_callback, Call); - dispatcher->post([&]() { post_watcher.ready(); }); + dispatcher->post(post_callback.AsStdFunction()); dispatcher->deferredDelete( - std::make_unique([&]() -> void { delete_watcher.ready(); })); + std::make_unique(delete_callback.AsStdFunction())); dispatcher->run(Dispatcher::RunType::NonBlock); } @@ -411,33 +389,27 @@ TEST_F(DispatcherImplTest, Post) { } TEST_F(DispatcherImplTest, PostExecuteAndDestructOrder) { - ReadyWatcher parent_watcher; - ReadyWatcher deferred_delete_watcher; - ReadyWatcher run_watcher1; - ReadyWatcher delete_watcher1; - ReadyWatcher run_watcher2; - ReadyWatcher delete_watcher2; + MockFunction parent_callback, deferred_delete_callback, run_callback1, delete_callback1, + run_callback2, delete_callback2; // Expect the following events to happen in order. The destructor of the post callback should run // before execution of the next post callback starts. The post callback runner should yield after // running each group of callbacks in a chain, so the deferred deletion should run before the // post callbacks that are also scheduled by the parent post callback. InSequence s; - EXPECT_CALL(parent_watcher, ready()); - EXPECT_CALL(deferred_delete_watcher, ready()); - EXPECT_CALL(run_watcher1, ready()); - EXPECT_CALL(delete_watcher1, ready()); - EXPECT_CALL(run_watcher2, ready()); - EXPECT_CALL(delete_watcher2, ready()); + EXPECT_CALL(parent_callback, Call); + EXPECT_CALL(deferred_delete_callback, Call); + EXPECT_CALL(run_callback1, Call); + EXPECT_CALL(delete_callback1, Call); + EXPECT_CALL(run_callback2, Call); + EXPECT_CALL(delete_callback2, Call); dispatcher_->post([&]() { - parent_watcher.ready(); - auto on_delete_task1 = - std::make_shared([&delete_watcher1]() { delete_watcher1.ready(); }); - dispatcher_->post([&run_watcher1, on_delete_task1]() { run_watcher1.ready(); }); - auto on_delete_task2 = - std::make_shared([&delete_watcher2]() { delete_watcher2.ready(); }); - dispatcher_->post([&run_watcher2, on_delete_task2]() { run_watcher2.ready(); }); + parent_callback.Call(); + auto on_delete_task1 = std::make_shared(delete_callback1.AsStdFunction()); + dispatcher_->post([&run_callback1, on_delete_task1]() { run_callback1.Call(); }); + auto on_delete_task2 = std::make_shared(delete_callback2.AsStdFunction()); + dispatcher_->post([&run_callback2, on_delete_task2]() { run_callback2.Call(); }); dispatcher_->post([this]() { { Thread::LockGuard lock(mu_); @@ -446,8 +418,8 @@ TEST_F(DispatcherImplTest, PostExecuteAndDestructOrder) { } cv_.notifyOne(); }); - dispatcher_->deferredDelete(std::make_unique( - [&deferred_delete_watcher]() -> void { deferred_delete_watcher.ready(); })); + dispatcher_->deferredDelete( + std::make_unique(deferred_delete_callback.AsStdFunction())); }); Thread::LockGuard lock(mu_); @@ -517,21 +489,17 @@ TEST_F(DispatcherImplTest, DispatcherThreadDeleted) { TEST(DispatcherThreadDeletedImplTest, DispatcherThreadDeletedAtNextCycle) { Api::ApiPtr api_(Api::createApiForTest()); DispatcherPtr dispatcher(api_->allocateDispatcher("test_thread")); - std::vector> watchers; - watchers.reserve(3); - for (int i = 0; i < 3; ++i) { - watchers.push_back(std::make_unique()); - } + std::vector> callbacks(3); dispatcher->deleteInDispatcherThread( - std::make_unique([&watchers]() { watchers[0]->ready(); })); - EXPECT_CALL(*watchers[0], ready()); + std::make_unique(callbacks[0].AsStdFunction())); + EXPECT_CALL(callbacks[0], Call); dispatcher->run(Event::Dispatcher::RunType::NonBlock); dispatcher->deleteInDispatcherThread( - std::make_unique([&watchers]() { watchers[1]->ready(); })); + std::make_unique(callbacks[1].AsStdFunction())); dispatcher->deleteInDispatcherThread( - std::make_unique([&watchers]() { watchers[2]->ready(); })); - EXPECT_CALL(*watchers[1], ready()); - EXPECT_CALL(*watchers[2], ready()); + std::make_unique(callbacks[2].AsStdFunction())); + EXPECT_CALL(callbacks[1], Call); + EXPECT_CALL(callbacks[2], Call); dispatcher->run(Event::Dispatcher::RunType::NonBlock); } @@ -545,47 +513,44 @@ class DispatcherShutdownTest : public testing::Test { }; TEST_F(DispatcherShutdownTest, ShutdownClearThreadLocalDeletables) { - ReadyWatcher watcher; + MockFunction callback; dispatcher_->deleteInDispatcherThread( - std::make_unique([&watcher]() { watcher.ready(); })); - EXPECT_CALL(watcher, ready()); + std::make_unique(callback.AsStdFunction())); + EXPECT_CALL(callback, Call); dispatcher_->shutdown(); } TEST_F(DispatcherShutdownTest, ShutdownDoesnotClearDeferredListOrPostCallback) { - ReadyWatcher watcher; - ReadyWatcher deferred_watcher; - ReadyWatcher post_watcher; + MockFunction callback, deferred_callback, post_callback; { InSequence s; - dispatcher_->deferredDelete(std::make_unique( - [&deferred_watcher]() { deferred_watcher.ready(); })); - dispatcher_->post([&post_watcher]() { post_watcher.ready(); }); + dispatcher_->deferredDelete( + std::make_unique(deferred_callback.AsStdFunction())); + dispatcher_->post(post_callback.AsStdFunction()); dispatcher_->deleteInDispatcherThread( - std::make_unique([&watcher]() { watcher.ready(); })); - EXPECT_CALL(watcher, ready()); + std::make_unique(callback.AsStdFunction())); + EXPECT_CALL(callback, Call); dispatcher_->shutdown(); - ::testing::Mock::VerifyAndClearExpectations(&watcher); - EXPECT_CALL(deferred_watcher, ready()); + ::testing::Mock::VerifyAndClearExpectations(&callback); + EXPECT_CALL(deferred_callback, Call); dispatcher_.reset(); } } TEST_F(DispatcherShutdownTest, DestroyClearAllList) { - ReadyWatcher watcher; - ReadyWatcher deferred_watcher; + MockFunction callback, deferred_callback; dispatcher_->deferredDelete( - std::make_unique([&deferred_watcher]() { deferred_watcher.ready(); })); + std::make_unique(deferred_callback.AsStdFunction())); dispatcher_->deleteInDispatcherThread( - std::make_unique([&watcher]() { watcher.ready(); })); + std::make_unique(callback.AsStdFunction())); { InSequence s; - EXPECT_CALL(deferred_watcher, ready()); - EXPECT_CALL(watcher, ready()); + EXPECT_CALL(deferred_callback, Call); + EXPECT_CALL(callback, Call); dispatcher_.reset(); } } @@ -807,11 +772,9 @@ TEST_F(NotStartedDispatcherImplTest, IsThreadSafe) { EXPECT_TRUE(dispatcher_->isThreadSafe()); } -class DispatcherMonotonicTimeTest : public testing::TestWithParam { +class DispatcherMonotonicTimeTest : public testing::Test { protected: DispatcherMonotonicTimeTest() : api_(Api::createApiForTest()) { - runtime_.mergeValues({{"envoy.restart_features.fix_dispatcher_approximate_now", - (GetParam() ? "true" : "false")}}); dispatcher_ = api_->allocateDispatcher("test_thread"); dispatcher_->initializeStats(scope_); } @@ -825,28 +788,23 @@ class DispatcherMonotonicTimeTest : public testing::TestWithParam { MonotonicTime time_; }; -INSTANTIATE_TEST_SUITE_P(DispatcherMonotonicTimeTests, DispatcherMonotonicTimeTest, - ::testing::ValuesIn({false, true})); - -TEST_P(DispatcherMonotonicTimeTest, UpdateApproximateMonotonicTime) { +TEST_F(DispatcherMonotonicTimeTest, UpdateApproximateMonotonicTime) { dispatcher_->updateApproximateMonotonicTime(); MonotonicTime time1 = dispatcher_->approximateMonotonicTime(); Event::TimerPtr timer = dispatcher_->createTimer([&] { // Approximate time should have been updated in this loop to 1s later. MonotonicTime time2 = dispatcher_->approximateMonotonicTime(); EXPECT_LT(time1, time2); - if (Runtime::runtimeFeatureEnabled("envoy.restart_features.fix_dispatcher_approximate_now")) { - // Time2 should be updated roughly 2000ms later than time1. - EXPECT_NEAR( - 2000, std::chrono::duration_cast(time2 - time1).count(), 100); - } + // Time2 should be updated roughly 2000ms later than time1. + EXPECT_NEAR(2000, std::chrono::duration_cast(time2 - time1).count(), + 100); }); timer->enableTimer(std::chrono::seconds(2)); dispatcher_->run(Dispatcher::RunType::Block); } -TEST_P(DispatcherMonotonicTimeTest, ApproximateMonotonicTime) { +TEST_F(DispatcherMonotonicTimeTest, ApproximateMonotonicTime) { // approximateMonotonicTime is constant within one event loop run. dispatcher_->post([this]() { { @@ -869,7 +827,7 @@ class TimerImplTest : public testing::Test { protected: TimerImplTest() { // Hook into event loop prepare and check events. - evwatch_prepare_new(&libevent_base_, onWatcherReady, &prepare_watcher_); + evwatch_prepare_new(&libevent_base_, callPrepareCallback, &prepare_callback_); evwatch_check_new(&libevent_base_, onCheck, this); } ~TimerImplTest() override { ASSERT(check_callbacks_.empty()); } @@ -910,7 +868,7 @@ class TimerImplTest : public testing::Test { Api::ApiPtr api_{Api::createApiForTest()}; DispatcherPtr dispatcher_{api_->allocateDispatcher("test_thread")}; event_base& libevent_base_{static_cast(*dispatcher_).base()}; - ReadyWatcher prepare_watcher_; + MockFunction prepare_callback_; std::vector callbacks_; private: @@ -956,32 +914,28 @@ TEST_F(TimerImplTest, TimerEnabledDisabled) { EXPECT_FALSE(timer->enabled()); timer->enableTimer(std::chrono::milliseconds(0)); EXPECT_TRUE(timer->enabled()); - EXPECT_CALL(prepare_watcher_, ready()); + EXPECT_CALL(prepare_callback_, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); EXPECT_FALSE(timer->enabled()); timer->enableHRTimer(std::chrono::milliseconds(0)); EXPECT_TRUE(timer->enabled()); - EXPECT_CALL(prepare_watcher_, ready()); + EXPECT_CALL(prepare_callback_, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); EXPECT_FALSE(timer->enabled()); } TEST_F(TimerImplTest, ChangeTimerBackwardsBeforeRun) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); + MockFunction callback1, callback2, callback3; + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); + Event::TimerPtr timer3 = dispatcher_->createTimer(callback3.AsStdFunction()); - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); - - ReadyWatcher watcher3; - Event::TimerPtr timer3 = dispatcher_->createTimer([&] { watcher3.ready(); }); - - // Expect watcher3 to trigger first because the deadlines for timers 1 and 2 was moved backwards. + // Expect callback3 to trigger first because the deadlines for timers 1 and 2 was moved backwards. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher3, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher1, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback3, Call); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback1, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(0)); timer2->enableTimer(std::chrono::milliseconds(1)); @@ -995,17 +949,15 @@ TEST_F(TimerImplTest, ChangeTimerBackwardsBeforeRun) { } TEST_F(TimerImplTest, ChangeTimerForwardsToZeroBeforeRun) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); + MockFunction callback1, callback2; + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); - - // Expect watcher1 to trigger first because timer1's deadline was moved forward. + // Expect callback1 to trigger first because timer1's deadline was moved forward. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(callback2, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(2)); timer2->enableTimer(std::chrono::milliseconds(1)); @@ -1017,17 +969,15 @@ TEST_F(TimerImplTest, ChangeTimerForwardsToZeroBeforeRun) { } TEST_F(TimerImplTest, ChangeTimerForwardsToNonZeroBeforeRun) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); - - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); + MockFunction callback1, callback2; + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); - // Expect watcher1 to trigger first because timer1's deadline was moved forward. + // Expect callback1 to trigger first because timer1's deadline was moved forward. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(callback2, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(3)); timer2->enableTimer(std::chrono::milliseconds(2)); @@ -1039,17 +989,15 @@ TEST_F(TimerImplTest, ChangeTimerForwardsToNonZeroBeforeRun) { } TEST_F(TimerImplTest, ChangeLargeTimerForwardToZeroBeforeRun) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); + MockFunction callback1, callback2; + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); - - // Expect watcher1 to trigger because timer1's deadline was moved forward. + // Expect callback1 to trigger because timer1's deadline was moved forward. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(prepare_watcher_, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(prepare_callback_, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::seconds(2000)); timer2->enableTimer(std::chrono::seconds(1000)); @@ -1058,17 +1006,15 @@ TEST_F(TimerImplTest, ChangeLargeTimerForwardToZeroBeforeRun) { } TEST_F(TimerImplTest, ChangeLargeTimerForwardToNonZeroBeforeRun) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); - - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); + MockFunction callback1, callback2; + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); - // Expect watcher1 to trigger because timer1's deadline was moved forward. + // Expect callback1 to trigger because timer1's deadline was moved forward. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(prepare_watcher_, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(prepare_callback_, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::seconds(2000)); timer2->enableTimer(std::chrono::seconds(1000)); @@ -1081,21 +1027,17 @@ TEST_F(TimerImplTest, ChangeLargeTimerForwardToNonZeroBeforeRun) { // Timers scheduled at different times execute in order. TEST_F(TimerImplTest, TimerOrdering) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); - - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); + MockFunction callback1, callback2, callback3; + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); + Event::TimerPtr timer3 = dispatcher_->createTimer(callback3.AsStdFunction()); - ReadyWatcher watcher3; - Event::TimerPtr timer3 = dispatcher_->createTimer([&] { watcher3.ready(); }); - - // Expect watcher calls to happen in order since timers have different times. + // Expect callback calls to happen in order since timers have different times. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher3, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback3, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(0)); @@ -1113,23 +1055,16 @@ TEST_F(TimerImplTest, TimerOrdering) { // Alarms that are scheduled to execute and are cancelled do not trigger. TEST_F(TimerImplTest, TimerOrderAndDisableAlarm) { - ReadyWatcher watcher3; - Event::TimerPtr timer3 = dispatcher_->createTimer([&] { watcher3.ready(); }); - - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); - - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { - timer2->disableTimer(); - watcher1.ready(); - }); + MockFunction callback1, callback2, callback3; + Event::TimerPtr timer3 = dispatcher_->createTimer(callback3.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); - // Expect watcher calls to happen in order since timers have different times. + // Expect callback calls to happen in order since timers have different times. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher3, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call).WillOnce([&]() { timer2->disableTimer(); }); + EXPECT_CALL(callback3, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(0)); timer2->enableTimer(std::chrono::milliseconds(1)); @@ -1147,38 +1082,31 @@ TEST_F(TimerImplTest, TimerOrderAndDisableAlarm) { // Change the registration time for a timer that is already activated by disabling and re-enabling // the timer. Verify that execution is delayed. TEST_F(TimerImplTest, TimerOrderDisableAndReschedule) { - ReadyWatcher watcher4; - Event::TimerPtr timer4 = dispatcher_->createTimer([&] { watcher4.ready(); }); - - ReadyWatcher watcher3; - Event::TimerPtr timer3 = dispatcher_->createTimer([&] { watcher3.ready(); }); + MockFunction callback1, callback2, callback3, callback4; + Event::TimerPtr timer4 = dispatcher_->createTimer(callback4.AsStdFunction()); + Event::TimerPtr timer3 = dispatcher_->createTimer(callback3.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); - - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { + // timer1 is expected to run first and reschedule timers 2 and 3. timer4 should fire before + // timer2 and timer3 since timer4's registration is unaffected. + InSequence s; + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call).WillOnce([&]() { timer2->disableTimer(); timer2->enableTimer(std::chrono::milliseconds(0)); timer3->disableTimer(); timer3->enableTimer(std::chrono::milliseconds(1)); - watcher1.ready(); }); - - // timer1 is expected to run first and reschedule timers 2 and 3. timer4 should fire before - // timer2 and timer3 since timer4's registration is unaffected. - InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher4, ready()); + EXPECT_CALL(callback4, Call); // Sleep during prepare to ensure that enough time has elapsed before timer evaluation to ensure // that timers 2 and 3 are picked up by the same loop iteration. Without the sleep the two // timers could execute in different loop iterations. - EXPECT_CALL(prepare_watcher_, ready()).WillOnce(testing::InvokeWithoutArgs([&]() { + EXPECT_CALL(prepare_callback_, Call).WillOnce([this]() { advanceLibeventTimeNextIteration(absl::Milliseconds(10)); - })); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher3, ready()); + }); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback3, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(0)); timer2->enableTimer(std::chrono::milliseconds(1)); @@ -1198,37 +1126,30 @@ TEST_F(TimerImplTest, TimerOrderDisableAndReschedule) { // Change the registration time for a timer that is already activated by re-enabling the timer // without calling disableTimer first. TEST_F(TimerImplTest, TimerOrderAndReschedule) { - ReadyWatcher watcher4; - Event::TimerPtr timer4 = dispatcher_->createTimer([&] { watcher4.ready(); }); - - ReadyWatcher watcher3; - Event::TimerPtr timer3 = dispatcher_->createTimer([&] { watcher3.ready(); }); - - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { watcher2.ready(); }); - - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { - timer2->enableTimer(std::chrono::milliseconds(0)); - timer3->enableTimer(std::chrono::milliseconds(1)); - watcher1.ready(); - }); + MockFunction callback1, callback2, callback3, callback4; + Event::TimerPtr timer4 = dispatcher_->createTimer(callback4.AsStdFunction()); + Event::TimerPtr timer3 = dispatcher_->createTimer(callback3.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); // Rescheduling timers that are already scheduled to run in the current event loop iteration has // no effect if the time delta is 0. Expect timers 1, 2 and 4 to execute in the original order. // Timer 3 is delayed since it is rescheduled with a non-zero delta. InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher4, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call).WillOnce([&]() { + timer2->enableTimer(std::chrono::milliseconds(0)); + timer3->enableTimer(std::chrono::milliseconds(1)); + }); + EXPECT_CALL(callback4, Call); // Sleep during prepare to ensure that enough time has elapsed before timer evaluation to ensure // that timers 2 and 3 are picked up by the same loop iteration. Without the sleep the two // timers could execute in different loop iterations. - EXPECT_CALL(prepare_watcher_, ready()).WillOnce(testing::InvokeWithoutArgs([&]() { + EXPECT_CALL(prepare_callback_, Call).WillOnce([this]() { advanceLibeventTimeNextIteration(absl::Milliseconds(10)); - })); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher3, ready()); + }); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback3, Call); runInEventLoop([&]() { timer1->enableTimer(std::chrono::milliseconds(0)); timer2->enableTimer(std::chrono::milliseconds(1)); @@ -1246,26 +1167,11 @@ TEST_F(TimerImplTest, TimerOrderAndReschedule) { } TEST_F(TimerImplTest, TimerChaining) { - ReadyWatcher watcher1; - Event::TimerPtr timer1 = dispatcher_->createTimer([&] { watcher1.ready(); }); - - ReadyWatcher watcher2; - Event::TimerPtr timer2 = dispatcher_->createTimer([&] { - watcher2.ready(); - timer1->enableTimer(std::chrono::milliseconds(0)); - }); - - ReadyWatcher watcher3; - Event::TimerPtr timer3 = dispatcher_->createTimer([&] { - watcher3.ready(); - timer2->enableTimer(std::chrono::milliseconds(0)); - }); - - ReadyWatcher watcher4; - Event::TimerPtr timer4 = dispatcher_->createTimer([&] { - watcher4.ready(); - timer3->enableTimer(std::chrono::milliseconds(0)); - }); + MockFunction callback1, callback2, callback3, callback4; + Event::TimerPtr timer4 = dispatcher_->createTimer(callback4.AsStdFunction()); + Event::TimerPtr timer3 = dispatcher_->createTimer(callback3.AsStdFunction()); + Event::TimerPtr timer2 = dispatcher_->createTimer(callback2.AsStdFunction()); + Event::TimerPtr timer1 = dispatcher_->createTimer(callback1.AsStdFunction()); timer4->enableTimer(std::chrono::milliseconds(0)); @@ -1274,14 +1180,20 @@ TEST_F(TimerImplTest, TimerChaining) { EXPECT_FALSE(timer3->enabled()); EXPECT_TRUE(timer4->enabled()); InSequence s; - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher4, ready()); - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher3, ready()); - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher1, ready()); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback4, Call).WillOnce([&]() { + timer3->enableTimer(std::chrono::milliseconds(0)); + }); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback3, Call).WillOnce([&]() { + timer2->enableTimer(std::chrono::milliseconds(0)); + }); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback2, Call).WillOnce([&]() { + timer1->enableTimer(std::chrono::milliseconds(0)); + }); + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback1, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); EXPECT_FALSE(timer1->enabled()); @@ -1291,21 +1203,14 @@ TEST_F(TimerImplTest, TimerChaining) { } TEST_F(TimerImplTest, TimerChainDisable) { - ReadyWatcher watcher; + MockFunction callback; Event::TimerPtr timer1; Event::TimerPtr timer2; Event::TimerPtr timer3; - auto timer_cb = [&] { - watcher.ready(); - timer1->disableTimer(); - timer2->disableTimer(); - timer3->disableTimer(); - }; - - timer1 = dispatcher_->createTimer(timer_cb); - timer2 = dispatcher_->createTimer(timer_cb); - timer3 = dispatcher_->createTimer(timer_cb); + timer1 = dispatcher_->createTimer(callback.AsStdFunction()); + timer2 = dispatcher_->createTimer(callback.AsStdFunction()); + timer3 = dispatcher_->createTimer(callback.AsStdFunction()); timer3->enableTimer(std::chrono::milliseconds(0)); timer2->enableTimer(std::chrono::milliseconds(0)); @@ -1315,28 +1220,25 @@ TEST_F(TimerImplTest, TimerChainDisable) { EXPECT_TRUE(timer2->enabled()); EXPECT_TRUE(timer3->enabled()); InSequence s; - // Only 1 call to watcher ready since the other 2 timers were disabled by the first timer. - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher, ready()); + // Only 1 call to callback since the other 2 timers were disabled by the first timer. + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback, Call).WillOnce([&]() { + timer1->disableTimer(); + timer2->disableTimer(); + timer3->disableTimer(); + }); dispatcher_->run(Dispatcher::RunType::NonBlock); } TEST_F(TimerImplTest, TimerChainDelete) { - ReadyWatcher watcher; + MockFunction callback; Event::TimerPtr timer1; Event::TimerPtr timer2; Event::TimerPtr timer3; - auto timer_cb = [&] { - watcher.ready(); - timer1.reset(); - timer2.reset(); - timer3.reset(); - }; - - timer1 = dispatcher_->createTimer(timer_cb); - timer2 = dispatcher_->createTimer(timer_cb); - timer3 = dispatcher_->createTimer(timer_cb); + timer1 = dispatcher_->createTimer(callback.AsStdFunction()); + timer2 = dispatcher_->createTimer(callback.AsStdFunction()); + timer3 = dispatcher_->createTimer(callback.AsStdFunction()); timer3->enableTimer(std::chrono::milliseconds(0)); timer2->enableTimer(std::chrono::milliseconds(0)); @@ -1346,9 +1248,13 @@ TEST_F(TimerImplTest, TimerChainDelete) { EXPECT_TRUE(timer2->enabled()); EXPECT_TRUE(timer3->enabled()); InSequence s; - // Only 1 call to watcher ready since the other 2 timers were deleted by the first timer. - EXPECT_CALL(prepare_watcher_, ready()); - EXPECT_CALL(watcher, ready()); + // Only 1 call to callback since the other 2 timers were deleted by the first timer. + EXPECT_CALL(prepare_callback_, Call); + EXPECT_CALL(callback, Call).WillOnce([&]() { + timer1.reset(); + timer2.reset(); + timer3.reset(); + }); dispatcher_->run(Dispatcher::RunType::NonBlock); } @@ -1511,61 +1417,61 @@ TEST_F(DispatcherWithWatchdogTest, PeriodicTouchTimer) { } TEST_F(DispatcherWithWatchdogTest, TouchBeforeEachPostCallback) { - ReadyWatcher watcher1; - ReadyWatcher watcher2; - ReadyWatcher watcher3; - dispatcher_->post([&]() { watcher1.ready(); }); - dispatcher_->post([&]() { watcher2.ready(); }); - dispatcher_->post([&]() { watcher3.ready(); }); + MockFunction callback1; + MockFunction callback2; + MockFunction callback3; + dispatcher_->post(callback1.AsStdFunction()); + dispatcher_->post(callback2.AsStdFunction()); + dispatcher_->post(callback3.AsStdFunction()); InSequence s; EXPECT_CALL(*watchdog_, touch()); - EXPECT_CALL(watcher1, ready()); + EXPECT_CALL(callback1, Call); EXPECT_CALL(*watchdog_, touch()); - EXPECT_CALL(watcher2, ready()); + EXPECT_CALL(callback2, Call); EXPECT_CALL(*watchdog_, touch()); - EXPECT_CALL(watcher3, ready()); + EXPECT_CALL(callback3, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); } TEST_F(DispatcherWithWatchdogTest, TouchBeforeDeferredDelete) { - ReadyWatcher watcher1; - ReadyWatcher watcher2; - ReadyWatcher watcher3; + MockFunction callback1; + MockFunction callback2; + MockFunction callback3; - DeferredTaskUtil::deferredRun(*dispatcher_, [&watcher1]() -> void { watcher1.ready(); }); - DeferredTaskUtil::deferredRun(*dispatcher_, [&watcher2]() -> void { watcher2.ready(); }); - DeferredTaskUtil::deferredRun(*dispatcher_, [&watcher3]() -> void { watcher3.ready(); }); + DeferredTaskUtil::deferredRun(*dispatcher_, callback1.AsStdFunction()); + DeferredTaskUtil::deferredRun(*dispatcher_, callback2.AsStdFunction()); + DeferredTaskUtil::deferredRun(*dispatcher_, callback3.AsStdFunction()); InSequence s; EXPECT_CALL(*watchdog_, touch()); - EXPECT_CALL(watcher1, ready()); - EXPECT_CALL(watcher2, ready()); - EXPECT_CALL(watcher3, ready()); + EXPECT_CALL(callback1, Call); + EXPECT_CALL(callback2, Call); + EXPECT_CALL(callback3, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); } TEST_F(DispatcherWithWatchdogTest, TouchBeforeSchedulableCallback) { - ReadyWatcher watcher; + MockFunction callback; - auto cb = dispatcher_->createSchedulableCallback([&]() { watcher.ready(); }); + auto cb = dispatcher_->createSchedulableCallback(callback.AsStdFunction()); cb->scheduleCallbackCurrentIteration(); InSequence s; EXPECT_CALL(*watchdog_, touch()); - EXPECT_CALL(watcher, ready()); + EXPECT_CALL(callback, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); } TEST_F(DispatcherWithWatchdogTest, TouchBeforeTimer) { - ReadyWatcher watcher; + MockFunction callback; - auto timer = dispatcher_->createTimer([&]() { watcher.ready(); }); + auto timer = dispatcher_->createTimer(callback.AsStdFunction()); timer->enableTimer(std::chrono::milliseconds(0)); InSequence s; EXPECT_CALL(*watchdog_, touch()); - EXPECT_CALL(watcher, ready()); + EXPECT_CALL(callback, Call); dispatcher_->run(Dispatcher::RunType::NonBlock); } @@ -1573,21 +1479,16 @@ TEST_F(DispatcherWithWatchdogTest, TouchBeforeFdEvent) { os_fd_t fd = os_sys_calls_.socket(AF_INET6, SOCK_DGRAM, 0).return_value_; ASSERT_TRUE(SOCKET_VALID(fd)); - ReadyWatcher watcher; + MockFunction callback; const FileTriggerType trigger = Event::PlatformDefaultTriggerType; - Event::FileEventPtr file_event = dispatcher_->createFileEvent( - fd, - [&](uint32_t) { - watcher.ready(); - return absl::OkStatus(); - }, - trigger, FileReadyType::Read); + Event::FileEventPtr file_event = + dispatcher_->createFileEvent(fd, callback.AsStdFunction(), trigger, FileReadyType::Read); file_event->activate(FileReadyType::Read); InSequence s; EXPECT_CALL(*watchdog_, touch()).Times(2); - EXPECT_CALL(watcher, ready()); + EXPECT_CALL(callback, Call).WillOnce(Return(absl::OkStatus())); dispatcher_->run(Dispatcher::RunType::NonBlock); } diff --git a/test/common/event/file_event_impl_test.cc b/test/common/event/file_event_impl_test.cc index 3157cedc0b42c..2476fc5c40a4d 100644 --- a/test/common/event/file_event_impl_test.cc +++ b/test/common/event/file_event_impl_test.cc @@ -6,7 +6,6 @@ #include "source/common/event/dispatcher_impl.h" #include "source/common/stats/isolated_store_impl.h" -#include "test/mocks/common.h" #include "test/test_common/environment.h" #include "test/test_common/utility.h" @@ -16,6 +15,8 @@ namespace Envoy { namespace Event { namespace { +using ::testing::MockFunction; + class FileEventImplTest : public testing::Test { public: FileEventImplTest() @@ -61,10 +62,10 @@ class FileEventImplActivateTest : public testing::TestWithParam(arg); - watcher->ready(); + static void callPrepareCallback(evwatch*, const evwatch_prepare_cb_info*, void* arg) { + // `arg` contains the MockFunction passed in from evwatch_prepare_new. + auto callback = static_cast*>(arg); + callback->Call(); } int domain() { return GetParam() == Network::Address::IpVersion::v4 ? AF_INET : AF_INET6; } @@ -82,10 +83,9 @@ TEST_P(FileEventImplActivateTest, Activate) { Api::ApiPtr api = Api::createApiForTest(); DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - ReadyWatcher read_event; - EXPECT_CALL(read_event, ready()); - ReadyWatcher write_event; - EXPECT_CALL(write_event, ready()); + MockFunction read_callback, write_callback; + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); const FileTriggerType trigger = Event::PlatformDefaultTriggerType; @@ -93,11 +93,11 @@ TEST_P(FileEventImplActivateTest, Activate) { fd, [&](uint32_t events) { if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, @@ -115,27 +115,24 @@ TEST_P(FileEventImplActivateTest, ActivateChaining) { Api::ApiPtr api = Api::createApiForTest(); DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - ReadyWatcher fd_event; - ReadyWatcher read_event; - ReadyWatcher write_event; + MockFunction fd_callback, read_callback, write_callback, prepare_callback; - ReadyWatcher prepare_watcher; - evwatch_prepare_new(&static_cast(dispatcher.get())->base(), onWatcherReady, - &prepare_watcher); + evwatch_prepare_new(&static_cast(dispatcher.get())->base(), callPrepareCallback, + &prepare_callback); const FileTriggerType trigger = Event::PlatformDefaultTriggerType; Event::FileEventPtr file_event = dispatcher->createFileEvent( fd, [&](uint32_t events) { - fd_event.ready(); + fd_callback.Call(); if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); file_event->activate(FileReadyType::Write); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, @@ -145,17 +142,17 @@ TEST_P(FileEventImplActivateTest, ActivateChaining) { // First loop iteration: handle scheduled read event and the real write event produced by poll. // Note that the real and injected events are combined and delivered in a single call to the fd // callback. - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(fd_event, ready()); - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(fd_callback, Call); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); // Second loop iteration: handle write and close events scheduled while handling read. - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(fd_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(fd_callback, Call); + EXPECT_CALL(write_callback, Call); if constexpr (Event::PlatformDefaultTriggerType != Event::FileTriggerType::EmulatedEdge) { // Third loop iteration: poll returned no new real events. - EXPECT_CALL(prepare_watcher, ready()); + EXPECT_CALL(prepare_callback, Call); } file_event->activate(FileReadyType::Read); @@ -170,28 +167,25 @@ TEST_P(FileEventImplActivateTest, SetEnableCancelsActivate) { Api::ApiPtr api = Api::createApiForTest(); DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - ReadyWatcher fd_event; - ReadyWatcher read_event; - ReadyWatcher write_event; + MockFunction fd_callback, read_callback, write_callback, prepare_callback; - ReadyWatcher prepare_watcher; - evwatch_prepare_new(&static_cast(dispatcher.get())->base(), onWatcherReady, - &prepare_watcher); + evwatch_prepare_new(&static_cast(dispatcher.get())->base(), callPrepareCallback, + &prepare_callback); const FileTriggerType trigger = Event::PlatformDefaultTriggerType; Event::FileEventPtr file_event = dispatcher->createFileEvent( fd, [&](uint32_t events) { - fd_event.ready(); + fd_callback.Call(); if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); file_event->activate(FileReadyType::Closed); file_event->setEnabled(FileReadyType::Write | FileReadyType::Closed); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, @@ -201,17 +195,17 @@ TEST_P(FileEventImplActivateTest, SetEnableCancelsActivate) { // First loop iteration: handle scheduled read event and the real write event produced by poll. // Note that the real and injected events are combined and delivered in a single call to the fd // callback. - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(fd_event, ready()); - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(fd_callback, Call); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); // Second loop iteration: handle real write event after resetting event mask via setEnabled. Close // injected event is discarded by the setEnable call. - EXPECT_CALL(prepare_watcher, ready()); - EXPECT_CALL(fd_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(prepare_callback, Call); + EXPECT_CALL(fd_callback, Call); + EXPECT_CALL(write_callback, Call); // Third loop iteration: poll returned no new real events. - EXPECT_CALL(prepare_watcher, ready()); + EXPECT_CALL(prepare_callback, Call); file_event->activate(FileReadyType::Read); dispatcher->run(Event::Dispatcher::RunType::NonBlock); @@ -221,20 +215,19 @@ TEST_P(FileEventImplActivateTest, SetEnableCancelsActivate) { #ifndef WIN32 // Libevent on Windows doesn't support edge trigger. TEST_F(FileEventImplTest, EdgeTrigger) { - ReadyWatcher read_event; - EXPECT_CALL(read_event, ready()); - ReadyWatcher write_event; - EXPECT_CALL(write_event, ready()); + MockFunction read_callback, write_callback; + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); Event::FileEventPtr file_event = dispatcher_->createFileEvent( fds_[0], [&](uint32_t events) { if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, @@ -246,8 +239,7 @@ TEST_F(FileEventImplTest, EdgeTrigger) { TEST_F(FileEventImplTest, LevelTrigger) { testing::InSequence s; - ReadyWatcher read_event; - ReadyWatcher write_event; + MockFunction read_callback, write_callback; int count = 0; Event::FileEventPtr file_event = dispatcher_->createFileEvent( @@ -258,11 +250,11 @@ TEST_F(FileEventImplTest, LevelTrigger) { dispatcher_->exit(); } if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, @@ -270,31 +262,31 @@ TEST_F(FileEventImplTest, LevelTrigger) { // Expect events to be delivered twice since count=2 and level events are delivered on each // iteration until the fd state changes. - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); count = 2; dispatcher_->run(Event::Dispatcher::RunType::Block); // Change the event mask to just Write and verify that only that event is delivered. - EXPECT_CALL(read_event, ready()).Times(0); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(read_callback, Call).Times(0); + EXPECT_CALL(write_callback, Call); file_event->setEnabled(FileReadyType::Write); count = 1; dispatcher_->run(Event::Dispatcher::RunType::Block); // Activate read, and verify it is delivered despite not being part of the enabled event mask. - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); file_event->activate(FileReadyType::Read); count = 1; dispatcher_->run(Event::Dispatcher::RunType::Block); // Activate read and then call setEnabled. Verify that the read event is not delivered; setEnabled // clears events from explicit calls to activate. - EXPECT_CALL(read_event, ready()).Times(0); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(read_callback, Call).Times(0); + EXPECT_CALL(write_callback, Call); file_event->activate(FileReadyType::Read); file_event->setEnabled(FileReadyType::Write); count = 1; @@ -303,8 +295,7 @@ TEST_F(FileEventImplTest, LevelTrigger) { TEST_F(FileEventImplTest, SetEnabled) { testing::InSequence s; - ReadyWatcher read_event; - ReadyWatcher write_event; + MockFunction read_callback, write_callback; const FileTriggerType trigger = Event::PlatformDefaultTriggerType; @@ -312,73 +303,72 @@ TEST_F(FileEventImplTest, SetEnabled) { fds_[0], [&](uint32_t events) { if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, trigger, FileReadyType::Read | FileReadyType::Write); - EXPECT_CALL(read_event, ready()); + EXPECT_CALL(read_callback, Call); file_event->setEnabled(FileReadyType::Read); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->setEnabled(FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); file_event->setEnabled(0); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); file_event->setEnabled(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - // Run a manual event to ensure that previous expectations are satisfied before moving on. - ReadyWatcher manual_event; - EXPECT_CALL(manual_event, ready()); - manual_event.ready(); + // Ensure that previous expectations are satisfied before moving on. + testing::Mock::VerifyAndClearExpectations(&read_callback); + testing::Mock::VerifyAndClearExpectations(&write_callback); clearReadable(); file_event->setEnabled(FileReadyType::Read); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->setEnabled(FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->setEnabled(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); // Repeat the previous registration, verify that write event is delivered again. - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->setEnabled(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); // Synthetic read events are delivered even if the active registration doesn't contain them. - EXPECT_CALL(read_event, ready()); + EXPECT_CALL(read_callback, Call); file_event->activate(FileReadyType::Read); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - // Run a manual event to ensure that previous expectations are satisfied before moving on. - EXPECT_CALL(manual_event, ready()); - manual_event.ready(); + // Ensure that previous expectations are satisfied before moving on. + testing::Mock::VerifyAndClearExpectations(&read_callback); + testing::Mock::VerifyAndClearExpectations(&write_callback); // Do a read activation followed setEnabled to verify that the activation is cleared. - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->activate(FileReadyType::Read); file_event->setEnabled(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); // Repeat the previous steps but with the same input to setEnabled to verify that the activation // is cleared even in cases where the setEnable mask hasn't changed. - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->activate(FileReadyType::Read); file_event->setEnabled(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); @@ -391,8 +381,7 @@ TEST_F(FileEventImplTest, RegisterIfEmulatedEdge) { } testing::InSequence s; - ReadyWatcher read_event; - ReadyWatcher write_event; + MockFunction read_callback, write_callback; const FileTriggerType trigger = Event::PlatformDefaultTriggerType; @@ -400,47 +389,47 @@ TEST_F(FileEventImplTest, RegisterIfEmulatedEdge) { fds_[0], [&](uint32_t events) { if (events & FileReadyType::Read) { - read_event.ready(); + read_callback.Call(); } if (events & FileReadyType::Write) { - write_event.ready(); + write_callback.Call(); } return absl::OkStatus(); }, trigger, FileReadyType::Read | FileReadyType::Write); - EXPECT_CALL(read_event, ready()).Times(0); - EXPECT_CALL(write_event, ready()).Times(0); + EXPECT_CALL(read_callback, Call).Times(0); + EXPECT_CALL(write_callback, Call).Times(0); file_event->unregisterEventIfEmulatedEdge(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(read_event, ready()); + EXPECT_CALL(read_callback, Call); file_event->registerEventIfEmulatedEdge(FileReadyType::Read); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(write_callback, Call); file_event->registerEventIfEmulatedEdge(FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(read_event, ready()); + EXPECT_CALL(read_callback, Call); file_event->registerEventIfEmulatedEdge(FileReadyType::Read | FileReadyType::Write); file_event->unregisterEventIfEmulatedEdge(FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(read_event, ready()).Times(0); - EXPECT_CALL(write_event, ready()).Times(0); + EXPECT_CALL(read_callback, Call).Times(0); + EXPECT_CALL(write_callback, Call).Times(0); file_event->unregisterEventIfEmulatedEdge(FileReadyType::Read); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - EXPECT_CALL(read_event, ready()); - EXPECT_CALL(write_event, ready()); + EXPECT_CALL(read_callback, Call); + EXPECT_CALL(write_callback, Call); file_event->registerEventIfEmulatedEdge(FileReadyType::Read | FileReadyType::Write); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); // Events are delivered once due to auto unregistration after they are delivered. - EXPECT_CALL(read_event, ready()).Times(0); - EXPECT_CALL(write_event, ready()).Times(0); + EXPECT_CALL(read_callback, Call).Times(0); + EXPECT_CALL(write_callback, Call).Times(0); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); } diff --git a/test/common/filter/config_discovery_impl_test.cc b/test/common/filter/config_discovery_impl_test.cc index 0c7582279fdaf..838d46fa0d256 100644 --- a/test/common/filter/config_discovery_impl_test.cc +++ b/test/common/filter/config_discovery_impl_test.cc @@ -62,7 +62,7 @@ class TestHttpFilterFactory : public TestFilterFactory, return [](Http::FilterChainFactoryCallbacks&) -> void {}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } bool isTerminalFilterByProto(const Protobuf::Message&, @@ -89,7 +89,7 @@ class TestNetworkFilterFactory return [](Network::FilterManager&) -> void {}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } bool isTerminalFilterByProto(const Protobuf::Message&, @@ -109,7 +109,7 @@ class TestListenerFilterFactory : public TestFilterFactory, return [](Network::ListenerFilterManager&) -> void {}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } }; @@ -125,7 +125,7 @@ class TestUdpListenerFilterFactory return [](Network::UdpListenerFilterManager&, Network::UdpReadFilterCallbacks&) -> void {}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } }; @@ -141,7 +141,7 @@ class TestUdpSessionFilterFactory return [](Network::UdpSessionFilterChainFactoryCallbacks&) -> void {}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } }; @@ -158,7 +158,7 @@ class TestQuicListenerFilterFactory return [](Network::QuicListenerFilterManager&) -> void {}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } }; @@ -218,7 +218,7 @@ class FilterConfigDiscoveryImplTest : public FilterConfigDiscoveryTestBase { config_source.add_type_urls(getTypeUrl()); config_source.set_apply_default_config_without_warming(!warm); if (default_configuration || !warm) { - ProtobufWkt::StringValue default_config; + Protobuf::StringValue default_config; config_source.mutable_default_config()->PackFrom(default_config); } diff --git a/test/common/formatter/command_extension.cc b/test/common/formatter/command_extension.cc index a29f6b8c9feb9..16aeda5db8a83 100644 --- a/test/common/formatter/command_extension.cc +++ b/test/common/formatter/command_extension.cc @@ -10,7 +10,7 @@ absl::optional TestFormatter::formatWithContext(const HttpFormatter return "TestFormatter"; } -ProtobufWkt::Value +Protobuf::Value TestFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { return ValueUtil::stringValue(formatWithContext(context, stream_info).value()); @@ -30,14 +30,14 @@ TestCommandFactory::createCommandParserFromProto(const Protobuf::Message& messag Server::Configuration::GenericFactoryContext&) { // Cast the config message to the actual type to test that it was constructed properly. [[maybe_unused]] const auto& config = - *Envoy::Protobuf::DynamicCastMessage(&message); + *Envoy::Protobuf::DynamicCastMessage(&message); return std::make_unique(); } std::set TestCommandFactory::configTypes() { return {"google.protobuf.StringValue"}; } ProtobufTypes::MessagePtr TestCommandFactory::createEmptyConfigProto() { - return std::make_unique(); + return std::make_unique(); } std::string TestCommandFactory::name() const { return "envoy.formatter.TestFormatter"; } @@ -48,7 +48,7 @@ AdditionalFormatter::formatWithContext(const HttpFormatterContext&, return "AdditionalFormatter"; } -ProtobufWkt::Value +Protobuf::Value AdditionalFormatter::formatValueWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { return ValueUtil::stringValue(formatWithContext(context, stream_info).value()); @@ -67,7 +67,7 @@ CommandParserPtr AdditionalCommandFactory::createCommandParserFromProto( const Protobuf::Message& message, Server::Configuration::GenericFactoryContext&) { // Cast the config message to the actual type to test that it was constructed properly. [[maybe_unused]] const auto& config = - *Envoy::Protobuf::DynamicCastMessage(&message); + *Envoy::Protobuf::DynamicCastMessage(&message); return std::make_unique(); } @@ -76,7 +76,7 @@ std::set AdditionalCommandFactory::configTypes() { } ProtobufTypes::MessagePtr AdditionalCommandFactory::createEmptyConfigProto() { - return std::make_unique(); + return std::make_unique(); } std::string AdditionalCommandFactory::name() const { return "envoy.formatter.AdditionalFormatter"; } @@ -86,14 +86,14 @@ FailCommandFactory::createCommandParserFromProto(const Protobuf::Message& messag Server::Configuration::GenericFactoryContext&) { // Cast the config message to the actual type to test that it was constructed properly. [[maybe_unused]] const auto& config = - *Envoy::Protobuf::DynamicCastMessage(&message); + *Envoy::Protobuf::DynamicCastMessage(&message); return nullptr; } std::set FailCommandFactory::configTypes() { return {"google.protobuf.UInt64Value"}; } ProtobufTypes::MessagePtr FailCommandFactory::createEmptyConfigProto() { - return std::make_unique(); + return std::make_unique(); } std::string FailCommandFactory::name() const { return "envoy.formatter.FailFormatter"; } diff --git a/test/common/formatter/command_extension.h b/test/common/formatter/command_extension.h index 6b3e89b3342e8..3b442c2e57976 100644 --- a/test/common/formatter/command_extension.h +++ b/test/common/formatter/command_extension.h @@ -17,9 +17,8 @@ class TestFormatter : public FormatterProvider { formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; class TestCommandParser : public CommandParser { @@ -45,9 +44,8 @@ class AdditionalFormatter : public FormatterProvider { formatWithContext(const HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const override; - ProtobufWkt::Value - formatValueWithContext(const HttpFormatterContext& context, - const StreamInfo::StreamInfo& stream_info) const override; + Protobuf::Value formatValueWithContext(const HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; }; class AdditionalCommandParser : public CommandParser { diff --git a/test/common/formatter/substitution_formatter_fuzz_test.cc b/test/common/formatter/substitution_formatter_fuzz_test.cc index df31aa446f808..d467e6699f56f 100644 --- a/test/common/formatter/substitution_formatter_fuzz_test.cc +++ b/test/common/formatter/substitution_formatter_fuzz_test.cc @@ -64,7 +64,7 @@ DEFINE_PROTO_FUZZER(const test::common::substitution::TestCase& input) { try { // Create struct for JSON formatter. - ProtobufWkt::Struct struct_for_json_formatter; + Protobuf::Struct struct_for_json_formatter; TestUtility::loadFromYaml(fmt::format(R"EOF( may_empty_a: '%REQ(may_empty)%' raw_bool_value: true @@ -111,7 +111,7 @@ DEFINE_PROTO_FUZZER(const test::common::substitution::TestCase& input) { formatter_omit_empty->formatWithContext(formatter_context, *stream_info); // Ensure the result is legal JSON. - ProtobufWkt::Struct proto_struct; + Protobuf::Struct proto_struct; TestUtility::loadFromJson(keep_empty_result, proto_struct); TestUtility::loadFromJson(omit_empty_result, proto_struct); diff --git a/test/common/formatter/substitution_formatter_speed_test.cc b/test/common/formatter/substitution_formatter_speed_test.cc index 33b71027c1d2b..1138fab417d96 100644 --- a/test/common/formatter/substitution_formatter_speed_test.cc +++ b/test/common/formatter/substitution_formatter_speed_test.cc @@ -12,7 +12,7 @@ namespace Envoy { namespace { std::unique_ptr makeJsonFormatter() { - ProtobufWkt::Struct struct_format; + Protobuf::Struct struct_format; const std::string format_yaml = R"EOF( remote_address: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%' start_time: '%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)%' @@ -82,7 +82,7 @@ static void BM_AccessLogFormatterTextMockJson(benchmark::State& state) { testing::NiceMock time_system; std::unique_ptr stream_info = makeStreamInfo(time_system); - ProtobufWkt::Struct struct_format; + Protobuf::Struct struct_format; const std::string format_yaml = R"EOF( remote_address: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%' start_time: '%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)%' diff --git a/test/common/formatter/substitution_formatter_test.cc b/test/common/formatter/substitution_formatter_test.cc index c6fee03003c67..b7d5f6247bd25 100644 --- a/test/common/formatter/substitution_formatter_test.cc +++ b/test/common/formatter/substitution_formatter_test.cc @@ -68,9 +68,8 @@ class StreamInfoFormatter : public FormatterProvider { const StreamInfo::StreamInfo& stream_info) const override { return formatter_->formatWithContext(context, stream_info); } - ProtobufWkt::Value - formatValueWithContext(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override { + Protobuf::Value formatValueWithContext(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override { return formatter_->formatValueWithContext(context, stream_info); } @@ -81,7 +80,7 @@ class StreamInfoFormatter : public FormatterProvider { class TestSerializedUnknownFilterState : public StreamInfo::FilterState::Object { public: ProtobufTypes::MessagePtr serializeAsProto() const override { - auto any = std::make_unique(); + auto any = std::make_unique(); any->set_type_url("UnknownType"); any->set_value("\xde\xad\xbe\xef"); return any; @@ -94,7 +93,7 @@ class TestSerializedStructFilterState : public StreamInfo::FilterState::Object { (*struct_.mutable_fields())["inner_key"] = ValueUtil::stringValue("inner_value"); } - explicit TestSerializedStructFilterState(const ProtobufWkt::Struct& s) : use_struct_(true) { + explicit TestSerializedStructFilterState(const Protobuf::Struct& s) : use_struct_(true) { struct_.CopyFrom(s); } @@ -104,20 +103,20 @@ class TestSerializedStructFilterState : public StreamInfo::FilterState::Object { ProtobufTypes::MessagePtr serializeAsProto() const override { if (use_struct_) { - auto s = std::make_unique(); + auto s = std::make_unique(); s->CopyFrom(struct_); return s; } - auto d = std::make_unique(); + auto d = std::make_unique(); d->CopyFrom(duration_); return d; } private: const bool use_struct_{false}; - ProtobufWkt::Struct struct_; - ProtobufWkt::Duration duration_; + Protobuf::Struct struct_; + Protobuf::Duration duration_; }; // Class used to test serializeAsString and serializeAsProto of FilterState @@ -128,7 +127,7 @@ class TestSerializedStringFilterState : public StreamInfo::FilterState::Object { return raw_string_ + " By PLAIN"; } ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(raw_string_ + " By TYPED"); return message; } @@ -2906,12 +2905,12 @@ TEST(SubstitutionFormatterTest, TraceIDFormatter) { * "com.test": {"test_key":"test_value","test_obj":{"inner_key":"inner_value"}} */ void populateMetadataTestData(envoy::config::core::v3::Metadata& metadata) { - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; auto& fields_map = *struct_obj.mutable_fields(); fields_map["test_key"] = ValueUtil::stringValue("test_value"); - ProtobufWkt::Struct struct_inner; + Protobuf::Struct struct_inner; (*struct_inner.mutable_fields())["inner_key"] = ValueUtil::stringValue("inner_value"); - ProtobufWkt::Value val; + Protobuf::Value val; *val.mutable_struct_value() = struct_inner; fields_map["test_obj"] = val; (*metadata.mutable_filter_metadata())["com.test"] = struct_obj; @@ -2930,7 +2929,7 @@ TEST(SubstitutionFormatterTest, DynamicMetadataFieldExtractor) { EXPECT_TRUE(val.find("\"test_key\":\"test_value\"") != std::string::npos); EXPECT_TRUE(val.find("\"test_obj\":{\"inner_key\":\"inner_value\"}") != std::string::npos); - ProtobufWkt::Value expected_val; + Protobuf::Value expected_val; expected_val.mutable_struct_value()->CopyFrom(metadata.filter_metadata().at("com.test")); EXPECT_THAT(formatter.formatValue(stream_info), ProtoEq(expected_val)); } @@ -2943,7 +2942,7 @@ TEST(SubstitutionFormatterTest, DynamicMetadataFieldExtractor) { DynamicMetadataFormatter formatter("com.test", {"test_obj"}, absl::optional()); EXPECT_EQ("{\"inner_key\":\"inner_value\"}", formatter.format(stream_info)); - ProtobufWkt::Value expected_val; + Protobuf::Value expected_val; (*expected_val.mutable_struct_value()->mutable_fields())["inner_key"] = ValueUtil::stringValue("inner_value"); EXPECT_THAT(formatter.formatValue(stream_info), ProtoEq(expected_val)); @@ -2983,9 +2982,9 @@ TEST(SubstitutionFormatterTest, DynamicMetadataFieldExtractor) { } { - ProtobufWkt::Value val; + Protobuf::Value val; val.set_number_value(std::numeric_limits::quiet_NaN()); - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; (*struct_obj.mutable_fields())["nan_val"] = val; (*metadata.mutable_filter_metadata())["com.test"] = struct_obj; @@ -2995,9 +2994,9 @@ TEST(SubstitutionFormatterTest, DynamicMetadataFieldExtractor) { } { - ProtobufWkt::Value val; + Protobuf::Value val; val.set_number_value(std::numeric_limits::infinity()); - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; (*struct_obj.mutable_fields())["inf_val"] = val; (*metadata.mutable_filter_metadata())["com.test"] = struct_obj; @@ -3040,7 +3039,7 @@ TEST(SubstitutionFormatterTest, FilterStateFormatter) { EXPECT_EQ("{\"inner_key\":\"inner_value\"}", formatter.format(stream_info)); - ProtobufWkt::Value expected; + Protobuf::Value expected; (*expected.mutable_struct_value()->mutable_fields())["inner_key"] = ValueUtil::stringValue("inner_value"); @@ -3587,7 +3586,7 @@ TEST(SubstitutionFormatterTest, GrpcStatusFormatterNumberTest) { } } -void verifyStructOutput(ProtobufWkt::Struct output, +void verifyStructOutput(Protobuf::Struct output, absl::node_hash_map expected_map) { for (const auto& pair : expected_map) { EXPECT_EQ(output.fields().at(pair.first).string_value(), pair.second); @@ -3609,7 +3608,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterPlainStringTest) { {"plain_string": "plain_string_value"} )EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( plain_string: plain_string_value )EOF", @@ -3632,7 +3631,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterPlainNumberTest) { {"plain_number": 400} )EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( plain_number: 400 )EOF", @@ -3651,7 +3650,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterTypesTest) { absl::optional protocol = Http::Protocol::Http11; EXPECT_CALL(stream_info, protocol()).WillRepeatedly(Return(protocol)); - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( string_type: plain_string_value struct_type: @@ -3664,7 +3663,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterTypesTest) { key_mapping); JsonFormatterImpl formatter(key_mapping, false); - const ProtobufWkt::Struct expected = TestUtility::jsonToStruct(R"EOF({ + const Protobuf::Struct expected = TestUtility::jsonToStruct(R"EOF({ "string_type": "plain_string_value", "struct_type": { "plain_string": "plain_string_value", @@ -3675,7 +3674,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterTypesTest) { "HTTP/1.1" ] })EOF"); - const ProtobufWkt::Struct out_struct = + const Protobuf::Struct out_struct = TestUtility::jsonToStruct(formatter.formatWithContext({}, stream_info)); EXPECT_TRUE(TestUtility::protoEqual(out_struct, expected)); } @@ -3689,7 +3688,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterNestedObjectsTest) { absl::optional protocol = Http::Protocol::Http11; EXPECT_CALL(stream_info, protocol()).WillRepeatedly(Return(protocol)); - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; // For both struct and list, we test 3 nesting levels of all types (string, struct and list). TestUtility::loadFromYaml(R"EOF( struct: @@ -3737,7 +3736,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterNestedObjectsTest) { )EOF", key_mapping); JsonFormatterImpl formatter(key_mapping, false); - const ProtobufWkt::Struct expected = TestUtility::jsonToStruct(R"EOF({ + const Protobuf::Struct expected = TestUtility::jsonToStruct(R"EOF({ "struct": { "struct_string": "plain_string_value", "struct_protocol": "HTTP/1.1", @@ -3795,7 +3794,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterNestedObjectsTest) { ], ], })EOF"); - const ProtobufWkt::Struct out_struct = + const Protobuf::Struct out_struct = TestUtility::jsonToStruct(formatter.formatWithContext({}, stream_info)); EXPECT_TRUE(TestUtility::protoEqual(out_struct, expected)); } @@ -3810,7 +3809,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterSingleOperatorTest) { absl::node_hash_map expected_json_map = {{"protocol", "HTTP/1.1"}}; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( protocol: '%PROTOCOL%' )EOF", @@ -3831,7 +3830,7 @@ TEST(SubstitutionFormatterTest, EmptyJsonFormatterTest) { absl::node_hash_map expected_json_map = {{"protocol", ""}}; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( protocol: '' )EOF", @@ -3859,7 +3858,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterNonExistentHeaderTest) { "some_response_header": "SOME_RESPONSE_HEADER" })EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( protocol: '%PROTOCOL%' some_request_header: '%REQ(some_request_header)%' @@ -3894,7 +3893,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterAlternateHeaderTest) { {"response_absent_header_or_response_absent_header", "RESPONSE_PRESENT_HEADER"}, {"response_present_header_or_response_absent_header", "RESPONSE_PRESENT_HEADER"}}; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( request_present_header_or_request_absent_header: '%REQ(request_present_header?request_absent_header)%' @@ -3939,7 +3938,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterDynamicMetadataTest) { "test_obj.inner_key": "inner_value" })EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key: '%DYNAMIC_METADATA(com.test:test_key)%' test_obj: '%DYNAMIC_METADATA(com.test:test_obj)%' @@ -3979,7 +3978,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterClusterMetadataTest) { "test_obj.non_existing_key": null })EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key: '%CLUSTER_METADATA(com.test:test_key)%' test_obj: '%CLUSTER_METADATA(com.test:test_obj)%' @@ -4007,7 +4006,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterClusterMetadataNoClusterInfoTest) { {"test_key": null} )EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key: '%CLUSTER_METADATA(com.test:test_key)%' )EOF", @@ -4057,7 +4056,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterUpstreamHostMetadataTest) { "test_obj.non_existing_key": null })EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key: '%UPSTREAM_METADATA(com.test:test_key)%' test_obj: '%UPSTREAM_METADATA(com.test:test_obj)%' @@ -4085,7 +4084,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterUpstreamHostMetadataNullPtrs) { {"test_key": null} )EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key: '%UPSTREAM_METADATA(com.test:test_key)%' )EOF", @@ -4136,7 +4135,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterFilterStateTest) { } )EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key: '%FILTER_STATE(test_key)%' test_obj: '%FILTER_STATE(test_obj)%' @@ -4154,7 +4153,7 @@ TEST(SubstitutionFormatterTest, FilterStateSpeciferTest) { Http::TestRequestHeaderMapImpl request_headers; Http::TestResponseHeaderMapImpl response_headers; Http::TestResponseTrailerMapImpl response_trailers; - StreamInfo::MockStreamInfo stream_info; + NiceMock stream_info; std::string body; HttpFormatterContext formatter_context(&request_headers, &response_headers, &response_trailers, @@ -4163,21 +4162,29 @@ TEST(SubstitutionFormatterTest, FilterStateSpeciferTest) { stream_info.filter_state_->setData( "test_key", std::make_unique("test_value"), StreamInfo::FilterState::StateType::ReadOnly); + stream_info.upstream_info_->setUpstreamFilterState( + stream_info.filter_state_); // Reuse the same filter state for test only. EXPECT_CALL(Const(stream_info), filterState()).Times(testing::AtLeast(1)); const std::string expected_json_map = R"EOF( { "test_key_plain": "test_value By PLAIN", "test_key_typed": "test_value By TYPED", - "test_key_field": "test_value" + "test_key_field": "test_value", + "upstream_test_key_plain": "test_value By PLAIN", + "upstream_test_key_typed": "test_value By TYPED", + "upstream_test_key_field": "test_value" } )EOF"; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key_plain: '%FILTER_STATE(test_key:PLAIN)%' test_key_typed: '%FILTER_STATE(test_key:TYPED)%' test_key_field: '%FILTER_STATE(test_key:FIELD:test_field)%' + upstream_test_key_plain: '%UPSTREAM_FILTER_STATE(test_key:PLAIN)%' + upstream_test_key_typed: '%UPSTREAM_FILTER_STATE(test_key:TYPED)%' + upstream_test_key_field: '%UPSTREAM_FILTER_STATE(test_key:FIELD:test_field)%' )EOF", key_mapping); JsonFormatterImpl formatter(key_mapping, false); @@ -4202,7 +4209,7 @@ TEST(SubstitutionFormatterTest, FilterStateErrorSpeciferTest) { StreamInfo::FilterState::StateType::ReadOnly); // 'ABCDE' is error specifier. - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key_plain: '%FILTER_STATE(test_key:ABCDE)%' test_key_typed: '%FILTER_STATE(test_key:TYPED)%' @@ -4214,7 +4221,7 @@ TEST(SubstitutionFormatterTest, FilterStateErrorSpeciferTest) { // Error specifier for PLAIN will cause an error if field is specified. TEST(SubstitutionFormatterTest, FilterStateErrorSpeciferFieldTest) { - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key_plain: '%FILTER_STATE(test_key:PLAIN:test_field)%' )EOF", @@ -4226,7 +4233,7 @@ TEST(SubstitutionFormatterTest, FilterStateErrorSpeciferFieldTest) { // Error specifier for FIELD will cause an exception to be thrown if no field is specified. TEST(SubstitutionFormatterTest, FilterStateErrorSpeciferFieldNoNameTest) { - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( test_key_plain: '%FILTER_STATE(test_key:FIELD)%' )EOF", @@ -4260,7 +4267,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterStartTimeTest) { absl::FormatTime("%Y-%m-%dT%H:%M:%E3S%z", absl::FromChrono(time), absl::LocalTimeZone())}, {"all_zeroes", "000000000.0.00.000"}}; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( simple_date: '%START_TIME(%Y/%m/%d)%' test_time: '%START_TIME(%s)%' @@ -4293,7 +4300,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterMultiTokenTest) { absl::node_hash_map expected_json_map = { {"multi_token_field", "HTTP/1.1 plainstring SOME_REQUEST_HEADER SOME_RESPONSE_HEADER"}}; - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( multi_token_field: '%PROTOCOL% plainstring %REQ(some_request_header)% %RESP(some_response_header)%' @@ -4330,7 +4337,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterTest) { .WillOnce(Return(MonotonicTime(std::chrono::nanoseconds(5000000)))); stream_info.downstream_timing_.onLastDownstreamRxByteReceived(time_system); - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( raw_bool_value: true raw_nummber_value: 6 @@ -4394,7 +4401,7 @@ TEST(SubstitutionFormatterTest, JsonFormatterWithOrderedPropertiesTest) { .WillOnce(Return(MonotonicTime(std::chrono::nanoseconds(5000000)))); stream_info.downstream_timing_.onLastDownstreamRxByteReceived(time_system); - ProtobufWkt::Struct key_mapping; + Protobuf::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( request_duration: '%REQUEST_DURATION%' bfield: valb diff --git a/test/common/grpc/async_client_impl_test.cc b/test/common/grpc/async_client_impl_test.cc index 095015feb8dfb..3dd8d511c7d57 100644 --- a/test/common/grpc/async_client_impl_test.cc +++ b/test/common/grpc/async_client_impl_test.cc @@ -529,6 +529,59 @@ TEST_F(EnvoyAsyncClientImplTest, StreamHttpClientException) { EXPECT_EQ(grpc_stream, nullptr); } +TEST_F(EnvoyAsyncClientImplTest, AsyncRequestDetach) { + NiceMock> grpc_callbacks; + Http::AsyncClient::StreamCallbacks* http_callbacks; + + StreamInfo::StreamInfoImpl stream_info{test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain}; + NiceMock http_stream; + ON_CALL(Const(http_stream), streamInfo()).WillByDefault(ReturnRef(stream_info)); + ON_CALL(http_stream, streamInfo()).WillByDefault(ReturnRef(stream_info)); + + EXPECT_CALL(http_client_, start(_, _)) + .WillOnce( + Invoke([&http_callbacks, &http_stream](Http::AsyncClient::StreamCallbacks& callbacks, + const Http::AsyncClient::StreamOptions&) { + http_callbacks = &callbacks; + return &http_stream; + })); + + const std::string expected_downstream_local_address = "5.5.5.5"; + EXPECT_CALL(grpc_callbacks, onCreateInitialMetadata(_)); + EXPECT_CALL(http_stream, sendHeaders(_, _)); + + // Prepare the parent context of this call. + auto connection_info_provider = std::make_shared( + std::make_shared(expected_downstream_local_address), nullptr); + + StreamInfo::StreamInfoImpl parent_stream_info{test_time_.timeSystem(), connection_info_provider, + StreamInfo::FilterState::LifeSpan::FilterChain}; + Http::AsyncClient::ParentContext parent_context{&parent_stream_info}; + testing::NiceMock watermark_callbacks; + auto parent_span = std::make_unique(); + + Http::AsyncClient::StreamOptions stream_options; + stream_options.setParentContext(parent_context); + stream_options.setSidestreamWatermarkCallbacks(&watermark_callbacks); + stream_options.setParentSpan(*parent_span); + + helloworld::HelloRequest request_msg; + auto grpc_request = grpc_client_->send(*method_descriptor_, request_msg, grpc_callbacks, + *parent_span, stream_options); + EXPECT_NE(grpc_request, nullptr); + + EXPECT_CALL(http_stream, removeWatermarkCallbacks()); + stream_info.setParentStreamInfo(parent_stream_info); // Mock Envoy setting parent stream info. + + grpc_request->detach(); + + EXPECT_FALSE(grpc_request->streamInfo().parentStreamInfo().has_value()); + + // Clean up by simulating a reset from the HTTP stream. + http_callbacks->onReset(); +} + } // namespace } // namespace Grpc } // namespace Envoy diff --git a/test/common/grpc/google_async_client_impl_test.cc b/test/common/grpc/google_async_client_impl_test.cc index f75d30528f555..c7f4a8804224b 100644 --- a/test/common/grpc/google_async_client_impl_test.cc +++ b/test/common/grpc/google_async_client_impl_test.cc @@ -230,6 +230,32 @@ TEST_F(EnvoyGoogleLessMockedAsyncClientImplTest, TestOverflow) { EXPECT_TRUE(grpc_stream->isAboveWriteBufferHighWatermark()); } +TEST_F(EnvoyGoogleLessMockedAsyncClientImplTest, AsyncRequestDetach) { + // Set an (unreasonably) low byte limit. + auto* google_grpc = config_.mutable_google_grpc(); + google_grpc->mutable_per_stream_buffer_limit_bytes()->set_value(1); + initialize(); + + NiceMock> grpc_callbacks; + + Http::AsyncClient::RequestOptions request_options; + auto parent_span = std::make_shared(); + StreamInfo::StreamInfoImpl stream_info{test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain}; + request_options.setParentSpan(*parent_span); + request_options.setParentContext(Http::AsyncClient::ParentContext{&stream_info}); + + helloworld::HelloRequest request_msg; + auto grpc_request = grpc_client_->send(*method_descriptor_, request_msg, grpc_callbacks, + *parent_span, request_options); + EXPECT_FALSE(grpc_request == nullptr); + + // Detach the request. No big sense for google async client but just test the code path. + grpc_request->detach(); + + grpc_request->cancel(); +} + } // namespace } // namespace Grpc } // namespace Envoy diff --git a/test/common/grpc/grpc_client_integration_test.cc b/test/common/grpc/grpc_client_integration_test.cc index f07ae717a115c..3e07e29c84d3d 100644 --- a/test/common/grpc/grpc_client_integration_test.cc +++ b/test/common/grpc/grpc_client_integration_test.cc @@ -264,12 +264,14 @@ TEST_P(GrpcClientIntegrationTest, BasicStreamWithBytesMeter) { auto upstream_meter = stream->grpc_stream_->streamInfo().getUpstreamBytesMeter(); uint64_t total_bytes_sent = upstream_meter->wireBytesSent(); uint64_t header_bytes_sent = upstream_meter->headerBytesSent(); + uint64_t decompressed_header_bytes_sent = upstream_meter->decompressedHeaderBytesSent(); // Verify the number of sent bytes that is tracked in stream info equals to the length of // request buffer. // Note, in HTTP2 codec, H2_FRAME_HEADER_SIZE is always included in bytes meter so we need to // account for it in the check here as well. EXPECT_EQ(total_bytes_sent - header_bytes_sent, send_buf->length() + Http::Http2::H2_FRAME_HEADER_SIZE); + EXPECT_GE(decompressed_header_bytes_sent, header_bytes_sent); stream->sendReply(/*check_response_size=*/true); stream->sendServerTrailers(Status::WellKnownGrpcStatus::Ok, "", empty_metadata_); @@ -344,14 +346,18 @@ TEST_P(GrpcClientIntegrationTest, MultiStreamWithBytesMeter) { auto upstream_meter_0 = stream_0->grpc_stream_->streamInfo().getUpstreamBytesMeter(); uint64_t total_bytes_sent = upstream_meter_0->wireBytesSent(); uint64_t header_bytes_sent = upstream_meter_0->headerBytesSent(); + uint64_t decompressed_header_bytes_sent = upstream_meter_0->decompressedHeaderBytesSent(); EXPECT_EQ(total_bytes_sent - header_bytes_sent, send_buf->length() + Http::Http2::H2_FRAME_HEADER_SIZE); + EXPECT_GE(decompressed_header_bytes_sent, header_bytes_sent); auto upstream_meter_1 = stream_1->grpc_stream_->streamInfo().getUpstreamBytesMeter(); uint64_t total_bytes_sent_1 = upstream_meter_1->wireBytesSent(); uint64_t header_bytes_sent_1 = upstream_meter_1->headerBytesSent(); + uint64_t decompressed_header_bytes_sent_1 = upstream_meter_1->decompressedHeaderBytesSent(); EXPECT_EQ(total_bytes_sent_1 - header_bytes_sent_1, send_buf->length() + Http::Http2::H2_FRAME_HEADER_SIZE); + EXPECT_GE(decompressed_header_bytes_sent_1, header_bytes_sent_1); stream_0->sendServerInitialMetadata(empty_metadata_); stream_0->sendReply(true); diff --git a/test/common/grpc/grpc_client_integration_test_harness.h b/test/common/grpc/grpc_client_integration_test_harness.h index 678714029532f..9f0721c4f176b 100644 --- a/test/common/grpc/grpc_client_integration_test_harness.h +++ b/test/common/grpc/grpc_client_integration_test_harness.h @@ -188,12 +188,15 @@ class HelloworldStream : public MockAsyncStreamCallbacks // Verify that the number of received byte that is tracked in the stream info equals to // the length of reply response buffer. auto upstream_meter = this->grpc_stream_->streamInfo().getUpstreamBytesMeter(); - uint64_t total_bytes_rev = upstream_meter->wireBytesReceived(); - uint64_t header_bytes_rev = upstream_meter->headerBytesReceived(); + uint64_t total_bytes_recv = upstream_meter->wireBytesReceived(); + uint64_t header_bytes_recv = upstream_meter->headerBytesReceived(); + uint64_t decompressed_header_bytes_recv = + upstream_meter->decompressedHeaderBytesReceived(); // In HTTP2 codec, H2_FRAME_HEADER_SIZE is always included in bytes meter so we need to // account for it in the check here as well. - EXPECT_EQ(total_bytes_rev - header_bytes_rev, + EXPECT_EQ(total_bytes_recv - header_bytes_recv, recv_buf->length() + Http::Http2::H2_FRAME_HEADER_SIZE); + EXPECT_GE(decompressed_header_bytes_recv, header_bytes_recv); } response_received_ = true; dispatcher_helper_.exitDispatcherIfNeeded(); diff --git a/test/common/http/BUILD b/test/common/http/BUILD index 868182b92b8d8..3c522bcda3803 100644 --- a/test/common/http/BUILD +++ b/test/common/http/BUILD @@ -652,6 +652,7 @@ envoy_cc_test( deps = [ "//source/common/formatter:formatter_extension_lib", "//source/common/http:header_mutation_lib", + "//test/mocks/server:server_factory_context_mocks", "//test/mocks/stream_info:stream_info_mocks", "//test/test_common:utility_lib", ], diff --git a/test/common/http/async_client_impl_test.cc b/test/common/http/async_client_impl_test.cc index 11cdd06c5103c..3f3ee8bf88442 100644 --- a/test/common/http/async_client_impl_test.cc +++ b/test/common/http/async_client_impl_test.cc @@ -2284,7 +2284,7 @@ TEST_F(AsyncClientImplTest, MetadataMatchCriteriaWithValidRouteEntry) { NiceMock route_entry; const auto metadata_criteria = - std::make_shared(ProtobufWkt::Struct()); + std::make_shared(Protobuf::Struct()); EXPECT_CALL(*route, routeEntry()).WillRepeatedly(Return(&route_entry)); EXPECT_CALL(route_entry, metadataMatchCriteria()).WillRepeatedly(Return(metadata_criteria.get())); @@ -2312,21 +2312,18 @@ class AsyncClientImplUnitTest : public AsyncClientImplTest { public: AsyncClientImplUnitTest() { envoy::config::route::v3::RetryPolicy proto_policy; - Upstream::RetryExtensionFactoryContextImpl factory_context( - client_.factory_context_.singletonManager()); - auto policy_or_error = - Router::RetryPolicyImpl::create(proto_policy, ProtobufMessage::getNullValidationVisitor(), - factory_context, client_.factory_context_); + auto policy_or_error = Router::RetryPolicyImpl::create( + proto_policy, ProtobufMessage::getNullValidationVisitor(), client_.factory_context_); THROW_IF_NOT_OK_REF(policy_or_error.status()); retry_policy_ = std::move(policy_or_error.value()); EXPECT_TRUE(retry_policy_.get()); route_impl_ = *NullRouteImpl::create( - client_.cluster_->name(), *retry_policy_, regex_engine_, absl::nullopt, + client_.cluster_->name(), retry_policy_, regex_engine_, absl::nullopt, Protobuf::RepeatedPtrField()); } - std::unique_ptr retry_policy_; + std::shared_ptr retry_policy_; Regex::GoogleReEngine regex_engine_; std::unique_ptr route_impl_; std::unique_ptr stream_ = std::move( @@ -2350,18 +2347,15 @@ class AsyncClientImplUnitTest : public AsyncClientImplTest { envoy::config::route::v3::RetryPolicy proto_policy; TestUtility::loadFromYaml(yaml_config, proto_policy); - Upstream::RetryExtensionFactoryContextImpl factory_context( - client_.factory_context_.singletonManager()); - auto policy_or_error = - Router::RetryPolicyImpl::create(proto_policy, ProtobufMessage::getNullValidationVisitor(), - factory_context, client_.factory_context_); + auto policy_or_error = Router::RetryPolicyImpl::create( + proto_policy, ProtobufMessage::getNullValidationVisitor(), client_.factory_context_); THROW_IF_NOT_OK_REF(policy_or_error.status()); retry_policy_ = std::move(policy_or_error.value()); EXPECT_TRUE(retry_policy_.get()); stream_ = std::move( Http::AsyncStreamImpl::create(client_, stream_callbacks_, - AsyncClient::StreamOptions().setRetryPolicy(*retry_policy_)) + AsyncClient::StreamOptions().setRetryPolicy(retry_policy_)) .value()); } @@ -2413,14 +2407,14 @@ retry_on: 5xx,gateway-error,connect-failure,reset auto& route_entry = getRouteFromStream(); - EXPECT_EQ(route_entry.retryPolicy().numRetries(), 10); - EXPECT_EQ(route_entry.retryPolicy().perTryTimeout(), std::chrono::seconds(30)); + EXPECT_EQ(route_entry.retryPolicy()->numRetries(), 10); + EXPECT_EQ(route_entry.retryPolicy()->perTryTimeout(), std::chrono::seconds(30)); EXPECT_EQ(Router::RetryPolicy::RETRY_ON_CONNECT_FAILURE | Router::RetryPolicy::RETRY_ON_5XX | Router::RetryPolicy::RETRY_ON_GATEWAY_ERROR | Router::RetryPolicy::RETRY_ON_RESET, - route_entry.retryPolicy().retryOn()); + route_entry.retryPolicy()->retryOn()); - EXPECT_EQ(route_entry.retryPolicy().baseInterval(), std::chrono::milliseconds(10)); - EXPECT_EQ(route_entry.retryPolicy().maxInterval(), std::chrono::seconds(30)); + EXPECT_EQ(route_entry.retryPolicy()->baseInterval(), std::chrono::milliseconds(10)); + EXPECT_EQ(route_entry.retryPolicy()->maxInterval(), std::chrono::seconds(30)); } TEST_F(AsyncClientImplUnitTest, AsyncStreamImplInitTestWithInvalidRetryPolicy) { @@ -2454,15 +2448,15 @@ retry_on: 5xx,gateway-error,connect-failure,reset,reset-before-request setRetryPolicy(yaml); auto& route_entry = getRouteFromStream(); - EXPECT_EQ(route_entry.retryPolicy().numRetries(), 10); - EXPECT_EQ(route_entry.retryPolicy().perTryTimeout(), std::chrono::seconds(30)); + EXPECT_EQ(route_entry.retryPolicy()->numRetries(), 10); + EXPECT_EQ(route_entry.retryPolicy()->perTryTimeout(), std::chrono::seconds(30)); EXPECT_EQ(Router::RetryPolicy::RETRY_ON_CONNECT_FAILURE | Router::RetryPolicy::RETRY_ON_5XX | Router::RetryPolicy::RETRY_ON_GATEWAY_ERROR | Router::RetryPolicy::RETRY_ON_RESET | Router::RetryPolicy::RETRY_ON_RESET_BEFORE_REQUEST, - route_entry.retryPolicy().retryOn()); + route_entry.retryPolicy()->retryOn()); - EXPECT_EQ(route_entry.retryPolicy().baseInterval(), std::chrono::milliseconds(10)); - EXPECT_EQ(route_entry.retryPolicy().maxInterval(), std::chrono::seconds(30)); + EXPECT_EQ(route_entry.retryPolicy()->baseInterval(), std::chrono::milliseconds(10)); + EXPECT_EQ(route_entry.retryPolicy()->maxInterval(), std::chrono::seconds(30)); } TEST_F(AsyncClientImplUnitTest, NullConfig) { @@ -2470,7 +2464,7 @@ TEST_F(AsyncClientImplUnitTest, NullConfig) { } TEST_F(AsyncClientImplUnitTest, NullVirtualHost) { - EXPECT_EQ(std::numeric_limits::max(), vhost_.retryShadowBufferLimit()); + EXPECT_EQ(std::numeric_limits::max(), vhost_.requestBodyBufferLimit()); } } // namespace Http diff --git a/test/common/http/conn_manager_impl_fuzz_test.cc b/test/common/http/conn_manager_impl_fuzz_test.cc index 53bc9d6ba4f5b..d7f367977e27c 100644 --- a/test/common/http/conn_manager_impl_fuzz_test.cc +++ b/test/common/http/conn_manager_impl_fuzz_test.cc @@ -160,6 +160,9 @@ class FuzzConfig : public ConnectionManagerConfig { return max_stream_duration_; } std::chrono::milliseconds streamIdleTimeout() const override { return stream_idle_timeout_; } + absl::optional streamFlushTimeout() const override { + return stream_flush_timeout_; + } std::chrono::milliseconds requestTimeout() const override { return request_timeout_; } std::chrono::milliseconds requestHeadersTimeout() const override { return request_headers_timeout_; @@ -282,6 +285,7 @@ class FuzzConfig : public ConnectionManagerConfig { bool http1_safe_max_connection_duration_{false}; absl::optional max_stream_duration_; std::chrono::milliseconds stream_idle_timeout_{}; + std::chrono::milliseconds stream_flush_timeout_{}; std::chrono::milliseconds request_timeout_{}; std::chrono::milliseconds request_headers_timeout_{}; std::chrono::milliseconds delayed_close_timeout_{}; diff --git a/test/common/http/conn_manager_impl_test.cc b/test/common/http/conn_manager_impl_test.cc index ac99c40776eec..992ab73bb9522 100644 --- a/test/common/http/conn_manager_impl_test.cc +++ b/test/common/http/conn_manager_impl_test.cc @@ -1245,10 +1245,10 @@ TEST_F(HttpConnectionManagerImplTest, DelegatingRouteEntryAllCalls) { .getApplicableRateLimit(0) .empty()); - EXPECT_EQ(default_route->routeEntry()->retryPolicy().numRetries(), - delegating_route_foo->routeEntry()->retryPolicy().numRetries()); - EXPECT_EQ(default_route->routeEntry()->retryPolicy().retryOn(), - delegating_route_foo->routeEntry()->retryPolicy().retryOn()); + EXPECT_EQ(default_route->routeEntry()->retryPolicy()->numRetries(), + delegating_route_foo->routeEntry()->retryPolicy()->numRetries()); + EXPECT_EQ(default_route->routeEntry()->retryPolicy()->retryOn(), + delegating_route_foo->routeEntry()->retryPolicy()->retryOn()); EXPECT_EQ(default_route->routeEntry()->internalRedirectPolicy().enabled(), delegating_route_foo->routeEntry()->internalRedirectPolicy().enabled()); @@ -1259,8 +1259,8 @@ TEST_F(HttpConnectionManagerImplTest, DelegatingRouteEntryAllCalls) { ->internalRedirectPolicy() .shouldRedirectForResponseCode(Code::OK)); - EXPECT_EQ(default_route->routeEntry()->retryShadowBufferLimit(), - delegating_route_foo->routeEntry()->retryShadowBufferLimit()); + EXPECT_EQ(default_route->routeEntry()->requestBodyBufferLimit(), + delegating_route_foo->routeEntry()->requestBodyBufferLimit()); EXPECT_EQ(default_route->routeEntry()->shadowPolicies().empty(), delegating_route_foo->routeEntry()->shadowPolicies().empty()); EXPECT_EQ(default_route->routeEntry()->timeout(), @@ -1322,15 +1322,20 @@ TEST_F(HttpConnectionManagerImplTest, DelegatingRouteEntryAllCalls) { EXPECT_EQ(default_route->routeName(), delegating_route_foo->routeName()); + Formatter::HttpFormatterContext formatter_context; + // Coverage for finalizeRequestHeaders NiceMock stream_info; - delegating_route_foo->routeEntry()->finalizeRequestHeaders(test_req_headers, stream_info, - true); + formatter_context.setRequestHeaders(test_req_headers); + delegating_route_foo->routeEntry()->finalizeRequestHeaders( + test_req_headers, formatter_context, stream_info, true); EXPECT_EQ("/new_endpoint/foo", test_req_headers.get_(Http::Headers::get().Path)); // Coverage for finalizeResponseHeaders Http::TestResponseHeaderMapImpl test_resp_headers; - delegating_route_foo->routeEntry()->finalizeResponseHeaders(test_resp_headers, stream_info); + formatter_context.setResponseHeaders(test_resp_headers); + delegating_route_foo->routeEntry()->finalizeResponseHeaders(test_resp_headers, + formatter_context, stream_info); EXPECT_EQ(test_resp_headers, Http::TestResponseHeaderMapImpl{}); return FilterHeadersStatus::StopIteration; @@ -1588,13 +1593,15 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlow) { tracing_config_ = std::make_unique( TracingConnectionManagerConfig{Tracing::OperationName::Ingress, conn_tracing_tags, percent1, percent2, percent1, false, 256}); - NiceMock route_tracing; - ON_CALL(route_tracing, getClientSampling()).WillByDefault(ReturnRef(percent1)); - ON_CALL(route_tracing, getRandomSampling()).WillByDefault(ReturnRef(percent2)); - ON_CALL(route_tracing, getOverallSampling()).WillByDefault(ReturnRef(percent1)); - ON_CALL(route_tracing, getCustomTags()).WillByDefault(ReturnRef(route_tracing_tags)); + NiceMock& route_tracing = + route_config_provider_.route_config_->route_->route_tracing_; + route_tracing.client_sampling_ = percent1; + route_tracing.random_sampling_ = percent2; + route_tracing.overall_sampling_ = percent1; + route_tracing.custom_tags_ = route_tracing_tags; + ON_CALL(*route_config_provider_.route_config_->route_, tracingConfig()) - .WillByDefault(Return(&route_tracing)); + .WillByDefault(Return(&route_config_provider_.route_config_->route_->route_tracing_)); EXPECT_CALL(*span, finishSpan()); EXPECT_CALL(*span, setTag(_, _)).Times(testing::AnyNumber()); @@ -1656,6 +1663,236 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlow) { EXPECT_EQ(0UL, tracing_stats_.random_sampling_.value()); } +TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanAndTraceDecisionRefreshAndUseDecision) { + setup(SetupOpts().setTracing(true)); + + std::shared_ptr filter(new NiceMock()); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .WillRepeatedly(Invoke([&](FilterChainManager& manager) -> bool { + auto factory = createDecoderFilterFactoryCb(filter); + manager.applyFilterFactoryCb({}, factory); + return true; + })); + + // Treat request as internal, otherwise x-request-id header will be overwritten. + use_remote_address_ = false; + EXPECT_CALL(random_, uuid()).Times(0); + + EXPECT_CALL(*codec_, dispatch(_)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data) -> Http::Status { + decoder_ = &conn_manager_->newStream(response_encoder_); + + RequestHeaderMapPtr headers{ + new TestRequestHeaderMapImpl{{":method", "GET"}, + {":authority", "host"}, + {":path", "/"}, + {"x-request-id", "125a4afb-6f55-a4ba-ad80-413f09f48a28"}}}; + + auto* span = new NiceMock(); + EXPECT_CALL(*tracer_, startSpan_(_, _, _, _)) + .WillOnce(Invoke([&](const Tracing::Config& config, Tracing::TraceContext&, + const StreamInfo::StreamInfo&, + const Tracing::Decision) -> Tracing::Span* { + EXPECT_EQ(Tracing::OperationName::Ingress, config.operationName()); + + return span; + })); + + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("tracing.global_enabled", + An(), _)) + .WillOnce(Return(true)); + + decoder_->decodeHeaders(std::move(headers), true); + + // The trace decision will be refreshed when the route is refreshed. + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("tracing.global_enabled", + An(), _)) + .WillOnce(Return(false)); + EXPECT_CALL(*span, useLocalDecision()).WillOnce(Return(true)); + EXPECT_CALL(*span, setSampled(false)); + + // Clear route cache and refresh the route to trigger a new trace decision. + filter->callbacks_->downstreamCallbacks()->clearRouteCache(); + filter->callbacks_->route(); + + EXPECT_CALL(*span, finishSpan()); + EXPECT_CALL(*span, setTag(_, _)).Times(testing::AnyNumber()); + + ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}}; + filter->callbacks_->streamInfo().setResponseCodeDetails(""); + filter->callbacks_->encodeHeaders(std::move(response_headers), true, "details"); + filter->callbacks_->activeSpan().setTag("service-cluster", "scoobydoo"); + data.drain(4); + + return Http::okStatus(); + })); + + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + EXPECT_EQ(0UL, tracing_stats_.service_forced_.value()); + EXPECT_EQ(0UL, tracing_stats_.random_sampling_.value()); + EXPECT_EQ(1UL, tracing_stats_.not_traceable_.value()); +} + +TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanAndTraceDecisionRefreshAndNotUseDecision) { + setup(SetupOpts().setTracing(true)); + + std::shared_ptr filter(new NiceMock()); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .WillRepeatedly(Invoke([&](FilterChainManager& manager) -> bool { + auto factory = createDecoderFilterFactoryCb(filter); + manager.applyFilterFactoryCb({}, factory); + return true; + })); + + // Treat request as internal, otherwise x-request-id header will be overwritten. + use_remote_address_ = false; + EXPECT_CALL(random_, uuid()).Times(0); + + EXPECT_CALL(*codec_, dispatch(_)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data) -> Http::Status { + decoder_ = &conn_manager_->newStream(response_encoder_); + + RequestHeaderMapPtr headers{ + new TestRequestHeaderMapImpl{{":method", "GET"}, + {":authority", "host"}, + {":path", "/"}, + {"x-request-id", "125a4afb-6f55-a4ba-ad80-413f09f48a28"}}}; + + auto* span = new NiceMock(); + EXPECT_CALL(*tracer_, startSpan_(_, _, _, _)) + .WillOnce(Invoke([&](const Tracing::Config& config, Tracing::TraceContext&, + const StreamInfo::StreamInfo&, + const Tracing::Decision) -> Tracing::Span* { + EXPECT_EQ(Tracing::OperationName::Ingress, config.operationName()); + + return span; + })); + + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("tracing.global_enabled", + An(), _)) + .WillOnce(Return(true)); + + decoder_->decodeHeaders(std::move(headers), true); + + // The trace decision will be refreshed when the route is refreshed. + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("tracing.global_enabled", + An(), _)) + .WillOnce(Return(false)); + EXPECT_CALL(*span, useLocalDecision()).WillOnce(Return(false)); + EXPECT_CALL(*span, setSampled(_)).Times(0); + + // Clear route cache and refresh the route to trigger a new trace decision. + filter->callbacks_->downstreamCallbacks()->clearRouteCache(); + filter->callbacks_->route(); + + EXPECT_CALL(*span, finishSpan()); + EXPECT_CALL(*span, setTag(_, _)).Times(testing::AnyNumber()); + + ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}}; + filter->callbacks_->streamInfo().setResponseCodeDetails(""); + filter->callbacks_->encodeHeaders(std::move(response_headers), true, "details"); + filter->callbacks_->activeSpan().setTag("service-cluster", "scoobydoo"); + data.drain(4); + + return Http::okStatus(); + })); + + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + EXPECT_EQ(0UL, tracing_stats_.service_forced_.value()); + EXPECT_EQ(0UL, tracing_stats_.random_sampling_.value()); + EXPECT_EQ(1UL, tracing_stats_.not_traceable_.value()); +} + +TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanButDisableTraceDecisionRefresh) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.trace_refresh_after_route_refresh", "false"}}); + + setup(SetupOpts().setTracing(true)); + + std::shared_ptr filter(new NiceMock()); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .WillRepeatedly(Invoke([&](FilterChainManager& manager) -> bool { + auto factory = createDecoderFilterFactoryCb(filter); + manager.applyFilterFactoryCb({}, factory); + return true; + })); + + // Treat request as internal, otherwise x-request-id header will be overwritten. + use_remote_address_ = false; + EXPECT_CALL(random_, uuid()).Times(0); + + EXPECT_CALL(*codec_, dispatch(_)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data) -> Http::Status { + decoder_ = &conn_manager_->newStream(response_encoder_); + + RequestHeaderMapPtr headers{ + new TestRequestHeaderMapImpl{{":method", "GET"}, + {":authority", "host"}, + {":path", "/"}, + {"x-request-id", "125a4afb-6f55-a4ba-ad80-413f09f48a28"}}}; + + auto* span = new NiceMock(); + EXPECT_CALL(*tracer_, startSpan_(_, _, _, _)) + .WillOnce(Invoke([&](const Tracing::Config& config, Tracing::TraceContext&, + const StreamInfo::StreamInfo&, + const Tracing::Decision) -> Tracing::Span* { + EXPECT_EQ(Tracing::OperationName::Ingress, config.operationName()); + + return span; + })); + + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("tracing.global_enabled", + An(), _)) + .WillOnce(Return(true)); + + decoder_->decodeHeaders(std::move(headers), true); + + // The trace decision will be refreshed when the route is refreshed. + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("tracing.global_enabled", + An(), _)) + .Times(0); + EXPECT_CALL(*span, useLocalDecision()).Times(0); + EXPECT_CALL(*span, setSampled(_)).Times(0); + + // Clear route cache and refresh the route. But this will not trigger a new trace + // decision because the feature is disabled. + filter->callbacks_->downstreamCallbacks()->clearRouteCache(); + filter->callbacks_->route(); + + EXPECT_CALL(*span, finishSpan()); + EXPECT_CALL(*span, setTag(_, _)).Times(testing::AnyNumber()); + + ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}}; + filter->callbacks_->streamInfo().setResponseCodeDetails(""); + filter->callbacks_->encodeHeaders(std::move(response_headers), true, "details"); + filter->callbacks_->activeSpan().setTag("service-cluster", "scoobydoo"); + data.drain(4); + + return Http::okStatus(); + })); + + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + EXPECT_EQ(1UL, tracing_stats_.service_forced_.value()); + EXPECT_EQ(0UL, tracing_stats_.random_sampling_.value()); + EXPECT_EQ(0UL, tracing_stats_.not_traceable_.value()); +} + TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlowIngressDecorator) { setup(); @@ -1739,7 +1976,7 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlowIngressDecorat route_config_provider_.route_config_->route_->decorator_.operation_ = "testOp"; ON_CALL(route_config_provider_.route_config_->route_->decorator_, propagate()) .WillByDefault(Return(false)); - EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()).Times(2); + EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()); EXPECT_CALL(route_config_provider_.route_config_->route_->decorator_, apply(_)) .WillOnce(Invoke( [&](const Tracing::Span& apply_to_span) -> void { EXPECT_EQ(span, &apply_to_span); })); @@ -1806,7 +2043,7 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlowIngressDecorat return span; })); route_config_provider_.route_config_->route_->decorator_.operation_ = "initOp"; - EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()).Times(2); + EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()); EXPECT_CALL(route_config_provider_.route_config_->route_->decorator_, apply(_)) .WillOnce(Invoke( [&](const Tracing::Span& apply_to_span) -> void { EXPECT_EQ(span, &apply_to_span); })); @@ -1889,7 +2126,7 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlowEgressDecorato return span; })); route_config_provider_.route_config_->route_->decorator_.operation_ = "testOp"; - EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()).Times(2); + EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()); EXPECT_CALL(route_config_provider_.route_config_->route_->decorator_, apply(_)) .WillOnce(Invoke( [&](const Tracing::Span& apply_to_span) -> void { EXPECT_EQ(span, &apply_to_span); })); @@ -1975,7 +2212,7 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlowEgressDecorato route_config_provider_.route_config_->route_->decorator_.operation_ = "testOp"; ON_CALL(route_config_provider_.route_config_->route_->decorator_, propagate()) .WillByDefault(Return(false)); - EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()).Times(2); + EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()); EXPECT_CALL(route_config_provider_.route_config_->route_->decorator_, apply(_)) .WillOnce(Invoke( [&](const Tracing::Span& apply_to_span) -> void { EXPECT_EQ(span, &apply_to_span); })); @@ -2059,7 +2296,7 @@ TEST_F(HttpConnectionManagerImplTest, StartAndFinishSpanNormalFlowEgressDecorato return span; })); route_config_provider_.route_config_->route_->decorator_.operation_ = "initOp"; - EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()).Times(2); + EXPECT_CALL(*route_config_provider_.route_config_->route_, decorator()); EXPECT_CALL(route_config_provider_.route_config_->route_->decorator_, apply(_)) .WillOnce(Invoke( [&](const Tracing::Span& apply_to_span) -> void { EXPECT_EQ(span, &apply_to_span); })); @@ -2301,9 +2538,9 @@ TEST_F(HttpConnectionManagerImplTest, TestFilterCanEnrichAccessLogs) { })); EXPECT_CALL(*filter, onStreamComplete()).WillOnce(Invoke([&]() { - ProtobufWkt::Value metadata_value; + Protobuf::Value metadata_value; metadata_value.set_string_value("value"); - ProtobufWkt::Struct metadata; + Protobuf::Struct metadata; metadata.mutable_fields()->insert({"field", metadata_value}); filter->callbacks_->streamInfo().setDynamicMetadata("metadata_key", metadata); })); @@ -3240,6 +3477,104 @@ TEST_F(HttpConnectionManagerImplTest, PerStreamIdleTimeoutRouteOverride) { filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); } +// Per-stream flush and idle timeouts have a somewhat complex precedence order, so here we test +// every combination of global and per-route flush and idle timeouts. Note that global idle timeout +// not being set or unset doesn't affect the behavior of the flush timeout since it's the same as +// the idle timeout being set explicitly to the default value. For this reason it's a constant in +// the below tests. +class IdleAndFlushTimeoutTestFixture : public HttpConnectionManagerImplMixin, + public testing::TestWithParam> { +public: + IdleAndFlushTimeoutTestFixture() + : global_flush_timeout_set_(std::get<0>(GetParam())), + route_flush_timeout_set_(std::get<1>(GetParam())), + route_idle_timeout_set_(std::get<2>(GetParam())) { + if (route_flush_timeout_set_) { + route_flush_timeout_ = std::chrono::milliseconds(30); + } + if (route_idle_timeout_set_) { + route_idle_timeout_ = std::chrono::milliseconds(40); + } + } + +protected: + const bool global_flush_timeout_set_; + const bool route_flush_timeout_set_; + const bool route_idle_timeout_set_; + absl::optional global_flush_timeout_{absl::nullopt}; + absl::optional route_flush_timeout_{absl::nullopt}; + absl::optional route_idle_timeout_{absl::nullopt}; +}; + +INSTANTIATE_TEST_SUITE_P(IdleAndFlushTimeoutTestFixture, IdleAndFlushTimeoutTestFixture, + testing::Combine(testing::Bool(), testing::Bool(), testing::Bool()), + [](const testing::TestParamInfo>& info) { + return absl::StrCat(std::get<0>(info.param) ? "GlobalFlushTimeoutSet" + : "NoGlobalFlushTimeout", + std::get<1>(info.param) ? "RouteFlushTimeoutSet" + : "NoRouteFlushTimeout", + std::get<2>(info.param) ? "RouteIdleTimeoutSet" + : "NoRouteIdleTimeout"); + }); + +TEST_P(IdleAndFlushTimeoutTestFixture, TestAllCases) { + stream_idle_timeout_ = std::chrono::milliseconds(10); // Constant across all cases. + if (global_flush_timeout_set_) { + stream_flush_timeout_ = std::chrono::milliseconds(20); + } + setup(); + ON_CALL(route_config_provider_.route_config_->route_->route_entry_, flushTimeout()) + .WillByDefault(Return(route_flush_timeout_)); + ON_CALL(route_config_provider_.route_config_->route_->route_entry_, idleTimeout()) + .WillByDefault(Return(route_idle_timeout_)); + + EXPECT_CALL(*codec_, dispatch(_)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data) -> Http::Status { + // Both timers will get initialized in all cases here. The value of the flush timeout + // just depends on whether it was set explicitly or inherited from the idle timeout. + EXPECT_CALL(response_encoder_.stream_, + setFlushTimeout(global_flush_timeout_set_ ? stream_flush_timeout_.value() + : stream_idle_timeout_)); + Event::MockTimer* idle_timer = setUpTimer(); + EXPECT_CALL(*idle_timer, enableTimer(stream_idle_timeout_, _)); + decoder_ = &conn_manager_->newStream(response_encoder_); + + RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{ + {":authority", "host"}, {":path", "/"}, {":method", "GET"}}}; + + if (route_flush_timeout_set_) { + // If a route flush timeout is set it will ALWAYS be used. + EXPECT_CALL(response_encoder_.stream_, setFlushTimeout(route_flush_timeout_.value())); + } else if (!global_flush_timeout_set_ && route_idle_timeout_set_) { + // If no route flush timeout is set and the global flush timeout was inherited from the + // idle timeout, adopt the route idle timeout. This is for backwards compatibility with + // existing Envoy behavior. + EXPECT_CALL(response_encoder_.stream_, setFlushTimeout(route_idle_timeout_.value())); + } else { + // One of the following is true: + // 1. No route flush or idle timeout is set, so there's nothing to do here. + // 2. The global flush timeout is set explicitly, so the route idle timeout is ignored. + EXPECT_CALL(response_encoder_.stream_, setFlushTimeout(_)).Times(0); + } + + EXPECT_CALL(*idle_timer, disableTimer()); + EXPECT_CALL(*idle_timer, enableTimer(route_idle_timeout_set_ ? route_idle_timeout_.value() + : stream_idle_timeout_, + _)); + + decoder_->decodeHeaders(std::move(headers), false); + + data.drain(4); + return Http::okStatus(); + })); + + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + EXPECT_EQ(0U, stats_.named_.downstream_rq_idle_timeout_.value()); + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); +} + // Per-route zero timeout overrides the global stream idle timeout. TEST_F(HttpConnectionManagerImplTest, PerStreamIdleTimeoutRouteZeroOverride) { stream_idle_timeout_ = std::chrono::milliseconds(10); @@ -4333,8 +4668,6 @@ TEST_F(ProxyStatusTest, PopulateProxyStatusWithDetailsAndResponseCode) { TEST_F(ProxyStatusTest, PopulateUnauthorizedProxyStatus) { TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.proxy_status_mapping_more_core_response_flags", "true"}}); proxy_status_config_ = std::make_unique(); proxy_status_config_->set_remove_details(false); @@ -4350,24 +4683,6 @@ TEST_F(ProxyStatusTest, PopulateUnauthorizedProxyStatus) { EXPECT_EQ(altered_headers->getStatusValue(), "403"); } -TEST_F(ProxyStatusTest, NoPopulateUnauthorizedProxyStatus) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.proxy_status_mapping_more_core_response_flags", "false"}}); - proxy_status_config_ = std::make_unique(); - proxy_status_config_->set_remove_details(false); - - initialize(); - - const ResponseHeaderMap* altered_headers = sendRequestWith( - 403, StreamInfo::CoreResponseFlag::UnauthorizedExternalService, /*details=*/"bar"); - - ASSERT_TRUE(altered_headers); - ASSERT_FALSE(altered_headers->ProxyStatus()); - EXPECT_EQ(altered_headers->getProxyStatusValue(), ""); - EXPECT_EQ(altered_headers->getStatusValue(), "403"); -} - TEST_F(ProxyStatusTest, PopulateProxyStatusWithDetails) { TestScopedRuntime scoped_runtime; proxy_status_config_ = std::make_unique(); diff --git a/test/common/http/conn_manager_impl_test_base.cc b/test/common/http/conn_manager_impl_test_base.cc index 36fb2509d7f5e..e40871cbe3c1f 100644 --- a/test/common/http/conn_manager_impl_test_base.cc +++ b/test/common/http/conn_manager_impl_test_base.cc @@ -61,6 +61,9 @@ class ConnectionManagerConfigProxyObject : public ConnectionManagerConfig { std::chrono::milliseconds streamIdleTimeout() const override { return parent_.streamIdleTimeout(); } + absl::optional streamFlushTimeout() const override { + return parent_.streamFlushTimeout(); + } std::chrono::milliseconds requestTimeout() const override { return parent_.requestTimeout(); } std::chrono::milliseconds requestHeadersTimeout() const override { return parent_.requestHeadersTimeout(); diff --git a/test/common/http/conn_manager_impl_test_base.h b/test/common/http/conn_manager_impl_test_base.h index 55377585d3973..dd5d0ebad5b57 100644 --- a/test/common/http/conn_manager_impl_test_base.h +++ b/test/common/http/conn_manager_impl_test_base.h @@ -122,6 +122,9 @@ class HttpConnectionManagerImplMixin : public ConnectionManagerConfig { return http1_safe_max_connection_duration_; } std::chrono::milliseconds streamIdleTimeout() const override { return stream_idle_timeout_; } + absl::optional streamFlushTimeout() const override { + return stream_flush_timeout_; + } std::chrono::milliseconds requestTimeout() const override { return request_timeout_; } std::chrono::milliseconds requestHeadersTimeout() const override { return request_headers_timeout_; @@ -285,10 +288,11 @@ class HttpConnectionManagerImplMixin : public ConnectionManagerConfig { absl::optional max_connection_duration_; bool http1_safe_max_connection_duration_{false}; std::chrono::milliseconds stream_idle_timeout_{}; + absl::optional stream_flush_timeout_{}; std::chrono::milliseconds request_timeout_{}; std::chrono::milliseconds request_headers_timeout_{}; std::chrono::milliseconds delayed_close_timeout_{}; - absl::optional max_stream_duration_{}; + absl::optional max_stream_duration_; NiceMock random_; NiceMock local_info_; NiceMock factory_context_; @@ -317,8 +321,8 @@ class HttpConnectionManagerImplMixin : public ConnectionManagerConfig { NiceMock upstream_conn_; // for websocket tests NiceMock conn_pool_; // for websocket tests RequestIDExtensionSharedPtr request_id_extension_; - std::vector ip_detection_extensions_{}; - std::vector early_header_mutations_{}; + std::vector ip_detection_extensions_; + std::vector early_header_mutations_; bool add_proxy_protocol_connection_state_ = true; const LocalReply::LocalReplyPtr local_reply_; diff --git a/test/common/http/conn_pool_grid_test.cc b/test/common/http/conn_pool_grid_test.cc index 48171a652c347..10f4549fc326d 100644 --- a/test/common/http/conn_pool_grid_test.cc +++ b/test/common/http/conn_pool_grid_test.cc @@ -318,6 +318,9 @@ TEST_F(ConnectivityGridTest, ImmediateSuccess) { // Test the first pool failing and the second connecting. TEST_F(ConnectivityGridTest, DoubleFailureThenSuccessSerial) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + initialize(); addHttp3AlternateProtocol(); EXPECT_EQ(grid_->http3Pool(), nullptr); @@ -353,6 +356,9 @@ TEST_F(ConnectivityGridTest, DoubleFailureThenSuccessSerial) { // Test HTTP/3 attempting to use the alternate pool immediately if it's connected and TCP not // delayed. TEST_F(ConnectivityGridTest, ThreeParallelConnections) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + initialize(); grid_->alternate_immediate_ = false; addHttp3AlternateProtocol(); @@ -520,13 +526,15 @@ TEST_F(ConnectivityGridTest, ParallelConnectionsNoTcpDelayQuicFailsTcpSucceeds) "reason", host_)); // HTTP/3 is still not marked broken yet. EXPECT_FALSE(grid_->isHttp3Broken()); - // Also force the alternate QUIC to fail. - EXPECT_LOG_CONTAINS( - "trace", "alternate pool failed to create connection to host 'hostname'", - grid_->callbacks(2)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, - "reason", host_)); - // HTTP/3 is still not marked broken yet. - EXPECT_FALSE(grid_->isHttp3Broken()); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + // Also force the alternate QUIC to fail. + EXPECT_LOG_CONTAINS( + "trace", "alternate pool failed to create connection to host 'hostname'", + grid_->callbacks(2)->onPoolFailure( + ConnectionPool::PoolFailureReason::LocalConnectionFailure, "reason", host_)); + // HTTP/3 is still not marked broken yet. + EXPECT_FALSE(grid_->isHttp3Broken()); + } // TCP succeeds. EXPECT_LOG_CONTAINS("trace", "http2 pool successfully connected to host 'hostname'", @@ -580,13 +588,15 @@ TEST_F(ConnectivityGridTest, ParallelConnectionsNoTcpDelayTcpAndQuicFail) { "reason", host_)); // HTTP/3 is still not marked broken yet. EXPECT_FALSE(grid_->isHttp3Broken()); - // Also force the alternate QUIC to fail. - EXPECT_LOG_CONTAINS( - "trace", "alternate pool failed to create connection to host 'hostname'", - grid_->callbacks(2)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, - "reason", host_)); - // HTTP/3 is still not marked broken yet. - EXPECT_FALSE(grid_->isHttp3Broken()); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + // Also force the alternate QUIC to fail. + EXPECT_LOG_CONTAINS( + "trace", "alternate pool failed to create connection to host 'hostname'", + grid_->callbacks(2)->onPoolFailure( + ConnectionPool::PoolFailureReason::LocalConnectionFailure, "reason", host_)); + // HTTP/3 is still not marked broken yet. + EXPECT_FALSE(grid_->isHttp3Broken()); + } // Force TCP to fail. EXPECT_LOG_CONTAINS( @@ -665,6 +675,9 @@ TEST_F(ConnectivityGridTest, ParallelConnectionsNoTcpDelayTcpFailsQuicSucceeds) // Same test as above but with the H3 alternate pool succeeding inline no TCP is attempted. TEST_F(ConnectivityGridTest, ParallelH3NoTcp) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + initialize(); grid_->alternate_immediate_ = false; addHttp3AlternateProtocol(); @@ -697,6 +710,9 @@ TEST_F(ConnectivityGridTest, ParallelH3NoTcp) { // Test all three connections in parallel, H3 failing and TCP connecting. TEST_F(ConnectivityGridTest, ParallelConnectionsTcpConnects) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + initialize(); grid_->alternate_immediate_ = false; addHttp3AlternateProtocol(); @@ -743,6 +759,9 @@ TEST_F(ConnectivityGridTest, ParallelConnectionsTcpConnects) { // Test all three connections in parallel, TCP fails and H3 connecting. TEST_F(ConnectivityGridTest, ParallelConnectionsTcpFailsFirst) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + initialize(); grid_->alternate_immediate_ = false; addHttp3AlternateProtocol(); @@ -792,6 +811,9 @@ TEST_F(ConnectivityGridTest, ParallelConnectionsTcpFailsFirst) { // Test the first pool failing inline but http/3 happy eyeballs succeeding inline TEST_F(ConnectivityGridTest, H3HappyEyeballsMeansNoH2Pool) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + // The alternate H3 pool will succeed inline. initialize(); grid_->alternate_failure_ = false; @@ -811,6 +833,9 @@ TEST_F(ConnectivityGridTest, H3HappyEyeballsMeansNoH2Pool) { // Test both connections happening in parallel and the second connecting. TEST_F(ConnectivityGridTest, TimeoutThenSuccessParallelH2Connects) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + initialize(); addHttp3AlternateProtocol(); EXPECT_EQ(grid_->http3Pool(), nullptr); @@ -847,6 +872,9 @@ TEST_F(ConnectivityGridTest, TimeoutThenSuccessParallelH2Connects) { // Test both connections happening in parallel and the second connecting. TEST_F(ConnectivityGridTest, TimeoutThenSuccessParallelH2ConnectsNoHE) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); + address_list_ = {*Network::Utility::resolveUrl("tcp://127.0.0.1:9000"), *Network::Utility::resolveUrl("tcp://127.0.0.1:9001")}; initialize(); @@ -925,13 +953,17 @@ TEST_F(ConnectivityGridTest, TimeoutThenSuccessParallelFirstConnects) { // Kick off the second and third connections. failover_timer->invokeCallback(); EXPECT_NE(grid_->http2Pool(), nullptr); - EXPECT_NE(grid_->alternate(), nullptr); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + EXPECT_NE(grid_->alternate(), nullptr); + } // onPoolFailure should not be passed up the first time. Instead the grid // should wait on the other pools - EXPECT_CALL(callbacks_.pool_failure_, ready()).Times(0); - grid_->callbacks(2)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, - "reason", host_); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + EXPECT_CALL(callbacks_.pool_failure_, ready()).Times(0); + grid_->callbacks(2)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, + "reason", host_); + } EXPECT_CALL(callbacks_.pool_failure_, ready()).Times(0); grid_->callbacks(1)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, @@ -1593,23 +1625,29 @@ TEST_F(ConnectivityGridTest, Http3FailedRecentlyThenFailsAgain) { EXPECT_TRUE(ConnectivityGridForTest::hasHttp3FailedRecently(*grid_)); // Getting onPoolFailure() from Http3 pool later should mark H3 broken. - grid_->createHttp3AlternatePool(); - EXPECT_CALL(*grid_->alternate(), newStream) - .WillOnce(Invoke( - [&](Http::ResponseDecoder&, ConnectionPool::Callbacks& callbacks, - const ConnectionPool::Instance::StreamOptions&) -> ConnectionPool::Cancellable* { - grid_->callbacks_.push_back(&callbacks); - return grid_->cancel_; - })); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + grid_->createHttp3AlternatePool(); + EXPECT_CALL(*grid_->alternate(), newStream) + .WillOnce(Invoke( + [&](Http::ResponseDecoder&, ConnectionPool::Callbacks& callbacks, + const ConnectionPool::Instance::StreamOptions&) -> ConnectionPool::Cancellable* { + grid_->callbacks_.push_back(&callbacks); + return grid_->cancel_; + })); + } grid_->callbacks(0)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, "reason", host_); - // Because the alternate pool is outstanding H3 is not broken. - EXPECT_FALSE(grid_->isHttp3Broken()); - - // When H3 alternate connects, there should not be a second up call. - EXPECT_CALL(callbacks_.pool_ready_, ready()).Times(0); - grid_->callbacks(2)->onPoolReady(encoder_, host_, info_, absl::nullopt); - EXPECT_FALSE(grid_->isHttp3Broken()); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + EXPECT_TRUE(grid_->isHttp3Broken()); + } else { + // Because the alternate pool is outstanding H3 is not broken. + EXPECT_FALSE(grid_->isHttp3Broken()); + + // When H3 alternate connects, there should not be a second up call. + EXPECT_CALL(callbacks_.pool_ready_, ready()).Times(0); + grid_->callbacks(2)->onPoolReady(encoder_, host_, info_, absl::nullopt); + EXPECT_FALSE(grid_->isHttp3Broken()); + } } // Same as above only the alternate pool connects after TCP. @@ -1636,15 +1674,17 @@ TEST_F(ConnectivityGridTest, Http3FailedRecentlyThenTCPThenAlternate) { EXPECT_TRUE(ConnectivityGridForTest::hasHttp3FailedRecently(*grid_)); // Getting onPoolFailure() from Http3 pool later should mark H3 broken. - grid_->createHttp3AlternatePool(); - EXPECT_CALL(*grid_->alternate(), newStream) - .WillOnce(Invoke( - [&](Http::ResponseDecoder&, ConnectionPool::Callbacks& callbacks, - const ConnectionPool::Instance::StreamOptions&) -> ConnectionPool::Cancellable* { - callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, - "reason", host_); - return nullptr; - })); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http3_happy_eyeballs")) { + grid_->createHttp3AlternatePool(); + EXPECT_CALL(*grid_->alternate(), newStream) + .WillOnce(Invoke( + [&](Http::ResponseDecoder&, ConnectionPool::Callbacks& callbacks, + const ConnectionPool::Instance::StreamOptions&) -> ConnectionPool::Cancellable* { + callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, + "reason", host_); + return nullptr; + })); + } grid_->callbacks(0)->onPoolFailure(ConnectionPool::PoolFailureReason::LocalConnectionFailure, "reason", host_); EXPECT_TRUE(grid_->isHttp3Broken()); diff --git a/test/common/http/filter_manager_test.cc b/test/common/http/filter_manager_test.cc index b2af2b000d010..8cde313bf66af 100644 --- a/test/common/http/filter_manager_test.cc +++ b/test/common/http/filter_manager_test.cc @@ -66,7 +66,7 @@ class FilterManagerTest : public testing::Test { LocalReplyFilterStateKey); EXPECT_EQ(fs_value->serializeAsString(), expected_name); - auto expected = std::make_unique(); + auto expected = std::make_unique(); expected->set_value(expected_name); EXPECT_TRUE(MessageDifferencer::Equals(*(fs_value->serializeAsProto()), *expected)); } @@ -180,7 +180,7 @@ TEST_F(FilterManagerTest, SendLocalReplyDuringDecodingGrpcClassiciation) { EXPECT_CALL(filter_manager_callbacks_, setResponseHeaders_(_)) .WillOnce(Invoke([](auto& response_headers) { EXPECT_THAT(response_headers, - HeaderHasValueRef(Http::Headers::get().ContentType, "application/grpc")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); })); EXPECT_CALL(filter_manager_callbacks_, resetIdleTimer()); EXPECT_CALL(filter_manager_callbacks_, encodeHeaders(_, _)); @@ -245,7 +245,7 @@ TEST_F(FilterManagerTest, SendLocalReplyDuringEncodingGrpcClassiciation) { .WillOnce(Invoke([](auto&) {})) .WillOnce(Invoke([](auto& response_headers) { EXPECT_THAT(response_headers, - HeaderHasValueRef(Http::Headers::get().ContentType, "application/grpc")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); })); EXPECT_CALL(filter_manager_callbacks_, encodeHeaders(_, _)); EXPECT_CALL(filter_manager_callbacks_, endStream()); diff --git a/test/common/http/header_mutation_test.cc b/test/common/http/header_mutation_test.cc index 467cc15824791..4908b81d45483 100644 --- a/test/common/http/header_mutation_test.cc +++ b/test/common/http/header_mutation_test.cc @@ -1,5 +1,6 @@ #include "source/common/http/header_mutation.h" +#include "test/mocks/server/server_factory_context.h" #include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" @@ -10,11 +11,13 @@ namespace Http { namespace { TEST(HeaderMutationsTest, BasicRemove) { + Server::Configuration::MockServerFactoryContext context; + ProtoHeaderMutatons proto_mutations; proto_mutations.Add()->set_remove("flag-header"); proto_mutations.Add()->set_remove("another-flag-header"); - auto mutations = HeaderMutations::create(proto_mutations).value(); + auto mutations = HeaderMutations::create(proto_mutations, context).value(); NiceMock stream_info; { @@ -34,7 +37,45 @@ TEST(HeaderMutationsTest, BasicRemove) { } } +TEST(HeaderMutationsTest, RemoveOnMatch) { + Server::Configuration::MockServerFactoryContext context; + + ProtoHeaderMutatons proto_mutations; + auto remove_on_match = proto_mutations.Add()->mutable_remove_on_match(); + remove_on_match->mutable_key_matcher()->set_exact("flag-header"); + auto remove_on_match2 = proto_mutations.Add()->mutable_remove_on_match(); + remove_on_match2->mutable_key_matcher()->set_prefix("remove-prefix-"); + + auto mutations = HeaderMutations::create(proto_mutations, context).value(); + NiceMock stream_info; + + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"not-flag-header", "not-flag-header-value"}, + {"remove-prefix-header1", "value1"}, + {"remove-prefix-header2", "value2"}, + {"keep-prefix-header3", "value3"}, + {":method", "GET"}, + {":path", "/"}, + {":authority", "host"}, + }; + + mutations->evaluateHeaders(headers, {&headers}, stream_info); + EXPECT_EQ("", headers.get_("flag-header")); + EXPECT_EQ("another-flag-header-value", headers.get_("another-flag-header")); + EXPECT_EQ("not-flag-header-value", headers.get_("not-flag-header")); + EXPECT_EQ("", headers.get_("remove-prefix-header1")); + EXPECT_EQ("", headers.get_("remove-prefix-header2")); + EXPECT_EQ("value3", headers.get_("keep-prefix-header3")); + } +} + TEST(HeaderMutationsTest, AllOperations) { + + Server::Configuration::MockServerFactoryContext context; + ProtoHeaderMutatons proto_mutations; // Step 1: Remove 'flag-header' header. proto_mutations.Add()->set_remove("flag-header"); @@ -63,7 +104,7 @@ TEST(HeaderMutationsTest, AllOperations) { append4->mutable_header()->set_value("flag-header-4-value"); append4->set_append_action(ProtoHeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD); - auto mutations = HeaderMutations::create(proto_mutations).value(); + auto mutations = HeaderMutations::create(proto_mutations, context).value(); NiceMock stream_info; // Remove 'flag-header' and try to append 'flag-header' with value 'another-flag-header-value'. @@ -191,6 +232,8 @@ TEST(HeaderMutationsTest, AllOperations) { } TEST(HeaderMutationsTest, KeepEmptyValue) { + Server::Configuration::MockServerFactoryContext context; + ProtoHeaderMutatons proto_mutations; // Step 1: Remove the header. proto_mutations.Add()->set_remove("flag-header"); @@ -202,7 +245,7 @@ TEST(HeaderMutationsTest, KeepEmptyValue) { append->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); append->set_keep_empty_value(true); - auto mutations = HeaderMutations::create(proto_mutations).value(); + auto mutations = HeaderMutations::create(proto_mutations, context).value(); NiceMock stream_info; { @@ -221,6 +264,8 @@ TEST(HeaderMutationsTest, KeepEmptyValue) { } TEST(HeaderMutationsTest, BasicOrder) { + Server::Configuration::MockServerFactoryContext context; + { ProtoHeaderMutatons proto_mutations; @@ -233,7 +278,7 @@ TEST(HeaderMutationsTest, BasicOrder) { // Step 2: Remove the header. proto_mutations.Add()->set_remove("flag-header"); - auto mutations = HeaderMutations::create(proto_mutations).value(); + auto mutations = HeaderMutations::create(proto_mutations, context).value(); NiceMock stream_info; Envoy::Http::TestRequestHeaderMapImpl headers = { @@ -258,7 +303,7 @@ TEST(HeaderMutationsTest, BasicOrder) { append->mutable_header()->set_value("%REQ(ANOTHER-FLAG-HEADER)%"); append->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - auto mutations = HeaderMutations::create(proto_mutations).value(); + auto mutations = HeaderMutations::create(proto_mutations, context).value(); NiceMock stream_info; Envoy::Http::TestRequestHeaderMapImpl headers = { @@ -273,10 +318,12 @@ TEST(HeaderMutationsTest, BasicOrder) { } TEST(HeaderMutationTest, Death) { + Server::Configuration::MockServerFactoryContext context; + ProtoHeaderMutatons proto_mutations; proto_mutations.Add(); - EXPECT_DEATH(HeaderMutations::create(proto_mutations).IgnoreError(), "unset oneof"); + EXPECT_DEATH(HeaderMutations::create(proto_mutations, context).IgnoreError(), "unset oneof"); } } // namespace diff --git a/test/common/http/http1/codec_impl_test.cc b/test/common/http/http1/codec_impl_test.cc index 556574323f0df..01cbae00e8921 100644 --- a/test/common/http/http1/codec_impl_test.cc +++ b/test/common/http/http1/codec_impl_test.cc @@ -218,7 +218,15 @@ class Http1ServerConnectionImplTest : public Http1CodecTestBase { auto status = codec_->dispatch(buffer); EXPECT_TRUE(status.ok()); EXPECT_EQ(0U, buffer.length()); + const StreamInfo::BytesMeterSharedPtr meter = response_encoder->getStream().bytesMeter(); + // Verifies BytesMeter accounting for header bytes received. + EXPECT_GT(meter->headerBytesReceived(), 0); + EXPECT_GE(meter->decompressedHeaderBytesReceived(), meter->headerBytesReceived()); + response_encoder->encodeHeaders(TestResponseHeaderMapImpl{{":status", "200"}}, true); + // Verifies BytesMeter accounting for header bytes sent. + EXPECT_GT(meter->headerBytesSent(), 0); + EXPECT_GE(meter->decompressedHeaderBytesSent(), meter->headerBytesSent()); } void createHeaderValidator() { @@ -2612,6 +2620,10 @@ TEST_F(Http1ClientConnectionImplTest, SimpleGetWithHeaderCasing) { TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/"}, {"my-custom-header", "hey"}}; EXPECT_TRUE(request_encoder.encodeHeaders(headers, true).ok()); EXPECT_EQ("GET / HTTP/1.1\r\nMy-Custom-Header: hey\r\n\r\n", output); + + EXPECT_GT(request_encoder.getStream().bytesMeter()->headerBytesSent(), 0); + EXPECT_GE(request_encoder.getStream().bytesMeter()->decompressedHeaderBytesSent(), + request_encoder.getStream().bytesMeter()->headerBytesSent()); } TEST_F(Http1ClientConnectionImplTest, FullyQualifiedGet) { diff --git a/test/common/http/http1/conn_pool_test.cc b/test/common/http/http1/conn_pool_test.cc index c2ca33c446552..7f500aee6fe6f 100644 --- a/test/common/http/http1/conn_pool_test.cc +++ b/test/common/http/http1/conn_pool_test.cc @@ -285,6 +285,8 @@ TEST_F(Http1ConnPoolImplTest, VerifyTimingStats) { deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); EXPECT_CALL(cluster_->stats_store_, deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), _)); ActiveTestRequest r1(*this, 0, ActiveTestRequest::Type::CreateConnection); r1.startRequest(); @@ -518,6 +520,8 @@ TEST_F(Http1ConnPoolImplTest, MeasureConnectTime) { // Cleanup, cause the connections to go away. while (!conn_pool_->test_clients_.empty()) { + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), _)); EXPECT_CALL( cluster_->stats_store_, deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); @@ -1187,6 +1191,152 @@ TEST_F(Http1ConnPoolDestructImplTest, CbAfterConnPoolDestroyed) { dispatcher_.clearDeferredDeleteList(); } +// Verifies that the upstream_rq_per_cx histogram is emitted correctly. +TEST_F(Http1ConnPoolImplTest, RequestTrackingMetric) { + // Set up expectations for all histograms that will be emitted on connection close + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), 3)); + + // Create connection and make 3 requests to test request counting + ActiveTestRequest r1(*this, 0, ActiveTestRequest::Type::CreateConnection); + r1.startRequest(); + conn_pool_->expectEnableUpstreamReady(); + r1.completeResponse(false); // Keep connection alive + + // Second request on same connection (HTTP/1.1 keep-alive) + ActiveTestRequest r2(*this, 0, ActiveTestRequest::Type::Immediate); + r2.startRequest(); + conn_pool_->expectEnableUpstreamReady(); + r2.completeResponse(false); // Keep connection alive + + // Third request on same connection + ActiveTestRequest r3(*this, 0, ActiveTestRequest::Type::Immediate); + r3.startRequest(); + conn_pool_->expectEnableUpstreamReady(); + r3.completeResponse(false); + + // Explicitly close connection to trigger metrics + EXPECT_CALL(*conn_pool_, onClientDestroy()); + conn_pool_->expectAndRunUpstreamReady(); + conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); +} + +// Verifies that the upstream_rq_per_cx histogram is emitted correctly for multiple connections. +TEST_F(Http1ConnPoolImplTest, RequestTrackingMultipleConnections) { + // Set up expectations for histograms from first connection + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), 2)); + + // Create first connection and handle 2 requests + ActiveTestRequest r1(*this, 0, ActiveTestRequest::Type::CreateConnection); + r1.startRequest(); + conn_pool_->expectEnableUpstreamReady(); + r1.completeResponse(false); // Keep connection alive + + ActiveTestRequest r2(*this, 0, ActiveTestRequest::Type::Immediate); + r2.startRequest(); + conn_pool_->expectEnableUpstreamReady(); + r2.completeResponse(false); // Keep connection alive + + // Close first connection + EXPECT_CALL(*conn_pool_, onClientDestroy()); + conn_pool_->expectAndRunUpstreamReady(); + conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); + + // Set up expectations for histograms from second connection + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), 1)); + + // Create second connection and handle 1 request + ActiveTestRequest r3(*this, 0, ActiveTestRequest::Type::CreateConnection); + r3.startRequest(); + conn_pool_->expectEnableUpstreamReady(); + r3.completeResponse(false); + + // Close second connection + EXPECT_CALL(*conn_pool_, onClientDestroy()); + conn_pool_->expectAndRunUpstreamReady(); + conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); +} + +// Test request tracking with single request per connection. +TEST_F(Http1ConnPoolImplTest, RequestTrackingSingleRequest) { + // Set up expectations for all histograms that will be emitted on connection close + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), 1)); + + // Create connection and send request + ActiveTestRequest r1(*this, 0, ActiveTestRequest::Type::CreateConnection); + r1.startRequest(); + + // Complete response but keep connection open + conn_pool_->expectEnableUpstreamReady(); + r1.completeResponse(false); + + // Explicitly close connection to trigger metrics + EXPECT_CALL(*conn_pool_, onClientDestroy()); + conn_pool_->expectAndRunUpstreamReady(); + conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); +} + +// Test that upstream_rq_per_cx metric is NOT recorded for failed connections +TEST_F(Http1ConnPoolImplTest, RequestTrackingConnectionFailureNoMetric) { + // This test verifies that failed connections don't record our specific metric + // We'll allow other histograms but specifically check that upstream_rq_per_cx is never called + + // Allow any other histogram calls + EXPECT_CALL(cluster_->stats_store_, deliverHistogramToSinks(_, _)).Times(testing::AnyNumber()); + + // But specifically ensure upstream_rq_per_cx is NEVER called + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), _)) + .Times(0); + + InSequence s; + + // Request should kick off a new connection + NiceMock outer_decoder; + ConnPoolCallbacks callbacks; + conn_pool_->expectClientCreate(); + Http::ConnectionPool::Cancellable* handle = + conn_pool_->newStream(outer_decoder, callbacks, {false, true}); + EXPECT_NE(nullptr, handle); + + // Simulate connection failure before handshake completion + EXPECT_CALL(*conn_pool_->test_clients_[0].connect_timer_, disableTimer()); + EXPECT_CALL(callbacks.pool_failure_, ready()); + conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + EXPECT_CALL(*conn_pool_, onClientDestroy()); + dispatcher_.clearDeferredDeleteList(); + + // Verify that the connection failure stats are incremented + EXPECT_EQ(1U, cluster_->traffic_stats_->upstream_cx_connect_fail_.value()); + EXPECT_EQ(1U, cluster_->traffic_stats_->upstream_rq_pending_failure_eject_.value()); + + // The key validation: our specific metric should never have been called + // (This is verified by the EXPECT_CALL with Times(0) above) +} + } // namespace } // namespace Http1 } // namespace Http diff --git a/test/common/http/http2/codec_impl_test.cc b/test/common/http/http2/codec_impl_test.cc index 72b733df4010a..a8eecaae1ab15 100644 --- a/test/common/http/http2/codec_impl_test.cc +++ b/test/common/http/http2/codec_impl_test.cc @@ -569,6 +569,12 @@ class Http2CodecImplTest : public ::testing::TestWithParamgetStream().bytesMeter(); + EXPECT_EQ(send_meter->headerBytesSent(), 0); + EXPECT_EQ(send_meter->decompressedHeaderBytesSent(), 0); + EXPECT_EQ(send_meter->headerBytesReceived(), 0); + EXPECT_EQ(send_meter->decompressedHeaderBytesReceived(), 0); + InSequence s; TestRequestHeaderMapImpl request_headers; HttpTestUtility::addDefaultHeaders(request_headers); @@ -578,6 +584,10 @@ TEST_P(Http2CodecImplTest, SimpleRequestResponse) { EXPECT_CALL(request_decoder_, decodeHeaders_(_, false)); EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, false).ok()); + // Verify BytesMeter send-side metrics. + EXPECT_GT(send_meter->headerBytesSent(), 0); + EXPECT_GE(send_meter->decompressedHeaderBytesSent(), send_meter->headerBytesSent()); + // Queue request body. Buffer::OwnedImpl request_body(std::string(1024, 'a')); request_encoder_->encodeData(request_body, true); @@ -586,6 +596,11 @@ TEST_P(Http2CodecImplTest, SimpleRequestResponse) { EXPECT_CALL(request_decoder_, decodeData(_, true)).Times(AtLeast(1)); driveToCompletion(); + // Verify BytesMeter receive-side metrics. + EXPECT_GT(response_encoder_->getStream().bytesMeter()->headerBytesReceived(), 0); + EXPECT_GE(response_encoder_->getStream().bytesMeter()->decompressedHeaderBytesReceived(), + response_encoder_->getStream().bytesMeter()->headerBytesReceived()); + TestResponseHeaderMapImpl response_headers{{":status", "200"}}; // Encode response headers. @@ -1125,7 +1140,7 @@ TEST_P(Http2CodecImplTest, TrailingHeadersLargeClientBody) { EXPECT_CALL(request_decoder_, decodeHeaders_(_, false)); EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, false).ok()); EXPECT_CALL(request_decoder_, decodeData(_, false)).Times(AtLeast(1)); - Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); + Buffer::OwnedImpl body(std::string(1024 * 512, 'a')); request_encoder_->encodeData(body, false); request_encoder_->encodeTrailers(TestRequestTrailerMapImpl{{"trailing", "header"}}); // Only drive the client so we can make sure we don't get any window updates. @@ -1455,7 +1470,7 @@ TEST_P(Http2CodecImplTest, DumpsStreamlessConnectionWithoutAllocatingMemory) { ostream.contents(), HasSubstr( "max_headers_kb_: 60, max_headers_count_: 100, " - "per_stream_buffer_limit_: 268435456, allow_metadata_: 0, " + "per_stream_buffer_limit_: 16777216, allow_metadata_: 0, " "stream_error_on_invalid_http_messaging_: 0, is_outbound_flood_monitored_control_frame_: " "0, dispatching_: 0, raised_goaway_: 0, " "pending_deferred_reset_streams_.size(): 0\n" @@ -1736,7 +1751,13 @@ TEST_P(Http2CodecImplDeferredResetTest, DeferredResetServerIfLocalEndStreamBefor response_encoder_->encodeHeaders(response_headers, false); Buffer::OwnedImpl body(std::string(32 * 1024, 'a')); EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()).Times(AnyNumber()); - auto flush_timer = new Event::MockTimer(&server_connection_.dispatcher_); + auto flush_timer = new Event::MockTimer(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); response_encoder_->encodeData(body, true); EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::LocalReset, _)); @@ -1776,7 +1797,13 @@ TEST_P(Http2CodecImplDeferredResetTest, LargeDataDeferredResetServerIfLocalEndSt response_encoder_->encodeHeaders(response_headers, false); Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()).Times(AnyNumber()); - auto flush_timer = new Event::MockTimer(&server_connection_.dispatcher_); + auto flush_timer = new Event::MockTimer(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); response_encoder_->encodeData(body, true); EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::LocalReset, _)); @@ -2175,7 +2202,13 @@ TEST_P(Http2CodecImplFlowControlTest, TrailingHeadersLargeServerBody) { // server, intentionally exhausting the window. driveServer(); driveClient(); - auto flush_timer = new NiceMock(&server_connection_.dispatcher_); + auto flush_timer = new NiceMock(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); response_encoder_->encodeTrailers(TestResponseTrailerMapImpl{{"trailing", "header"}}); @@ -2211,7 +2244,13 @@ TEST_P(Http2CodecImplFlowControlTest, TrailingHeadersLargeServerBodyFlushTimeout driveToCompletion(); EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()); EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); - auto flush_timer = new NiceMock(&server_connection_.dispatcher_); + auto flush_timer = new NiceMock(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); response_encoder_->encodeData(body, false); @@ -2227,6 +2266,7 @@ TEST_P(Http2CodecImplFlowControlTest, TrailingHeadersLargeServerBodyFlushTimeout EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(0); EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()); EXPECT_CALL(client_stream_callbacks, onResetStream(StreamResetReason::RemoteReset, _)); + ENVOY_LOG_MISC(debug, "invoke callback"); flush_timer->invokeCallback(); driveToCompletion(); EXPECT_EQ(1, server_stats_store_.counter("http2.tx_flush_timeout").value()); @@ -2252,7 +2292,13 @@ TEST_P(Http2CodecImplFlowControlTest, LargeServerBodyFlushTimeout) { driveToCompletion(); // The server enables the flush timer under encodeData(). The client then decodes some data. - auto flush_timer = new NiceMock(&server_connection_.dispatcher_); + auto flush_timer = new NiceMock(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); @@ -2293,7 +2339,13 @@ TEST_P(Http2CodecImplFlowControlTest, LargeServerBodyFlushTimeoutAfterGoaway) { driveToCompletion(); // The server enables the flush timer under encodeData(). The client then decodes some data. - auto flush_timer = new NiceMock(&server_connection_.dispatcher_); + auto flush_timer = new NiceMock(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); @@ -2427,7 +2479,13 @@ TEST_P(Http2CodecImplFlowControlTest, RstStreamOnPendingFlushTimeoutFlood) { // client stream windows should have 5535 bytes left and the next frame should overflow it. // nghttp2 sends 1 DATA frame for the remainder of the client window and it should make // outbound frame queue 1 away from overflow. - auto flush_timer = new NiceMock(&server_connection_.dispatcher_); + auto flush_timer = new NiceMock(); + EXPECT_CALL(server_connection_.dispatcher_, + createScaledTypedTimer_(Event::ScaledTimerType::HttpDownstreamStreamFlush, _)) + .WillOnce(Invoke([flush_timer](Event::ScaledTimerType, Event::TimerCb cb) { + flush_timer->callback_ = cb; + return flush_timer; + })); EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); Buffer::OwnedImpl large_body(std::string(6 * 1024, '1')); response_encoder_->encodeData(large_body, true); @@ -3763,7 +3821,7 @@ TEST_P(Http2CodecImplTest, ShouldWaitForDeferredBodyToProcessBeforeProcessingTra // Force the stream to buffer data at the receiving codec. server_->getStream(1)->readDisable(true); - const uint32_t request_body_size = 1024 * 1024; + const uint32_t request_body_size = 1024 * 512; Buffer::OwnedImpl body(std::string(request_body_size, 'a')); request_encoder_->encodeData(body, false); driveToCompletion(); @@ -3819,7 +3877,7 @@ TEST_P(Http2CodecImplTest, ShouldBufferDeferredBodyNoEndstream) { // Force the stream to buffer data at the receiving codec. server_->getStream(1)->readDisable(true); - Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); + Buffer::OwnedImpl body(std::string(1024 * 512, 'a')); request_encoder_->encodeData(body, false); driveToCompletion(); @@ -3839,6 +3897,9 @@ TEST_P(Http2CodecImplTest, ShouldBufferDeferredBodyNoEndstream) { EXPECT_CALL(request_decoder_, decodeData(_, false)); process_buffered_data_callback->invokeCallback(); } + + // Dispatch potential frames from server, for example, the window update frames. + driveToCompletion(); } TEST_P(Http2CodecImplTest, ShouldBufferDeferredBodyWithEndStream) { @@ -3975,7 +4036,7 @@ TEST_P(Http2CodecImplTest, EXPECT_FALSE(process_buffered_data_callback->enabled_); server_->getStream(1)->readDisable(true); - const uint32_t request_body_size = 1024 * 1024; + const uint32_t request_body_size = 1024 * 512; Buffer::OwnedImpl body(std::string(request_body_size, 'a')); request_encoder_->encodeData(body, false); driveToCompletion(); @@ -4304,13 +4365,39 @@ TEST_P(Http2CodecImplTest, ServerDispatchLoadShedPointCanCauseServerToSendGoAway EXPECT_EQ(1, server_stats_store_.counter("http2.goaway_sent").value()); } -TEST_P(Http2CodecImplTest, ServerDispatchLoadShedPointIsOnlyConsultedOncePerDispatch) { +TEST_P(Http2CodecImplTest, ServerDispatchLoadShedPointSendGoAwayAndClose) { + expect_buffered_data_on_teardown_ = true; + initialize(); + ASSERT_EQ(0, server_stats_store_.counter("http2.goaway_sent").value()); - int times_shed_load_invoked = 0; + TestRequestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + EXPECT_CALL(server_->server_go_away_and_close_on_dispatch, shouldShedLoad()) + .WillOnce(Return(true)); + EXPECT_CALL(client_callbacks_, onGoAway(_)); + + EXPECT_CALL(request_decoder_, decodeHeaders_(_, _)).Times(0); + EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok()); + + driveToCompletion(); + + EXPECT_EQ(1, server_stats_store_.counter("http2.goaway_sent").value()); +} + +TEST_P(Http2CodecImplTest, ServerDispatchLoadShedPointsAreOnlyConsultedOncePerDispatch) { + initialize(); + + int times_shed_load_goaway_invoked = 0; EXPECT_CALL(server_->server_go_away_on_dispatch, shouldShedLoad()) - .WillRepeatedly(Invoke([×_shed_load_invoked]() { - ++times_shed_load_invoked; + .WillRepeatedly(Invoke([×_shed_load_goaway_invoked]() { + ++times_shed_load_goaway_invoked; + return false; + })); + int times_shed_load_goaway_and_close_invoked = 0; + EXPECT_CALL(server_->server_go_away_and_close_on_dispatch, shouldShedLoad()) + .WillRepeatedly(Invoke([×_shed_load_goaway_and_close_invoked]() { + ++times_shed_load_goaway_and_close_invoked; return false; })); @@ -4338,7 +4425,8 @@ TEST_P(Http2CodecImplTest, ServerDispatchLoadShedPointIsOnlyConsultedOncePerDisp // All the newly created streams are queued in the connection buffer. EXPECT_CALL(request_decoder_, decodeHeaders_(_, true)).Times(num_streams_to_create); driveToCompletion(); - EXPECT_EQ(1, times_shed_load_invoked); + EXPECT_EQ(1, times_shed_load_goaway_invoked); + EXPECT_EQ(1, times_shed_load_goaway_and_close_invoked); EXPECT_EQ(0, server_stats_store_.counter("http2.goaway_sent").value()); } diff --git a/test/common/http/http2/codec_impl_test_util.h b/test/common/http/http2/codec_impl_test_util.h index b98cdd410a6ff..b8573acd61a7c 100644 --- a/test/common/http/http2/codec_impl_test_util.h +++ b/test/common/http/http2/codec_impl_test_util.h @@ -1,6 +1,7 @@ #pragma once #include "envoy/http/codec.h" +#include "envoy/server/overload/load_shed_point.h" #include "source/common/http/http2/codec_impl.h" #include "source/common/http/utility.h" @@ -63,11 +64,20 @@ class TestCodecOverloadManagerProvider { public: TestCodecOverloadManagerProvider() { ON_CALL(overload_manager_, getLoadShedPoint(testing::_)) - .WillByDefault(testing::Return(&server_go_away_on_dispatch)); + .WillByDefault(testing::Invoke([this](absl::string_view name) -> Server::LoadShedPoint* { + if (name == Server::LoadShedPointName::get().H2ServerGoAwayOnDispatch) { + return &server_go_away_on_dispatch; + } + if (name == Server::LoadShedPointName::get().H2ServerGoAwayAndCloseOnDispatch) { + return &server_go_away_and_close_on_dispatch; + } + return nullptr; + })); } testing::NiceMock overload_manager_; testing::NiceMock server_go_away_on_dispatch; + testing::NiceMock server_go_away_and_close_on_dispatch; }; class TestServerConnectionImpl : public TestCodecStatsProvider, diff --git a/test/common/http/http2/conn_pool_test.cc b/test/common/http/http2/conn_pool_test.cc index 1cb7a1c052f2c..e082c1274be2c 100644 --- a/test/common/http/http2/conn_pool_test.cc +++ b/test/common/http/http2/conn_pool_test.cc @@ -970,6 +970,8 @@ TEST_F(Http2ConnPoolImplTest, VerifyConnectionTimingStats) { r1.inner_decoder_->decodeHeaders( ResponseHeaderMapPtr{new TestResponseHeaderMapImpl{{":status", "200"}}}, true); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), _)); EXPECT_CALL(cluster_->stats_store_, deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); @@ -987,12 +989,12 @@ TEST_F(Http2ConnPoolImplTest, VerifyBufferLimits) { InSequence s; expectClientCreate(8192); ActiveTestRequest r1(*this, 0, false); - // 1 stream. HTTP/2 defaults to 536870912 streams/connection. - CHECK_STATE(0 /*active*/, 1 /*pending*/, 536870912 /*capacity*/); + // 1 stream. HTTP/2 defaults to 1024 streams/connection. + CHECK_STATE(0 /*active*/, 1 /*pending*/, 1024 /*capacity*/); expectClientConnect(0, r1); // capacity goes down by one as one stream is used. - CHECK_STATE(1 /*active*/, 0 /*pending*/, 536870911 /*capacity*/); + CHECK_STATE(1 /*active*/, 0 /*pending*/, 1023 /*capacity*/); EXPECT_CALL(r1.inner_encoder_, encodeHeaders(_, true)); EXPECT_TRUE( r1.callbacks_.outer_encoder_ @@ -1986,6 +1988,105 @@ TEST_F(InitialStreamsLimitTest, InitialStreamsLimitRespectMaxRequests) { EXPECT_EQ(100, ActiveClient::calculateInitialStreamsLimit(cache_, origin_, mock_host_)); } +// Verifies the upstream_rq_per_cx histogram correctly tracks multiple concurrent HTTP/2 streams on +// the same connection. +TEST_F(Http2ConnPoolImplTest, RequestTrackingMultipleStreams) { + // Allow multiple concurrent streams on a single connection + cluster_->http2_options_.mutable_max_concurrent_streams()->set_value(5); + + // Set up expectations for all histograms that will be emitted on connection close + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + // Use _ to capture any value and let the test show us what it actually is + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), _)); + + InSequence s; + + // Create first request - this will create a new connection + expectClientCreate(); + ActiveTestRequest r1(*this, 0, false); + expectClientConnect(0, r1); + + // Create second and third requests - these should reuse the same connection due to multiplexing + ActiveTestRequest r2(*this, 0, true); // expect_connected=true means reuse connection + ActiveTestRequest r3(*this, 0, true); // expect_connected=true means reuse connection + + // Complete all requests + completeRequest(r1); + completeRequest(r2); + completeRequest(r3); + + // Close the connection to trigger metrics emission + // Set expectation first, then raise the event + EXPECT_CALL(*this, onClientDestroy()); + test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); +} + +// Verify request tracking for HTTP/2 with 5 concurrent streams. +TEST_F(Http2ConnPoolImplTest, RequestTrackingFiveStreams) { + // Allow multiple concurrent streams on a single connection + cluster_->http2_options_.mutable_max_concurrent_streams()->set_value(10); + + // Set up expectations for all histograms that will be emitted on connection close + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_connect_ms"), _)); + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_cx_length_ms"), _)); + // Test with 5 requests to see if it correctly tracks all of them + EXPECT_CALL(cluster_->stats_store_, + deliverHistogramToSinks(Property(&Stats::Metric::name, "upstream_rq_per_cx"), 5)); + + InSequence s; + + // Create first request - this will create a new connection + expectClientCreate(); + ActiveTestRequest r1(*this, 0, false); + expectClientConnect(0, r1); + + // Create 4 more requests - these should reuse the same connection due to HTTP/2 multiplexing + ActiveTestRequest r2(*this, 0, true); + ActiveTestRequest r3(*this, 0, true); + ActiveTestRequest r4(*this, 0, true); + ActiveTestRequest r5(*this, 0, true); + + // Complete all requests + completeRequest(r1); + completeRequest(r2); + completeRequest(r3); + completeRequest(r4); + completeRequest(r5); + + // Close the connection to trigger metrics emission + EXPECT_CALL(*this, onClientDestroy()); + test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); +} + +// Test that failed connections don't record upstream_rq_per_cx metric +TEST_F(Http2ConnPoolImplTest, RequestTrackingConnectionFailureNoMetric) { + // Create a request that will trigger connection creation + expectClientCreate(); + ActiveTestRequest r(*this, 0, false); + + // DO NOT call expectClientConnect - let the connection fail before handshake completion + // This should not record any upstream_rq_per_cx metric due to hasHandshakeCompleted() check + + EXPECT_CALL(r.callbacks_.pool_failure_, ready()); + + // Close/fail the connection BEFORE it becomes ready (before handshake completion) + EXPECT_CALL(*this, onClientDestroy()); + test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + dispatcher_.clearDeferredDeleteList(); + + // Verify the connection failure was recorded + EXPECT_EQ(1U, cluster_->traffic_stats_->upstream_cx_destroy_.value()); + EXPECT_EQ(1U, cluster_->traffic_stats_->upstream_cx_destroy_remote_.value()); +} + } // namespace Http2 } // namespace Http } // namespace Envoy diff --git a/test/common/http/http2/http2_frame.h b/test/common/http/http2/http2_frame.h index 5e83cfb063108..91702d656f31e 100644 --- a/test/common/http/http2/http2_frame.h +++ b/test/common/http/http2/http2_frame.h @@ -63,6 +63,15 @@ class Http2Frame { Metadata = 77, }; + enum class Setting : uint16_t { + HeaderTableSize = 0x1, + EnablePush = 0x2, + MaxConcurrentStreams = 0x3, + InitialWindowSize = 0x4, + MaxFrameSize = 0x5, + MaxHeaderListSize = 0x6, + }; + enum class SettingsFlags : uint8_t { None = 0, Ack = 1, diff --git a/test/common/http/http3/conn_pool_test.cc b/test/common/http/http3/conn_pool_test.cc index 77014d7eda84d..67f12a2e500f1 100644 --- a/test/common/http/http3/conn_pool_test.cc +++ b/test/common/http/http3/conn_pool_test.cc @@ -281,6 +281,17 @@ TEST_F(Http3ConnPoolImplTest, NewAndDrainClientBeforeConnect) { cancellable->cancel(Envoy::ConnectionPool::CancelPolicy::CloseExcess); } +TEST_F(Http3ConnPoolImplTest, MigrationEnabledNoDrain) { + quic_info_.migration_config_.migrate_session_on_network_change = true; + createNewStream(); + EXPECT_FALSE(pool_->isIdle()); + // Draining non-migratable connections should not drain the connection which might be able to + // migrate. + pool_->drainConnections( + Envoy::ConnectionPool::DrainBehavior::DrainExistingNonMigratableConnections); + EXPECT_FALSE(pool_->isIdle()); +} + } // namespace Http3 } // namespace Http } // namespace Envoy diff --git a/test/common/http/http_server_properties_cache_manager_test.cc b/test/common/http/http_server_properties_cache_manager_test.cc index cfad02480867d..d94949eee65f7 100644 --- a/test/common/http/http_server_properties_cache_manager_test.cc +++ b/test/common/http/http_server_properties_cache_manager_test.cc @@ -109,9 +109,11 @@ TEST_F(HttpServerPropertiesCacheManagerTest, GetCacheForConflictingOptions) { initialize(); HttpServerPropertiesCacheSharedPtr cache1 = manager_->getCache(options1_, dispatcher_); options2_.set_name(options1_.name()); - EXPECT_ENVOY_BUG(manager_->getCache(options2_, dispatcher_), - "options specified alternate protocols cache 'name1' with different settings " - "first 'name: \"name1\""); + // Same as EXPECT_ENVOY_BUG + EXPECT_DEBUG_DEATH( + manager_->getCache(options2_, dispatcher_), + ::testing::ContainsRegex("(?s)options specified alternate protocols cache 'name1' with " + "different settings first '.*name: \"name1\"")); } } // namespace diff --git a/test/common/http/mixed_conn_pool_test.cc b/test/common/http/mixed_conn_pool_test.cc index fcfbdac94f9f8..1d0f2dee17990 100644 --- a/test/common/http/mixed_conn_pool_test.cc +++ b/test/common/http/mixed_conn_pool_test.cc @@ -70,8 +70,9 @@ class MixedConnPoolImplTest : public testing::Test { void testAlpnHandshake(absl::optional protocol); TestScopedRuntime scoped_runtime; - // The default capacity for HTTP/2 streams. - uint32_t expected_capacity_{536870912}; + // The default capacity for HTTP/2 streams. This is determined by both the HTTP2 options + // (DEFAULT_MAX_CONCURRENT_STREAMS) and DEFAULT_MAX_STREAMS of connection pool. + uint32_t expected_capacity_{1024}; }; TEST_F(MixedConnPoolImplTest, AlpnTest) { diff --git a/test/common/http/utility_test.cc b/test/common/http/utility_test.cc index 74218f2d8d763..c9f4ccd4db309 100644 --- a/test/common/http/utility_test.cc +++ b/test/common/http/utility_test.cc @@ -653,6 +653,32 @@ TEST(HttpUtility, parseHttp2Settings) { http2_options.max_inbound_window_update_frames_per_data_frame_sent().value()); } + { + + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.safe_http2_options", "false"}}); + + using ::Envoy::Http2::Utility::OptionsLimits; + auto http2_options = parseHttp2OptionsFromV3Yaml("{}"); + EXPECT_EQ(OptionsLimits::DEFAULT_HPACK_TABLE_SIZE, http2_options.hpack_table_size().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS_LEGACY, + http2_options.max_concurrent_streams().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE_LEGACY, + http2_options.initial_stream_window_size().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE_LEGACY, + http2_options.initial_connection_window_size().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES, + http2_options.max_outbound_frames().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES, + http2_options.max_outbound_control_frames().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD, + http2_options.max_consecutive_inbound_frames_with_empty_payload().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM, + http2_options.max_inbound_priority_frames_per_stream().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT, + http2_options.max_inbound_window_update_frames_per_data_frame_sent().value()); + } + { const std::string yaml = R"EOF( hpack_table_size: 1 @@ -709,7 +735,7 @@ TEST(HttpUtility, ValidateStreamErrorsWithHcm) { .value()); // If the HCM value is present it will take precedence over the old value. - ProtobufWkt::BoolValue hcm_value; + Protobuf::BoolValue hcm_value; hcm_value.set_value(false); EXPECT_FALSE(Envoy::Http2::Utility::initializeAndValidateOptions(http2_options, true, hcm_value) .value() @@ -732,7 +758,7 @@ TEST(HttpUtility, ValidateStreamErrorsWithHcm) { TEST(HttpUtility, ValidateStreamErrorConfigurationForHttp1) { envoy::config::core::v3::Http1ProtocolOptions http1_options; - ProtobufWkt::BoolValue hcm_value; + Protobuf::BoolValue hcm_value; NiceMock context; NiceMock validation_visitor; @@ -772,7 +798,7 @@ TEST(HttpUtility, ValidateStreamErrorConfigurationForHttp1) { TEST(HttpUtility, AllowCustomMethods) { envoy::config::core::v3::Http1ProtocolOptions http1_options; - ProtobufWkt::BoolValue hcm_value; + Protobuf::BoolValue hcm_value; NiceMock context; NiceMock validation_visitor; diff --git a/test/common/json/json_fuzz_test.cc b/test/common/json/json_fuzz_test.cc index ca5ca39f72887..4479047e22d3b 100644 --- a/test/common/json/json_fuzz_test.cc +++ b/test/common/json/json_fuzz_test.cc @@ -22,11 +22,11 @@ DEFINE_FUZZER(const uint8_t* buf, size_t len) { std::string json_string{reinterpret_cast(buf), len}; // Load via Protobuf JSON parsing, if we can. - ProtobufWkt::Struct message; + Protobuf::Struct message; try { MessageUtil::loadFromJson(json_string, message); // We should be able to serialize, parse again and get the same result. - ProtobufWkt::Struct message2; + Protobuf::Struct message2; // This can sometimes fail on too deep recursion in case protobuf parsing is configured to have // less recursion depth than json parsing in the proto library. // This is the only version of MessageUtil::getJsonStringFromMessage function safe to use on @@ -40,10 +40,10 @@ DEFINE_FUZZER(const uint8_t* buf, size_t len) { // MessageUtil::getYamlStringFromMessage automatically convert types, so we have to do another // round-trip. std::string yaml = MessageUtil::getYamlStringFromMessage(message); - ProtobufWkt::Struct yaml_message; + Protobuf::Struct yaml_message; TestUtility::loadFromYaml(yaml, yaml_message); - ProtobufWkt::Struct message3; + Protobuf::Struct message3; TestUtility::loadFromYaml(MessageUtil::getYamlStringFromMessage(yaml_message), message3); FUZZ_ASSERT(TestUtility::protoEqual(yaml_message, message3)); } catch (const Envoy::EnvoyException& e) { diff --git a/test/common/json/json_loader_test.cc b/test/common/json/json_loader_test.cc index aaf004c58c286..0be63f7f925c0 100644 --- a/test/common/json/json_loader_test.cc +++ b/test/common/json/json_loader_test.cc @@ -505,15 +505,15 @@ TEST_F(JsonLoaderTest, LoadFromStruct) { ], })EOF"; - const ProtobufWkt::Struct src = TestUtility::jsonToStruct(json_string); + const Protobuf::Struct src = TestUtility::jsonToStruct(json_string); ObjectSharedPtr json = Factory::loadFromProtobufStruct(src); const auto output_json = json->asJsonString(); EXPECT_TRUE(TestUtility::jsonStringEqual(output_json, json_string)); } TEST_F(JsonLoaderTest, LoadFromStructUnknownValueCase) { - ProtobufWkt::Struct src; - ProtobufWkt::Value value_not_set; + Protobuf::Struct src; + Protobuf::Value value_not_set; (*src.mutable_fields())["field"] = value_not_set; EXPECT_THROW_WITH_MESSAGE(Factory::loadFromProtobufStruct(src), EnvoyException, "Protobuf value case not implemented"); diff --git a/test/common/json/json_sanitizer_corpus/very_large_file b/test/common/json/json_sanitizer_corpus/very_large_file new file mode 100644 index 0000000000000..f3421134ee8df --- /dev/null +++ b/test/common/json/json_sanitizer_corpus/very_large_file @@ -0,0 +1,500 @@ + + + + + Some ASCII string + Test ASCII String + Some UTF8 strings + + àéèçù + 日本語 + 汉语/漢語 + 한국어/조선말 + русский язык + الْعَرَبيّة + עִבְרִית + język polski + हिन्दी + + Keys & "entities" + hellow world & others <nodes> are "fun!?' + Boolean + + Another Boolean + + Some Int + 32434543632 + Some Real + 58654.347656 + Some Date + 2009-02-12T22:23:00Z + Some Data + + MDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1 + w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qm + w6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAow + MTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXD + uSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbD + qSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAx + MjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5 + JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOp + IicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEy + MzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7kl + IcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6ki + Jygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIz + NDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUh + wqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSIn + KC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0 + NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHC + pzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIico + LcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1 + Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKn + Oi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygt + w6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2 + Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6 + LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3D + qF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3 + ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzov + Oy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOo + X8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4 + OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87 + Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hf + w6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5 + VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6Lzsu + LD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/D + p8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlU + RVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4s + Pz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8On + w6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRF + U1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/ + Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fD + oCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVT + VDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+ + PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8Og + KT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNU + MDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48 + fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6Ap + PSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1Qw + MTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+ + I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9 + K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAx + MjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4j + e1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0r + wrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEy + MzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7 + W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvC + sCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIz + NDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tb + fGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8Kw + JMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0 + NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8 + YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAk + wqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1 + Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xg + XF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTC + oyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2 + Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBc + XkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKj + JF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3 + ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxe + QF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMk + XsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4 + OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5A + XX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRe + wqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5 + dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBd + fcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7C + qCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0 + ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19 + wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKo + KsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRl + c3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3C + pAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgq + wrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVz + dCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKk + CjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrC + tcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0 + JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQK + MDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1 + w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qm + w6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAow + MTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXD + uSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbD + qSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAx + MjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5 + JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOp + IicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEy + MzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7kl + IcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6ki + Jygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIz + NDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUh + wqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSIn + KC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0 + NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHC + pzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIico + LcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1 + Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKn + Oi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygt + w6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2 + Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6 + LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3D + qF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3 + ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzov + Oy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOo + X8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4 + OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87 + Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hf + w6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5 + VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6Lzsu + LD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/D + p8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlU + RVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4s + Pz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLc󠁴OoX8On + w6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRF + U1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/ + Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fD + oCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVT + VDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+ + PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8Og + KT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNU + MDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48 + fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6Ap + PSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1Qw + MTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+ + I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9 + K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAx + MjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4j + e1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0r + wrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEy + MzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7 + W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvC + sCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIz + NDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tb + fGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8Kw + JMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0 + NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8 + YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAk + wqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1 + Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xg + XF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTC + oyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2 + Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBc + XkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKj + JF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3 + ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxe + QF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMk + XsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4 + OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5A + XX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRe + wqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5 + dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBd + fcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7C + qCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0 + ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19 + wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKo + KsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRl + c3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3C + pAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgq + wrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVz + dCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKk + CjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrC + tcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0 + JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQK + MDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1 + w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qm + w6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAow + MTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXD + uSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbD + qSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAx + MjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5 + JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOp + IicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEy + MzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7kl + IcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6ki + Jygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIz + NDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUh + wqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSIn + KC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0 + NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHC + pzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIico + LcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1 + Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKn + Oi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygt + w6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2 + Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6 + LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3D + qF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3 + ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzov + Oy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOo + X8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4 + OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87 + Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hf + w6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5 + VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6Lzsu + LD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/D + p8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlU + RVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4s + Pz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8On + w6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRF + U1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/ + Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fD + oCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVT + VDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+ + PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8Og + KT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNU + MDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48 + fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6Ap + PSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1Qw + MTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+ + I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9 + K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAx + MjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4j + e1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0r + wrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEy + MzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7 + W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvC + sCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIz + NDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tb + fGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8Kw + JMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0 + NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8 + YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAk + wqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1 + Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xg + XF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTC + oyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2 + Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBc + XkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKj + JF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3 + ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxe + QF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMk + XsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4 + OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5A + XX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRe + wqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5 + dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBd + fcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7C + qCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0 + ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19 + wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKo + KsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRl + c3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3C + pAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgq + wrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVz + dCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKk + CjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrC + tcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0 + JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQK + MDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1 + w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qm + w6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAow + MTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXD + uSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbD + qSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAx + MjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5 + JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOp + IicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEy + MzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7kl + IcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6ki + Jygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIz + NDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUh + wqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSIn + KC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0 + NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHC + pzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIico + LcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1 + Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKn + Oi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygt + w6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2 + Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6 + LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3D + qF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3 + ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzov + Oy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOo + X8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4 + OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87 + Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hf + w6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5 + VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6Lzsu + LD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/D + p8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlU + RVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4s + Pz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8On + w6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRF + U1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/ + Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fD + oCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVT + VDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+ + PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8Og + KT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNU + MDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48 + fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6Ap + PSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1Qw + MTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+ + I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9 + K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAx + MjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4j + e1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0r + wrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEy + MzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7 + W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvC + sCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIz + NDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tb + fGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8Kw + JMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0 + NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8 + YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAk + wqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1 + Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xg + XF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTC + oyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2 + Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBc + XkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKj + JF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3 + ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxe + QF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMk + XsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4 + OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5A + XX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRe + wqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5 + dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBd + fcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7C + qCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0 + ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19 + wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKo + KsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRl + c3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3C + pAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgq + wrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVz + dCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKk + CjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrC + tcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0 + JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQK + MDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1 + w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qm + w6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAow + MTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXD + uSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbD + qSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAx + MjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5 + JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOp + IicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEy + MzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7kl + IcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6ki + Jygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIz + NDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUh + wqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSIn + KC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0 + NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHC + pzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIico + LcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1 + Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKn + Oi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygt + w6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2 + Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6 + LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3D + qF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3 + ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzov + Oy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOo + X8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4 + OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87 + Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hf + w6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5 + VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6Lzsu + LD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/D + p8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlU + RVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4s + Pz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8On + w6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRF + U1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/ + Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fD + oCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVT + VDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+ + PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8Og + KT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNU + MDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48 + fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6Ap + PSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1Qw + MTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+ + I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9 + K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAx + MjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4j + e1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0r + wrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEy + MzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7 + W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvC + sCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIz + NDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tb + fGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8Kw + JMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0 + NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8 + YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAk + wqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1 + Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xg + XF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTC + oyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2 + Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBc + XkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKj + JF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3 + ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxe + QF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMk + XsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4 + OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5A + XX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRe + wqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5 + dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBd + fcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7C + qCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0 + ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19 + wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKo + KsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRl + c3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3C + pAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgq + wrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVz + dCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKk + CjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrC + tcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0 + JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQK + MDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1 + w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qm + w6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAow + MTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXD + uSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbD + qSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAx + MjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5 + JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOp + IicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEy + MzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7kl + IcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6ki + Jygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIz + NDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUh + wqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSIn + KC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0 + NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHC + pzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIico + LcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1 + Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKn + Oi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygt + w6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2 + Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6 + LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3D + qF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3 + ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzov + Oy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOo + X8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4 + OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87 + Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hf + w6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5 + VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6Lzsu + LD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/D + p8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlU + RVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fDoCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4s + Pz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVTVDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8On + w6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRF + U1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8OgKT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/ + Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNUMDEyMzQ1Njc4OXRlc3Qmw6kiJygtw6hfw6fD + oCk9K8KwJMKjJF7CqCrCtcO5JSHCpzovOy4sPz48fiN7W3xgXF5AXX3CpAowMTIzNDU2Nzg5VEVT + VDAxMjM0NTY3ODl0ZXN0JsOpIicoLcOoX8Onw6ApPSvCsCTCoyRewqgqwrXDuSUhwqc6LzsuLD8+ + PH4je1t8YFxeQF19wqQKMDEyMzQ1Njc4OVRFU1QwMTIzNDU2Nzg5dGVzdCbDqSInKC3DqF/Dp8Og + KT0rwrAkwqMkXsKoKsK1w7klIcKnOi87Liw/Pjx+I3tbfGBcXkBdfcKkCjAxMjM0NTY3ODlURVNU + MDEyMzQ1Njc4OXRlc3Qmw diff --git a/test/common/json/json_sanitizer_fuzz_test.cc b/test/common/json/json_sanitizer_fuzz_test.cc index ed6d33758ea18..026b82386031e 100644 --- a/test/common/json/json_sanitizer_fuzz_test.cc +++ b/test/common/json/json_sanitizer_fuzz_test.cc @@ -12,6 +12,12 @@ namespace Envoy { namespace Fuzz { DEFINE_FUZZER(const uint8_t* buf, size_t len) { + // Cap the input string by 32KiB. The fuzzer should be able to detect issues + // for smaller inputs. + if (len > 32 * 1024) { + ENVOY_LOG_MISC(warn, "The input buffer is longer than 32KiB, skipping"); + return; + } FuzzedDataProvider provider(buf, len); std::string buffer1, buffer2, errmsg; while (provider.remaining_bytes() != 0) { diff --git a/test/common/json/json_utility_test.cc b/test/common/json/json_utility_test.cc index 036efdd487345..ec0e2edc1a521 100644 --- a/test/common/json/json_utility_test.cc +++ b/test/common/json/json_utility_test.cc @@ -8,19 +8,19 @@ namespace Envoy { namespace Json { namespace { -std::string toJson(const ProtobufWkt::Value& v) { +std::string toJson(const Protobuf::Value& v) { std::string json_string; Utility::appendValueToString(v, json_string); return json_string; } TEST(JsonUtilityTest, AppendValueToString) { - ProtobufWkt::Value v; + Protobuf::Value v; // null EXPECT_EQ(toJson(v), "null"); - v.set_null_value(ProtobufWkt::NULL_VALUE); + v.set_null_value(Protobuf::NULL_VALUE); EXPECT_EQ(toJson(v), "null"); // bool diff --git a/test/common/listener_manager/BUILD b/test/common/listener_manager/BUILD index 13d975af0228e..5171fb943dd40 100644 --- a/test/common/listener_manager/BUILD +++ b/test/common/listener_manager/BUILD @@ -49,7 +49,7 @@ envoy_cc_test_library( "//source/common/listener_manager:listener_manager_lib", "//source/common/tls:server_context_lib", "//source/extensions/api_listeners/default_api_listener:api_listener_lib", - "//source/extensions/common/matcher:trie_matcher_lib", + "//source/extensions/common/matcher:ip_range_matcher_lib", "//test/mocks/init:init_mocks", "//test/mocks/matcher:matcher_mocks", "//test/mocks/network:network_mocks", @@ -177,6 +177,7 @@ envoy_cc_test( "//source/common/listener_manager:lds_api_lib", "//source/common/protobuf:utility_lib", "//test/mocks/config:config_mocks", + "//test/mocks/config:xds_manager_mocks", "//test/mocks/init:init_mocks", "//test/mocks/protobuf:protobuf_mocks", "//test/mocks/server:listener_manager_mocks", diff --git a/test/common/listener_manager/filter_chain_manager_impl_test.cc b/test/common/listener_manager/filter_chain_manager_impl_test.cc index b0b8d0cf41f69..ef90051de934a 100644 --- a/test/common/listener_manager/filter_chain_manager_impl_test.cc +++ b/test/common/listener_manager/filter_chain_manager_impl_test.cc @@ -264,6 +264,43 @@ TEST_P(FilterChainManagerImplTest, DuplicateContextsAreNotBuilt) { .ok()); } +TEST_P(FilterChainManagerImplTest, UpdateFilterChainsBetweenVersions) { + std::vector filter_chain_messages; + + for (int i = 0; i < 2; i++) { + envoy::config::listener::v3::FilterChain new_filter_chain = filter_chain_template_; + new_filter_chain.set_name(absl::StrCat("filter_chain_", i)); + new_filter_chain.mutable_filter_chain_match()->mutable_destination_port()->set_value(10000 + i); + filter_chain_messages.push_back(std::move(new_filter_chain)); + } + + auto filter_chain = std::make_shared(); + EXPECT_CALL(filter_chain_factory_builder_, buildFilterChain(_, _, _)) + .WillOnce(Return(filter_chain)); + EXPECT_TRUE(filter_chain_manager_ + ->addFilterChains(GetParam() ? &matcher_ : nullptr, + std::vector{ + &filter_chain_messages[0]}, + nullptr, filter_chain_factory_builder_, *filter_chain_manager_) + .ok()); + + FilterChainManagerImpl new_filter_chain_manager{addresses_, parent_context_, init_manager_, + *filter_chain_manager_}; + EXPECT_CALL(filter_chain_factory_builder_, buildFilterChain(_, _, _)); + EXPECT_TRUE(new_filter_chain_manager + .addFilterChains(GetParam() ? &matcher_ : nullptr, + std::vector{ + &filter_chain_messages[1]}, + nullptr, filter_chain_factory_builder_, new_filter_chain_manager) + .ok()); + + // The new filter chain manager is based on the previous filter chain manager, but it has a new + // filter chain that is not in the previous filter chain manager, so we expect the previous + // filter chains to be drained. + EXPECT_EQ(filter_chain_manager_->drainingFilterChains().size(), 1); + EXPECT_EQ(filter_chain_manager_->drainingFilterChains()[0], filter_chain); +} + TEST_P(FilterChainManagerImplTest, CreatedFilterChainFactoryContextHasIndependentDrainClose) { std::vector filter_chain_messages; for (int i = 0; i < 3; i++) { diff --git a/test/common/listener_manager/lds_api_test.cc b/test/common/listener_manager/lds_api_test.cc index 1482978d31926..381ce80c8b37b 100644 --- a/test/common/listener_manager/lds_api_test.cc +++ b/test/common/listener_manager/lds_api_test.cc @@ -8,6 +8,7 @@ #include "source/common/protobuf/utility.h" #include "test/mocks/config/mocks.h" +#include "test/mocks/config/xds_manager.h" #include "test/mocks/init/mocks.h" #include "test/mocks/protobuf/mocks.h" #include "test/mocks/server/listener_manager.h" @@ -39,9 +40,9 @@ class LdsApiTest : public testing::Test { void setup() { envoy::config::core::v3::ConfigSource lds_config; EXPECT_CALL(init_manager_, add(_)); - lds_ = - std::make_unique(lds_config, nullptr, cluster_manager_, init_manager_, - *store_.rootScope(), listener_manager_, validation_visitor_); + lds_ = std::make_unique(lds_config, nullptr, xds_manager_, cluster_manager_, + init_manager_, *store_.rootScope(), listener_manager_, + validation_visitor_); EXPECT_CALL(*cluster_manager_.subscription_factory_.subscription_, start(_)); init_target_handle_->initialize(init_watcher_); lds_callbacks_ = cluster_manager_.subscription_factory_.callbacks_; @@ -91,6 +92,7 @@ class LdsApiTest : public testing::Test { return listener; } + NiceMock xds_manager_; std::shared_ptr> grpc_mux_; NiceMock cluster_manager_; Init::MockManager init_manager_; diff --git a/test/common/listener_manager/listener_manager_impl_test.cc b/test/common/listener_manager/listener_manager_impl_test.cc index df7d31e64c9de..2d9d08198a6d5 100644 --- a/test/common/listener_manager/listener_manager_impl_test.cc +++ b/test/common/listener_manager/listener_manager_impl_test.cc @@ -25,7 +25,7 @@ #include "source/common/protobuf/protobuf.h" #include "source/common/router/string_accessor_impl.h" #include "source/common/tls/ssl_socket.h" -#include "source/extensions/common/matcher/trie_matcher.h" +#include "source/extensions/common/matcher/ip_range_matcher.h" #include "source/extensions/filters/listener/original_dst/original_dst.h" #include "source/extensions/filters/listener/tls_inspector/tls_inspector.h" @@ -858,6 +858,36 @@ bind_to_port: false EXPECT_EQ(1UL, server_.stats_store_.counterFromString("listener.127.0.0.1_1234.foo").value()); } +TEST_P(ListenerManagerImplTest, ListenerWithTargetNetworkNamespace) { + constexpr absl::string_view listener_yaml_tmpl = R"EOF( +name: listener_with_ns +address: + socket_address: + address: 127.0.0.1 + port_value: 0 + network_namespace_filepath: "{}" +filter_chains: +- filters: + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcp + cluster: cluster_0 +)EOF"; + + const std::string namespace_path = "/var/run/netns/test_listener_ns"; + envoy::config::listener::v3::Listener listener_config = + parseListenerFromV3Yaml(fmt::format(listener_yaml_tmpl, namespace_path)); + + auto status = manager_->addOrUpdateListener(listener_config, "", true); +#if defined(__linux__) + // On Linux, adding the listener should succeed. + EXPECT_TRUE(status.ok()); +#else + EXPECT_FALSE(status.ok()); +#endif +} + TEST_P(ListenerManagerImplTest, MultipleSocketTypeSpecifiedInAddresses) { const std::string yaml = R"EOF( name: "foo" @@ -6661,6 +6691,59 @@ TEST_P(ListenerManagerImplWithRealFiltersTest, MptcpNotSupported) { "listener mptcp-udp: enable_mptcp is set but MPTCP is not supported by the operating system"); } +// Test that hasCompatibleAddress returns false if network namespace is different. +TEST_P(ListenerManagerImplTest, HasCompatibleAddressWithNetNs) { + const std::string yaml_config1 = R"EOF( +name: listener_0 +address: + socket_address: + address: 127.0.0.1 + port_value: 10001 + network_namespace_filepath: "/var/run/netns/ns1" +filter_chains: {} +)EOF"; + + const std::string yaml_config2 = R"EOF( +name: listener_0 +address: + socket_address: + address: 127.0.0.1 + port_value: 10001 + network_namespace_filepath: "/var/run/netns/ns2" +filter_chains: {} +)EOF"; + + const std::string yaml_config3 = R"EOF( +name: listener_0 +address: + socket_address: + address: 127.0.0.1 + port_value: 10001 + network_namespace_filepath: "/var/run/netns/ns1" +filter_chains: {} +)EOF"; + + envoy::config::listener::v3::Listener config1 = + TestUtility::parseYaml(yaml_config1); + envoy::config::listener::v3::Listener config2 = + TestUtility::parseYaml(yaml_config2); + envoy::config::listener::v3::Listener config3 = + TestUtility::parseYaml(yaml_config3); + + auto listener1 = ListenerImpl::create(config1, "", *manager_, config1.name(), false, false, + MessageUtil::hash(config1)); + ASSERT_TRUE(listener1.ok()); + auto listener2 = ListenerImpl::create(config2, "", *manager_, config2.name(), false, false, + MessageUtil::hash(config2)); + ASSERT_TRUE(listener2.ok()); + auto listener3 = ListenerImpl::create(config3, "", *manager_, config3.name(), false, false, + MessageUtil::hash(config3)); + ASSERT_TRUE(listener3.ok()); + + EXPECT_FALSE(listener1.value()->hasCompatibleAddress(*(listener2.value()))); + EXPECT_TRUE(listener1.value()->hasCompatibleAddress(*(listener3.value()))); +} + // Set the resolver to the default IP resolver. The address resolver logic is unit tested in // resolver_impl_test.cc. TEST_P(ListenerManagerImplWithRealFiltersTest, AddressResolver) { @@ -8242,6 +8325,228 @@ TEST_P(ListenerManagerImplWithRealFiltersTest, EmptyConnectionBalanceConfig) { #endif } +// Test mock socket interface for custom address testing. +class TestCustomSocketInterface : public Network::SocketInterfaceBase { +public: + TestCustomSocketInterface() = default; + + // Network::SocketInterface + Network::IoHandlePtr socket(Network::Socket::Type socket_type, Network::Address::Type addr_type, + Network::Address::IpVersion version, bool socket_v6only, + const Network::SocketCreationOptions& options) const override { + UNREFERENCED_PARAMETER(socket_v6only); + UNREFERENCED_PARAMETER(options); + // Create a regular socket for testing + if (socket_type == Network::Socket::Type::Stream && addr_type == Network::Address::Type::Ip) { + int domain = (version == Network::Address::IpVersion::v4) ? AF_INET : AF_INET6; + int sock_fd = ::socket(domain, SOCK_STREAM, 0); + if (sock_fd == -1) { + return nullptr; + } + was_called_ = true; + return std::make_unique(sock_fd); + } + return nullptr; + } + + Network::IoHandlePtr socket(Network::Socket::Type socket_type, + const Network::Address::InstanceConstSharedPtr addr, + const Network::SocketCreationOptions& options) const override { + // Delegate to the other socket method + return socket(socket_type, addr->type(), + addr->ip() ? addr->ip()->version() : Network::Address::IpVersion::v4, false, + options); + } + + bool ipFamilySupported(int domain) override { return domain == AF_INET || domain == AF_INET6; } + + // Server::Configuration::BootstrapExtensionFactory + Server::BootstrapExtensionPtr + createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override { + UNREFERENCED_PARAMETER(config); + UNREFERENCED_PARAMETER(context); + return nullptr; // Not used in test + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return nullptr; // Not used in test + } + + std::string name() const override { return "test.custom.socket.interface"; } + + // Test helper + bool wasCalled() const { return was_called_; } + void resetCalled() { was_called_ = false; } + +private: + mutable bool was_called_{false}; +}; + +// Test address that returns a custom socket interface +class TestCustomAddress : public Network::Address::Instance { +public: + TestCustomAddress(const Network::SocketInterface& custom_interface) + : address_string_("127.0.0.1:0"), logical_name_("custom://test-address"), + custom_interface_(custom_interface), + ipv4_instance_(std::make_shared("127.0.0.1", 0)) {} + + // Network::Address::Instance + bool operator==(const Instance& rhs) const override { return address_string_ == rhs.asString(); } + Network::Address::Type type() const override { return Network::Address::Type::Ip; } + const std::string& asString() const override { return address_string_; } + absl::string_view asStringView() const override { return address_string_; } + const std::string& logicalName() const override { return logical_name_; } + const Network::Address::Ip* ip() const override { return ipv4_instance_->ip(); } + const Network::Address::Pipe* pipe() const override { return nullptr; } + const Network::Address::EnvoyInternalAddress* envoyInternalAddress() const override { + return nullptr; + } + absl::optional networkNamespace() const override { return absl::nullopt; } + const sockaddr* sockAddr() const override { return ipv4_instance_->sockAddr(); } + socklen_t sockAddrLen() const override { return ipv4_instance_->sockAddrLen(); } + absl::string_view addressType() const override { return "test_custom"; } + + // Return the custom socket interface + const Network::SocketInterface& socketInterface() const override { return custom_interface_; } + +private: + std::string address_string_; + std::string logical_name_; + const Network::SocketInterface& custom_interface_; + Network::Address::InstanceConstSharedPtr ipv4_instance_; +}; + +// Test address that returns the default socket interface +class TestDefaultAddress : public Network::Address::Instance { +public: + TestDefaultAddress() + : address_string_("127.0.0.1:0"), logical_name_("default://test-address"), + ipv4_instance_(std::make_shared("127.0.0.1", 0)) {} + + // Network::Address::Instance + bool operator==(const Instance& rhs) const override { return address_string_ == rhs.asString(); } + Network::Address::Type type() const override { return Network::Address::Type::Ip; } + const std::string& asString() const override { return address_string_; } + absl::string_view asStringView() const override { return address_string_; } + const std::string& logicalName() const override { return logical_name_; } + const Network::Address::Ip* ip() const override { return ipv4_instance_->ip(); } + const Network::Address::Pipe* pipe() const override { return nullptr; } + const Network::Address::EnvoyInternalAddress* envoyInternalAddress() const override { + return nullptr; + } + absl::optional networkNamespace() const override { return absl::nullopt; } + const sockaddr* sockAddr() const override { return ipv4_instance_->sockAddr(); } + socklen_t sockAddrLen() const override { return ipv4_instance_->sockAddrLen(); } + absl::string_view addressType() const override { return "test_default"; } + + // Return the default socket interface + const Network::SocketInterface& socketInterface() const override { + return Network::SocketInterfaceSingleton::get(); + } + +private: + std::string address_string_; + std::string logical_name_; + Network::Address::InstanceConstSharedPtr ipv4_instance_; +}; + +TEST_P(ListenerManagerImplTest, CustomSocketInterfaceIsUsedWhenAddressSpecifiesIt) { + auto custom_interface = std::make_unique(); + TestCustomSocketInterface* custom_interface_ptr = custom_interface.get(); + + auto custom_address = std::make_shared(*custom_interface); + + // Create listener factory to test the implementation + ProdListenerComponentFactory real_listener_factory(server_); + + Network::Socket::OptionsSharedPtr options = nullptr; + Network::SocketCreationOptions creation_options; + + // Verify that the custom address returns the custom interface + EXPECT_NE(&custom_address->socketInterface(), &Network::SocketInterfaceSingleton::get()); + + // The listener factory should use the custom socket interface + auto socket_result = real_listener_factory.createListenSocket( + custom_address, Network::Socket::Type::Stream, options, + ListenerComponentFactory::BindType::NoBind, creation_options, 0 /* worker_index */); + + // The socket creation should succeed + EXPECT_TRUE(socket_result.ok()); + if (socket_result.ok()) { + auto socket = socket_result.value(); + EXPECT_NE(socket, nullptr); + // Verify the socket was created with the expected address + EXPECT_EQ(socket->connectionInfoProvider().localAddress()->logicalName(), + custom_address->logicalName()); + } + + // Verify the custom interface was actually called + EXPECT_TRUE(custom_interface_ptr->wasCalled()); +} + +TEST_P(ListenerManagerImplTest, DefaultSocketInterfaceIsUsedWhenAddressUsesDefault) { + auto default_address = std::make_shared(); + + // Create listener factory to test the implementation + ProdListenerComponentFactory real_listener_factory(server_); + + Network::Socket::OptionsSharedPtr options = nullptr; + Network::SocketCreationOptions creation_options; + + // Verify that the default address returns the default interface + EXPECT_EQ(&default_address->socketInterface(), &Network::SocketInterfaceSingleton::get()); + + // The listener factory should use the standard socket creation path + auto socket_result = real_listener_factory.createListenSocket( + default_address, Network::Socket::Type::Stream, options, + ListenerComponentFactory::BindType::NoBind, creation_options, 0 /* worker_index */); + + // The socket creation should succeed + EXPECT_TRUE(socket_result.ok()); + if (socket_result.ok()) { + auto socket = socket_result.value(); + EXPECT_NE(socket, nullptr); + // Verify the socket was created with the expected address + EXPECT_EQ(socket->connectionInfoProvider().localAddress()->logicalName(), + default_address->logicalName()); + } +} + +TEST_P(ListenerManagerImplTest, CustomSocketInterfaceFailureIsHandledGracefully) { + // Create a failing custom socket interface + class FailingCustomSocketInterface : public TestCustomSocketInterface { + public: + Network::IoHandlePtr socket(Network::Socket::Type socket_type, + const Network::Address::InstanceConstSharedPtr addr, + const Network::SocketCreationOptions& options) const override { + UNREFERENCED_PARAMETER(socket_type); + UNREFERENCED_PARAMETER(addr); + UNREFERENCED_PARAMETER(options); + // Always return nullptr to simulate failure + return nullptr; + } + }; + + auto failing_interface = std::make_unique(); + auto custom_address = std::make_shared(*failing_interface); + + // Create listener factory to test the implementation + ProdListenerComponentFactory real_listener_factory(server_); + + Network::Socket::OptionsSharedPtr options = nullptr; + Network::SocketCreationOptions creation_options; + + // The listener factory should handle the failure gracefully + auto socket_result = real_listener_factory.createListenSocket( + custom_address, Network::Socket::Type::Stream, options, + ListenerComponentFactory::BindType::NoBind, creation_options, 0 /* worker_index */); + + // The socket creation should fail with the expected error + EXPECT_FALSE(socket_result.ok()); + EXPECT_EQ(socket_result.status().message(), "failed to create socket using custom interface"); +} + INSTANTIATE_TEST_SUITE_P(Matcher, ListenerManagerImplTest, ::testing::Values(false)); INSTANTIATE_TEST_SUITE_P(Matcher, ListenerManagerImplWithRealFiltersTest, ::testing::Values(false, true)); @@ -8250,6 +8555,76 @@ INSTANTIATE_TEST_SUITE_P(Matcher, ListenerManagerImplForInPlaceFilterChainUpdate INSTANTIATE_TEST_SUITE_P(Matcher, ListenerManagerImplWithDispatcherStatsTest, ::testing::Values(false)); +// Test address implementation for reverse connection testing +class TestReverseConnectionAddress : public Network::Address::Instance { +public: + TestReverseConnectionAddress() + : address_string_("127.0.0.1:0"), // Dummy IP address + logical_name_( + "rc://test-node:test-cluster:test-tenant@remote-cluster:1"), // Address with the same + // format as reverse + // connection addresses + ipv4_instance_(std::make_shared("127.0.0.1", 0)) {} + + // Network::Address::Instance + bool operator==(const Instance& rhs) const override { return address_string_ == rhs.asString(); } + Network::Address::Type type() const override { return Network::Address::Type::Ip; } + const std::string& asString() const override { return address_string_; } + absl::string_view asStringView() const override { return address_string_; } + const std::string& logicalName() const override { return logical_name_; } + const Network::Address::Ip* ip() const override { return ipv4_instance_->ip(); } + const Network::Address::Pipe* pipe() const override { return nullptr; } + const Network::Address::EnvoyInternalAddress* envoyInternalAddress() const override { + return nullptr; + } + absl::optional networkNamespace() const override { return absl::nullopt; } + const sockaddr* sockAddr() const override { return ipv4_instance_->sockAddr(); } + socklen_t sockAddrLen() const override { return ipv4_instance_->sockAddrLen(); } + absl::string_view addressType() const override { return "test_reverse_connection"; } + const Network::SocketInterface& socketInterface() const override { + return Network::SocketInterfaceSingleton::get(); + } + +private: + std::string address_string_; + std::string logical_name_; + Network::Address::InstanceConstSharedPtr ipv4_instance_; +}; + +TEST_P(ListenerManagerImplTest, ReverseConnectionAddressUsesCorrectSocketInterface) { + auto reverse_connection_address = std::make_shared(); + + // Verify the address has the expected logical name format + EXPECT_TRUE(absl::StartsWith(reverse_connection_address->logicalName(), "rc://")); + EXPECT_EQ(reverse_connection_address->logicalName(), + "rc://test-node:test-cluster:test-tenant@remote-cluster:1"); + // Verify asString() returns the localhost address + EXPECT_EQ(reverse_connection_address->asString(), "127.0.0.1:0"); + + // Create listener factory to test the actual implementation + ProdListenerComponentFactory real_listener_factory(server_); + + Network::Socket::OptionsSharedPtr options = nullptr; + Network::SocketCreationOptions creation_options; + + // This should use the default socket interface returned by the address's + // socketInterface() method. + auto socket_result = real_listener_factory.createListenSocket( + reverse_connection_address, Network::Socket::Type::Stream, options, + ListenerComponentFactory::BindType::NoBind, creation_options, 0 /* worker_index */ + ); + + // The socket creation should succeed and use the address's socket interface + EXPECT_TRUE(socket_result.ok()); + if (socket_result.ok()) { + auto socket = socket_result.value(); + EXPECT_NE(socket, nullptr); + // Verify the socket was created with the expected address + EXPECT_EQ(socket->connectionInfoProvider().localAddress()->logicalName(), + reverse_connection_address->logicalName()); + } +} + } // namespace } // namespace Server } // namespace Envoy diff --git a/test/common/matcher/exact_map_matcher_test.cc b/test/common/matcher/exact_map_matcher_test.cc index b635a25b3bf93..2dc71b5e0c4c1 100644 --- a/test/common/matcher/exact_map_matcher_test.cc +++ b/test/common/matcher/exact_map_matcher_test.cc @@ -109,7 +109,7 @@ TEST(ExactMapMatcherTest, RecursiveMatching) { std::make_unique( DataInputGetResult{DataInputGetResult::DataAvailability::AllDataAvailable, "match"}), stringOnMatch("no_match")); - matcher->addChild("match", OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/sub_matcher, + matcher->addChild("match", OnMatch{/*.action_=*/nullptr, /*.matcher=*/sub_matcher, /*.keep_matching=*/false}); TestData data; @@ -127,7 +127,7 @@ TEST(ExactMapMatcherTest, RecursiveMatchingOnNoMatch) { std::unique_ptr> matcher = *ExactMapMatcher::create( std::make_unique( DataInputGetResult{DataInputGetResult::DataAvailability::AllDataAvailable, "blah"}), - OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/sub_matcher, + OnMatch{/*.action_=*/nullptr, /*.matcher=*/sub_matcher, /*.keep_matching=*/false}); matcher->addChild("match", stringOnMatch("match")); @@ -156,14 +156,14 @@ TEST(ExactMapMatcherTest, RecursiveMatchingWithKeepMatching) { std::unique_ptr> matcher = *ExactMapMatcher::create( std::make_unique( DataInputGetResult{DataInputGetResult::DataAvailability::AllDataAvailable, "match"}), - OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/top_on_no_match_matcher, + OnMatch{/*.action_=*/nullptr, /*.matcher=*/top_on_no_match_matcher, /*.keep_matching=*/false}); - matcher->addChild("match", OnMatch{/*.action_cb=*/nullptr, + matcher->addChild("match", OnMatch{/*.action_=*/nullptr, /*.matcher=*/sub_matcher_match_keeps_matching, /*.keep_matching=*/true}); - std::vector skipped_results{}; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results{}; + SkippedMatchCb skipped_match_cb = [&skipped_results](const ActionConstSharedPtr& cb) { skipped_results.push_back(cb); }; TestData data; diff --git a/test/common/matcher/list_matcher_test.cc b/test/common/matcher/list_matcher_test.cc index 42246922f1ccd..aa83f429e4bae 100644 --- a/test/common/matcher/list_matcher_test.cc +++ b/test/common/matcher/list_matcher_test.cc @@ -40,8 +40,8 @@ TEST(ListMatcherTest, KeepMatching) { matcher.addMatcher(createSingleMatcher("string", [](auto) { return true; }), stringOnMatch("matched", /*keep_matching=*/false)); - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; auto result = matcher.match(TestData(), skipped_match_cb); @@ -56,8 +56,8 @@ TEST(ListMatcherTest, KeepMatchingOnNoMatch) { matcher.addMatcher(createSingleMatcher("string", [](auto) { return true; }), stringOnMatch("keep matching 2", /*keep_matching=*/true)); - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](const ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](const ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; auto result = matcher.match(TestData(), skipped_match_cb); @@ -84,17 +84,17 @@ TEST(ListMatcherTest, KeepMatchingWithRecursion) { Envoy::Matcher::ListMatcher matcher(stringOnMatch("top_level on_no_match")); matcher.addMatcher(createSingleMatcher("string", [](auto) { return true; }), - OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/sub_matcher_1, + OnMatch{/*.action_=*/nullptr, /*.matcher=*/sub_matcher_1, /*.keep_matching=*/false}); matcher.addMatcher(createSingleMatcher("string", [](auto) { return true; }), - OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/sub_matcher_2, + OnMatch{/*.action_=*/nullptr, /*.matcher=*/sub_matcher_2, /*.keep_matching=*/true}); matcher.addMatcher(createSingleMatcher("string", [](auto) { return true; }), - OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/sub_matcher_3, + OnMatch{/*.action_=*/nullptr, /*.matcher=*/sub_matcher_3, /*.keep_matching=*/false}); - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; MatchResult result = matcher.match(TestData(), skipped_match_cb); @@ -120,17 +120,17 @@ TEST(ListMatcherTest, KeepMatchingWithRecursiveOnNoMatch) { stringOnMatch("on_no_match sub match", /*keep_matching=*/true)); Envoy::Matcher::ListMatcher matcher( - OnMatch{/*action_cb=*/nullptr, + OnMatch{/*action_=*/nullptr, /*matcher=*/on_no_match_sub_matcher, /*keep_matching=*/false}); matcher.addMatcher( createSingleMatcher("string", [](auto) { return true; }), - OnMatch{/*action_cb=*/nullptr, /*matcher=*/sub_matcher_1, /*keep_matching=*/true}); + OnMatch{/*action_=*/nullptr, /*matcher=*/sub_matcher_1, /*keep_matching=*/true}); matcher.addMatcher( createSingleMatcher("string", [](auto) { return true; }), - OnMatch{/*action_cb=*/nullptr, /*matcher=*/sub_matcher_2, /*keep_matching=*/false}); + OnMatch{/*action_=*/nullptr, /*matcher=*/sub_matcher_2, /*keep_matching=*/false}); - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; MatchResult result = matcher.match(TestData(), skipped_match_cb); diff --git a/test/common/matcher/matcher_test.cc b/test/common/matcher/matcher_test.cc index d4072671da910..09b31e650f582 100644 --- a/test/common/matcher/matcher_test.cc +++ b/test/common/matcher/matcher_test.cc @@ -907,8 +907,8 @@ TEST_P(MatcherAmbiguousTest, KeepMatchingSupportInEvaluation) { validation_visitor_.setSupportKeepMatching(true); std::shared_ptr> matcher = createMatcherFromYaml(yaml)(); - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; const auto result = evaluateMatch(*matcher, TestData(), skipped_match_cb); @@ -1020,8 +1020,8 @@ TEST_P(MatcherAmbiguousTest, KeepMatchingWithRecursiveMatcher) { // Expect the nested matchers with keep_matching to be skipped and also the top-level // keep_matching setting to skip the result of the first sub-matcher. - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; MatchResult result = evaluateMatch(*matcher, TestData(), skipped_match_cb); @@ -1056,8 +1056,8 @@ TEST_P(MatcherAmbiguousTest, KeepMatchingWithUnsupportedReentry) { validation_visitor_.setSupportKeepMatching(true); std::shared_ptr> matcher = createMatcherFromYaml(yaml)(); - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; MatchResult result = evaluateMatch(*matcher, TestData(), skipped_match_cb); @@ -1130,12 +1130,12 @@ TEST_P(MatcherAmbiguousTest, KeepMatchingWithFailingNestedMatcher) { stringOnMatch("fail")); matcher->addMatcher(createSingleMatcher("string", [](auto) { return true; }), - OnMatch{/*.action_cb=*/nullptr, /*.matcher=*/nested_matcher, + OnMatch{/*.action_=*/nullptr, /*.matcher=*/nested_matcher, /*.keep_matching=*/true}); // Expect re-entry to fail due to the nested matcher. - std::vector skipped_results; - SkippedMatchCb skipped_match_cb = [&skipped_results](ActionFactoryCb cb) { + std::vector skipped_results; + SkippedMatchCb skipped_match_cb = [&skipped_results](ActionConstSharedPtr cb) { skipped_results.push_back(cb); }; MatchResult result = evaluateMatch(*matcher, TestData(), skipped_match_cb); diff --git a/test/common/matcher/test_utility.h b/test/common/matcher/test_utility.h index 808decee787bc..15ada874b3633 100644 --- a/test/common/matcher/test_utility.h +++ b/test/common/matcher/test_utility.h @@ -35,7 +35,7 @@ class TestCommonProtocolInputFactory : public CommonProtocolInputFactory { } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return factory_name_; } @@ -72,7 +72,7 @@ class TestDataInputStringFactory : public DataInputFactory { } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "string"; } @@ -95,7 +95,7 @@ class TestDataInputBoolFactory : public DataInputFactory { } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "bool"; } @@ -116,7 +116,7 @@ class TestDataInputFloatFactory : public DataInputFactory { } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "float"; } @@ -151,7 +151,7 @@ struct TestMatcher : public InputMatcher { }; // An action that evaluates to a proto StringValue. -struct StringAction : public ActionBase { +struct StringAction : public ActionBase { explicit StringAction(const std::string& string) : string_(string) {} const std::string string_; @@ -162,14 +162,14 @@ struct StringAction : public ActionBase { // Factory for StringAction. class StringActionFactory : public ActionFactory { public: - ActionFactoryCb createActionFactoryCb(const Protobuf::Message& config, absl::string_view&, - ProtobufMessage::ValidationVisitor&) override { - const auto& string = dynamic_cast(config); - return [string]() { return std::make_unique(string.value()); }; + ActionConstSharedPtr createAction(const Protobuf::Message& config, absl::string_view&, + ProtobufMessage::ValidationVisitor&) override { + const auto& string = dynamic_cast(config); + return std::make_shared(string.value()); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "string_action"; } }; @@ -194,7 +194,7 @@ class NeverMatchFactory : public InputMatcherFactory { } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "never_match"; } @@ -228,12 +228,12 @@ class CustomStringMatcherFactory : public InputMatcherFactory { InputMatcherFactoryCb createInputMatcherFactoryCb(const Protobuf::Message& config, Server::Configuration::ServerFactoryContext&) override { - const auto& string = dynamic_cast(config); + const auto& string = dynamic_cast(config); return [string]() { return std::make_unique(string.value()); }; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "custom_match"; } @@ -273,14 +273,9 @@ void PrintTo(const FieldMatchResult& result, std::ostream* os) { } } -// Creates a StringAction from a provided string. -std::unique_ptr stringValue(absl::string_view value) { - return std::make_unique(std::string(value)); -} - // Creates an OnMatch that evaluates to a StringValue with the provided value. template OnMatch stringOnMatch(absl::string_view value, bool keep_matching = false) { - return OnMatch{[s = std::string(value)]() { return stringValue(s); }, nullptr, keep_matching}; + return OnMatch{std::make_shared(std::string(value)), nullptr, keep_matching}; } inline void PrintTo(const Action& action, std::ostream* os) { @@ -291,23 +286,14 @@ inline void PrintTo(const Action& action, std::ostream* os) { *os << "{type=" << action.typeUrl() << "}"; } -inline void PrintTo(const ActionFactoryCb& action_cb, std::ostream* os) { - if (action_cb == nullptr) { - *os << "nullptr"; - return; - } - ActionPtr action = action_cb(); - PrintTo(*action, os); -} - inline void PrintTo(const MatchResult& result, std::ostream* os) { if (result.isInsufficientData()) { *os << "InsufficientData"; } else if (result.isNoMatch()) { *os << "NoMatch"; } else if (result.isMatch()) { - *os << "Match{ActionFactoryCb="; - PrintTo(result.actionFactory(), os); + *os << "Match{Action="; + PrintTo(*result.action(), os); *os << "}"; } else { *os << "UnknownState"; @@ -319,9 +305,9 @@ inline void PrintTo(const MatchTree& matcher, std::ostream* os) { } inline void PrintTo(const OnMatch& on_match, std::ostream* os) { - if (on_match.action_cb_) { - *os << "{action_cb_="; - PrintTo(on_match.action_cb_, os); + if (on_match.action_) { + *os << "{action_="; + PrintTo(on_match.action_, os); *os << "}"; } else if (on_match.matcher_) { *os << "{matcher_="; @@ -339,26 +325,25 @@ MATCHER(HasInsufficientData, "") { } MATCHER_P(IsActionWithType, matcher, "") { - // Takes an ActionFactoryCb argument, and compares its action type against matcher. + // Takes an ActionConstSharedPtr argument, and compares its action type against matcher. if (arg == nullptr) { return false; } - ActionPtr action = arg(); - return ::testing::ExplainMatchResult(testing::Matcher(matcher), - action->typeUrl(), result_listener); + return ::testing::ExplainMatchResult(testing::Matcher(matcher), arg->typeUrl(), + result_listener); } MATCHER_P(IsStringAction, matcher, "") { - // Takes an ActionFactoryCb argument, and compares its StringAction's string against matcher. + // Takes an ActionConstSharedPtr argument, and compares its StringAction's string against matcher. if (arg == nullptr) { return false; } - ActionPtr action = arg(); - if (action->typeUrl() != "google.protobuf.StringValue") { + + if (arg->typeUrl() != "google.protobuf.StringValue") { return false; } return ::testing::ExplainMatchResult(testing::Matcher(matcher), - action->template getTyped().string_, + arg->template getTyped().string_, result_listener); } @@ -368,8 +353,7 @@ MATCHER_P(HasStringAction, matcher, "") { if (!arg.isMatch()) { return false; } - return ::testing::ExplainMatchResult(IsStringAction(matcher), arg.actionFactory(), - result_listener); + return ::testing::ExplainMatchResult(IsStringAction(matcher), arg.action(), result_listener); } MATCHER_P(HasActionWithType, matcher, "") { @@ -378,8 +362,7 @@ MATCHER_P(HasActionWithType, matcher, "") { if (!arg.isMatch()) { return false; } - return ::testing::ExplainMatchResult(IsActionWithType(matcher), arg.actionFactory(), - result_listener); + return ::testing::ExplainMatchResult(IsActionWithType(matcher), arg.action(), result_listener); } MATCHER(HasNoMatch, "") { diff --git a/test/common/network/BUILD b/test/common/network/BUILD index 34b851995a56c..a78fe172f9357 100644 --- a/test/common/network/BUILD +++ b/test/common/network/BUILD @@ -64,6 +64,24 @@ envoy_benchmark_test( benchmark_binary = "address_impl_speed_test", ) +envoy_cc_benchmark_binary( + name = "lc_trie_ip_list_speed_test", + srcs = ["lc_trie_ip_list_speed_test.cc"], + rbe_pool = "6gig", + deps = [ + "//source/common/network:cidr_range_lib", + "//source/common/network:lc_trie_lib", + "//source/common/network:utility_lib", + "//test/test_common:utility_lib", + "@com_github_google_benchmark//:benchmark", + ], +) + +envoy_benchmark_test( + name = "lc_trie_ip_list_speed_test_benchmark_test", + benchmark_binary = "lc_trie_ip_list_speed_test", +) + envoy_cc_test( name = "cidr_range_test", srcs = ["cidr_range_test.cc"], @@ -387,6 +405,15 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "ip_address_parsing_test", + srcs = ["ip_address_parsing_test.cc"], + rbe_pool = "6gig", + deps = [ + "//source/common/network:ip_address_parsing_lib", + ], +) + envoy_cc_fuzz_test( name = "udp_fuzz_test", srcs = ["udp_fuzz.cc"], diff --git a/test/common/network/address_impl_test.cc b/test/common/network/address_impl_test.cc index d3210aa76331a..84a5b260b1ef5 100644 --- a/test/common/network/address_impl_test.cc +++ b/test/common/network/address_impl_test.cc @@ -29,6 +29,10 @@ namespace Network { namespace Address { namespace { +Ipv6Instance v4MappedV6Instance(const std::string& address) { + return Ipv6Instance(address, /*port=*/0, /*sock_interface=*/nullptr, /*v6only=*/false); +} + bool addressesEqual(const InstanceConstSharedPtr& a, const Instance& b) { if (a == nullptr || a->type() != Type::Ip || b.type() != Type::Ip) { return false; @@ -202,6 +206,16 @@ TEST(Ipv4InstanceTest, PortOnly) { EXPECT_FALSE(address.ip()->isUnicastAddress()); } +TEST(Ipv4InstanceTest, NetnsComparison) { + Ipv4Instance address1("1.2.3.4", nullptr, "/var/run/netns/11111"); + Ipv4Instance address2("1.2.3.4", nullptr, "/var/run/netns/22222"); + // Same netns as address1. + Ipv4Instance address3("1.2.3.4", nullptr, "/var/run/netns/11111"); + + EXPECT_EQ(address1, address3); + EXPECT_NE(address1, address2); +} + TEST(Ipv4InstanceTest, Multicast) { Ipv4Instance address("230.0.0.1"); EXPECT_EQ("230.0.0.1:0", address.asString()); @@ -228,6 +242,36 @@ TEST(Ipv4InstanceTest, Broadcast) { EXPECT_FALSE(address.ip()->isUnicastAddress()); } +TEST(Ipv4InstanceTest, LinkLocal) { + // Link-local addresses. + EXPECT_TRUE(Ipv4Instance("169.254.0.0").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv4Instance("169.254.42.43").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv4Instance("169.254.255.255").ip()->isLinkLocalAddress()); + + // Not link-local addresses. + EXPECT_FALSE(Ipv4Instance("169.255.0.0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv4Instance("169.255.255.255").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv4Instance("170.254.0.0").ip()->isLinkLocalAddress()); +} + +TEST(Ipv4InstanceTest, Teredo) { + // Teredo addresses are not applicable to IPv4. + EXPECT_FALSE(Ipv4Instance("20.1.1.1").ip()->isTeredoAddress()); + EXPECT_FALSE(Ipv4Instance("200.1.1.1").ip()->isTeredoAddress()); +} + +TEST(Ipv4InstanceTest, SiteLocal) { + // Site-local addresses are not applicable to IPv4. + EXPECT_FALSE(Ipv4Instance("1.2.3.4").ip()->isSiteLocalAddress()); + EXPECT_FALSE(Ipv4Instance("200.1.1.1").ip()->isSiteLocalAddress()); +} + +TEST(Ipv4InstanceTest, UniqueLocal) { + // Unique Local Addresses (ULA) are not applicable to IPv4. + EXPECT_FALSE(Ipv4Instance("1.2.3.4").ip()->isUniqueLocalAddress()); + EXPECT_FALSE(Ipv4Instance("200.1.1.1").ip()->isUniqueLocalAddress()); +} + TEST(Ipv4InstanceTest, BadAddress) { EXPECT_THROW(Ipv4Instance("foo"), EnvoyException); EXPECT_THROW(Ipv4Instance("bar", 1), EnvoyException); @@ -300,6 +344,16 @@ TEST(Ipv6InstanceTest, ScopeIdStripping) { EXPECT_EQ(0U, no_scope_address->ip()->ipv6()->scopeId()); } +TEST(Ipv6InstanceTest, NetnsCompare) { + Ipv6Instance address1("::0001", 80, nullptr, true, "/var/run/netns/11111"); + Ipv6Instance address2("::0001", 80, nullptr, true, "/var/run/netns/22222"); + // Same netns as address1. + Ipv6Instance address3("::0001", 80, nullptr, true, "/var/run/netns/11111"); + + EXPECT_NE(address1, address2); + EXPECT_EQ(address1, address3); +} + TEST(Ipv6InstanceTest, PortOnly) { Ipv6Instance address(443); EXPECT_EQ("[::]:443", address.asString()); @@ -341,6 +395,85 @@ TEST(Ipv6InstanceTest, Broadcast) { EXPECT_FALSE(address.ip()->isUnicastAddress()); } +TEST(Ipv6InstanceTest, LinkLocal) { + // Link-local addresses are in the range "fe80::0" to "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff". + EXPECT_TRUE(Ipv6Instance("fe80:0:0:0:0:0:0:0").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fe80::0").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fe80::1").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fe80::42:43").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fe80::ffff:ffff:ffff:ffff").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fe81::1").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fe90::1").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("febf::0").ip()->isLinkLocalAddress()); + EXPECT_TRUE(Ipv6Instance("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff").ip()->isLinkLocalAddress()); + + // Not link-local addresses. + EXPECT_FALSE(Ipv6Instance("::fe80").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv6Instance("fec0::0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv6Instance("ff00::0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv6Instance("ab80::0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv6Instance("abcd::0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(Ipv6Instance("::ffff").ip()->isLinkLocalAddress()); +} + +TEST(Ipv6InstanceTest, V4MappedLinkLocal) { + // Link-local addresses in the range ::ffff:169.254.0.0/16. + EXPECT_TRUE(v4MappedV6Instance("::ffff:169.254.0.0").ip()->isLinkLocalAddress()); + EXPECT_TRUE(v4MappedV6Instance("::ffff:169.254.42.42").ip()->isLinkLocalAddress()); + EXPECT_TRUE(v4MappedV6Instance("::ffff:169.254.255.255").ip()->isLinkLocalAddress()); + + // Not link-local addresses. + EXPECT_FALSE(v4MappedV6Instance("::ffff:169.255.0.0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(v4MappedV6Instance("::ffff:170.254.0.0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(v4MappedV6Instance("::ffff:0.0.0.0").ip()->isLinkLocalAddress()); + EXPECT_FALSE(v4MappedV6Instance("::ffff:192.168.1.1").ip()->isLinkLocalAddress()); + EXPECT_FALSE(v4MappedV6Instance("::ffff:10.54.1.1").ip()->isLinkLocalAddress()); +} + +TEST(Ipv6InstanceTest, Teredo) { + // Teredo addresses are in the range 2001::/32. + EXPECT_TRUE(Ipv6Instance("2001:0:0:0:0:0:0:0").ip()->isTeredoAddress()); + EXPECT_TRUE(Ipv6Instance("2001::1").ip()->isTeredoAddress()); + EXPECT_TRUE(Ipv6Instance("2001::42:43").ip()->isTeredoAddress()); + EXPECT_TRUE(Ipv6Instance("2001::ffff:ffff:ffff:ffff").ip()->isTeredoAddress()); + + // Not Teredo addresses. + EXPECT_FALSE(Ipv6Instance("2002::0").ip()->isTeredoAddress()); + EXPECT_FALSE(Ipv6Instance("2002::1").ip()->isTeredoAddress()); + EXPECT_FALSE(Ipv6Instance("3001::1").ip()->isTeredoAddress()); +} + +TEST(Ipv6InstanceTest, UniqueLocal) { + // Unique Local Addresses (ULA) are in the range fc00::/7. + EXPECT_TRUE(Ipv6Instance("fc00:0:0:0:0:0:0:0").ip()->isUniqueLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fc00::1").ip()->isUniqueLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fc00::42:43").ip()->isUniqueLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fdff::ffff:ffff:ffff:ffff:ffff:ffff").ip()->isUniqueLocalAddress()); + + // Not ULA addresses. + EXPECT_FALSE(Ipv6Instance("fec0:0:0:0:0:0:0:0").ip()->isUniqueLocalAddress()); + EXPECT_FALSE( + Ipv6Instance("feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff").ip()->isUniqueLocalAddress()); + EXPECT_FALSE(Ipv6Instance("fe00::0").ip()->isUniqueLocalAddress()); + EXPECT_FALSE(Ipv6Instance("fe80::0").ip()->isUniqueLocalAddress()); + EXPECT_FALSE(Ipv6Instance("ff00::0").ip()->isUniqueLocalAddress()); +} + +TEST(Ipv6InstanceTest, SiteLocal) { + // Site-local addresses are in the range fec0::/10. + EXPECT_TRUE(Ipv6Instance("fec0:0:0:0:0:0:0:0").ip()->isSiteLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fec0::1").ip()->isSiteLocalAddress()); + EXPECT_TRUE(Ipv6Instance("fec0::42:43").ip()->isSiteLocalAddress()); + EXPECT_TRUE(Ipv6Instance("feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff").ip()->isSiteLocalAddress()); + + // Not site-local addresses. + EXPECT_FALSE(Ipv6Instance("fc00:0:0:0:0:0:0:0").ip()->isSiteLocalAddress()); + EXPECT_FALSE(Ipv6Instance("fdff::ffff:ffff:ffff:ffff:ffff:ffff").ip()->isSiteLocalAddress()); + EXPECT_FALSE(Ipv6Instance("ff00::0").ip()->isSiteLocalAddress()); + EXPECT_FALSE(Ipv6Instance("2002::1").ip()->isSiteLocalAddress()); + EXPECT_FALSE(Ipv6Instance("3001::1").ip()->isSiteLocalAddress()); +} + TEST(Ipv6InstanceTest, BadAddress) { EXPECT_THROW(Ipv6Instance("foo"), EnvoyException); EXPECT_THROW(Ipv6Instance("bar", 1), EnvoyException); diff --git a/test/common/network/connection_impl_test.cc b/test/common/network/connection_impl_test.cc index b2b6e90a38feb..7b873439af520 100644 --- a/test/common/network/connection_impl_test.cc +++ b/test/common/network/connection_impl_test.cc @@ -209,14 +209,18 @@ class ConnectionImplTestBase { dispatcher_->run(Event::Dispatcher::RunType::Block); } - void disconnect(bool wait_for_remote_close) { + void disconnect(bool wait_for_remote_close, bool client_socket_closed = false) { if (client_write_buffer_) { EXPECT_CALL(*client_write_buffer_, drain(_)) .Times(AnyNumber()) .WillRepeatedly( Invoke([&](uint64_t size) -> void { client_write_buffer_->baseDrain(size); })); } - EXPECT_CALL(client_callbacks_, onEvent(ConnectionEvent::LocalClose)); + // client_socket_closed is set when the client socket has been moved, + // and the LocalClose won't be raised. + if (!client_socket_closed) { + EXPECT_CALL(client_callbacks_, onEvent(ConnectionEvent::LocalClose)); + } client_connection_->close(ConnectionCloseType::NoFlush); if (wait_for_remote_close) { EXPECT_CALL(server_callbacks_, onEvent(ConnectionEvent::RemoteClose)) @@ -4584,6 +4588,21 @@ TEST_P(ClientConnectionWithCustomRawBufferSocketTest, TransportSocketCallbacks) disconnect(false); } +TEST_P(ConnectionImplTest, TestConstSocketAccess) { + setUpBasicConnection(); + connect(); + + // Test const access to socket. + const Network::Connection& const_connection = *client_connection_; + const auto& socket_ref = const_connection.getSocket(); + EXPECT_NE(socket_ref, nullptr); + + // Verify that const and non-const getSocket return the same socket. + EXPECT_EQ(&socket_ref, &client_connection_->getSocket()); + + disconnect(true); +} + } // namespace } // namespace Network } // namespace Envoy diff --git a/test/common/network/ip_address_parsing_test.cc b/test/common/network/ip_address_parsing_test.cc new file mode 100644 index 0000000000000..d07a40f40b0f6 --- /dev/null +++ b/test/common/network/ip_address_parsing_test.cc @@ -0,0 +1,138 @@ +#include + +#include "source/common/network/ip_address_parsing.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Network { + +TEST(IpAddressParsingTest, ParseIPv4Valid) { + // Test standard dotted-quad notation. + auto sa4_or = IpAddressParsing::parseIPv4("127.0.0.1", /*port=*/8080); + ASSERT_TRUE(sa4_or.ok()); + const sockaddr_in sa4 = sa4_or.value(); + EXPECT_EQ(AF_INET, sa4.sin_family); + EXPECT_EQ(htons(8080), sa4.sin_port); + EXPECT_EQ(htonl(INADDR_LOOPBACK), sa4.sin_addr.s_addr); + + // Test another valid IPv4. + auto sa4_or2 = IpAddressParsing::parseIPv4("192.168.1.1", /*port=*/443); + ASSERT_TRUE(sa4_or2.ok()); + const sockaddr_in sa4_2 = sa4_or2.value(); + EXPECT_EQ(AF_INET, sa4_2.sin_family); + EXPECT_EQ(htons(443), sa4_2.sin_port); + char buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &sa4_2.sin_addr, buf, INET_ADDRSTRLEN); + EXPECT_EQ("192.168.1.1", std::string(buf)); + + // Test edge case IPs. + EXPECT_TRUE(IpAddressParsing::parseIPv4("0.0.0.0", 0).ok()); + EXPECT_TRUE(IpAddressParsing::parseIPv4("255.255.255.255", 65535).ok()); +} + +TEST(IpAddressParsingTest, ParseIPv4Invalid) { + // Test incomplete addresses. + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2.3", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("1", 0).ok()); + + // Test out-of-range octets. + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2.3.256", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("256.0.0.1", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2.3.999", 0).ok()); + + // Test non-numeric input. + EXPECT_FALSE(IpAddressParsing::parseIPv4("not_an_ip", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("abc.def.ghi.jkl", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("", 0).ok()); + + // Test IPv6 addresses (should fail for IPv4 parser). + EXPECT_FALSE(IpAddressParsing::parseIPv4("::1", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("fe80::1", 0).ok()); + + // Test that inet_pton() correctly rejects non-standard formats. + // These formats might be accepted by some liberal parsers but should be rejected + // by inet_pton() which enforces strict dotted-quad notation. + EXPECT_FALSE(IpAddressParsing::parseIPv4("127.1", 0).ok()); // Short form + EXPECT_FALSE(IpAddressParsing::parseIPv4("0x7f.0.0.1", 0).ok()); // Hex notation + // Note: Some platforms (e.g., macOS) may accept octal notation in inet_pton(), + // so we skip this test to maintain platform compatibility. + + // Test extra dots. + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2.3.4.", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4(".1.2.3.4", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("1..2.3.4", 0).ok()); + + // Test with spaces. + EXPECT_FALSE(IpAddressParsing::parseIPv4(" 1.2.3.4", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2.3.4 ", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv4("1.2. 3.4", 0).ok()); +} + +TEST(IpAddressParsingTest, ParseIPv6Valid) { + // Test loopback. + auto sa6_or = IpAddressParsing::parseIPv6("::1", /*port=*/443); + ASSERT_TRUE(sa6_or.ok()); + const sockaddr_in6 sa6 = sa6_or.value(); + EXPECT_EQ(AF_INET6, sa6.sin6_family); + EXPECT_EQ(htons(443), sa6.sin6_port); + in6_addr loopback = IN6ADDR_LOOPBACK_INIT; + EXPECT_EQ(0, memcmp(&loopback, &sa6.sin6_addr, sizeof(in6_addr))); + + // Test other valid IPv6 addresses. + EXPECT_TRUE(IpAddressParsing::parseIPv6("::", 0).ok()); + EXPECT_TRUE(IpAddressParsing::parseIPv6("::ffff:127.0.0.1", 0).ok()); // IPv4-mapped + EXPECT_TRUE(IpAddressParsing::parseIPv6("2001:db8::1", 0).ok()); + EXPECT_TRUE(IpAddressParsing::parseIPv6("fe80::1", 0).ok()); + EXPECT_TRUE(IpAddressParsing::parseIPv6("2001:0db8:0000:0000:0000:0000:0000:0001", 0).ok()); + + // Test IPv6 with scope (this is why we need getaddrinfo() for IPv6). + // Note: Actual scope parsing depends on platform support. +#ifdef __linux__ + // Numeric scope ID. + auto sa6_scope = IpAddressParsing::parseIPv6("fe80::1%2", 80); + ASSERT_TRUE(sa6_scope.ok()); + EXPECT_EQ(2, sa6_scope.value().sin6_scope_id); +#endif +} + +TEST(IpAddressParsingTest, ParseIPv6Invalid) { + // Test invalid characters. + EXPECT_FALSE(IpAddressParsing::parseIPv6("::g", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6("gggg::1", 0).ok()); + + // Test invalid format. + EXPECT_FALSE(IpAddressParsing::parseIPv6("1:::1", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6(":::1", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6("1::2::3", 0).ok()); // Multiple :: + + // Test non-IP strings. + EXPECT_FALSE(IpAddressParsing::parseIPv6("not_an_ip", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6("", 0).ok()); + + // Test IPv4 addresses (should fail for IPv6 parser). + EXPECT_FALSE(IpAddressParsing::parseIPv6("127.0.0.1", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6("192.168.1.1", 0).ok()); + + // Test too many groups. + EXPECT_FALSE(IpAddressParsing::parseIPv6("1:2:3:4:5:6:7:8:9", 0).ok()); + + // Test with spaces. + EXPECT_FALSE(IpAddressParsing::parseIPv6(" ::1", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6("::1 ", 0).ok()); + EXPECT_FALSE(IpAddressParsing::parseIPv6(":: 1", 0).ok()); +} + +TEST(IpAddressParsingTest, PortBoundaries) { + // Test port boundaries for IPv4. + EXPECT_TRUE(IpAddressParsing::parseIPv4("127.0.0.1", 0).ok()); + EXPECT_TRUE(IpAddressParsing::parseIPv4("127.0.0.1", 65535).ok()); + + // Test port boundaries for IPv6. + EXPECT_TRUE(IpAddressParsing::parseIPv6("::1", 0).ok()); + EXPECT_TRUE(IpAddressParsing::parseIPv6("::1", 65535).ok()); +} + +} // namespace Network +} // namespace Envoy diff --git a/test/common/network/lc_trie_ip_list_speed_test.cc b/test/common/network/lc_trie_ip_list_speed_test.cc new file mode 100644 index 0000000000000..62a4b1b6a0da5 --- /dev/null +++ b/test/common/network/lc_trie_ip_list_speed_test.cc @@ -0,0 +1,286 @@ +// Performance benchmark comparing LcTrie vs Linear Search for IP range matching +// in RBAC and access control scenarios. + +#include +#include + +#include "source/common/network/cidr_range.h" +#include "source/common/network/lc_trie.h" +#include "source/common/network/utility.h" +#include "source/common/protobuf/protobuf.h" + +#include "test/test_common/utility.h" + +#include "benchmark/benchmark.h" + +namespace Envoy { +namespace Network { +namespace Address { + +// Generate realistic IP ranges for testing RBAC scenarios. +class IpRangeGenerator { +public: + IpRangeGenerator() : generator_(42) {} // Fixed seed for reproducibility. + + Protobuf::RepeatedPtrField generateIpv4Ranges(size_t count) { + Protobuf::RepeatedPtrField ranges; + + std::uniform_int_distribution ip_dist(0, 0xFFFFFFFF); + std::uniform_int_distribution prefix_dist(8, 32); // Realistic CIDR prefixes + + for (size_t i = 0; i < count; ++i) { + auto* range = ranges.Add(); + + // Generate a random IPv4 address + uint32_t ip = ip_dist(generator_); + range->set_address_prefix(fmt::format("{}.{}.{}.{}", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, + (ip >> 8) & 0xFF, ip & 0xFF)); + + // Set a realistic prefix length + range->mutable_prefix_len()->set_value(prefix_dist(generator_)); + } + + return ranges; + } + + Protobuf::RepeatedPtrField generateIpv6Ranges(size_t count) { + Protobuf::RepeatedPtrField ranges; + + std::uniform_int_distribution segment_dist(0, 0xFFFF); + std::uniform_int_distribution prefix_dist(48, 128); // Realistic IPv6 prefixes + + for (size_t i = 0; i < count; ++i) { + auto* range = ranges.Add(); + + // Generate a random IPv6 address + range->set_address_prefix(fmt::format( + "{}:{}:{}:{}:{}:{}:{}:{}", segment_dist(generator_), segment_dist(generator_), + segment_dist(generator_), segment_dist(generator_), segment_dist(generator_), + segment_dist(generator_), segment_dist(generator_), segment_dist(generator_))); + + range->mutable_prefix_len()->set_value(prefix_dist(generator_)); + } + + return ranges; + } + + std::vector generateTestIps(size_t count, bool ipv6 = false) { + std::vector ips; + ips.reserve(count); + + if (ipv6) { + std::uniform_int_distribution segment_dist(0, 0xFFFF); + for (size_t i = 0; i < count; ++i) { + const std::string ip_str = fmt::format( + "{}:{}:{}:{}:{}:{}:{}:{}", segment_dist(generator_), segment_dist(generator_), + segment_dist(generator_), segment_dist(generator_), segment_dist(generator_), + segment_dist(generator_), segment_dist(generator_), segment_dist(generator_)); + auto ip = Utility::parseInternetAddressNoThrow(ip_str); + if (ip) { + ips.push_back(ip); + } + } + } else { + std::uniform_int_distribution ip_dist(0, 0xFFFFFFFF); + for (size_t i = 0; i < count; ++i) { + uint32_t ip = ip_dist(generator_); + const std::string ip_str = fmt::format("{}.{}.{}.{}", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, + (ip >> 8) & 0xFF, ip & 0xFF); + auto parsed_ip = Utility::parseInternetAddressNoThrow(ip_str); + if (parsed_ip) { + ips.push_back(parsed_ip); + } + } + } + + return ips; + } + +private: + std::mt19937 generator_; +}; + +// Helper function to convert protobuf ranges to CidrRange vector. +std::vector +protobufToCidrRanges(const Protobuf::RepeatedPtrField& ranges) { + std::vector cidr_ranges; + cidr_ranges.reserve(ranges.size()); + for (const auto& range : ranges) { + auto cidr_result = CidrRange::create(range); + if (cidr_result.ok()) { + cidr_ranges.push_back(std::move(cidr_result.value())); + } + } + return cidr_ranges; +} + +// Benchmark linear search IP list implementation. +static void BM_LinearIpListMatching(benchmark::State& state) { + const size_t num_ranges = state.range(0); + const size_t num_queries = 1000; + + IpRangeGenerator generator; + auto ranges = generator.generateIpv4Ranges(num_ranges); + auto test_ips = generator.generateTestIps(num_queries); + + auto ip_list_result = IpList::create(ranges); + if (!ip_list_result.ok()) { + state.SkipWithError("Failed to create IpList"); + return; + } + auto ip_list = std::move(ip_list_result.value()); + + // Pre-generate random queries for consistent benchmark. + std::mt19937 rng(12345); + std::uniform_int_distribution dist(0, test_ips.size() - 1); + std::vector query_indices; + for (size_t i = 0; i < 1024; ++i) { + query_indices.push_back(dist(rng)); + } + + size_t query_idx = 0; + for (auto _ : state) { + const auto& query_ip = test_ips[query_indices[query_idx % 1024]]; + bool result = ip_list->contains(*query_ip); + benchmark::DoNotOptimize(result); + query_idx++; + } + + state.SetItemsProcessed(state.iterations()); + state.SetLabel(fmt::format("LinearSearch_{}ranges", num_ranges)); +} + +// Benchmark LcTrie IP list implementation using existing Network::LcTrie::LcTrie. +static void BM_LcTrieIpListMatching(benchmark::State& state) { + const size_t num_ranges = state.range(0); + const size_t num_queries = 1000; + + IpRangeGenerator generator; + auto ranges = generator.generateIpv4Ranges(num_ranges); + auto test_ips = generator.generateTestIps(num_queries); + + // Convert protobuf ranges to CidrRange vector. + auto cidr_ranges = protobufToCidrRanges(ranges); + if (cidr_ranges.empty()) { + state.SkipWithError("Failed to convert ranges to CidrRange"); + return; + } + + // Create LC Trie directly following the pattern from Unified IP Matcher. + Network::LcTrie::LcTrie trie( + std::vector>>{{true, cidr_ranges}}); + + // Pre-generate random queries for consistent benchmark. + std::mt19937 rng(12345); + std::uniform_int_distribution dist(0, test_ips.size() - 1); + std::vector query_indices; + for (size_t i = 0; i < 1024; ++i) { + query_indices.push_back(dist(rng)); + } + + size_t query_idx = 0; + for (auto _ : state) { + const auto& query_ip = test_ips[query_indices[query_idx % 1024]]; + bool result = !trie.getData(query_ip).empty(); + benchmark::DoNotOptimize(result); + query_idx++; + } + + state.SetItemsProcessed(state.iterations()); + state.SetLabel(fmt::format("LcTrie_{}ranges", num_ranges)); +} + +// IPv6 benchmarks +static void BM_LinearIpListMatchingIPv6(benchmark::State& state) { + const size_t num_ranges = state.range(0); + const size_t num_queries = 1000; + + IpRangeGenerator generator; + auto ranges = generator.generateIpv6Ranges(num_ranges); + auto test_ips = generator.generateTestIps(num_queries, true); + + auto ip_list_result = IpList::create(ranges); + if (!ip_list_result.ok()) { + state.SkipWithError("Failed to create IpList"); + return; + } + auto ip_list = std::move(ip_list_result.value()); + + // Pre-generate random queries for consistent benchmark. + std::mt19937 rng(12345); + std::uniform_int_distribution dist(0, test_ips.size() - 1); + std::vector query_indices; + for (size_t i = 0; i < 512; ++i) { + query_indices.push_back(dist(rng)); + } + + size_t query_idx = 0; + for (auto _ : state) { + if (query_idx < query_indices.size()) { + const auto& query_ip = test_ips[query_indices[query_idx % 512]]; + bool result = ip_list->contains(*query_ip); + benchmark::DoNotOptimize(result); + query_idx++; + } + } + + state.SetItemsProcessed(state.iterations()); + state.SetLabel(fmt::format("LinearSearch_IPv6_{}ranges", num_ranges)); +} + +static void BM_LcTrieIpListMatchingIPv6(benchmark::State& state) { + const size_t num_ranges = state.range(0); + const size_t num_queries = 1000; + + IpRangeGenerator generator; + auto ranges = generator.generateIpv6Ranges(num_ranges); + auto test_ips = generator.generateTestIps(num_queries, true); + + // Convert protobuf ranges to CidrRange vector. + auto cidr_ranges = protobufToCidrRanges(ranges); + if (cidr_ranges.empty()) { + state.SkipWithError("Failed to convert ranges to CidrRange"); + return; + } + + // Create LC Trie directly following the pattern from Unified IP Matcher. + Network::LcTrie::LcTrie trie( + std::vector>>{{true, cidr_ranges}}); + + // Pre-generate random queries for consistent benchmark. + std::mt19937 rng(12345); + std::uniform_int_distribution dist(0, test_ips.size() - 1); + std::vector query_indices; + for (size_t i = 0; i < 512; ++i) { + query_indices.push_back(dist(rng)); + } + + size_t query_idx = 0; + for (auto _ : state) { + if (query_idx < query_indices.size()) { + const auto& query_ip = test_ips[query_indices[query_idx % 512]]; + bool result = !trie.getData(query_ip).empty(); + benchmark::DoNotOptimize(result); + query_idx++; + } + } + + state.SetItemsProcessed(state.iterations()); + state.SetLabel(fmt::format("LcTrie_IPv6_{}ranges", num_ranges)); +} + +// Comprehensive benchmarks for RBAC scenarios +BENCHMARK(BM_LinearIpListMatching)->Range(10, 5000)->Unit(benchmark::kNanosecond); +BENCHMARK(BM_LcTrieIpListMatching)->Range(10, 5000)->Unit(benchmark::kNanosecond); + +// Focused benchmarks for common RBAC policy sizes +BENCHMARK(BM_LinearIpListMatching)->Arg(25)->Arg(50)->Arg(100)->Arg(250)->Arg(500)->Arg(1000); +BENCHMARK(BM_LcTrieIpListMatching)->Arg(25)->Arg(50)->Arg(100)->Arg(250)->Arg(500)->Arg(1000); + +// IPv6 benchmarks for realistic dual-stack scenarios +BENCHMARK(BM_LinearIpListMatchingIPv6)->Arg(50)->Arg(200)->Arg(500); +BENCHMARK(BM_LcTrieIpListMatchingIPv6)->Arg(50)->Arg(200)->Arg(500); + +} // namespace Address +} // namespace Network +} // namespace Envoy diff --git a/test/common/network/multi_connection_base_impl_test.cc b/test/common/network/multi_connection_base_impl_test.cc index 0093a9835703c..6bccf6a512074 100644 --- a/test/common/network/multi_connection_base_impl_test.cc +++ b/test/common/network/multi_connection_base_impl_test.cc @@ -1211,5 +1211,12 @@ TEST_F(MultiConnectionBaseImplTest, SetSocketOptionFailedTest) { EXPECT_FALSE(impl_->setSocketOption(sockopt_name, sockopt_val)); } +TEST_F(MultiConnectionBaseImplTest, GetSocketPanics) { + setupMultiConnectionImpl(2); + + // getSocket() should panic as it's not implemented for MultiConnectionBaseImpl. + EXPECT_DEATH(impl_->getSocket(), "not implemented"); +} + } // namespace Network } // namespace Envoy diff --git a/test/common/network/udp_listener_impl_test.cc b/test/common/network/udp_listener_impl_test.cc index 196afc332e629..1c3d83d5ae7f8 100644 --- a/test/common/network/udp_listener_impl_test.cc +++ b/test/common/network/udp_listener_impl_test.cc @@ -238,33 +238,6 @@ TEST_P(UdpListenerImplTest, LimitNumberOfReadsPerLoop) { dispatcher_->run(Event::Dispatcher::RunType::Block); } -#ifdef UDP_GRO -TEST_P(UdpListenerImplTest, GroLargeDatagramRecvmsg) { - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.udp_socket_apply_aggregated_read_limit")) { - return; - } - setup(true); - - ON_CALL(override_syscall_, supportsUdpGro()).WillByDefault(Return(true)); - client_.write(std::string(32768, 'a'), *send_to_addr_); - const std::string second("second"); - client_.write(second, *send_to_addr_); - - EXPECT_CALL(listener_callbacks_, onReadReady()); - EXPECT_CALL(listener_callbacks_, onDatagramsDropped(_)).Times(AtLeast(1)); - EXPECT_CALL(listener_callbacks_, onData(_)).WillOnce(Invoke([&](const UdpRecvData& data) -> void { - validateRecvCallbackParams(data, 1); - EXPECT_EQ(data.buffer_->toString(), second); - - dispatcher_->exit(); - })); - - dispatcher_->run(Event::Dispatcher::RunType::Block); - EXPECT_EQ(1, listener_->packetsDropped()); -} -#endif - /** * Tests UDP listener for read and write callbacks with actual data. */ @@ -667,10 +640,8 @@ TEST_P(UdpListenerImplTest, UdpGroBasic) { // Set msg_iovec EXPECT_EQ(msg->msg_iovlen, 1); memcpy(msg->msg_iov[0].iov_base, stacked_message.data(), stacked_message.length()); - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.udp_socket_apply_aggregated_read_limit")) { - EXPECT_EQ(msg->msg_iov[0].iov_len, 64 * 1024); - } + // The aggregated read limit is now always applied. + EXPECT_EQ(msg->msg_iov[0].iov_len, 64 * 1024); msg->msg_iov[0].iov_len = stacked_message.length(); // Set control headers @@ -738,10 +709,7 @@ TEST_P(UdpListenerImplTest, UdpGroBasic) { } TEST_P(UdpListenerImplTest, GroLargeDatagramRecvmsgNoDrop) { - if (!Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.udp_socket_apply_aggregated_read_limit")) { - return; - } + // The aggregated read limit is now always applied. setup(true); ON_CALL(override_syscall_, supportsUdpGro()).WillByDefault(Return(true)); @@ -771,10 +739,7 @@ TEST_P(UdpListenerImplTest, GroLargeDatagramRecvmsgNoDrop) { // of same size, regardless of MAX_NUM_PACKETS_PER_EVENT_LOOP or listener_callbacks_ provided limit. // But once MAX_NUM_PACKETS_PER_EVENT_LOOP of packets are processed, read will stop. TEST_P(UdpListenerImplTest, UdpGroReadLimit) { - if (!Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.udp_socket_apply_aggregated_read_limit")) { - return; - } + // The aggregated read limit is now always applied. setup(true); EXPECT_CALL(listener_callbacks_, numPacketsExpectedPerEventLoop()).WillRepeatedly(Return(32)); diff --git a/test/common/network/utility_test.cc b/test/common/network/utility_test.cc index 06056f2216e84..67ce194d0f53d 100644 --- a/test/common/network/utility_test.cc +++ b/test/common/network/utility_test.cc @@ -831,6 +831,36 @@ TEST_F(ExecInNetnsTest, OpenFail) { // Expecting failure. auto result = Utility::execInNetworkNamespace([]() -> int { return 0; }, "bleh"); EXPECT_FALSE(result.ok()); + EXPECT_TRUE(result.status().message().starts_with("failed to open netns file")); +} + +TEST_F(ExecInNetnsTest, FailtoReturnToOriginalNetns) { + EXPECT_DEATH( + { + // Make the tests use mock syscalls. + testing::StrictMock linux_os_syscalls; + testing::StrictMock os_syscalls; + TestThreadsafeSingletonInjector os_calls(&os_syscalls); + TestThreadsafeSingletonInjector linux_os_calls( + &linux_os_syscalls); + + EXPECT_CALL(os_syscalls, open(_, O_RDONLY)) + .WillRepeatedly( + Invoke([](const char*, int) -> Api::SysCallIntResult { return {1337, 0}; })); + EXPECT_CALL(os_syscalls, close(_)).WillRepeatedly(Invoke([](int) -> Api::SysCallIntResult { + return {0, 0}; + })); + + // Succeed on the first network namespace syscall, which would jump to a different netns. + // The second call, which would jump back to the original netns, should fail. This is an + // unrecoverable error, so it should result in process death. + EXPECT_CALL(linux_os_syscalls, setns(_, _)) + .WillOnce(Invoke([](int, int) -> Api::SysCallIntResult { return {0, 0}; })) + .WillOnce(Invoke([](int, int) -> Api::SysCallIntResult { return {-1, -1}; })); + + auto _ = Utility::execInNetworkNamespace([]() -> int { return 0; }, "bleh"); + }, + "failed to restore original netns .*"); } #endif diff --git a/test/common/orca/BUILD b/test/common/orca/BUILD index ec4803a557d9c..9a00e98464778 100644 --- a/test/common/orca/BUILD +++ b/test/common/orca/BUILD @@ -20,7 +20,7 @@ envoy_cc_test( "//test/test_common:status_utility_lib", "//test/test_common:utility_lib", "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", - "@com_github_fmtlib_fmt//:fmtlib", + "@com_github_fmtlib_fmt//:fmt", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", ], @@ -36,7 +36,7 @@ envoy_cc_test( "//test/test_common:status_utility_lib", "//test/test_common:utility_lib", "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", - "@com_github_fmtlib_fmt//:fmtlib", + "@com_github_fmtlib_fmt//:fmt", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", ], @@ -64,7 +64,7 @@ envoy_cc_fuzz_test( "//test/test_common:status_utility_lib", "//test/test_common:utility_lib", "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", - "@com_github_fmtlib_fmt//:fmtlib", + "@com_github_fmtlib_fmt//:fmt", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", ], diff --git a/test/common/protobuf/deterministic_hash_test.cc b/test/common/protobuf/deterministic_hash_test.cc index f5ebf94a18060..c2b1028d44fb6 100644 --- a/test/common/protobuf/deterministic_hash_test.cc +++ b/test/common/protobuf/deterministic_hash_test.cc @@ -499,5 +499,20 @@ TEST(HashTest, AnyWithKnownTypeMismatch) { EXPECT_NE(hash(a1), hash(a2)); } +TEST(HashTest, ValidateRepeatedAnyMismatchingType) { + deterministichashtest::AnyContainer a1, a2; + deterministichashtest::Recursion value; + value.set_index(1); + a1.mutable_any()->PackFrom(value); + a2.mutable_any()->PackFrom(value); + // Set a2 Any to a mismatching invalid type. + a2.mutable_any()->set_type_url("RawMessage"); + EXPECT_NE(hash(a1), hash(a2)); + + // Set a2 Any to a mismatching valid type. + a2.mutable_any()->set_type_url("google.protobuf.Struct"); + EXPECT_NE(hash(a1), hash(a2)); +} + } // namespace DeterministicProtoHash } // namespace Envoy diff --git a/test/common/protobuf/deterministic_hash_test.proto b/test/common/protobuf/deterministic_hash_test.proto index 5f56d35d41b01..09e64b20c8f32 100644 --- a/test/common/protobuf/deterministic_hash_test.proto +++ b/test/common/protobuf/deterministic_hash_test.proto @@ -36,6 +36,7 @@ message RepeatedFields { repeated float floats = 9; repeated FooEnum enums = 10; repeated Recursion messages = 11; + repeated AnyContainer anys = 12; }; message SingleFields { diff --git a/test/common/protobuf/utility_test.cc b/test/common/protobuf/utility_test.cc index 41f25d27cee5e..24e0fa62beac1 100644 --- a/test/common/protobuf/utility_test.cc +++ b/test/common/protobuf/utility_test.cc @@ -45,8 +45,8 @@ namespace Envoy { using ::testing::HasSubstr; -bool checkProtoEquality(const ProtobufWkt::Value& proto1, std::string text_proto2) { - ProtobufWkt::Value proto2; +bool checkProtoEquality(const Protobuf::Value& proto1, std::string text_proto2) { + Protobuf::Value proto2; if (!Protobuf::TextFormat::ParseFromString(text_proto2, &proto2)) { return false; } @@ -172,25 +172,25 @@ TEST_F(ProtobufUtilityTest, EvaluateFractionalPercent) { } // namespace ProtobufPercentHelper TEST_F(ProtobufUtilityTest, MessageUtilHash) { - ProtobufWkt::Struct s; + Protobuf::Struct s; (*s.mutable_fields())["ab"].set_string_value("fgh"); (*s.mutable_fields())["cde"].set_string_value("ij"); - ProtobufWkt::Struct s2; + Protobuf::Struct s2; (*s2.mutable_fields())["ab"].set_string_value("ij"); (*s2.mutable_fields())["cde"].set_string_value("fgh"); - ProtobufWkt::Struct s3; + Protobuf::Struct s3; (*s3.mutable_fields())["ac"].set_string_value("fgh"); (*s3.mutable_fields())["cdb"].set_string_value("ij"); - ProtobufWkt::Any a1; + Protobuf::Any a1; a1.PackFrom(s); // The two base64 encoded Struct to test map is identical to the struct above, this tests whether // a map is deterministically serialized and hashed. - ProtobufWkt::Any a2 = a1; + Protobuf::Any a2 = a1; a2.set_value(Base64::decode("CgsKA2NkZRIEGgJpagoLCgJhYhIFGgNmZ2g=")); - ProtobufWkt::Any a3 = a1; + Protobuf::Any a3 = a1; a3.set_value(Base64::decode("CgsKAmFiEgUaA2ZnaAoLCgNjZGUSBBoCaWo=")); - ProtobufWkt::Any a4, a5; + Protobuf::Any a4, a5; a4.PackFrom(s2); a5.PackFrom(s3); @@ -207,7 +207,7 @@ TEST_F(ProtobufUtilityTest, MessageUtilHash) { } TEST_F(ProtobufUtilityTest, RepeatedPtrUtilDebugString) { - Protobuf::RepeatedPtrField repeated; + Protobuf::RepeatedPtrField repeated; EXPECT_EQ("[]", RepeatedPtrUtil::debugString(repeated)); repeated.Add()->set_value(10); EXPECT_THAT(RepeatedPtrUtil::debugString(repeated), @@ -293,7 +293,7 @@ TEST_F(ProtobufUtilityTest, ValidateUnknownFieldsNestedAny) { } TEST_F(ProtobufUtilityTest, JsonConvertAnyUnknownMessageType) { - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.set_type_url("type.googleapis.com/bad.type.url"); source_any.set_value("asdf"); auto status = MessageUtil::getJsonStringFromMessage(source_any, true).status(); @@ -301,14 +301,14 @@ TEST_F(ProtobufUtilityTest, JsonConvertAnyUnknownMessageType) { } TEST_F(ProtobufUtilityTest, JsonConvertKnownGoodMessage) { - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(envoy::config::bootstrap::v3::Bootstrap::default_instance()); EXPECT_THAT(MessageUtil::getJsonStringFromMessageOrError(source_any, true), testing::HasSubstr("@type")); } TEST_F(ProtobufUtilityTest, JsonConvertOrErrorAnyWithUnknownMessageType) { - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.set_type_url("type.googleapis.com/bad.type.url"); source_any.set_value("asdf"); EXPECT_THAT(MessageUtil::getJsonStringFromMessageOrError(source_any), @@ -404,7 +404,7 @@ watchdog: { miss_timeout: 1s })EOF"; // An unknown field (or with wrong type) in a message is rejected. TEST_F(ProtobufUtilityTest, LoadBinaryProtoUnknownFieldFromFile) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); const std::string filename = TestEnvironment::writeStringToFileForTest("proto.pb", source_duration.SerializeAsString()); @@ -416,7 +416,7 @@ TEST_F(ProtobufUtilityTest, LoadBinaryProtoUnknownFieldFromFile) { // Multiple unknown fields (or with wrong type) in a message are rejected. TEST_F(ProtobufUtilityTest, LoadBinaryProtoUnknownMultipleFieldsFromFile) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); source_duration.set_nanos(42); const std::string filename = @@ -864,20 +864,20 @@ TEST_F(ProtobufUtilityTest, RedactAny) { // Empty `Any` can be trivially redacted. TEST_F(ProtobufUtilityTest, RedactEmptyAny) { - ProtobufWkt::Any actual; + Protobuf::Any actual; TestUtility::loadFromYaml(R"EOF( '@type': type.googleapis.com/envoy.test.Sensitive )EOF", actual); - ProtobufWkt::Any expected = actual; + Protobuf::Any expected = actual; MessageUtil::redact(actual); EXPECT_TRUE(TestUtility::protoEqual(expected, actual)); } // Messages packed into `Any` with unknown type URLs are skipped. TEST_F(ProtobufUtilityTest, RedactAnyWithUnknownTypeUrl) { - ProtobufWkt::Any actual; + Protobuf::Any actual; // Note, `loadFromYaml` validates the type when populating `Any`, so we have to pass the real type // first and substitute an unknown message type after loading. TestUtility::loadFromYaml(R"EOF( @@ -887,7 +887,7 @@ sensitive_string: This field is sensitive, but we have no way of knowing. actual); actual.set_type_url("type.googleapis.com/envoy.unknown.Message"); - ProtobufWkt::Any expected = actual; + Protobuf::Any expected = actual; MessageUtil::redact(actual); EXPECT_TRUE(TestUtility::protoEqual(expected, actual)); } @@ -1130,9 +1130,9 @@ TYPED_TEST(TypedStructUtilityTest, RedactEmptyTypeUrlTypedStruct) { } TEST_F(ProtobufUtilityTest, RedactEmptyTypeUrlAny) { - ProtobufWkt::Any actual; + Protobuf::Any actual; MessageUtil::redact(actual); - ProtobufWkt::Any expected = actual; + Protobuf::Any expected = actual; EXPECT_TRUE(TestUtility::protoEqual(expected, actual)); } @@ -1213,29 +1213,29 @@ TEST_F(ProtobufUtilityTest, SanitizeUTF8) { } TEST_F(ProtobufUtilityTest, KeyValueStruct) { - const ProtobufWkt::Struct obj = MessageUtil::keyValueStruct("test_key", "test_value"); + const Protobuf::Struct obj = MessageUtil::keyValueStruct("test_key", "test_value"); EXPECT_EQ(obj.fields_size(), 1); - EXPECT_EQ(obj.fields().at("test_key").kind_case(), ProtobufWkt::Value::KindCase::kStringValue); + EXPECT_EQ(obj.fields().at("test_key").kind_case(), Protobuf::Value::KindCase::kStringValue); EXPECT_EQ(obj.fields().at("test_key").string_value(), "test_value"); } TEST_F(ProtobufUtilityTest, KeyValueStructMap) { - const ProtobufWkt::Struct obj = MessageUtil::keyValueStruct( + const Protobuf::Struct obj = MessageUtil::keyValueStruct( {{"test_key", "test_value"}, {"test_another_key", "test_another_value"}}); EXPECT_EQ(obj.fields_size(), 2); - EXPECT_EQ(obj.fields().at("test_key").kind_case(), ProtobufWkt::Value::KindCase::kStringValue); + EXPECT_EQ(obj.fields().at("test_key").kind_case(), Protobuf::Value::KindCase::kStringValue); EXPECT_EQ(obj.fields().at("test_key").string_value(), "test_value"); EXPECT_EQ(obj.fields().at("test_another_key").kind_case(), - ProtobufWkt::Value::KindCase::kStringValue); + Protobuf::Value::KindCase::kStringValue); EXPECT_EQ(obj.fields().at("test_another_key").string_value(), "test_another_value"); } TEST_F(ProtobufUtilityTest, ValueUtilEqual_NullValues) { - ProtobufWkt::Value v1, v2; - v1.set_null_value(ProtobufWkt::NULL_VALUE); - v2.set_null_value(ProtobufWkt::NULL_VALUE); + Protobuf::Value v1, v2; + v1.set_null_value(Protobuf::NULL_VALUE); + v2.set_null_value(Protobuf::NULL_VALUE); - ProtobufWkt::Value other; + Protobuf::Value other; other.set_string_value("s"); EXPECT_TRUE(ValueUtil::equal(v1, v2)); @@ -1243,7 +1243,7 @@ TEST_F(ProtobufUtilityTest, ValueUtilEqual_NullValues) { } TEST_F(ProtobufUtilityTest, ValueUtilEqual_StringValues) { - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("s"); v2.set_string_value("s"); v3.set_string_value("not_s"); @@ -1253,7 +1253,7 @@ TEST_F(ProtobufUtilityTest, ValueUtilEqual_StringValues) { } TEST_F(ProtobufUtilityTest, ValueUtilEqual_NumberValues) { - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_number_value(1.0); v2.set_number_value(1.0); v3.set_number_value(100.0); @@ -1263,7 +1263,7 @@ TEST_F(ProtobufUtilityTest, ValueUtilEqual_NumberValues) { } TEST_F(ProtobufUtilityTest, ValueUtilEqual_BoolValues) { - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_bool_value(true); v2.set_bool_value(true); v3.set_bool_value(false); @@ -1273,13 +1273,13 @@ TEST_F(ProtobufUtilityTest, ValueUtilEqual_BoolValues) { } TEST_F(ProtobufUtilityTest, ValueUtilEqual_StructValues) { - ProtobufWkt::Value string_val1, string_val2, bool_val; + Protobuf::Value string_val1, string_val2, bool_val; string_val1.set_string_value("s1"); string_val2.set_string_value("s2"); bool_val.set_bool_value(true); - ProtobufWkt::Value v1, v2, v3, v4; + Protobuf::Value v1, v2, v3, v4; v1.mutable_struct_value()->mutable_fields()->insert({"f1", string_val1}); v1.mutable_struct_value()->mutable_fields()->insert({"f2", bool_val}); @@ -1297,7 +1297,7 @@ TEST_F(ProtobufUtilityTest, ValueUtilEqual_StructValues) { } TEST_F(ProtobufUtilityTest, ValueUtilEqual_ListValues) { - ProtobufWkt::Value v1, v2, v3, v4; + Protobuf::Value v1, v2, v3, v4; v1.mutable_list_value()->add_values()->set_string_value("s"); v1.mutable_list_value()->add_values()->set_bool_value(true); @@ -1315,14 +1315,14 @@ TEST_F(ProtobufUtilityTest, ValueUtilEqual_ListValues) { } TEST_F(ProtobufUtilityTest, ValueUtilHash) { - ProtobufWkt::Value v; + Protobuf::Value v; v.set_string_value("s1"); EXPECT_NE(ValueUtil::hash(v), 0); } TEST_F(ProtobufUtilityTest, MessageUtilLoadYamlDouble) { - ProtobufWkt::DoubleValue v; + Protobuf::DoubleValue v; MessageUtil::loadFromYaml("value: 1.0", v, ProtobufMessage::getNullValidationVisitor()); EXPECT_DOUBLE_EQ(1.0, v.value()); } @@ -1386,7 +1386,7 @@ TEST(LoadFromYamlExceptionTest, ParserException) { } TEST_F(ProtobufUtilityTest, HashedValue) { - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("s"); v2.set_string_value("s"); v3.set_string_value("not_s"); @@ -1401,7 +1401,7 @@ TEST_F(ProtobufUtilityTest, HashedValue) { } TEST_F(ProtobufUtilityTest, HashedValueStdHash) { - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("s"); v2.set_string_value("s"); v3.set_string_value("not_s"); @@ -1420,22 +1420,22 @@ TEST_F(ProtobufUtilityTest, HashedValueStdHash) { TEST_F(ProtobufUtilityTest, AnyBytes) { { - ProtobufWkt::StringValue source; + Protobuf::StringValue source; source.set_value("abc"); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source); EXPECT_EQ(*MessageUtil::anyToBytes(source_any), "abc"); } { - ProtobufWkt::BytesValue source; + Protobuf::BytesValue source; source.set_value("\x01\x02\x03"); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source); EXPECT_EQ(*MessageUtil::anyToBytes(source_any), "\x01\x02\x03"); } { envoy::config::cluster::v3::Filter filter; - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(filter); EXPECT_EQ(*MessageUtil::anyToBytes(source_any), source_any.value()); } @@ -1443,19 +1443,19 @@ TEST_F(ProtobufUtilityTest, AnyBytes) { // MessageUtility::anyConvert() with the wrong type throws. TEST_F(ProtobufUtilityTest, AnyConvertWrongType) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source_duration); EXPECT_THROW_WITH_REGEX( - TestUtility::anyConvert(source_any), EnvoyException, + TestUtility::anyConvert(source_any), EnvoyException, R"(Unable to unpack as google.protobuf.Timestamp:.*[\n]*\[type.googleapis.com/google.protobuf.Duration\] .*)"); } // Validated exception thrown when anyConvertAndValidate observes a PGV failures. TEST_F(ProtobufUtilityTest, AnyConvertAndValidateFailedValidation) { envoy::config::cluster::v3::Filter filter; - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(filter); EXPECT_THROW(MessageUtil::anyConvertAndValidate( source_any, ProtobufMessage::getStrictValidationVisitor()), @@ -1463,11 +1463,11 @@ TEST_F(ProtobufUtilityTest, AnyConvertAndValidateFailedValidation) { } TEST_F(ProtobufUtilityTest, UnpackToWrongType) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source_duration); - ProtobufWkt::Timestamp dst; + Protobuf::Timestamp dst; EXPECT_THAT( MessageUtil::unpackTo(source_any, dst).message(), testing::ContainsRegex( @@ -1478,7 +1478,7 @@ TEST_F(ProtobufUtilityTest, UnpackToSameVersion) { { API_NO_BOOST(envoy::api::v2::Cluster) source; source.set_drain_connections_on_host_removal(true); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source); API_NO_BOOST(envoy::api::v2::Cluster) dst; ASSERT_TRUE(MessageUtil::unpackTo(source_any, dst).ok()); @@ -1487,7 +1487,7 @@ TEST_F(ProtobufUtilityTest, UnpackToSameVersion) { { API_NO_BOOST(envoy::config::cluster::v3::Cluster) source; source.set_ignore_health_on_host_removal(true); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source); API_NO_BOOST(envoy::config::cluster::v3::Cluster) dst; ASSERT_TRUE(MessageUtil::unpackTo(source_any, dst).ok()); @@ -1497,11 +1497,11 @@ TEST_F(ProtobufUtilityTest, UnpackToSameVersion) { // MessageUtility::unpackTo() with the right type. TEST_F(ProtobufUtilityTest, UnpackToNoThrowRightType) { - ProtobufWkt::Duration src_duration; + Protobuf::Duration src_duration; src_duration.set_seconds(42); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(src_duration); - ProtobufWkt::Duration dst_duration; + Protobuf::Duration dst_duration; EXPECT_OK(MessageUtil::unpackTo(source_any, dst_duration)); // Source and destination are expected to be equal. EXPECT_EQ(src_duration, dst_duration); @@ -1509,11 +1509,11 @@ TEST_F(ProtobufUtilityTest, UnpackToNoThrowRightType) { // MessageUtility::unpackTo() with the wrong type. TEST_F(ProtobufUtilityTest, UnpackToNoThrowWrongType) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(42); - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.PackFrom(source_duration); - ProtobufWkt::Timestamp dst; + Protobuf::Timestamp dst; auto status = MessageUtil::unpackTo(source_any, dst); EXPECT_TRUE(absl::IsInternal(status)); EXPECT_THAT( @@ -1569,7 +1569,7 @@ TEST_F(ProtobufUtilityTest, LoadFromJsonNoBoosting) { TEST_F(ProtobufUtilityTest, JsonConvertSuccess) { envoy::config::bootstrap::v3::Bootstrap source; source.set_flags_path("foo"); - ProtobufWkt::Struct tmp; + Protobuf::Struct tmp; envoy::config::bootstrap::v3::Bootstrap dest; TestUtility::jsonConvert(source, tmp); TestUtility::jsonConvert(tmp, dest); @@ -1577,18 +1577,18 @@ TEST_F(ProtobufUtilityTest, JsonConvertSuccess) { } TEST_F(ProtobufUtilityTest, JsonConvertUnknownFieldSuccess) { - const ProtobufWkt::Struct obj = MessageUtil::keyValueStruct("test_key", "test_value"); + const Protobuf::Struct obj = MessageUtil::keyValueStruct("test_key", "test_value"); envoy::config::bootstrap::v3::Bootstrap bootstrap; EXPECT_NO_THROW( MessageUtil::jsonConvert(obj, ProtobufMessage::getNullValidationVisitor(), bootstrap)); } TEST_F(ProtobufUtilityTest, JsonConvertFail) { - ProtobufWkt::Duration source_duration; + Protobuf::Duration source_duration; source_duration.set_seconds(-281474976710656); - ProtobufWkt::Struct dest_struct; + Protobuf::Struct dest_struct; std::string expected_duration_text = R"pb(seconds: -281474976710656)pb"; - ProtobufWkt::Duration expected_duration_proto; + Protobuf::Duration expected_duration_proto; Protobuf::TextFormat::ParseFromString(expected_duration_text, &expected_duration_proto); EXPECT_THROW(TestUtility::jsonConvert(source_duration, dest_struct), EnvoyException); } @@ -1598,7 +1598,7 @@ TEST_F(ProtobufUtilityTest, JsonConvertCamelSnake) { envoy::config::bootstrap::v3::Bootstrap bootstrap; // Make sure we use a field eligible for snake/camel case translation. bootstrap.mutable_cluster_manager()->set_local_cluster_name("foo"); - ProtobufWkt::Struct json; + Protobuf::Struct json; TestUtility::jsonConvert(bootstrap, json); // Verify we can round-trip. This didn't cause the #3665 regression, but useful as a sanity check. TestUtility::loadFromJson(MessageUtil::getJsonStringFromMessageOrError(json, false), bootstrap); @@ -1616,7 +1616,7 @@ TEST_F(ProtobufUtilityTest, JsonConvertValueSuccess) { { envoy::config::bootstrap::v3::Bootstrap source; source.set_flags_path("foo"); - ProtobufWkt::Value tmp; + Protobuf::Value tmp; envoy::config::bootstrap::v3::Bootstrap dest; EXPECT_TRUE(MessageUtil::jsonConvertValue(source, tmp)); TestUtility::jsonConvert(tmp, dest); @@ -1624,20 +1624,20 @@ TEST_F(ProtobufUtilityTest, JsonConvertValueSuccess) { } { - ProtobufWkt::StringValue source; + Protobuf::StringValue source; source.set_value("foo"); - ProtobufWkt::Value dest; + Protobuf::Value dest; EXPECT_TRUE(MessageUtil::jsonConvertValue(source, dest)); - ProtobufWkt::Value expected; + Protobuf::Value expected; expected.set_string_value("foo"); EXPECT_THAT(dest, ProtoEq(expected)); } { - ProtobufWkt::Duration source; + Protobuf::Duration source; source.set_seconds(-281474976710656); - ProtobufWkt::Value dest; + Protobuf::Value dest; EXPECT_FALSE(MessageUtil::jsonConvertValue(source, dest)); } } @@ -1687,7 +1687,7 @@ flags_path: foo)EOF"; } TEST_F(ProtobufUtilityTest, GetYamlStringFromProtoInvalidAny) { - ProtobufWkt::Any source_any; + Protobuf::Any source_any; source_any.set_type_url("type.googleapis.com/bad.type.url"); source_any.set_value("asdf"); EXPECT_THROW(MessageUtil::getYamlStringFromMessage(source_any, true), EnvoyException); @@ -1695,29 +1695,29 @@ TEST_F(ProtobufUtilityTest, GetYamlStringFromProtoInvalidAny) { TEST(DurationUtilTest, OutOfRange) { { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_seconds(-1); EXPECT_THROW(DurationUtil::durationToMilliseconds(duration), EnvoyException); } { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_nanos(-1); EXPECT_THROW(DurationUtil::durationToMilliseconds(duration), EnvoyException); } // Invalid number of nanoseconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_nanos(1000000000); EXPECT_THROW(DurationUtil::durationToMilliseconds(duration), EnvoyException); } { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_seconds(Protobuf::util::TimeUtil::kDurationMaxSeconds + 1); EXPECT_THROW(DurationUtil::durationToMilliseconds(duration), EnvoyException); } // Invalid number of seconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; constexpr int64_t kMaxInt64Nanoseconds = (std::numeric_limits::max() - 999999999) / (1000 * 1000 * 1000); duration.set_seconds(kMaxInt64Nanoseconds + 1); @@ -1725,7 +1725,7 @@ TEST(DurationUtilTest, OutOfRange) { } // Max valid seconds and nanoseconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; constexpr int64_t kMaxInt64Nanoseconds = (std::numeric_limits::max() - 999999999) / (1000 * 1000 * 1000); duration.set_seconds(kMaxInt64Nanoseconds); @@ -1734,7 +1734,7 @@ TEST(DurationUtilTest, OutOfRange) { } // Invalid combined seconds and nanoseconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; constexpr int64_t kMaxInt64Nanoseconds = std::numeric_limits::max() / (1000 * 1000 * 1000); duration.set_seconds(kMaxInt64Nanoseconds); @@ -1746,7 +1746,7 @@ TEST(DurationUtilTest, OutOfRange) { TEST(DurationUtilTest, NoThrow) { { // In range test - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_seconds(5); duration.set_nanos(10000000); const auto result = DurationUtil::durationToMillisecondsNoThrow(duration); @@ -1755,33 +1755,33 @@ TEST(DurationUtilTest, NoThrow) { } // Below are out-of-range tests { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_seconds(-1); const auto result = DurationUtil::durationToMillisecondsNoThrow(duration); EXPECT_FALSE(result.ok()); } { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_nanos(-1); const auto result = DurationUtil::durationToMillisecondsNoThrow(duration); EXPECT_FALSE(result.ok()); } // Invalid number of nanoseconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_nanos(1000000000); const auto result = DurationUtil::durationToMillisecondsNoThrow(duration); EXPECT_FALSE(result.ok()); } { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; duration.set_seconds(Protobuf::util::TimeUtil::kDurationMaxSeconds + 1); const auto result = DurationUtil::durationToMillisecondsNoThrow(duration); EXPECT_FALSE(result.ok()); } // Invalid number of seconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; constexpr int64_t kMaxInt64Nanoseconds = (std::numeric_limits::max() - 999999999) / (1000 * 1000 * 1000); duration.set_seconds(kMaxInt64Nanoseconds + 1); @@ -1790,7 +1790,7 @@ TEST(DurationUtilTest, NoThrow) { } // Max valid seconds and nanoseconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; constexpr int64_t kMaxInt64Nanoseconds = (std::numeric_limits::max() - 999999999) / (1000 * 1000 * 1000); duration.set_seconds(kMaxInt64Nanoseconds); @@ -1800,7 +1800,7 @@ TEST(DurationUtilTest, NoThrow) { } // Invalid combined seconds and nanoseconds. { - ProtobufWkt::Duration duration; + Protobuf::Duration duration; constexpr int64_t kMaxInt64Nanoseconds = std::numeric_limits::max() / (1000 * 1000 * 1000); duration.set_seconds(kMaxInt64Nanoseconds); @@ -2193,7 +2193,7 @@ TEST_P(TimestampUtilTest, SystemClockToTimestampTest) { auto time_original = epoch_time + std::chrono::milliseconds(GetParam()); // And convert that to Timestamp. - ProtobufWkt::Timestamp timestamp; + Protobuf::Timestamp timestamp; TimestampUtil::systemClockToTimestamp(time_original, timestamp); // Then convert that Timestamp back into a time_point, @@ -2241,9 +2241,8 @@ TEST(TypeUtilTest, TypeUrlHelperFunction) { class StructUtilTest : public ProtobufUtilityTest { protected: - ProtobufWkt::Struct updateSimpleStruct(const ProtobufWkt::Value& v0, - const ProtobufWkt::Value& v1) { - ProtobufWkt::Struct obj, with; + Protobuf::Struct updateSimpleStruct(const Protobuf::Value& v0, const Protobuf::Value& v1) { + Protobuf::Struct obj, with; (*obj.mutable_fields())["key"] = v0; (*with.mutable_fields())["key"] = v1; StructUtil::update(obj, with); @@ -2270,7 +2269,7 @@ TEST_F(StructUtilTest, StructUtilUpdateScalars) { { const auto obj = updateSimpleStruct(ValueUtil::nullValue(), ValueUtil::nullValue()); - EXPECT_EQ(obj.fields().at("key").kind_case(), ProtobufWkt::Value::KindCase::kNullValue); + EXPECT_EQ(obj.fields().at("key").kind_case(), Protobuf::Value::KindCase::kNullValue); } } @@ -2278,7 +2277,7 @@ TEST_F(StructUtilTest, StructUtilUpdateDifferentKind) { { const auto obj = updateSimpleStruct(ValueUtil::stringValue("v0"), ValueUtil::numberValue(1)); auto& val = obj.fields().at("key"); - EXPECT_EQ(val.kind_case(), ProtobufWkt::Value::KindCase::kNumberValue); + EXPECT_EQ(val.kind_case(), Protobuf::Value::KindCase::kNumberValue); EXPECT_EQ(val.number_value(), 1); } @@ -2287,13 +2286,13 @@ TEST_F(StructUtilTest, StructUtilUpdateDifferentKind) { updateSimpleStruct(ValueUtil::structValue(MessageUtil::keyValueStruct("subkey", "v0")), ValueUtil::stringValue("v1")); auto& val = obj.fields().at("key"); - EXPECT_EQ(val.kind_case(), ProtobufWkt::Value::KindCase::kStringValue); + EXPECT_EQ(val.kind_case(), Protobuf::Value::KindCase::kStringValue); EXPECT_EQ(val.string_value(), "v1"); } } TEST_F(StructUtilTest, StructUtilUpdateList) { - ProtobufWkt::Struct obj, with; + Protobuf::Struct obj, with; auto& list = *(*obj.mutable_fields())["key"].mutable_list_value(); list.add_values()->set_string_value("v0"); @@ -2311,7 +2310,7 @@ TEST_F(StructUtilTest, StructUtilUpdateList) { } TEST_F(StructUtilTest, StructUtilUpdateNewKey) { - ProtobufWkt::Struct obj, with; + Protobuf::Struct obj, with; (*obj.mutable_fields())["key0"].set_number_value(1); (*with.mutable_fields())["key1"].set_number_value(1); StructUtil::update(obj, with); @@ -2322,14 +2321,14 @@ TEST_F(StructUtilTest, StructUtilUpdateNewKey) { } TEST_F(StructUtilTest, StructUtilUpdateRecursiveStruct) { - ProtobufWkt::Struct obj, with; + Protobuf::Struct obj, with; *(*obj.mutable_fields())["tags"].mutable_struct_value() = MessageUtil::keyValueStruct("tag0", "1"); *(*with.mutable_fields())["tags"].mutable_struct_value() = MessageUtil::keyValueStruct("tag1", "1"); StructUtil::update(obj, with); - ASSERT_EQ(obj.fields().at("tags").kind_case(), ProtobufWkt::Value::KindCase::kStructValue); + ASSERT_EQ(obj.fields().at("tags").kind_case(), Protobuf::Value::KindCase::kStructValue); const auto& tags = obj.fields().at("tags").struct_value().fields(); EXPECT_TRUE(ValueUtil::equal(tags.at("tag0"), ValueUtil::stringValue("1"))); EXPECT_TRUE(ValueUtil::equal(tags.at("tag1"), ValueUtil::stringValue("1"))); diff --git a/test/common/protobuf/value_util_fuzz_test.cc b/test/common/protobuf/value_util_fuzz_test.cc index 1999427d5ca9a..b0076bc009aca 100644 --- a/test/common/protobuf/value_util_fuzz_test.cc +++ b/test/common/protobuf/value_util_fuzz_test.cc @@ -5,7 +5,7 @@ namespace Envoy { namespace Fuzz { -DEFINE_PROTO_FUZZER(const ProtobufWkt::Value& input) { ValueUtil::equal(input, input); } +DEFINE_PROTO_FUZZER(const Protobuf::Value& input) { ValueUtil::equal(input, input); } } // namespace Fuzz } // namespace Envoy diff --git a/test/common/quic/BUILD b/test/common/quic/BUILD index b1702ef161916..bc61e050ff7d6 100644 --- a/test/common/quic/BUILD +++ b/test/common/quic/BUILD @@ -332,6 +332,7 @@ envoy_cc_test( rbe_pool = "6gig", deps = envoy_select_enable_http3([ "//source/common/quic:envoy_quic_utils_lib", + "//source/common/runtime:runtime_lib", "//test/mocks/api:api_mocks", "//test/test_common:threadsafe_singleton_injector_lib", "@com_github_google_quiche//:quic_test_tools_test_utils_lib", diff --git a/test/common/quic/envoy_quic_client_session_test.cc b/test/common/quic/envoy_quic_client_session_test.cc index 654489fef5fa7..4d214f50d62ac 100644 --- a/test/common/quic/envoy_quic_client_session_test.cc +++ b/test/common/quic/envoy_quic_client_session_test.cc @@ -59,7 +59,7 @@ class TestEnvoyQuicClientConnection : public EnvoyQuicClientConnection { quic::ConnectionIdGeneratorInterface& generator) : EnvoyQuicClientConnection(server_connection_id, helper, alarm_factory, &writer, false, supported_versions, dispatcher, std::move(connection_socket), - generator, /*prefer_gro=*/true) { + generator) { SetEncrypter(quic::ENCRYPTION_FORWARD_SECURE, std::make_unique(quic::ENCRYPTION_FORWARD_SECURE)); InstallDecrypter(quic::ENCRYPTION_FORWARD_SECURE, @@ -128,7 +128,7 @@ class EnvoyQuicClientSessionTest : public testing::TestWithParam cache; diff --git a/test/common/quic/envoy_quic_client_stream_test.cc b/test/common/quic/envoy_quic_client_stream_test.cc index 6ae790c29d1d0..473dd22af5e05 100644 --- a/test/common/quic/envoy_quic_client_stream_test.cc +++ b/test/common/quic/envoy_quic_client_stream_test.cc @@ -46,8 +46,7 @@ class EnvoyQuicClientStreamTest : public testing::Test { quic_connection_(new MockEnvoyQuicClientConnection( quic::test::TestConnectionId(), connection_helper_, alarm_factory_, &writer_, /*owns_writer=*/false, {quic_version_}, *dispatcher_, - createConnectionSocket(peer_addr_, self_addr_, nullptr, /*prefer_gro=*/true), - connection_id_generator_)), + createConnectionSocket(peer_addr_, self_addr_, nullptr), connection_id_generator_)), quic_session_(quic_config_, {quic_version_}, std::unique_ptr(quic_connection_), *dispatcher_, quic_config_.GetInitialStreamFlowControlWindowToSend() * 2, @@ -281,17 +280,22 @@ TEST_F(EnvoyQuicClientStreamTest, PostRequestAndResponseWithAccounting) { EXPECT_EQ(absl::nullopt, quic_stream_->http1StreamEncoderOptions()); EXPECT_EQ(0, quic_stream_->bytesMeter()->wireBytesSent()); EXPECT_EQ(0, quic_stream_->bytesMeter()->headerBytesSent()); + EXPECT_EQ(0, quic_stream_->bytesMeter()->decompressedHeaderBytesSent()); const auto result = quic_stream_->encodeHeaders(request_headers_, false); EXPECT_TRUE(result.ok()); EXPECT_EQ(quic_stream_->stream_bytes_written(), quic_stream_->bytesMeter()->wireBytesSent()); EXPECT_EQ(quic_stream_->stream_bytes_written(), quic_stream_->bytesMeter()->headerBytesSent()); + EXPECT_LE(quic_stream_->stream_bytes_written(), + quic_stream_->bytesMeter()->decompressedHeaderBytesSent()); - uint64_t body_bytes = quic_stream_->stream_bytes_written(); + uint64_t header_bytes = quic_stream_->stream_bytes_written(); quic_stream_->encodeData(request_body_, false); - body_bytes = quic_stream_->stream_bytes_written() - body_bytes; + uint64_t body_bytes = quic_stream_->stream_bytes_written() - header_bytes; EXPECT_EQ(quic_stream_->stream_bytes_written(), quic_stream_->bytesMeter()->wireBytesSent()); EXPECT_EQ(quic_stream_->stream_bytes_written() - body_bytes, quic_stream_->bytesMeter()->headerBytesSent()); + EXPECT_LE(quic_stream_->stream_bytes_written() - body_bytes, + quic_stream_->bytesMeter()->decompressedHeaderBytesSent()); quic_stream_->encodeTrailers(request_trailers_); EXPECT_EQ(quic_stream_->stream_bytes_written(), quic_stream_->bytesMeter()->wireBytesSent()); EXPECT_EQ(quic_stream_->stream_bytes_written() - body_bytes, @@ -299,11 +303,14 @@ TEST_F(EnvoyQuicClientStreamTest, PostRequestAndResponseWithAccounting) { EXPECT_EQ(0, quic_stream_->bytesMeter()->wireBytesReceived()); EXPECT_EQ(0, quic_stream_->bytesMeter()->headerBytesReceived()); + EXPECT_EQ(0, quic_stream_->bytesMeter()->decompressedHeaderBytesReceived()); size_t offset = receiveResponseHeaders(false); // Received header bytes do not include the HTTP/3 frame overhead. EXPECT_EQ(quic_stream_->stream_bytes_read() - 2, quic_stream_->bytesMeter()->headerBytesReceived()); + EXPECT_LE(quic_stream_->stream_bytes_read() - 2, + quic_stream_->bytesMeter()->decompressedHeaderBytesReceived()); EXPECT_EQ(quic_stream_->stream_bytes_read(), quic_stream_->bytesMeter()->wireBytesReceived()); EXPECT_CALL(stream_decoder_, decodeTrailers_(_)) .WillOnce(Invoke([](const Http::ResponseTrailerMapPtr& headers) { @@ -738,10 +745,10 @@ TEST_F(EnvoyQuicClientStreamTest, EncodeTrailersOnClosedStream) { TEST_F(EnvoyQuicClientStreamTest, EncodeCapsule) { setUpCapsuleProtocol(false, true); Buffer::OwnedImpl buffer(capsule_fragment_); - EXPECT_CALL(*quic_connection_, SendMessage(_, _, _)) - .WillOnce([this](quic::QuicMessageId, absl::Span message, bool) { - EXPECT_EQ(message.data()->AsStringView(), datagram_fragment_); - return quic::MESSAGE_STATUS_SUCCESS; + EXPECT_CALL(*quic_connection_, SendDatagram(_, _, _)) + .WillOnce([this](quic::QuicDatagramId, absl::Span datagram, bool) { + EXPECT_EQ(datagram.data()->AsStringView(), datagram_fragment_); + return quic::DATAGRAM_STATUS_SUCCESS; }); quic_stream_->encodeData(buffer, /*end_stream=*/true); EXPECT_CALL(stream_callbacks_, onResetStream(_, _)); @@ -750,7 +757,7 @@ TEST_F(EnvoyQuicClientStreamTest, EncodeCapsule) { TEST_F(EnvoyQuicClientStreamTest, DecodeHttp3Datagram) { setUpCapsuleProtocol(true, false); EXPECT_CALL(stream_decoder_, decodeData(BufferStringEqual(capsule_fragment_), _)); - quic_session_.OnMessageReceived(datagram_fragment_); + quic_session_.OnDatagramReceived(datagram_fragment_); EXPECT_CALL(stream_callbacks_, onResetStream(_, _)); } diff --git a/test/common/quic/envoy_quic_h3_fuzz.proto b/test/common/quic/envoy_quic_h3_fuzz.proto index 8f768a867c131..0e6e84ef78e50 100644 --- a/test/common/quic/envoy_quic_h3_fuzz.proto +++ b/test/common/quic/envoy_quic_h3_fuzz.proto @@ -174,8 +174,8 @@ message QuicStopSendingFrame { uint32 error_code = 3; } -message QuicMessageFrame { - uint32 message_id = 1; +message QuicDatagramFrame { + uint32 datagram_id = 1; bytes data = 2; } @@ -213,7 +213,7 @@ message QuicFrame { QuicPathResponseFrame path_response = 18; QuicPathChallengeFrame path_challenge = 19; QuicStopSendingFrame stop_sending = 20; - QuicMessageFrame message_frame = 21; + QuicDatagramFrame datagram_frame = 21; QuicNewTokenFrame new_token = 22; QuickAckFrequencyFrame ack_frequency = 23; } diff --git a/test/common/quic/envoy_quic_h3_fuzz_helper.cc b/test/common/quic/envoy_quic_h3_fuzz_helper.cc index 6897306effaca..bb53108da6e8e 100644 --- a/test/common/quic/envoy_quic_h3_fuzz_helper.cc +++ b/test/common/quic/envoy_quic_h3_fuzz_helper.cc @@ -312,8 +312,8 @@ QuicPacketizer::QuicPacketPtr QuicPacketizer::serializePacket(const QuicFrame& f auto quic_stop = quic::QuicStopSendingFrame(f.control_frame_id(), f.stream_id(), error_code); return serialize(quic::QuicFrame(quic_stop)); } - case QuicFrame::kMessageFrame: - return serializeMessageFrame(frame.message_frame()); + case QuicFrame::kDatagramFrame: + return serializeDatagramFrame(frame.datagram_frame()); case QuicFrame::kNewToken: return serializeNewTokenFrame(frame.new_token()); case QuicFrame::kAckFrequency: { @@ -404,12 +404,12 @@ QuicPacketizer::serializeNewTokenFrame(const test::common::quic::QuicNewTokenFra } QuicPacketizer::QuicPacketPtr -QuicPacketizer::serializeMessageFrame(const test::common::quic::QuicMessageFrame& frame) { +QuicPacketizer::serializeDatagramFrame(const test::common::quic::QuicDatagramFrame& frame) { char buffer[1024]; auto message = frame.data(); size_t len = std::min(message.size(), sizeof(buffer)); memcpy(buffer, message.data(), len); - auto message_frame = quic::QuicMessageFrame(buffer, len); + auto message_frame = quic::QuicDatagramFrame(buffer, len); return serialize(quic::QuicFrame(&message_frame)); } diff --git a/test/common/quic/envoy_quic_h3_fuzz_helper.h b/test/common/quic/envoy_quic_h3_fuzz_helper.h index 3bbd2205748b9..7f3231551c3e0 100644 --- a/test/common/quic/envoy_quic_h3_fuzz_helper.h +++ b/test/common/quic/envoy_quic_h3_fuzz_helper.h @@ -48,7 +48,7 @@ class QuicPacketizer { QuicPacketPtr serialize(quic::QuicFrame frame); QuicPacketPtr serializeStreamFrame(const test::common::quic::QuicStreamFrame& frame); QuicPacketPtr serializeNewTokenFrame(const test::common::quic::QuicNewTokenFrame& frame); - QuicPacketPtr serializeMessageFrame(const test::common::quic::QuicMessageFrame& frame); + QuicPacketPtr serializeDatagramFrame(const test::common::quic::QuicDatagramFrame& frame); QuicPacketPtr serializeCryptoFrame(const test::common::quic::QuicCryptoFrame& frame); QuicPacketPtr serializeAckFrame(const test::common::quic::QuicAckFrame& frame); QuicPacketPtr diff --git a/test/common/quic/envoy_quic_h3_fuzz_test.cc b/test/common/quic/envoy_quic_h3_fuzz_test.cc index 61e395327ae72..1133ef7d32e7e 100644 --- a/test/common/quic/envoy_quic_h3_fuzz_test.cc +++ b/test/common/quic/envoy_quic_h3_fuzz_test.cc @@ -139,7 +139,7 @@ struct Harness { auto connection_socket = Quic::createConnectionSocket(peer_addr_, self_addr_, nullptr); auto connection = std::make_unique( quic::test::TestConnectionId(), srv_addr_, cli_addr_, *connection_helper_, *alarm_factory_, - &writer_, false, quic::ParsedQuicVersionVector{quic_version_}, std::move(connection_socket), + &writer_, quic::ParsedQuicVersionVector{quic_version_}, std::move(connection_socket), generator_, nullptr); auto decrypter = std::make_unique(quic::Perspective::IS_SERVER); diff --git a/test/common/quic/envoy_quic_proof_source_test.cc b/test/common/quic/envoy_quic_proof_source_test.cc index e3b0b9e831a19..a51dfa36722b8 100644 --- a/test/common/quic/envoy_quic_proof_source_test.cc +++ b/test/common/quic/envoy_quic_proof_source_test.cc @@ -198,7 +198,7 @@ class EnvoyQuicProofSourceTest : public ::testing::Test { getDefaultTlsCertificateSelectorConfigFactory(); ASSERT_TRUE(factory); ASSERT_EQ("envoy.tls.certificate_selectors.default", factory->name()); - const ProtobufWkt::Any any; + const Protobuf::Any any; absl::Status creation_status = absl::OkStatus(); auto tls_certificate_selector_factory_cb = factory->createTlsCertificateSelectorFactory( any, factory_context_, ProtobufMessage::getNullValidationVisitor(), creation_status, true); diff --git a/test/common/quic/envoy_quic_server_stream_test.cc b/test/common/quic/envoy_quic_server_stream_test.cc index d798aeb92f251..b1163035d0457 100644 --- a/test/common/quic/envoy_quic_server_stream_test.cc +++ b/test/common/quic/envoy_quic_server_stream_test.cc @@ -375,10 +375,13 @@ TEST_F(EnvoyQuicServerStreamTest, PostRequestAndResponseWithAccounting) { EXPECT_EQ(absl::nullopt, quic_stream_->http1StreamEncoderOptions()); EXPECT_EQ(0, quic_stream_->bytesMeter()->wireBytesReceived()); EXPECT_EQ(0, quic_stream_->bytesMeter()->headerBytesReceived()); + EXPECT_EQ(0, quic_stream_->bytesMeter()->decompressedHeaderBytesReceived()); size_t offset = receiveRequestHeaders(false); // Received header bytes do not include the HTTP/3 frame overhead. EXPECT_EQ(quic_stream_->stream_bytes_read() - 2, quic_stream_->bytesMeter()->headerBytesReceived()); + EXPECT_LE(quic_stream_->stream_bytes_read() - 2, + quic_stream_->bytesMeter()->decompressedHeaderBytesReceived()); EXPECT_EQ(quic_stream_->stream_bytes_read(), quic_stream_->bytesMeter()->wireBytesReceived()); size_t body_size = receiveRequestBody(offset, request_body_, true, request_body_.size() * 2); EXPECT_EQ(quic_stream_->stream_bytes_read(), quic_stream_->bytesMeter()->wireBytesReceived()); @@ -389,13 +392,18 @@ TEST_F(EnvoyQuicServerStreamTest, PostRequestAndResponseWithAccounting) { quic_stream_->bytesMeter()->wireBytesReceived()); EXPECT_EQ(0, quic_stream_->bytesMeter()->wireBytesSent()); EXPECT_EQ(0, quic_stream_->bytesMeter()->headerBytesSent()); + EXPECT_EQ(0, quic_stream_->bytesMeter()->decompressedHeaderBytesSent()); quic_stream_->encodeHeaders(response_headers_, /*end_stream=*/false); EXPECT_GE(27, quic_stream_->bytesMeter()->headerBytesSent()); EXPECT_GE(27, quic_stream_->bytesMeter()->wireBytesSent()); + EXPECT_GE(quic_stream_->bytesMeter()->decompressedHeaderBytesSent(), + quic_stream_->bytesMeter()->headerBytesSent()); quic_stream_->encodeTrailers(response_trailers_); EXPECT_GE(52, quic_stream_->bytesMeter()->headerBytesSent()); EXPECT_GE(52, quic_stream_->bytesMeter()->wireBytesSent()); + EXPECT_GE(quic_stream_->bytesMeter()->decompressedHeaderBytesSent(), + quic_stream_->bytesMeter()->headerBytesSent()); } TEST_F(EnvoyQuicServerStreamTest, DecodeHeadersBodyAndTrailers) { @@ -890,10 +898,10 @@ TEST_F(EnvoyQuicServerStreamTest, MetadataNotSupported) { TEST_F(EnvoyQuicServerStreamTest, EncodeCapsule) { setUpCapsuleProtocol(false, true); Buffer::OwnedImpl buffer(capsule_fragment_); - EXPECT_CALL(quic_connection_, SendMessage(_, _, _)) - .WillOnce([this](quic::QuicMessageId, absl::Span message, bool) { + EXPECT_CALL(quic_connection_, SendDatagram(_, _, _)) + .WillOnce([this](quic::QuicDatagramId, absl::Span message, bool) { EXPECT_EQ(message.data()->AsStringView(), datagram_fragment_); - return quic::MESSAGE_STATUS_SUCCESS; + return quic::DATAGRAM_STATUS_SUCCESS; }); quic_stream_->encodeData(buffer, /*end_stream=*/true); } @@ -901,7 +909,7 @@ TEST_F(EnvoyQuicServerStreamTest, EncodeCapsule) { TEST_F(EnvoyQuicServerStreamTest, DecodeHttp3Datagram) { setUpCapsuleProtocol(true, false); EXPECT_CALL(stream_decoder_, decodeData(BufferStringEqual(capsule_fragment_), _)); - quic_session_.OnMessageReceived(datagram_fragment_); + quic_session_.OnDatagramReceived(datagram_fragment_); } #endif diff --git a/test/common/quic/envoy_quic_utils_test.cc b/test/common/quic/envoy_quic_utils_test.cc index 299a2cbdcbbb6..9597fd3283c9a 100644 --- a/test/common/quic/envoy_quic_utils_test.cc +++ b/test/common/quic/envoy_quic_utils_test.cc @@ -1,4 +1,5 @@ #include "source/common/quic/envoy_quic_utils.h" +#include "source/common/runtime/runtime_features.h" #include "test/mocks/api/mocks.h" #include "test/test_common/threadsafe_singleton_injector.h" diff --git a/test/common/quic/http_datagram_handler_test.cc b/test/common/quic/http_datagram_handler_test.cc index 440bd9aa13b5d..2cc35df0fe493 100644 --- a/test/common/quic/http_datagram_handler_test.cc +++ b/test/common/quic/http_datagram_handler_test.cc @@ -40,7 +40,7 @@ class MockStream : public quic::QuicSpdyStream { : quic::QuicSpdyStream(kStreamId, spdy_session, quic::BIDIRECTIONAL) {} MOCK_METHOD(void, OnBodyAvailable, (), (override)); - MOCK_METHOD(quic::MessageStatus, SendHttp3Datagram, (absl::string_view data), (override)); + MOCK_METHOD(quic::DatagramStatus, SendHttp3Datagram, (absl::string_view data), (override)); MOCK_METHOD(void, WriteOrBufferBody, (absl::string_view data, bool fin), (override)); }; @@ -77,8 +77,8 @@ TEST_F(HttpDatagramHandlerTest, Http3DatagramToCapsule) { TEST_F(HttpDatagramHandlerTest, CapsuleToHttp3Datagram) { EXPECT_CALL(stream_, SendHttp3Datagram(testing::Eq(datagram_payload_))) - .WillOnce(testing::Return(quic::MessageStatus::MESSAGE_STATUS_SUCCESS)) - .WillOnce(testing::Return(quic::MessageStatus::MESSAGE_STATUS_BLOCKED)); + .WillOnce(testing::Return(quic::DatagramStatus::DATAGRAM_STATUS_SUCCESS)) + .WillOnce(testing::Return(quic::DatagramStatus::DATAGRAM_STATUS_BLOCKED)); EXPECT_TRUE( http_datagram_handler_.encodeCapsuleFragment(capsule_fragment_, /*end_stream=*/false)); EXPECT_TRUE( @@ -102,7 +102,7 @@ TEST_F(HttpDatagramHandlerTest, SendCapsulesWithUnknownType) { TEST_F(HttpDatagramHandlerTest, SendHttp3DatagramInternalError) { EXPECT_CALL(stream_, SendHttp3Datagram(_)) - .WillOnce(testing::Return(quic::MessageStatus::MESSAGE_STATUS_INTERNAL_ERROR)); + .WillOnce(testing::Return(quic::DatagramStatus::DATAGRAM_STATUS_INTERNAL_ERROR)); EXPECT_FALSE( http_datagram_handler_.encodeCapsuleFragment(capsule_fragment_, /*end_stream*/ false)); } @@ -111,7 +111,7 @@ TEST_F(HttpDatagramHandlerTest, SendHttp3DatagramTooEarly) { // If SendHttp3Datagram is called before receiving SETTINGS from a peer, HttpDatagramHandler // drops the datagram without resetting the stream. EXPECT_CALL(stream_, SendHttp3Datagram(_)) - .WillOnce(testing::Return(quic::MessageStatus::MESSAGE_STATUS_SETTINGS_NOT_RECEIVED)); + .WillOnce(testing::Return(quic::DatagramStatus::DATAGRAM_STATUS_SETTINGS_NOT_RECEIVED)); EXPECT_TRUE( http_datagram_handler_.encodeCapsuleFragment(capsule_fragment_, /*end_stream*/ false)); } diff --git a/test/common/quic/platform/quic_platform_test.cc b/test/common/quic/platform/quic_platform_test.cc index c7c71a6772c5e..15e37b581f68c 100644 --- a/test/common/quic/platform/quic_platform_test.cc +++ b/test/common/quic/platform/quic_platform_test.cc @@ -98,12 +98,12 @@ TEST_F(QuicPlatformTest, QuicClientStats) { QUIC_CLIENT_HISTOGRAM_ENUM("my.enum.histogram", TestEnum::ONE, TestEnum::COUNT, "doc"); QUIC_CLIENT_HISTOGRAM_BOOL("my.bool.histogram", false, "doc"); QUIC_CLIENT_HISTOGRAM_TIMES("my.timing.histogram", QuicTime::Delta::FromSeconds(5), - QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSecond(3600), + QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSeconds(3600), 100, "doc"); QUIC_CLIENT_HISTOGRAM_COUNTS("my.count.histogram", 123, 0, 1000, 100, "doc"); QuicClientSparseHistogram("my.sparse.histogram", 345); // Make sure compiler doesn't report unused-parameter error. - bool should_be_used; + bool should_be_used = false; QUIC_CLIENT_HISTOGRAM_BOOL("my.bool.histogram", should_be_used, "doc"); } @@ -121,7 +121,7 @@ TEST_F(QuicPlatformTest, QuicExportedStats) { QUIC_HISTOGRAM_ENUM("my.enum.histogram", TestEnum::ONE, TestEnum::COUNT, "doc"); QUIC_HISTOGRAM_BOOL("my.bool.histogram", false, "doc"); QUIC_HISTOGRAM_TIMES("my.timing.histogram", QuicTime::Delta::FromSeconds(5), - QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSecond(3600), 100, + QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSeconds(3600), 100, "doc"); QUIC_HISTOGRAM_COUNTS("my.count.histogram", 123, 0, 1000, 100, "doc"); } @@ -141,7 +141,7 @@ TEST_F(QuicPlatformTest, QuicServerStats) { QUIC_SERVER_HISTOGRAM_ENUM("my.enum.histogram", TestEnum::ONE, TestEnum::COUNT, "doc"); QUIC_SERVER_HISTOGRAM_BOOL("my.bool.histogram", false, "doc"); QUIC_SERVER_HISTOGRAM_TIMES("my.timing.histogram", QuicTime::Delta::FromSeconds(5), - QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSecond(3600), + QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSeconds(3600), 100, "doc"); QUIC_SERVER_HISTOGRAM_COUNTS("my.count.histogram", 123, 0, 1000, 100, "doc"); } diff --git a/test/common/quic/quic_filter_manager_connection_impl_test.cc b/test/common/quic/quic_filter_manager_connection_impl_test.cc index f233a1e54b4dd..423b1f206dc69 100644 --- a/test/common/quic/quic_filter_manager_connection_impl_test.cc +++ b/test/common/quic/quic_filter_manager_connection_impl_test.cc @@ -153,5 +153,10 @@ TEST_F(QuicFilterManagerConnectionImplTest, SetSocketOption) { EXPECT_FALSE(impl_.setSocketOption(sockopt_name, sockopt_val)); } +TEST_F(QuicFilterManagerConnectionImplTest, GetSocketPanics) { + // getSocket() should panic as it's not implemented for QuicFilterManagerConnectionImpl. + EXPECT_DEATH(impl_.getSocket(), "not implemented"); +} + } // namespace Quic } // namespace Envoy diff --git a/test/common/quic/test_utils.h b/test/common/quic/test_utils.h index a0144144ea3eb..e60d922788a11 100644 --- a/test/common/quic/test_utils.h +++ b/test/common/quic/test_utils.h @@ -48,12 +48,12 @@ class MockEnvoyQuicServerConnection : public EnvoyQuicServerConnection { quic::QuicPacketWriter& writer, quic::QuicSocketAddress self_address, quic::QuicSocketAddress peer_address, const quic::ParsedQuicVersionVector& supported_versions, Network::Socket& listen_socket, quic::ConnectionIdGeneratorInterface& generator) - : EnvoyQuicServerConnection( - quic::test::TestConnectionId(), self_address, peer_address, helper, alarm_factory, - &writer, /*owns_writer=*/false, supported_versions, - createServerConnectionSocket(listen_socket.ioHandle(), self_address, peer_address, - "example.com", "h3-29"), - generator, nullptr) {} + : EnvoyQuicServerConnection(quic::test::TestConnectionId(), self_address, peer_address, + helper, alarm_factory, &writer, supported_versions, + createServerConnectionSocket(listen_socket.ioHandle(), + self_address, peer_address, + "example.com", "h3-29"), + generator, nullptr) {} Network::Connection::ConnectionStats& connectionStats() const { return QuicNetworkConnection::connectionStats(); @@ -62,8 +62,8 @@ class MockEnvoyQuicServerConnection : public EnvoyQuicServerConnection { MOCK_METHOD(void, SendConnectionClosePacket, (quic::QuicErrorCode, quic::QuicIetfTransportErrorCodes, const std::string&)); MOCK_METHOD(bool, SendControlFrame, (const quic::QuicFrame& frame)); - MOCK_METHOD(quic::MessageStatus, SendMessage, - (quic::QuicMessageId, absl::Span, bool)); + MOCK_METHOD(quic::DatagramStatus, SendDatagram, + (quic::QuicDatagramId, absl::Span, bool)); MOCK_METHOD(void, dumpState, (std::ostream&, int), (const)); }; @@ -79,10 +79,10 @@ class MockEnvoyQuicClientConnection : public EnvoyQuicClientConnection { quic::ConnectionIdGeneratorInterface& generator) : EnvoyQuicClientConnection(server_connection_id, helper, alarm_factory, writer, owns_writer, supported_versions, dispatcher, std::move(connection_socket), - generator, /*prefer_gro=*/true) {} + generator) {} - MOCK_METHOD(quic::MessageStatus, SendMessage, - (quic::QuicMessageId, absl::Span, bool)); + MOCK_METHOD(quic::DatagramStatus, SendDatagram, + (quic::QuicDatagramId, absl::Span, bool)); }; class TestQuicCryptoStream : public quic::test::MockQuicCryptoStream { diff --git a/test/common/router/BUILD b/test/common/router/BUILD index 6e9ff2a8cfe27..01afcd9cefa37 100644 --- a/test/common/router/BUILD +++ b/test/common/router/BUILD @@ -490,6 +490,7 @@ envoy_cc_test( "//test/common/stream_info:test_int_accessor_lib", "//test/mocks/api:api_mocks", "//test/mocks/http:http_mocks", + "//test/mocks/server:server_factory_context_mocks", "//test/mocks/ssl:ssl_mocks", "//test/mocks/stream_info:stream_info_mocks", "//test/mocks/upstream:host_mocks", diff --git a/test/common/router/config_impl_integration_test.cc b/test/common/router/config_impl_integration_test.cc index 72b06ecc769e0..c8ce490360f89 100644 --- a/test/common/router/config_impl_integration_test.cc +++ b/test/common/router/config_impl_integration_test.cc @@ -39,13 +39,13 @@ class FakeClusterSpecifierPluginFactoryConfig : public ClusterSpecifierPluginFac ClusterSpecifierPluginSharedPtr createClusterSpecifierPlugin(const Protobuf::Message& config, Server::Configuration::ServerFactoryContext&) override { - const auto& typed_config = dynamic_cast(config); + const auto& typed_config = dynamic_cast(config); return std::make_shared( typed_config.fields().at("name").string_value()); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.router.cluster_specifier_plugin.fake"; } diff --git a/test/common/router/config_impl_test.cc b/test/common/router/config_impl_test.cc index dba82528815cc..70c9c2c027227 100644 --- a/test/common/router/config_impl_test.cc +++ b/test/common/router/config_impl_test.cc @@ -46,6 +46,7 @@ namespace { using ::testing::_; using ::testing::ContainerEq; +using ::testing::ContainsRegex; using ::testing::ElementsAre; using ::testing::Eq; using ::testing::IsEmpty; @@ -446,7 +447,8 @@ TEST_F(RouteMatcherTest, TestConnectRoutes) { genHeaders("bat3.com", "/api/locations?works=true", "CONNECT"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rewrote?works=true", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rewrote?works=true", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("bat3.com", headers.get_(Http::Headers::get().Host)); } @@ -467,7 +469,8 @@ TEST_F(RouteMatcherTest, TestConnectRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("bat3.com", "/api/locations?works=true", "CONNECT"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("bat3.com:10", headers.get_(Http::Headers::get().Host)); } // No port addition for CONNECT with port @@ -475,7 +478,8 @@ TEST_F(RouteMatcherTest, TestConnectRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("bat3.com:20", "/api/locations?works=true", "CONNECT"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("bat3.com:20", headers.get_(Http::Headers::get().Host)); } @@ -883,7 +887,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("www2", route_entry->clusterName()); EXPECT_EQ("www2", virtualHostName(route.get())); EXPECT_EQ("/api/new_endpoint/foo", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/api/new_endpoint/foo", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/new_endpoint/foo", headers.get_(Http::Headers::get().EnvoyOriginalPath)); } @@ -896,7 +901,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("www2", route_entry->clusterName()); EXPECT_EQ("www2", virtualHostName(route.get())); EXPECT_EQ("/api/new_endpoint/foo", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, false); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, false); EXPECT_EQ("/api/new_endpoint/foo", headers.get_(Http::Headers::get().Path)); EXPECT_FALSE(headers.has(Http::Headers::get().EnvoyOriginalPath)); } @@ -907,7 +913,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { genHeaders("api.lyft.com", "/api/locations?works=true", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rewrote?works=true", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rewrote?works=true", headers.get_(Http::Headers::get().Path)); } @@ -915,7 +922,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/foo", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/bar", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/bar", headers.get_(Http::Headers::get().Path)); } @@ -928,7 +936,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("www2", route_entry->clusterName()); EXPECT_EQ("www2", virtualHostName(route.get())); EXPECT_EQ("/forreg1_rewritten_endpoint/foo", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/forreg1_rewritten_endpoint/foo", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/newforreg1_endpoint/foo", headers.get_(Http::Headers::get().EnvoyOriginalPath)); } @@ -943,7 +952,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("www2", route_entry->clusterName()); EXPECT_EQ("www2", virtualHostName(route.get())); EXPECT_EQ("/nXwforrXg2_Xndpoint/tXX?test=me", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/nXwforrXg2_Xndpoint/tXX?test=me", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/newforreg2_endpoint/tee?test=me", headers.get_(Http::Headers::get().EnvoyOriginalPath)); @@ -958,7 +968,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("www2", route_entry->clusterName()); EXPECT_EQ("www2", virtualHostName(route.get())); EXPECT_EQ("/VxVct/pVth/fVr/rVgVx1", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/VxVct/pVth/fVr/rVgVx1", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/exact/path/for/regex1", headers.get_(Http::Headers::get().EnvoyOriginalPath)); } @@ -974,7 +985,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("www2", virtualHostName(route.get())); EXPECT_EQ("/VxVct/pVth/fVr/rVgVx1?test=aeiou", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/VxVct/pVth/fVr/rVgVx1?test=aeiou", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/exact/path/for/regex1?test=aeiou", headers.get_(Http::Headers::get().EnvoyOriginalPath)); @@ -987,7 +999,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ(absl::optional(), route_entry->currentUrlPathAfterRewrite(headers)); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("new_host", headers.get_(Http::Headers::get().Host)); EXPECT_EQ("api.lyft.com", headers.getEnvoyOriginalHostValue()); // Config setting append_x_forwarded_host is false (by default). Expect empty x-forwarded-host @@ -995,8 +1008,9 @@ TEST_F(RouteMatcherTest, TestRoutes) { EXPECT_EQ("", headers.get_(Http::Headers::get().ForwardedHost)); Http::TestRequestHeaderMapImpl headers2 = genHeaders("api.lyft.com", "/host/rewrite/me", "GET"); + const Formatter::HttpFormatterContext formatter_context2(&headers2); // Host rewrite testing with x-envoy-* headers suppressed. - route_entry->finalizeRequestHeaders(headers2, stream_info, false); + route_entry->finalizeRequestHeaders(headers2, formatter_context2, stream_info, false); EXPECT_EQ("new_host", headers2.get_(Http::Headers::get().Host)); EXPECT_EQ("", headers2.getEnvoyOriginalHostValue()); } @@ -1007,7 +1021,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { genHeaders("api.lyft.com", "/rewrite-host-with-header-value", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("rewrote", headers.get_(Http::Headers::get().Host)); EXPECT_EQ("api.lyft.com", headers.getEnvoyOriginalHostValue()); EXPECT_EQ("api.lyft.com", headers.get_(Http::Headers::get().ForwardedHost)); @@ -1019,7 +1034,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { genHeaders("api.lyft.com", "/do-not-rewrite-host-with-header-value", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("api.lyft.com", headers.get_(Http::Headers::get().Host)); EXPECT_EQ("", headers.getEnvoyOriginalHostValue()); EXPECT_EQ("", headers.get_(Http::Headers::get().ForwardedHost)); @@ -1031,7 +1047,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { genHeaders("api.lyft.com", "/rewrite-host-with-path-regex/envoyproxy.io", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("envoyproxy.io", headers.get_(Http::Headers::get().Host)); EXPECT_EQ("api.lyft.com", headers.getEnvoyOriginalHostValue()); EXPECT_EQ("api.lyft.com", headers.get_(Http::Headers::get().ForwardedHost)); @@ -1043,7 +1060,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { "api.lyft.com", "/rewrite-host-with-path-regex/envoyproxy.io?query=query", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("envoyproxy.io", headers.get_(Http::Headers::get().Host)); EXPECT_EQ("api.lyft.com", headers.getEnvoyOriginalHostValue()); EXPECT_EQ("api.lyft.com", headers.get_(Http::Headers::get().ForwardedHost)); @@ -1055,7 +1073,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { genHeaders("api.lyft.com", "/API/locations?works=true", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rewrote?works=true", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rewrote?works=true", headers.get_(Http::Headers::get().Path)); } @@ -1063,7 +1082,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/fooD", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/cAndy", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/cAndy", headers.get_(Http::Headers::get().Path)); } @@ -1072,7 +1092,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/FOO", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/FOO", headers.get_(Http::Headers::get().Path)); } @@ -1080,7 +1101,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/ApPles", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/ApPles", headers.get_(Http::Headers::get().Path)); } @@ -1089,7 +1111,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/oLDhost/rewrite/me", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("api.lyft.com", headers.get_(Http::Headers::get().Host)); } @@ -1098,7 +1121,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/Tart", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_FALSE(route_entry->currentUrlPathAfterRewrite(headers).has_value()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/Tart", headers.get_(Http::Headers::get().Path)); } @@ -1107,7 +1131,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/newhost/rewrite/me", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("new_host", headers.get_(Http::Headers::get().Host)); } @@ -1116,7 +1141,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("bat.com", "/647", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rewrote", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rewrote", headers.get_(Http::Headers::get().Path)); } @@ -1125,14 +1151,16 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("bat.com", "/970?foo=true", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rewrote?foo=true", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rewrote?foo=true", headers.get_(Http::Headers::get().Path)); } { Http::TestRequestHeaderMapImpl headers = genHeaders("bat.com", "/foo/bar/238?bar=true", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rewrote?bar=true", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rewrote?bar=true", headers.get_(Http::Headers::get().Path)); } @@ -1141,7 +1169,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { Http::TestRequestHeaderMapImpl headers = genHeaders("bat.com", "/xx/yy/6472", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/four/6472/endpoint/xx/yy", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/four/6472/endpoint/xx/yy", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/xx/yy/6472", headers.get_(Http::Headers::get().EnvoyOriginalPath)); } @@ -1152,7 +1181,8 @@ TEST_F(RouteMatcherTest, TestRoutes) { const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/four/6472/endpoint/xx/yy?test=foo", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/four/6472/endpoint/xx/yy?test=foo", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("/xx/yy/6472?test=foo", headers.get_(Http::Headers::get().EnvoyOriginalPath)); } @@ -1365,14 +1395,16 @@ TEST_F(RouteMatcherTest, TestMatchTree) { { Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("match_tree", headers.get_("x-route-header")); } { Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/bar", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("match_tree_2", headers.get_("x-route-header")); } Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/baz", "GET"); @@ -1415,11 +1447,12 @@ TEST_F(RouteMatcherTest, TestMatchInvalidInput) { {"www2", "root_www2", "www2_staging", "instant-server"}, {}); TestConfigImpl give_me_a_name(parseRouteConfigurationFromYaml(yaml), factory_context_, true, creation_status_); - EXPECT_EQ( + EXPECT_THAT( creation_status_.message(), - "requirement violation while creating route match tree: INVALID_ARGUMENT: Route table can " - "only match on request headers, saw " - "type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput"); + ContainsRegex("requirement violation while creating route match tree: INVALID_ARGUMENT: " + "Route table can " + "only match on request headers, saw " + "type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput")); } // Validates that we fail creating a route config if an invalid data input is used. @@ -1582,7 +1615,8 @@ TEST_F(RouteMatcherTest, TestRouteList) { { Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/foo/1", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("match_tree_1_1", headers.get_("x-route-header")); } @@ -1590,7 +1624,8 @@ TEST_F(RouteMatcherTest, TestRouteList) { Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/foo/2/bar", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("match_tree_1_2", headers.get_("x-route-header")); } @@ -1599,7 +1634,8 @@ TEST_F(RouteMatcherTest, TestRouteList) { genHeaders("lyft.com", "/new_endpoint/foo/match_header", "GET"); headers.setCopy(Http::LowerCaseString("x-match-header"), "matched"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("match_tree_1_3", headers.get_("x-route-header")); } @@ -1611,7 +1647,8 @@ TEST_F(RouteMatcherTest, TestRouteList) { { Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/bar", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("match_tree_2", headers.get_("x-route-header")); } Http::TestRequestHeaderMapImpl headers = genHeaders("lyft.com", "/new_endpoint/baz", "GET"); @@ -1766,7 +1803,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveRequestHeaders) { Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("route-override", headers.get_("x-global-header1")); EXPECT_EQ("route-override", headers.get_("x-vhost-header1")); EXPECT_EQ("route-new_endpoint", headers.get_("x-route-header")); @@ -1789,7 +1827,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveRequestHeaders) { { Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("vhost-override", headers.get_("x-global-header1")); EXPECT_EQ("vhost1-www2", headers.get_("x-vhost-header1")); EXPECT_EQ("route-allpath", headers.get_("x-route-header")); @@ -1810,7 +1849,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveRequestHeaders) { { Http::TestRequestHeaderMapImpl headers = genHeaders("www-staging.lyft.net", "/foo", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("global1", headers.get_("x-global-header1")); EXPECT_EQ("vhost1-www2_staging", headers.get_("x-vhost-header1")); EXPECT_EQ("route-allprefix", headers.get_("x-route-header")); @@ -1830,7 +1870,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveRequestHeaders) { { Http::TestRequestHeaderMapImpl headers = genHeaders("api.lyft.com", "/", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("global1", headers.get_("x-global-header1")); auto transforms = route_entry->requestHeaderTransforms(stream_info); EXPECT_THAT(transforms.headers_to_append_or_add, @@ -1858,8 +1899,9 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddWithAppendFalse) { { Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/endpoint", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); - // Added headers. + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, + true); // Added headers. EXPECT_EQ("global", headers.get_("x-global-header")); EXPECT_EQ("vhost-www2", headers.get_("x-vhost-header")); EXPECT_EQ("route-endpoint", headers.get_("x-route-header")); @@ -1886,8 +1928,9 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddWithAppendFalse) { { Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); - // Added headers. + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, + true); // Added headers. EXPECT_EQ("global", headers.get_("x-global-header")); EXPECT_EQ("vhost-www2", headers.get_("x-vhost-header")); EXPECT_FALSE(headers.has("x-route-header")); @@ -1910,8 +1953,9 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddWithAppendFalse) { { Http::TestRequestHeaderMapImpl headers = genHeaders("www.example.com", "/", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); - // Added headers. + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, + true); // Added headers. EXPECT_EQ("global", headers.get_("x-global-header")); EXPECT_FALSE(headers.has("x-vhost-header")); EXPECT_FALSE(headers.has("x-route-header")); @@ -1940,8 +1984,9 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddWithAppendFalseMostSpecificWins) { Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/endpoint", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); - // Added headers. + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, + true); // Added headers. EXPECT_EQ("route-endpoint", headers.get_("x-global-header")); EXPECT_EQ("route-endpoint", headers.get_("x-vhost-header")); EXPECT_EQ("route-endpoint", headers.get_("x-route-header")); @@ -1967,8 +2012,9 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddWithAppendFalseMostSpecificWins) { Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); - // Added headers. + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, + true); // Added headers. EXPECT_EQ("vhost-www2", headers.get_("x-global-header")); EXPECT_EQ("vhost-www2", headers.get_("x-vhost-header")); EXPECT_FALSE(headers.has("x-route-header")); @@ -2004,7 +2050,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeaders) { genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("route-override", headers.get_("x-global-header1")); EXPECT_EQ("route-override", headers.get_("x-vhost-header1")); EXPECT_EQ("route-override", headers.get_("x-route-header")); @@ -2027,7 +2074,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeaders) { Http::TestRequestHeaderMapImpl req_headers = genHeaders("www.lyft.com", "/", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("vhost-override", headers.get_("x-global-header1")); EXPECT_EQ("vhost1-www2", headers.get_("x-vhost-header1")); EXPECT_EQ("route-allpath", headers.get_("x-route-header")); @@ -2050,7 +2098,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeaders) { genHeaders("www-staging.lyft.net", "/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("global1", headers.get_("x-global-header1")); EXPECT_EQ("vhost1-www2_staging", headers.get_("x-vhost-header1")); EXPECT_EQ("route-allprefix", headers.get_("x-route-header")); @@ -2069,7 +2118,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeaders) { Http::TestRequestHeaderMapImpl req_headers = genHeaders("api.lyft.com", "/", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("global1", headers.get_("x-global-header1")); auto transforms = route_entry->responseHeaderTransforms(stream_info); EXPECT_THAT(transforms.headers_to_append_or_add, @@ -2096,7 +2146,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeadersOverwriteIfExistOrAdd) { genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("global1", headers.get_("x-global-header1")); EXPECT_EQ("vhost1-www2", headers.get_("x-vhost-header1")); EXPECT_EQ("route-override", headers.get_("x-route-header")); @@ -2126,7 +2177,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeadersAddIfAbsent) { genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers{{":status", "200"}, {"x-route-header", "exist-value"}}; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("route-override", headers.get_("x-global-header1")); EXPECT_EQ("route-override", headers.get_("x-vhost-header1")); // If related header is exist in the headers then do nothing. @@ -2158,7 +2210,8 @@ TEST_F(RouteMatcherTest, TestAddRemoveResponseHeadersAppendMostSpecificWins) { genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("route-override", headers.get_("x-global-header1")); EXPECT_EQ("route-override", headers.get_("x-vhost-header1")); EXPECT_EQ("route-override", headers.get_("x-route-header")); @@ -2192,7 +2245,7 @@ class HeaderTransformsDoFormattingTest : public RouteMatcherTest { {0}: - header: key: x-has-variable - value: "%PER_REQUEST_STATE(testing)%" + value: "%FILTER_STATE(testing:PLAIN)%" append_action: OVERWRITE_IF_EXISTS_OR_ADD )EOF"; const std::string yaml = @@ -2216,8 +2269,8 @@ class HeaderTransformsDoFormattingTest : public RouteMatcherTest { genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); - + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); auto transforms = run_request_header_test ? route_entry->requestHeaderTransforms(stream_info, /*do_formatting=*/true) @@ -2228,9 +2281,9 @@ class HeaderTransformsDoFormattingTest : public RouteMatcherTest { transforms = run_request_header_test ? route_entry->requestHeaderTransforms(stream_info, /*do_formatting=*/false) : route_entry->responseHeaderTransforms(stream_info, /*do_formatting=*/false); - EXPECT_THAT( - transforms.headers_to_overwrite_or_add, - ElementsAre(Pair(Http::LowerCaseString("x-has-variable"), "%PER_REQUEST_STATE(testing)%"))); + EXPECT_THAT(transforms.headers_to_overwrite_or_add, + ElementsAre(Pair(Http::LowerCaseString("x-has-variable"), + "%FILTER_STATE(testing:PLAIN)%"))); } }; @@ -2272,7 +2325,8 @@ most_specific_header_mutations_wins: true Http::TestRequestHeaderMapImpl req_headers = genHeaders("www.lyft.com", "/cacheable", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_FALSE(headers.has("cache-control")); } @@ -2280,7 +2334,8 @@ most_specific_header_mutations_wins: true Http::TestRequestHeaderMapImpl req_headers = genHeaders("www.lyft.com", "/foo", "GET"); const RouteEntry* route_entry = config.route(req_headers, 0)->routeEntry(); Http::TestResponseHeaderMapImpl headers; - route_entry->finalizeResponseHeaders(headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&req_headers, &headers); + route_entry->finalizeResponseHeaders(headers, formatter_context, stream_info); EXPECT_EQ("private", headers.get_("cache-control")); } } @@ -3581,7 +3636,8 @@ TEST_F(RouteMatcherTest, ClusterHeader) { // Make sure things forward and don't crash. // TODO(mattklein123): Make this a real test of behavior. EXPECT_EQ(std::chrono::milliseconds(0), route->routeEntry()->timeout()); - route->routeEntry()->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route->routeEntry()->finalizeRequestHeaders(headers, formatter_context, stream_info, true); route->routeEntry()->priority(); route->routeEntry()->rateLimitPolicy(); route->routeEntry()->retryPolicy(); @@ -3816,7 +3872,7 @@ TEST_F(RouteMatcherTest, ClusterSpecifierPlugin) { mock_cluster_specifier_plugin_3]( const Protobuf::Message& config, Server::Configuration::CommonFactoryContext&) -> ClusterSpecifierPluginSharedPtr { - const auto& typed_config = dynamic_cast(config); + const auto& typed_config = dynamic_cast(config); if (auto iter = typed_config.fields().find("a"); iter == typed_config.fields().end()) { return nullptr; } else if (iter->second.string_value() == "test1") { @@ -3894,7 +3950,7 @@ TEST_F(RouteMatcherTest, UnknownClusterSpecifierPluginName) { mock_cluster_specifier_plugin_3]( const Protobuf::Message& config, Server::Configuration::CommonFactoryContext&) -> ClusterSpecifierPluginSharedPtr { - const auto& typed_config = dynamic_cast(config); + const auto& typed_config = dynamic_cast(config); if (auto iter = typed_config.fields().find("a"); iter == typed_config.fields().end()) { return nullptr; } else if (iter->second.string_value() == "test1") { @@ -4708,56 +4764,56 @@ TEST_F(RouteMatcherTest, Retry) { config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(std::chrono::milliseconds(0), config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryIdleTimeout()); + ->perTryIdleTimeout()); EXPECT_EQ(1U, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_CONNECT_FAILURE, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); EXPECT_EQ(std::chrono::milliseconds(0), config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(1, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(0U, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); EXPECT_EQ(std::chrono::milliseconds(1000), config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(std::chrono::milliseconds(5000), config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryIdleTimeout()); + ->perTryIdleTimeout()); EXPECT_EQ(3U, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_CONNECT_FAILURE | RetryPolicy::RETRY_ON_5XX | RetryPolicy::RETRY_ON_GATEWAY_ERROR | RetryPolicy::RETRY_ON_RESET, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); } class TestRetryOptionsPredicateFactory : public Upstream::RetryOptionsPredicateFactory { @@ -4770,7 +4826,7 @@ class TestRetryOptionsPredicateFactory : public Upstream::RetryOptionsPredicateF ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom empty config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "test_retry_options_predicate_factory"; } @@ -4780,7 +4836,7 @@ TEST_F(RouteMatcherTest, RetryVirtualHostLevel) { const std::string yaml = R"EOF( virtual_hosts: - domains: [www.lyft.com] - per_request_buffer_limit_bytes: 8 + request_body_buffer_limit: 8 name: www retry_policy: num_retries: 3 @@ -4792,7 +4848,7 @@ TEST_F(RouteMatcherTest, RetryVirtualHostLevel) { "@type": type.googleapis.com/google.protobuf.Struct routes: - match: {prefix: /foo} - per_request_buffer_limit_bytes: 7 + request_body_buffer_limit: 7 route: cluster: www retry_policy: @@ -4819,23 +4875,26 @@ TEST_F(RouteMatcherTest, RetryVirtualHostLevel) { config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(1U, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_CONNECT_FAILURE, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); EXPECT_EQ(7U, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() - ->retryShadowBufferLimit()); + ->requestBodyBufferLimit()); + EXPECT_EQ(7U, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) + ->routeEntry() + ->requestBodyBufferLimit()); EXPECT_EQ(1U, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOptionsPredicates() + ->retryOptionsPredicates() .size()); // Virtual Host level retry policy kicks in. @@ -4843,39 +4902,48 @@ TEST_F(RouteMatcherTest, RetryVirtualHostLevel) { config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(3U, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_CONNECT_FAILURE | RetryPolicy::RETRY_ON_5XX | RetryPolicy::RETRY_ON_GATEWAY_ERROR | RetryPolicy::RETRY_ON_RESET, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); + EXPECT_EQ(8U, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) + ->routeEntry() + ->requestBodyBufferLimit()); + EXPECT_EQ(8U, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) + ->routeEntry() + ->requestBodyBufferLimit()); EXPECT_EQ(std::chrono::milliseconds(1000), config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(3U, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_CONNECT_FAILURE | RetryPolicy::RETRY_ON_5XX | RetryPolicy::RETRY_ON_GATEWAY_ERROR | RetryPolicy::RETRY_ON_RESET, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); + EXPECT_EQ(8U, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) + ->routeEntry() + ->requestBodyBufferLimit()); EXPECT_EQ(8U, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() - ->retryShadowBufferLimit()); + ->requestBodyBufferLimit()); EXPECT_EQ(1U, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOptionsPredicates() + ->retryOptionsPredicates() .size()); } @@ -4914,46 +4982,46 @@ TEST_F(RouteMatcherTest, GrpcRetry) { config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(1U, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_CONNECT_FAILURE, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); EXPECT_EQ(std::chrono::milliseconds(0), config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(1, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(0U, config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); EXPECT_EQ(std::chrono::milliseconds(1000), config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .perTryTimeout()); + ->perTryTimeout()); EXPECT_EQ(3U, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .numRetries()); + ->numRetries()); EXPECT_EQ(RetryPolicy::RETRY_ON_5XX | RetryPolicy::RETRY_ON_GRPC_DEADLINE_EXCEEDED | RetryPolicy::RETRY_ON_GRPC_RESOURCE_EXHAUSTED, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .retryOn()); + ->retryOn()); } // Test route-specific retry back-off intervals. @@ -5003,47 +5071,47 @@ TEST_F(RouteMatcherTest, RetryBackOffIntervals) { config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .baseInterval()); + ->baseInterval()); EXPECT_EQ(absl::nullopt, config.route(genHeaders("www.lyft.com", "/foo", "GET"), 0) ->routeEntry() ->retryPolicy() - .maxInterval()); + ->maxInterval()); EXPECT_EQ(absl::optional(100), config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .baseInterval()); + ->baseInterval()); EXPECT_EQ(absl::optional(500), config.route(genHeaders("www.lyft.com", "/bar", "GET"), 0) ->routeEntry() ->retryPolicy() - .maxInterval()); + ->maxInterval()); // Sub-millisecond interval converted to 1 ms. EXPECT_EQ(absl::optional(1), config.route(genHeaders("www.lyft.com", "/baz", "GET"), 0) ->routeEntry() ->retryPolicy() - .baseInterval()); + ->baseInterval()); EXPECT_EQ(absl::optional(1), config.route(genHeaders("www.lyft.com", "/baz", "GET"), 0) ->routeEntry() ->retryPolicy() - .maxInterval()); + ->maxInterval()); EXPECT_EQ(absl::nullopt, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .baseInterval()); + ->baseInterval()); EXPECT_EQ(absl::nullopt, config.route(genHeaders("www.lyft.com", "/", "GET"), 0) ->routeEntry() ->retryPolicy() - .maxInterval()); + ->maxInterval()); } // Test invalid route-specific retry back-off configs. @@ -5115,29 +5183,29 @@ TEST_F(RouteMatcherTest, RateLimitedRetryBackOff) { EXPECT_EQ(true, config.route(genHeaders("www.lyft.com", "/no-backoff", "GET"), 0) ->routeEntry() ->retryPolicy() - .resetHeaders() + ->resetHeaders() .empty()); EXPECT_EQ(std::chrono::milliseconds(300000), config.route(genHeaders("www.lyft.com", "/no-backoff", "GET"), 0) ->routeEntry() ->retryPolicy() - .resetMaxInterval()); + ->resetMaxInterval()); // has sub millisecond interval EXPECT_EQ(1, config.route(genHeaders("www.lyft.com", "/sub-ms-interval", "GET"), 0) ->routeEntry() ->retryPolicy() - .resetHeaders() + ->resetHeaders() .size()); EXPECT_EQ(std::chrono::milliseconds(1), config.route(genHeaders("www.lyft.com", "/sub-ms-interval", "GET"), 0) ->routeEntry() ->retryPolicy() - .resetMaxInterval()); + ->resetMaxInterval()); // a typical configuration Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/typical-backoff", "GET"); - const auto& retry_policy = config.route(headers, 0)->routeEntry()->retryPolicy(); + const auto& retry_policy = *config.route(headers, 0)->routeEntry()->retryPolicy(); EXPECT_EQ(2, retry_policy.resetHeaders().size()); Http::TestResponseHeaderMapImpl expected_0{{"Retry-After", "2"}}; @@ -6244,7 +6312,7 @@ class BazFactory : public HttpRouteTypedMetadataFactory { std::string name() const override { return "baz"; } // Returns nullptr (conversion failure) if d is empty. std::unique_ptr - parse(const ProtobufWkt::Struct& d) const override { + parse(const Protobuf::Struct& d) const override { if (d.fields().find("name") != d.fields().end()) { return std::make_unique(d.fields().at("name").string_value()); } @@ -6252,7 +6320,7 @@ class BazFactory : public HttpRouteTypedMetadataFactory { } std::unique_ptr - parse(const ProtobufWkt::Any&) const override { + parse(const Protobuf::Any&) const override { return nullptr; } }; @@ -6363,8 +6431,8 @@ TEST_F(RouteMatcherTest, WeightedClusters) { Http::TestRequestHeaderMapImpl request_headers; Http::TestResponseHeaderMapImpl response_headers; StreamInfo::MockStreamInfo stream_info; - EXPECT_CALL(stream_info, getRequestHeaders).WillRepeatedly(Return(&request_headers)); - route_entry->finalizeResponseHeaders(response_headers, stream_info); + const Formatter::HttpFormatterContext formatter_context(&request_headers, &response_headers); + route_entry->finalizeResponseHeaders(response_headers, formatter_context, stream_info); EXPECT_EQ(response_headers, Http::TestResponseHeaderMapImpl{}); } @@ -6791,11 +6859,13 @@ TEST_F(RouteMatcherTest, TestWeightedClusterHeaderManipulation) { const RouteEntry* route_entry = cached_route->routeEntry(); EXPECT_EQ("cluster1", route_entry->clusterName()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers, &resp_headers); + + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("cluster1", headers.get_("x-req-cluster")); EXPECT_EQ("new_host1", headers.getHostValue()); - route_entry->finalizeResponseHeaders(resp_headers, stream_info); + route_entry->finalizeResponseHeaders(resp_headers, formatter_context, stream_info); EXPECT_EQ("cluster1", resp_headers.get_("x-resp-cluster")); EXPECT_FALSE(resp_headers.has("x-remove-cluster1")); } @@ -6807,10 +6877,12 @@ TEST_F(RouteMatcherTest, TestWeightedClusterHeaderManipulation) { const RouteEntry* route_entry = cached_route->routeEntry(); EXPECT_EQ("cluster2", route_entry->clusterName()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers, &resp_headers); + + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("cluster2", headers.get_("x-req-cluster")); - route_entry->finalizeResponseHeaders(resp_headers, stream_info); + route_entry->finalizeResponseHeaders(resp_headers, formatter_context, stream_info); EXPECT_EQ("cluster2", resp_headers.get_("x-resp-cluster")); EXPECT_FALSE(resp_headers.has("x-remove-cluster2")); } @@ -6853,11 +6925,13 @@ TEST_F(RouteMatcherTest, TestWeightedClusterClusterHeaderHeaderManipulation) { const RouteEntry* route_entry = dynamic_route->routeEntry(); EXPECT_EQ("cluster1", route_entry->clusterName()); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers, &resp_headers); + + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("cluster-adding-this-value", headers.get_("x-req-cluster")); EXPECT_EQ("new_host1", headers.getHostValue()); - route_entry->finalizeResponseHeaders(resp_headers, stream_info); + route_entry->finalizeResponseHeaders(resp_headers, formatter_context, stream_info); EXPECT_EQ("cluster-adding-this-value", resp_headers.get_("x-resp-cluster")); EXPECT_FALSE(resp_headers.has("x-remove-cluster1")); } @@ -7473,6 +7547,9 @@ TEST_F(CustomRequestHeadersTest, AddNewHeader) { - header: key: x-client-ip value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" + - header: + key: trace-id-from-formatter + value: "%TRACE_ID%" request_headers_to_add: - header: key: x-client-ip @@ -7484,8 +7561,15 @@ TEST_F(CustomRequestHeadersTest, AddNewHeader) { creation_status_); Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/new_endpoint/foo", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + + NiceMock active_span; + EXPECT_CALL(active_span, getTraceId()).WillRepeatedly(Return("trace-id")); + + auto formatter_context = + Formatter::HttpFormatterContext().setRequestHeaders(headers).setActiveSpan(active_span); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("127.0.0.1", headers.get_("x-client-ip")); + EXPECT_EQ("trace-id", headers.get_("trace-id-from-formatter")); } TEST_F(CustomRequestHeadersTest, CustomHeaderWrongFormat) { @@ -7524,14 +7608,14 @@ TEST_F(CustomRequestHeadersTest, CustomHeaderWrongFormat) { } TEST(MetadataMatchCriteriaImpl, Create) { - auto v1 = ProtobufWkt::Value(); + auto v1 = Protobuf::Value(); v1.set_string_value("v1"); - auto v2 = ProtobufWkt::Value(); + auto v2 = Protobuf::Value(); v2.set_number_value(2.0); - auto v3 = ProtobufWkt::Value(); + auto v3 = Protobuf::Value(); v3.set_bool_value(true); - auto metadata_struct = ProtobufWkt::Struct(); + auto metadata_struct = Protobuf::Struct(); auto mutable_fields = metadata_struct.mutable_fields(); mutable_fields->insert({"a", v1}); mutable_fields->insert({"b", v2}); @@ -7554,14 +7638,14 @@ TEST(MetadataMatchCriteriaImpl, Create) { } TEST(MetadataMatchCriteriaImpl, Merge) { - auto pv1 = ProtobufWkt::Value(); + auto pv1 = Protobuf::Value(); pv1.set_string_value("v1"); - auto pv2 = ProtobufWkt::Value(); + auto pv2 = Protobuf::Value(); pv2.set_number_value(2.0); - auto pv3 = ProtobufWkt::Value(); + auto pv3 = Protobuf::Value(); pv3.set_bool_value(true); - auto parent_struct = ProtobufWkt::Struct(); + auto parent_struct = Protobuf::Struct(); auto parent_fields = parent_struct.mutable_fields(); parent_fields->insert({"a", pv1}); parent_fields->insert({"b", pv2}); @@ -7569,14 +7653,14 @@ TEST(MetadataMatchCriteriaImpl, Merge) { auto parent_matches = MetadataMatchCriteriaImpl(parent_struct); - auto v1 = ProtobufWkt::Value(); + auto v1 = Protobuf::Value(); v1.set_string_value("override1"); - auto v2 = ProtobufWkt::Value(); + auto v2 = Protobuf::Value(); v2.set_string_value("v2"); - auto v3 = ProtobufWkt::Value(); + auto v3 = Protobuf::Value(); v3.set_string_value("override3"); - auto metadata_struct = ProtobufWkt::Struct(); + auto metadata_struct = Protobuf::Struct(); auto mutable_fields = metadata_struct.mutable_fields(); mutable_fields->insert({"a", v1}); mutable_fields->insert({"b++", v2}); @@ -7603,14 +7687,14 @@ TEST(MetadataMatchCriteriaImpl, Merge) { } TEST(MetadataMatchCriteriaImpl, Filter) { - auto pv1 = ProtobufWkt::Value(); + auto pv1 = Protobuf::Value(); pv1.set_string_value("v1"); - auto pv2 = ProtobufWkt::Value(); + auto pv2 = Protobuf::Value(); pv2.set_number_value(2.0); - auto pv3 = ProtobufWkt::Value(); + auto pv3 = Protobuf::Value(); pv3.set_bool_value(true); - auto metadata_matches = ProtobufWkt::Struct(); + auto metadata_matches = Protobuf::Struct(); auto parent_fields = metadata_matches.mutable_fields(); parent_fields->insert({"a", pv1}); parent_fields->insert({"b", pv2}); @@ -8961,7 +9045,8 @@ TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchRewrite) { genHeaders("path.prefix.com", "/rewrite?param=true#fragment", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/new/api?param=true#fragment", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("rewrite-cluster", route_entry->clusterName()); EXPECT_EQ("/new/api?param=true#fragment", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); @@ -8974,7 +9059,8 @@ TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchRewrite) { const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/new/api/this?param=true#fragment", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("rewrite-cluster", route_entry->clusterName()); EXPECT_EQ("/new/api/this?param=true#fragment", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); @@ -9165,7 +9251,9 @@ TEST_F(RouteConfigurationV2, RegexPrefixWithNoRewriteWorksWhenPathChanged) { // no re-write was specified; so this should not throw NiceMock stream_info; - EXPECT_NO_THROW(route_entry->finalizeRequestHeaders(headers, stream_info, false)); + const Formatter::HttpFormatterContext formatter_context(&headers); + EXPECT_NO_THROW( + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, false)); } } @@ -9257,7 +9345,7 @@ TEST_F(RouteConfigurationV2, RetriableStatusCodes) { creation_status_); Http::TestRequestHeaderMapImpl headers = genRedirectHeaders("idle.lyft.com", "/regex", true, false); - const auto& retry_policy = config.route(headers, 0)->routeEntry()->retryPolicy(); + const auto& retry_policy = *config.route(headers, 0)->routeEntry()->retryPolicy(); const std::vector expected_codes{100, 200}; EXPECT_EQ(expected_codes, retry_policy.retriableStatusCodes()); } @@ -9286,7 +9374,7 @@ TEST_F(RouteConfigurationV2, RetriableHeaders) { creation_status_); Http::TestRequestHeaderMapImpl headers = genRedirectHeaders("idle.lyft.com", "/regex", true, false); - const auto& retry_policy = config.route(headers, 0)->routeEntry()->retryPolicy(); + const auto& retry_policy = *config.route(headers, 0)->routeEntry()->retryPolicy(); ASSERT_EQ(2, retry_policy.retriableHeaders().size()); Http::TestResponseHeaderMapImpl expected_0{{":status", "500"}}; @@ -9475,7 +9563,7 @@ TEST_F(RouteConfigurationV2, RetryPluginsAreNotReused) { creation_status_); Http::TestRequestHeaderMapImpl headers = genRedirectHeaders("idle.lyft.com", "/regex", true, false); - const auto& retry_policy = config.route(headers, 0)->routeEntry()->retryPolicy(); + const auto& retry_policy = *config.route(headers, 0)->routeEntry()->retryPolicy(); const auto priority1 = retry_policy.retryPriority(); const auto priority2 = retry_policy.retryPriority(); EXPECT_NE(priority1, priority2); @@ -9722,7 +9810,8 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteSimple) { Http::TestRequestHeaderMapImpl headers = genHeaders("path.prefix.com", "/rest/one/two", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rest/two/one", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rest/two/one", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -9758,7 +9847,9 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteDoubleEqual) { const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/en/gb/ilp==/dGasdA/?key1=test1&key2=test2", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/en/gb/ilp==/dGasdA/?key1=test1&key2=test2", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -9793,7 +9884,8 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteTripleEqualVariable) { genHeaders("path.prefix.com", "/one/two/==na/three", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/==na/three/two", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/==na/three/two", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -9828,7 +9920,8 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteDoubleEqualVariable) { genHeaders("path.prefix.com", "/one/two/=na/three", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/=na/three/two", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/=na/three/two", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -9865,7 +9958,8 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteDoubleEqualInWildcard) { const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/en/gb/ilp/dGasdA==/?key1=test1&key2=test2", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/en/gb/ilp/dGasdA==/?key1=test1&key2=test2", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -9901,7 +9995,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardFilenameUnnamed) { genHeaders("path.prefix.com", "/rest/one/two/song.m3u8", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rest/one/two", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rest/one/two", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); @@ -9909,7 +10004,9 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardFilenameUnnamed) { genHeaders("path.prefix.com", "/rest/one/two/item/another/song.m3u8", "GET"); const RouteEntry* route_entry_multi = config.route(headers_multi, 0)->routeEntry(); EXPECT_EQ("/rest/one/two", route_entry_multi->currentUrlPathAfterRewrite(headers_multi)); - route_entry->finalizeRequestHeaders(headers_multi, stream_info, true); + const Formatter::HttpFormatterContext formatter_context_multi(&headers_multi); + route_entry_multi->finalizeRequestHeaders(headers_multi, formatter_context_multi, stream_info, + true); EXPECT_EQ("/rest/one/two", headers_multi.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers_multi.get_(Http::Headers::get().Host)); } @@ -9946,7 +10043,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardFilenameQueryParameters) { const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/one?one=0&two=1&three=2&four=3&go=ls", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/one?one=0&two=1&three=2&four=3&go=ls", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -9982,7 +10080,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardFilename) { genHeaders("path.prefix.com", "/rest/one/two/song.m3u8", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/rest/one/two", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/rest/one/two", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10022,7 +10121,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardComplexWildcardWithQueryParameter) const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/abc@xyz.com-FL0001090004/entries?fields=FULL&client_type=WEB", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/abc@xyz.com-FL0001090004/entries?fields=FULL&client_type=WEB", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); @@ -10059,7 +10159,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardFilenameDir) { genHeaders("path.prefix.com", "/rest/one/two/root/sub/song.mp4", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/root/sub", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/root/sub", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10094,7 +10195,8 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteSimpleTwo) { Http::TestRequestHeaderMapImpl headers = genHeaders("path.prefix.com", "/rest/one/two", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/two/one", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/two/one", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10143,14 +10245,15 @@ TEST_F(RouteMatcherTest, PatternMatchRewriteCaseSensitive) { Http::TestRequestHeaderMapImpl headers = genHeaders("path.prefix.com", "/rest/one/two", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/two/one", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/two/one", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); headers = genHeaders("path.prefix.com", "/REST/one/two", "GET"); route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/TEST/one", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/TEST/one", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10250,7 +10353,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardMiddleThreePartVariableNamed) { genHeaders("path.prefix.com", "/rest/one/previous/videos/three/end", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/previous/videos/three", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/previous/videos/three", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10317,7 +10421,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardUnnamedVariable) { Http::TestRequestHeaderMapImpl headers = genHeaders("path.prefix.com", "/rest/one/two", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/two", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/two", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10353,7 +10458,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardAtEndVariable) { genHeaders("path.prefix.com", "/rest/one/two/three/four", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/one", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/one", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10389,7 +10495,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardAtEndVariableNamed) { genHeaders("path.prefix.com", "/rest/one/two/three/four", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/two/three/four", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/two/three/four", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10425,7 +10532,8 @@ TEST_F(RouteMatcherTest, PatternMatchWildcardMiddleVariableNamed) { genHeaders("path.prefix.com", "/rest/one/videos/three/end", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/videos/three", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/videos/three", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10461,7 +10569,8 @@ TEST_F(RouteMatcherTest, PatternMatchCaseSensitiveVariableNames) { genHeaders("path.prefix.com", "/rest/lower/upper/end", "GET"); const RouteEntry* route_entry = config.route(headers, 0)->routeEntry(); EXPECT_EQ("/upper/lower", route_entry->currentUrlPathAfterRewrite(headers)); - route_entry->finalizeRequestHeaders(headers, stream_info, true); + const Formatter::HttpFormatterContext formatter_context(&headers); + route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true); EXPECT_EQ("/upper/lower", headers.get_(Http::Headers::get().Path)); EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); } @@ -10644,7 +10753,7 @@ class PerFilterConfigsTest : public testing::Test, public ConfigImplTestBase { : registered_factory_(factory_), registered_default_factory_(default_factory_) {} struct DerivedFilterConfig : public RouteSpecificFilterConfig { - ProtobufWkt::Timestamp config_; + Protobuf::Timestamp config_; }; class TestFilterConfig : public Extensions::HttpFilters::Common::EmptyHttpFilterConfig { public: @@ -10655,11 +10764,11 @@ class PerFilterConfigsTest : public testing::Test, public ConfigImplTestBase { PANIC("not implemented"); } ProtobufTypes::MessagePtr createEmptyRouteConfigProto() override { - return ProtobufTypes::MessagePtr{new ProtobufWkt::Timestamp()}; + return ProtobufTypes::MessagePtr{new Protobuf::Timestamp()}; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Override this to guarantee that we have a different factory mapping by-type. - return ProtobufTypes::MessagePtr{new ProtobufWkt::Timestamp()}; + return ProtobufTypes::MessagePtr{new Protobuf::Timestamp()}; } std::set configTypes() override { return {"google.protobuf.Timestamp"}; } absl::StatusOr @@ -10680,7 +10789,7 @@ class PerFilterConfigsTest : public testing::Test, public ConfigImplTestBase { PANIC("not implemented"); } ProtobufTypes::MessagePtr createEmptyRouteConfigProto() override { - return ProtobufTypes::MessagePtr{new ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Protobuf::Struct()}; } std::set configTypes() override { return {"google.protobuf.Struct"}; } }; @@ -11665,37 +11774,8 @@ TEST_F(RouteMatcherTest, RequestMirrorPoliciesWithTraceSampled) { factory_context_.cluster_manager_.initializeClusters( {"www2", "some_cluster", "some_cluster2", "some_cluster3"}, {}); - // Test with runtime flag disabled (old behavior) - { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.shadow_policy_inherit_trace_sampling", "false"}}); - - TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true, - creation_status_); - - Http::TestRequestHeaderMapImpl headers = genHeaders("www.databricks.com", "/foo", "GET"); - const auto& shadow_policies = config.route(headers, 0)->routeEntry()->shadowPolicies(); - - // First policy should have trace sampled = false - EXPECT_TRUE(shadow_policies[0]->traceSampled().has_value()); - EXPECT_FALSE(shadow_policies[0]->traceSampled().value()); - - // Second policy should have trace sampled = true - EXPECT_TRUE(shadow_policies[1]->traceSampled().has_value()); - EXPECT_TRUE(shadow_policies[1]->traceSampled().value()); - - // With flag disabled, unspecified should default to true - EXPECT_TRUE(shadow_policies[2]->traceSampled().has_value()); - EXPECT_TRUE(shadow_policies[2]->traceSampled().value()); - } - // Test with runtime flag enabled (new behavior) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.shadow_policy_inherit_trace_sampling", "true"}}); - TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true, creation_status_); @@ -11715,6 +11795,31 @@ TEST_F(RouteMatcherTest, RequestMirrorPoliciesWithTraceSampled) { } } +// Test that route-level request_body_buffer_limit takes precedence over virtual_host +// request_body_buffer_limit +TEST_F(RouteConfigurationV2, RequestBodyBufferLimitPrecedenceRouteOverridesVirtualHost) { + const std::string yaml = R"EOF( +virtual_hosts: +- domains: [test.example.com] + name: test_host + request_body_buffer_limit: 32768 + routes: + - match: {prefix: /test} + route: + cluster: backend + request_body_buffer_limit: 4194304 +)EOF"; + + factory_context_.cluster_manager_.initializeClusters({"backend"}, {}); + TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true, + creation_status_); + EXPECT_TRUE(creation_status_.ok()); + + Http::TestRequestHeaderMapImpl headers = genHeaders("test.example.com", "/test", "GET"); + const RouteEntry* route = config.route(headers, 0)->routeEntry(); + EXPECT_EQ(4194304U, route->requestBodyBufferLimit()); +} + } // namespace } // namespace Router } // namespace Envoy diff --git a/test/common/router/delegating_route_impl_test.cc b/test/common/router/delegating_route_impl_test.cc index 84c434a397f29..23561ae19d74d 100644 --- a/test/common/router/delegating_route_impl_test.cc +++ b/test/common/router/delegating_route_impl_test.cc @@ -50,14 +50,15 @@ TEST(DelegatingRouteEntry, DelegatingRouteEntryTest) { Http::TestRequestHeaderMapImpl request_headers; Http::TestResponseHeaderMapImpl response_headers; StreamInfo::MockStreamInfo stream_info; + const Formatter::HttpFormatterContext formatter_context(&request_headers, &response_headers); - TEST_METHOD(finalizeResponseHeaders, response_headers, stream_info); + TEST_METHOD(finalizeResponseHeaders, response_headers, formatter_context, stream_info); TEST_METHOD(responseHeaderTransforms, stream_info); TEST_METHOD(clusterName); TEST_METHOD(clusterNotFoundResponseCode); TEST_METHOD(corsPolicy); TEST_METHOD(currentUrlPathAfterRewrite, request_headers); - TEST_METHOD(finalizeRequestHeaders, request_headers, stream_info, true); + TEST_METHOD(finalizeRequestHeaders, request_headers, formatter_context, stream_info, true); TEST_METHOD(requestHeaderTransforms, stream_info); TEST_METHOD(hashPolicy); TEST_METHOD(hedgePolicy); @@ -67,7 +68,7 @@ TEST(DelegatingRouteEntry, DelegatingRouteEntryTest) { TEST_METHOD(pathMatcher); TEST_METHOD(pathRewriter); TEST_METHOD(internalRedirectPolicy); - TEST_METHOD(retryShadowBufferLimit); + TEST_METHOD(requestBodyBufferLimit); TEST_METHOD(shadowPolicies); TEST_METHOD(timeout); TEST_METHOD(idleTimeout); diff --git a/test/common/router/header_formatter_test.cc b/test/common/router/header_formatter_test.cc index e843fd18a3e16..7056659980b51 100644 --- a/test/common/router/header_formatter_test.cc +++ b/test/common/router/header_formatter_test.cc @@ -17,6 +17,7 @@ #include "test/common/stream_info/test_int_accessor.h" #include "test/mocks/api/mocks.h" #include "test/mocks/http/mocks.h" +#include "test/mocks/server/server_factory_context.h" #include "test/mocks/ssl/mocks.h" #include "test/mocks/stream_info/mocks.h" #include "test/mocks/upstream/host.h" @@ -73,14 +74,11 @@ TEST(HeaderParserTest, TestParse) { {"%DOWNSTREAM_DIRECT_LOCAL_ADDRESS%", {"127.0.0.2:0"}, {}}, {"%DOWNSTREAM_DIRECT_LOCAL_PORT%", {"0"}, {}}, {"%DOWNSTREAM_DIRECT_LOCAL_ADDRESS_WITHOUT_PORT%", {"127.0.0.2"}, {}}, - {"%UPSTREAM_METADATA([\"ns\", \"key\"])%", {"value"}, {}}, - {"[%UPSTREAM_METADATA([\"ns\", \"key\"])%", {"[value"}, {}}, - {"%UPSTREAM_METADATA([\"ns\", \"key\"])%]", {"value]"}, {}}, - {"[%UPSTREAM_METADATA([\"ns\", \"key\"])%]", {"[value]"}, {}}, - {"%UPSTREAM_METADATA([\"ns\", \t \"key\"])%", {"value"}, {}}, - {"%UPSTREAM_METADATA([\"ns\", \n \"key\"])%", {"value"}, {}}, - {"%UPSTREAM_METADATA( \t [ \t \"ns\" \t , \t \"key\" \t ] \t )%", {"value"}, {}}, - {R"EOF(%UPSTREAM_METADATA(["\"quoted\"", "\"key\""])%)EOF", {"value"}, {}}, + {"%UPSTREAM_METADATA(ns:key)%", {"value"}, {}}, + {"[%UPSTREAM_METADATA(ns:key)%", {"[value"}, {}}, + {"%UPSTREAM_METADATA(ns:key)%]", {"value]"}, {}}, + {"[%UPSTREAM_METADATA(ns:key)%]", {"[value]"}, {}}, + {"%UPSTREAM_METADATA(ns:key)%", {"value"}, {}}, {"%UPSTREAM_REMOTE_ADDRESS%", {"10.0.0.1:443"}, {}}, {"%UPSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%", {"10.0.0.1"}, {}}, {"%UPSTREAM_REMOTE_PORT%", {"443"}, {}}, @@ -242,6 +240,10 @@ TEST(HeaderParserTest, TestParse) { } TEST(HeaderParser, TestMetadataTranslator) { + NiceMock context; + ScopedThreadLocalServerContextSetter setter(context); + EXPECT_CALL(context.runtime_loader_, countDeprecatedFeatureUse()).Times(testing::AtLeast(1)); + struct TestCase { std::string input_; std::string expected_output_; @@ -281,6 +283,10 @@ TEST(HeaderParser, TestMetadataTranslatorExceptions) { } TEST(HeaderParser, TestPerFilterStateTranslator) { + NiceMock context; + ScopedThreadLocalServerContextSetter setter(context); + EXPECT_CALL(context.runtime_loader_, countDeprecatedFeatureUse()).Times(testing::AtLeast(1)); + struct TestCase { std::string input_; std::string expected_output_; @@ -431,7 +437,11 @@ TEST(HeaderParserTest, EvaluateHeaderValuesWithNullStreamInfo) { EXPECT_FALSE(header_map.has("empty")); } -TEST(HeaderParserTest, EvaluateEmptyHeaders) { +TEST(HeaderParserTest, EvaluateEmptyHeadersWithLegacyFormat) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.remove_legacy_route_formatter", "false"}}); + const std::string yaml = R"EOF( match: { prefix: "/new_endpoint" } route: @@ -457,6 +467,32 @@ match: { prefix: "/new_endpoint" } EXPECT_FALSE(header_map.has("x-key")); } +TEST(HeaderParserTest, EvaluateEmptyHeaders) { + const std::string yaml = R"EOF( +match: { prefix: "/new_endpoint" } +route: + cluster: "www2" + prefix_rewrite: "/api/new_endpoint" +request_headers_to_add: + - header: + key: "x-key" + value: "%UPSTREAM_METADATA(namespace:key)%" + append_action: APPEND_IF_EXISTS_OR_ADD +)EOF"; + + HeaderParserPtr req_header_parser = + HeaderParser::configure(parseRouteFromV3Yaml(yaml).request_headers_to_add()).value(); + Http::TestRequestHeaderMapImpl header_map{{":method", "POST"}}; + std::shared_ptr> host( + new NiceMock()); + NiceMock stream_info; + auto metadata = std::make_shared(); + stream_info.upstreamInfo()->setUpstreamHost(host); + ON_CALL(*host, metadata()).WillByDefault(Return(metadata)); + req_header_parser->evaluateHeaders(header_map, stream_info); + EXPECT_FALSE(header_map.has("x-key")); +} + TEST(HeaderParserTest, EvaluateStaticHeaders) { const std::string yaml = R"EOF( match: { prefix: "/new_endpoint" } @@ -508,7 +544,7 @@ match: { prefix: "/new_endpoint" } value: "%PROTOCOL%%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" - header: key: "x-metadata" - value: "%UPSTREAM_METADATA([\"namespace\", \"%key%\"])%" + value: "%UPSTREAM_METADATA(namespace:%key%)%" - header: key: "x-per-request" value: "%PER_REQUEST_STATE(testing)%" diff --git a/test/common/router/header_parser_corpus/clusterfuzz-testcase-header_parser_fuzz_test-5163306626580480 b/test/common/router/header_parser_corpus/clusterfuzz-testcase-header_parser_fuzz_test-5163306626580480 index 57041ba397712..e4ba6b448326e 100644 --- a/test/common/router/header_parser_corpus/clusterfuzz-testcase-header_parser_fuzz_test-5163306626580480 +++ b/test/common/router/header_parser_corpus/clusterfuzz-testcase-header_parser_fuzz_test-5163306626580480 @@ -1,7 +1,7 @@ headers_to_add { header { key: "P" - value: "%PER_REQUEST_STATE(oB]$T)%" + value: "%FILTER_STATE(oB]$T)%" } } headers_to_add { @@ -13,13 +13,13 @@ headers_to_add { headers_to_add { header { key: "A" - value: "%PER_REQUEST_STATE(dB]$T)%" + value: "%FILTER_STATE(dB]$T)%" } } headers_to_add { header { key: "\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177" - value: "%PER_REQUEST_STATE(dB]$T)%" + value: "%FILTER_STATE(dB]$T)%" } append { value: true diff --git a/test/common/router/rds_impl_test.cc b/test/common/router/rds_impl_test.cc index 5d9ce3db6426b..c34e93ddb000e 100644 --- a/test/common/router/rds_impl_test.cc +++ b/test/common/router/rds_impl_test.cc @@ -1176,30 +1176,27 @@ name: foo_route_config ->dynamic_route_configs() .size()); - for (bool normalize_config : std::vector({true, false})) { - Runtime::maybeSetRuntimeGuard("envoy.reloadable_features.normalize_rds_provider_config", - normalize_config); - envoy::extensions::filters::network::http_connection_manager::v3::Rds rds2; - rds2 = rds_; - // The following is valid only when normalize_config is true: - // Modify parameters which should not affect the provider. In other words, the same provider - // should be picked, regardless of the fact that initial_fetch_timeout is different for both - // configs. - rds2.mutable_config_source()->mutable_initial_fetch_timeout()->set_seconds( - rds_.config_source().initial_fetch_timeout().seconds() + 1); - - RouteConfigProviderSharedPtr provider2 = - route_config_provider_manager_->createRdsRouteConfigProvider( - rds2, server_factory_context_, "foo_prefix", outer_init_manager_); + // Test that modifying the initial_fetch_timeout in the config_source doesn't affect + // provider selection due to config normalization. + envoy::extensions::filters::network::http_connection_manager::v3::Rds rds2; + rds2 = rds_; + // Modify parameters which should not affect the provider. The same provider should be picked, + // regardless of the fact that initial_fetch_timeout is different for both configs. + rds2.mutable_config_source()->mutable_initial_fetch_timeout()->set_seconds( + rds_.config_source().initial_fetch_timeout().seconds() + 1); - EXPECT_TRUE(server_factory_context_.cluster_manager_.subscription_factory_.callbacks_ - ->onConfigUpdate(decoded_resources.refvec_, "provider2") - .ok()); - EXPECT_EQ(normalize_config ? 1UL : 2UL, - route_config_provider_manager_->dumpRouteConfigs(universal_name_matcher) - ->dynamic_route_configs() - .size()); - } + RouteConfigProviderSharedPtr provider2 = + route_config_provider_manager_->createRdsRouteConfigProvider( + rds2, server_factory_context_, "foo_prefix", outer_init_manager_); + + EXPECT_TRUE(server_factory_context_.cluster_manager_.subscription_factory_.callbacks_ + ->onConfigUpdate(decoded_resources.refvec_, "provider2") + .ok()); + // We expect only 1 provider since the configurations are considered equivalent after + // normalization. + EXPECT_EQ(1UL, route_config_provider_manager_->dumpRouteConfigs(universal_name_matcher) + ->dynamic_route_configs() + .size()); } } // namespace diff --git a/test/common/router/retry_state_impl_test.cc b/test/common/router/retry_state_impl_test.cc index a4cf679837487..3e3f99a804932 100644 --- a/test/common/router/retry_state_impl_test.cc +++ b/test/common/router/retry_state_impl_test.cc @@ -56,8 +56,7 @@ class RouterRetryStateImplTest : public testing::Test { } void setup(Http::RequestHeaderMap& request_headers) { - - state_ = RetryStateImpl::create(policy_, request_headers, cluster_, &virtual_cluster_, + state_ = RetryStateImpl::create(*policy_, request_headers, cluster_, &virtual_cluster_, route_stats_context_, factory_context_, dispatcher_, Upstream::ResourcePriority::Default); } @@ -166,7 +165,7 @@ class RouterRetryStateImplTest : public testing::Test { void TearDown() override { cleanupOutstandingResources(); } Event::SimulatedTimeSystem test_time_; - NiceMock policy_; + std::shared_ptr> policy_ = TestRetryPolicy::createMock(); NiceMock cluster_; TestVirtualCluster virtual_cluster_; Stats::IsolatedStoreImpl stats_store_; @@ -502,7 +501,7 @@ TEST_F(RouterRetryStateImplTest, PolicyRetriable4xxReset) { } TEST_F(RouterRetryStateImplTest, RetriableStatusCodes) { - policy_.retriable_status_codes_.push_back(409); + policy_->retriable_status_codes_.push_back(409); verifyPolicyWithRemoteResponse("retriable-status-codes", "409", false /* is_grpc */); } @@ -533,7 +532,7 @@ TEST_F(RouterRetryStateImplTest, NoRetryUponTooEarlyStatusCodeWithDownstreamEarl } TEST_F(RouterRetryStateImplTest, RetriableStatusCodesUpstreamReset) { - policy_.retriable_status_codes_.push_back(409); + policy_->retriable_status_codes_.push_back(409); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-retry-on", "retriable-status-codes"}}; setup(request_headers); EXPECT_TRUE(state_->enabled()); @@ -606,13 +605,13 @@ TEST_F(RouterRetryStateImplTest, RetriableStatusCodesHeader) { // Test that when 'retriable-headers' policy is set via request header, certain configured headers // trigger retries. TEST_F(RouterRetryStateImplTest, RetriableHeadersPolicySetViaRequestHeader) { - policy_.retry_on_ = RetryPolicy::RETRY_ON_5XX; + policy_->retry_on_ = RetryPolicy::RETRY_ON_5XX; Protobuf::RepeatedPtrField matchers; auto* matcher = matchers.Add(); matcher->set_name("X-Upstream-Pushback"); - policy_.retriable_headers_ = + policy_->retriable_headers_ = Http::HeaderUtility::buildHeaderMatcherVector(matchers, factory_context_); // No retries based on response headers: retry mode isn't enabled. @@ -644,7 +643,7 @@ TEST_F(RouterRetryStateImplTest, RetriableHeadersPolicySetViaRequestHeader) { // Test that when 'retriable-headers' policy is set via retry policy configuration, // configured header matcher conditions trigger retries. TEST_F(RouterRetryStateImplTest, RetriableHeadersPolicyViaRetryPolicyConfiguration) { - policy_.retry_on_ = RetryPolicy::RETRY_ON_RETRIABLE_HEADERS; + policy_->retry_on_ = RetryPolicy::RETRY_ON_RETRIABLE_HEADERS; Protobuf::RepeatedPtrField matchers; @@ -664,7 +663,7 @@ TEST_F(RouterRetryStateImplTest, RetriableHeadersPolicyViaRetryPolicyConfigurati matcher4->mutable_range_match()->set_start(500); matcher4->mutable_range_match()->set_end(505); - policy_.retriable_headers_ = + policy_->retriable_headers_ = Http::HeaderUtility::buildHeaderMatcherVector(matchers, factory_context_); auto should_retry_with_response = [this](const Http::TestResponseHeaderMapImpl& response_headers, @@ -784,7 +783,7 @@ TEST_F(RouterRetryStateImplTest, RetriableHeadersSetViaRequestHeader) { // Test merging retriable headers set via request headers and via config file. TEST_F(RouterRetryStateImplTest, RetriableHeadersMergedConfigAndRequestHeaders) { - policy_.retry_on_ = RetryPolicy::RETRY_ON_RETRIABLE_HEADERS; + policy_->retry_on_ = RetryPolicy::RETRY_ON_RETRIABLE_HEADERS; Protobuf::RepeatedPtrField matchers; @@ -794,7 +793,7 @@ TEST_F(RouterRetryStateImplTest, RetriableHeadersMergedConfigAndRequestHeaders) matcher->mutable_string_match()->set_exact("200"); matcher->set_invert_match(true); - policy_.retriable_headers_ = + policy_->retriable_headers_ = Http::HeaderUtility::buildHeaderMatcherVector(matchers, factory_context_); // No retries according to config. @@ -856,7 +855,7 @@ TEST_F(RouterRetryStateImplTest, PolicyLimitedByRequestHeaders) { matcher2->set_name(":method"); matcher2->mutable_string_match()->set_exact("HEAD"); - policy_.retriable_request_headers_ = + policy_->retriable_request_headers_ = Http::HeaderUtility::buildHeaderMatcherVector(matchers, factory_context_); { @@ -917,8 +916,8 @@ TEST_F(RouterRetryStateImplTest, PolicyLimitedByRequestHeaders) { } TEST_F(RouterRetryStateImplTest, RouteConfigNoRetriesAllowed) { - policy_.num_retries_ = 0; - policy_.retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; + policy_->num_retries_ = 0; + policy_->retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; setup(); EXPECT_TRUE(state_->enabled()); @@ -935,8 +934,8 @@ TEST_F(RouterRetryStateImplTest, RouteConfigNoRetriesAllowed) { } TEST_F(RouterRetryStateImplTest, RouteConfigNoHeaderConfig) { - policy_.num_retries_ = 1; - policy_.retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; + policy_->num_retries_ = 1; + policy_->retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; Http::TestRequestHeaderMapImpl request_headers; setup(request_headers); EXPECT_TRUE(state_->enabled()); @@ -966,7 +965,7 @@ TEST_F(RouterRetryStateImplTest, NoAvailableRetries) { TEST_F(RouterRetryStateImplTest, MaxRetriesHeader) { // The max retries header will take precedence over the policy - policy_.num_retries_ = 4; + policy_->num_retries_ = 4; Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-retry-on", "connect-failure"}, {"x-envoy-retry-grpc-on", "cancelled"}, {"x-envoy-max-retries", "3"}}; @@ -1011,8 +1010,8 @@ TEST_F(RouterRetryStateImplTest, MaxRetriesHeader) { } TEST_F(RouterRetryStateImplTest, Backoff) { - policy_.num_retries_ = 5; - policy_.retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; + policy_->num_retries_ = 5; + policy_->retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; Http::TestRequestHeaderMapImpl request_headers; setup(request_headers); EXPECT_TRUE(state_->enabled()); @@ -1082,10 +1081,10 @@ TEST_F(RouterRetryStateImplTest, Backoff) { // Test customized retry back-off intervals. TEST_F(RouterRetryStateImplTest, CustomBackOffInterval) { - policy_.num_retries_ = 10; - policy_.retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; - policy_.base_interval_ = std::chrono::milliseconds(100); - policy_.max_interval_ = std::chrono::milliseconds(1200); + policy_->num_retries_ = 10; + policy_->retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; + policy_->base_interval_ = std::chrono::milliseconds(100); + policy_->max_interval_ = std::chrono::milliseconds(1200); Http::TestRequestHeaderMapImpl request_headers; setup(request_headers); EXPECT_TRUE(state_->enabled()); @@ -1134,9 +1133,9 @@ TEST_F(RouterRetryStateImplTest, CustomBackOffInterval) { // Test the default maximum retry back-off interval. TEST_F(RouterRetryStateImplTest, CustomBackOffIntervalDefaultMax) { - policy_.num_retries_ = 10; - policy_.retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; - policy_.base_interval_ = std::chrono::milliseconds(100); + policy_->num_retries_ = 10; + policy_->retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; + policy_->base_interval_ = std::chrono::milliseconds(100); Http::TestRequestHeaderMapImpl request_headers; setup(request_headers); EXPECT_TRUE(state_->enabled()); @@ -1197,7 +1196,7 @@ TEST_F(RouterRetryStateImplTest, ParseRateLimitedResetInterval) { reset_header_2->set_name("X-RateLimit-Reset"); reset_header_2->set_format(envoy::config::route::v3::RetryPolicy::UNIX_TIMESTAMP); - policy_.reset_headers_ = ResetHeaderParserImpl::buildResetHeaderParserVector(reset_headers); + policy_->reset_headers_ = ResetHeaderParserImpl::buildResetHeaderParserVector(reset_headers); // Failure case: Matches reset header (seconds) but exceeds max_interval (>5min) { @@ -1263,8 +1262,8 @@ TEST_F(RouterRetryStateImplTest, RateLimitedRetryBackoffStrategy) { reset_header->set_name("Retry-After"); reset_header->set_format(envoy::config::route::v3::RetryPolicy::SECONDS); - policy_.num_retries_ = 4; - policy_.reset_headers_ = ResetHeaderParserImpl::buildResetHeaderParserVector(reset_headers); + policy_->num_retries_ = 4; + policy_->reset_headers_ = ResetHeaderParserImpl::buildResetHeaderParserVector(reset_headers); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-retry-on", "5xx"}}; setup(request_headers); @@ -1320,8 +1319,8 @@ TEST_F(RouterRetryStateImplTest, RateLimitedRetryBackoffStrategy) { } TEST_F(RouterRetryStateImplTest, HostSelectionAttempts) { - policy_.host_selection_max_attempts_ = 2; - policy_.retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; + policy_->host_selection_max_attempts_ = 2; + policy_->retry_on_ = RetryPolicy::RETRY_ON_CONNECT_FAILURE; setup(); diff --git a/test/common/router/route_corpus/regex_parsing_error b/test/common/router/route_corpus/regex_parsing_error index 50caaf4f85e42..a551973008ef2 100644 --- a/test/common/router/route_corpus/regex_parsing_error +++ b/test/common/router/route_corpus/regex_parsing_error @@ -14,7 +14,7 @@ config { name: "." typed_config { type_url: "m/envoy.config.route.v3.Route" - value: "\n\002\n\000\022\t\n\001v*\0015\242\002\000J\005\n\003\n\0011JF\nB\n\001$\022=%START_TIME((%%%fenvoy.filters.http.router%\034f%256\\002\\0N\\ss)% \001J\005\n\003\n\001$J\205\001\n\202\001\n\001$\022}%START_TIME((%%%fenvoy%PER_REQUEST_STATE(%fenvoy.type.v3.Int64Ra%TUEST_STATE(%f%ss[%%s.filters.http.router%\034f%256\\002\\0N\\ss)%J\010\n\006\n\0011\022\001\003b\001?b\021x-forwarded-protob\021x-forwarded-protor\001v\202\001\000" + value: "\n\002\n\000\022\t\n\001v*\0015\242\002\000J\005\n\003\n\0011JF\nB\n\001$\022=%START_TIME((%%%fenvoy.filters.http.router%\034f%256\\002\\0N\\ss)% \001J\005\n\003\n\001$J\205\001\n\202\001\n\001$\022}%START_TIME((%%%fenvoy%FILTER_STATE(%fenvoy.type.v3.Int64Ra%TUEST_STATE(%f%ss[%%s.filters.http.router%\034f%256\\002\\0N\\ss)%J\010\n\006\n\0011\022\001\003b\001?b\021x-forwarded-protob\021x-forwarded-protor\001v\202\001\000" } } } diff --git a/test/common/router/route_corpus/wrong_UPSTREAM_HEADER_BYTES_RECEIVED b/test/common/router/route_corpus/wrong_UPSTREAM_HEADER_BYTES_RECEIVED index 268698223fa0d..07d36871db874 100644 --- a/test/common/router/route_corpus/wrong_UPSTREAM_HEADER_BYTES_RECEIVED +++ b/test/common/router/route_corpus/wrong_UPSTREAM_HEADER_BYTES_RECEIVED @@ -13,7 +13,7 @@ config { request_headers_to_add { header { key: "[" - value: "`%START_TIME()%%REQ(T_||?|STARTO2s)%%UPSTREAM_HEADER_BYTES_RECEIVED%%PER_REQUEST_STATE(%f(%f%sRESPONSE_259462C_Swwwwww`TART_TIME()%%REQ(T_||?|STARTOC_Swwwwww`%START_TIME()%%REQ(T_||?|STARTOC_SwwwwwwUB(UB(OD)%T%START_TIME()%%REQ(T_<|?|STARTOC_SwwwwwwUB(UB(OD)%TA" + value: "`%START_TIME()%%REQ(T_||?|STARTO2s)%%UPSTREAM_HEADER_BYTES_RECEIVED%%FILTER_STATE(%f(%f%sRESPONSE_259462C_Swwwwww`TART_TIME()%%REQ(T_||?|STARTOC_Swwwwww`%START_TIME()%%REQ(T_||?|STARTOC_SwwwwwwUB(UB(OD)%T%START_TIME()%%REQ(T_<|?|STARTOC_SwwwwwwUB(UB(OD)%TA" } } } diff --git a/test/common/router/route_fuzz_test.cc b/test/common/router/route_fuzz_test.cc index ecf9bfb0e08ef..b6f271bfdacd4 100644 --- a/test/common/router/route_fuzz_test.cc +++ b/test/common/router/route_fuzz_test.cc @@ -154,9 +154,10 @@ DEFINE_PROTO_FUZZER(const test::common::router::RouteTestCase& input) { ProtobufMessage::getNullValidationVisitor(), true), std::shared_ptr); auto headers = Fuzz::fromHeaders(input.headers()); + const Formatter::HttpFormatterContext formatter_context{&headers}; auto route = config->route(headers, stream_info, input.random_value()).route; if (route != nullptr && route->routeEntry() != nullptr) { - route->routeEntry()->finalizeRequestHeaders(headers, stream_info, true); + route->routeEntry()->finalizeRequestHeaders(headers, formatter_context, stream_info, true); } ENVOY_LOG_MISC(trace, "Success"); } catch (const EnvoyException& e) { diff --git a/test/common/router/router_2_test.cc b/test/common/router/router_2_test.cc index 4dfbd96387228..1b2837ecf8f52 100644 --- a/test/common/router/router_2_test.cc +++ b/test/common/router/router_2_test.cc @@ -29,7 +29,15 @@ TEST_F(RouterTestSuppressEnvoyHeaders, Http1Upstream) { Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeRequestHeaders(_, _, false)); + + EXPECT_CALL(callbacks_.route_->route_entry_, finalizeRequestHeaders(_, _, _, false)) + .WillOnce(Invoke([this](Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo&, bool) { + EXPECT_EQ(&context.requestHeaders(), &headers); + EXPECT_EQ(&context.activeSpan(), &callbacks_.active_span_); + })); + router_->decodeHeaders(headers, true); EXPECT_FALSE(headers.has("x-envoy-expected-rq-timeout-ms")); @@ -112,6 +120,9 @@ class WatermarkTest : public RouterTestBase { WatermarkTest() : RouterTestBase(false, false, false, false, Protobuf::RepeatedPtrField{}) { EXPECT_CALL(callbacks_, activeSpan()).WillRepeatedly(ReturnRef(span_)); + // Add default mock for requestBodyBufferLimit which is called during router initialization. + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()) + .WillRepeatedly(Return(std::numeric_limits::max())); }; void sendRequest(bool header_only_request = true, bool pool_ready = true) { diff --git a/test/common/router/router_test.cc b/test/common/router/router_test.cc index 2ebcd49eba84d..51a2bad28822e 100644 --- a/test/common/router/router_test.cc +++ b/test/common/router/router_test.cc @@ -54,7 +54,6 @@ #include "gtest/gtest.h" using testing::_; -using testing::AtLeast; using testing::InSequence; using testing::Invoke; using testing::InvokeWithoutArgs; @@ -85,6 +84,9 @@ class RouterTest : public RouterTestBase { RouterTest() : RouterTestBase(false, false, false, false, Protobuf::RepeatedPtrField{}) { EXPECT_CALL(callbacks_, activeSpan()).WillRepeatedly(ReturnRef(span_)); + // Add default mock for requestBodyBufferLimit which is called during router initialization. + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()) + .WillRepeatedly(Return(std::numeric_limits::max())); ON_CALL(cm_.thread_local_cluster_, chooseHost(_)).WillByDefault(Invoke([this] { return Upstream::HostSelectionResponse{cm_.thread_local_cluster_.lb_.host_}; })); @@ -475,7 +477,13 @@ TEST_F(RouterTest, Http1Upstream) { Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeRequestHeaders(_, _, true)); + EXPECT_CALL(callbacks_.route_->route_entry_, finalizeRequestHeaders(_, _, _, true)) + .WillOnce(Invoke([this](Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo&, bool) { + EXPECT_EQ(&context.requestHeaders(), &headers); + EXPECT_EQ(&context.activeSpan(), &span_); + })); router_->decodeHeaders(headers, true); EXPECT_EQ("10", headers.get_("x-envoy-expected-rq-timeout-ms")); @@ -811,7 +819,6 @@ TEST_F(RouterTest, NoHost) { EXPECT_CALL(callbacks_, encodeData(_, true)); EXPECT_CALL(callbacks_.stream_info_, setResponseFlag(StreamInfo::CoreResponseFlag::NoHealthyUpstream)); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeResponseHeaders(_, _)); Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); @@ -846,7 +853,6 @@ TEST_F(RouterTest, MaintenanceMode) { EXPECT_CALL(callbacks_, encodeData(_, true)); EXPECT_CALL(callbacks_.stream_info_, setResponseFlag(StreamInfo::CoreResponseFlag::UpstreamOverflow)); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeResponseHeaders(_, _)); Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); @@ -894,7 +900,6 @@ TEST_F(RouterTest, DropOverloadDropped) { EXPECT_CALL(callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); EXPECT_CALL(callbacks_, encodeData(_, true)); EXPECT_CALL(callbacks_.stream_info_, setResponseFlag(StreamInfo::CoreResponseFlag::DropOverLoad)); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeResponseHeaders(_, _)); Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); @@ -1028,7 +1033,7 @@ class MockRetryOptionsPredicate : public Upstream::RetryOptionsPredicate { // Also verify retry options predicates work. TEST_F(RouterTest, EnvoyAttemptCountInRequestUpdatedInRetries) { auto retry_options_predicate = std::make_shared(); - callbacks_.route_->route_entry_.retry_policy_.retry_options_predicates_.emplace_back( + callbacks_.route_->route_entry_.retry_policy_->retry_options_predicates_.emplace_back( retry_options_predicate); setIncludeAttemptCountInRequest(true); @@ -1150,7 +1155,7 @@ TEST_F(RouterTest, EnvoyAttemptCountInResponsePresentWithLocalReply) { // Pool failure, so upstream request was never initiated. EXPECT_EQ(0U, callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value()); - EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); // Expect 1 error for connection failure EXPECT_EQ(callbacks_.details(), "upstream_reset_before_response_started{remote_connection_failure}"); EXPECT_EQ(1U, callbacks_.stream_info_.attemptCount().value()); @@ -1226,7 +1231,6 @@ TEST_F(RouterTest, AllDebugConfig) { Http::TestResponseHeaderMapImpl response_headers{{":status", "204"}, {"x-envoy-not-forwarded", "true"}}; EXPECT_CALL(callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeResponseHeaders(_, _)); Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); @@ -1908,7 +1912,7 @@ TEST_F(RouterTest, UpstreamTimeoutWithAltResponse) { TEST_F(RouterTest, UpstreamPerTryIdleTimeout) { InSequence s; - callbacks_.route_->route_entry_.retry_policy_.per_try_idle_timeout_ = + callbacks_.route_->route_entry_.retry_policy_->per_try_idle_timeout_ = std::chrono::milliseconds(3000); // This pattern helps ensure that we're actually invoking the callback. @@ -1979,7 +1983,7 @@ TEST_F(RouterTest, UpstreamPerTryIdleTimeout) { TEST_F(RouterTest, UpstreamPerTryIdleTimeoutSuccess) { InSequence s; - callbacks_.route_->route_entry_.retry_policy_.per_try_idle_timeout_ = + callbacks_.route_->route_entry_.retry_policy_->per_try_idle_timeout_ = std::chrono::milliseconds(3000); NiceMock encoder; @@ -2184,7 +2188,7 @@ TEST_F(RouterTest, UpstreamPerTryTimeoutExcludesNewStream) { // canceled). Also verify retry options predicates work. TEST_F(RouterTest, HedgedPerTryTimeoutFirstRequestSucceeds) { auto retry_options_predicate = std::make_shared(); - callbacks_.route_->route_entry_.retry_policy_.retry_options_predicates_.emplace_back( + callbacks_.route_->route_entry_.retry_policy_->retry_options_predicates_.emplace_back( retry_options_predicate); enableHedgeOnPerTryTimeout(); @@ -2675,7 +2679,7 @@ TEST_F(RouterTest, BadHeadersDroppedIfPreviousRetryScheduled) { // has sent any of the body. Also verify retry options predicates work. TEST_F(RouterTest, RetryRequestBeforeBody) { auto retry_options_predicate = std::make_shared(); - callbacks_.route_->route_entry_.retry_policy_.retry_options_predicates_.emplace_back( + callbacks_.route_->route_entry_.retry_policy_->retry_options_predicates_.emplace_back( retry_options_predicate); NiceMock encoder1; @@ -2694,7 +2698,7 @@ TEST_F(RouterTest, RetryRequestBeforeBody) { NiceMock encoder2; expectNewStreamWithImmediateEncoder(encoder2, &response_decoder, Http::Protocol::Http10); - EXPECT_CALL(encoder2, encodeHeaders(HeaderHasValueRef("myheader", "present"), false)); + EXPECT_CALL(encoder2, encodeHeaders(ContainsHeader("myheader", "present"), false)); router_->retry_state_->callback_(); EXPECT_EQ(2U, callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value()); @@ -2747,7 +2751,7 @@ TEST_F(RouterTest, RetryRequestDuringBody) { NiceMock encoder2; expectNewStreamWithImmediateEncoder(encoder2, &response_decoder, Http::Protocol::Http10); - EXPECT_CALL(encoder2, encodeHeaders(HeaderHasValueRef("myheader", "present"), false)); + EXPECT_CALL(encoder2, encodeHeaders(ContainsHeader("myheader", "present"), false)); EXPECT_CALL(encoder2, encodeData(BufferStringEqual(body1), false)); router_->retry_state_->callback_(); EXPECT_EQ(2U, @@ -2805,7 +2809,7 @@ TEST_F(RouterTest, RetryRequestDuringBodyDataBetweenAttemptsNotEndStream) { NiceMock encoder2; expectNewStreamWithImmediateEncoder(encoder2, &response_decoder, Http::Protocol::Http10); - EXPECT_CALL(encoder2, encodeHeaders(HeaderHasValueRef("myheader", "present"), false)); + EXPECT_CALL(encoder2, encodeHeaders(ContainsHeader("myheader", "present"), false)); EXPECT_CALL(encoder2, encodeData(BufferStringEqual(body1 + body2), false)); router_->retry_state_->callback_(); EXPECT_EQ(2U, @@ -2888,7 +2892,7 @@ TEST_F(RouterTest, RetryRequestDuringBodyCompleteBetweenAttempts) { NiceMock encoder2; expectNewStreamWithImmediateEncoder(encoder2, &response_decoder, Http::Protocol::Http10); - EXPECT_CALL(encoder2, encodeHeaders(HeaderHasValueRef("myheader", "present"), false)); + EXPECT_CALL(encoder2, encodeHeaders(ContainsHeader("myheader", "present"), false)); EXPECT_CALL(encoder2, encodeData(BufferStringEqual(body1 + body2), true)); router_->retry_state_->callback_(); EXPECT_EQ(2U, @@ -2938,7 +2942,7 @@ TEST_F(RouterTest, RetryRequestDuringBodyTrailerBetweenAttempts) { NiceMock encoder2; expectNewStreamWithImmediateEncoder(encoder2, &response_decoder, Http::Protocol::Http10); - EXPECT_CALL(encoder2, encodeHeaders(HeaderHasValueRef("myheader", "present"), false)); + EXPECT_CALL(encoder2, encodeHeaders(ContainsHeader("myheader", "present"), false)); EXPECT_CALL(encoder2, encodeData(BufferStringEqual(body1), false)); EXPECT_CALL(encoder2, encodeTrailers(HeaderMapEqualRef(&trailers))); router_->retry_state_->callback_(); @@ -2965,7 +2969,7 @@ TEST_F(RouterTest, RetryRequestDuringBodyBufferLimitExceeded) { EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); EXPECT_CALL(callbacks_, addDecodedData(_, true)) .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); - EXPECT_CALL(callbacks_.route_->route_entry_, retryShadowBufferLimit()).WillOnce(Return(10)); + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(10)); NiceMock encoder1; Http::ResponseDecoder* response_decoder = nullptr; @@ -2983,8 +2987,180 @@ TEST_F(RouterTest, RetryRequestDuringBodyBufferLimitExceeded) { router_->retry_state_->expectResetRetry(); encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); - // Complete request while there is no upstream request. - const std::string body2(50, 'a'); + // Send additional 15 bytes - total 55 bytes, which should exceed request body buffer limit (50). + const std::string body2(15, 'y'); + Buffer::OwnedImpl buf2(body2); + router_->decodeData(buf2, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// Test that router uses request_body_buffer_limit when configured instead of +// per_request_buffer_limit. +TEST_F(RouterTest, RequestBodyBufferLimitExceeded) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + // Configure a large request body buffer limit (50 bytes) but small request buffer limit (10 + // bytes). + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(50)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{ + {"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}, {"myheader", "present"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send 40 bytes - should be within request body buffer limit (50) but exceeds retry limit (10). + const std::string body1(40, 'x'); + Buffer::OwnedImpl buf1(body1); + EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true)); + router_->decodeData(buf1, false); + + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Send additional 15 bytes - total 55 bytes, which should exceed request body buffer limit (50). + const std::string body2(15, 'y'); + Buffer::OwnedImpl buf2(body2); + router_->decodeData(buf2, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// Test when request_body_buffer_limit is set we should use request_body_buffer_limit +// regardless of other settings. +TEST_F(RouterTest, BufferLimitLogicCase1RequestBodyBufferLimitSet) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + + // Case 1: request_body_buffer_limit=60, per_request_buffer_limit_bytes=20 + // Should use request_body_buffer_limit = 60 + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(60)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send initial data (55 bytes) + const std::string body1(55, 'x'); + Buffer::OwnedImpl buf1(body1); + EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true)); + router_->decodeData(buf1, false); + + // Simulate upstream failure to trigger retry logic + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Send additional data (10 bytes) - total 65 bytes should exceed limit of 60 + const std::string body2(10, 'y'); + Buffer::OwnedImpl buf2(body2); + router_->decodeData(buf2, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// When per_request_buffer_limit_bytes is set but request_body_buffer_limit is not set, +// we should use min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes). +TEST_F(RouterTest, BufferLimitLogicCase2PerRequestSetRequestBodyNotSet) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + // Set up the connection buffer limit mock to return 40 as expected + EXPECT_CALL(callbacks_, decoderBufferLimit()).WillRepeatedly(Return(40)); + + // Case 2: per_request_buffer_limit_bytes=20, request_body_buffer_limit=not set + // Should use min(20, connection_buffer_limit) = 20 (since connection limit is default 40) + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(20)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send initial data (15 bytes) + const std::string body1(15, 'x'); + Buffer::OwnedImpl buf1(body1); + EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true)); + router_->decodeData(buf1, false); + + // Simulate upstream failure to trigger retry logic + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Send additional data (10 bytes) - total 25 bytes should exceed limit of 20 + const std::string body2(10, 'y'); + Buffer::OwnedImpl buf2(body2); + router_->decodeData(buf2, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// Test that when connection limit is smaller than per_request limit, +// we use min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes) = connection limit. +TEST_F(RouterTest, BufferLimitLogicCase2ConnectionLimitSmaller) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + // Set up the connection buffer limit mock to return 40 as expected + EXPECT_CALL(callbacks_, decoderBufferLimit()).WillRepeatedly(Return(40)); + + // Case 2: per_request_buffer_limit_bytes=50, request_body_buffer_limit=not set + // Should use min(50, connection_limit) = min(50, 40) = 40 + // With consolidated approach, the effective limit should be 40 + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(40)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send initial data (35 bytes) + const std::string body1(35, 'x'); + Buffer::OwnedImpl buf1(body1); + EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true)); + router_->decodeData(buf1, false); + + // Simulate upstream failure to trigger retry logic + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Send additional data (10 bytes) - total 45 bytes should exceed connection limit of 40 + const std::string body2(10, 'y'); Buffer::OwnedImpl buf2(body2); router_->decodeData(buf2, false); @@ -2995,6 +3171,172 @@ TEST_F(RouterTest, RetryRequestDuringBodyBufferLimitExceeded) { EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); } +// Test that when neither fields are set we use per_connection_buffer_limit_bytes. +TEST_F(RouterTest, BufferLimitLogicCase3NeitherFieldSet) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + // Set up the connection buffer limit mock to return 40 as expected + EXPECT_CALL(callbacks_, decoderBufferLimit()).WillRepeatedly(Return(40)); + + // Case 3: both fields not set + // Should use connection_limit = 40 (default from RouterTestBase) + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()) + .WillRepeatedly(Return(std::numeric_limits::max())); // Not set + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send initial data (35 bytes) + const std::string body1(35, 'x'); + Buffer::OwnedImpl buf1(body1); + EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true)); + router_->decodeData(buf1, false); + + // Simulate upstream failure to trigger retry logic + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Send additional data (10 bytes) - total 45 bytes should exceed connection limit of 40 + const std::string body2(10, 'y'); + Buffer::OwnedImpl buf2(body2); + router_->decodeData(buf2, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// Test edge case: Zero limits should prevent buffering +TEST_F(RouterTest, BufferLimitLogicEdgeCaseZeroLimits) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + + // Set request_body_buffer_limit to 0 (should prevent any buffering) + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(0)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Simulate upstream failure to trigger retry logic first + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Send even 1 byte - should immediately exceed the 0 limit + const std::string body(1, 'x'); + Buffer::OwnedImpl buf(body); + EXPECT_CALL(*router_->retry_state_, enabled()).WillRepeatedly(Return(true)); + + // Should trigger buffer limit exceeded immediately + router_->decodeData(buf, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// Test mixed buffer limit scenarios with multiple content chunks +TEST_F(RouterTest, BufferLimitLogicMultipleDataChunks) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + EXPECT_CALL(callbacks_, decoderBufferLimit()).WillRepeatedly(Return(40)); + + // Buffer limit that should allow multiple small chunks but fail on larger ones + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(25)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send small chunks that accumulate near the limit + EXPECT_CALL(*router_->retry_state_, enabled()).WillRepeatedly(Return(true)); + + // First chunk: 10 bytes + const std::string body1(10, 'a'); + Buffer::OwnedImpl buf1(body1); + router_->decodeData(buf1, false); + + // Second chunk: 10 more bytes (total 20, still under 25) + const std::string body2(10, 'b'); + Buffer::OwnedImpl buf2(body2); + router_->decodeData(buf2, false); + + // Simulate upstream failure to trigger retry logic + router_->retry_state_->expectResetRetry(); + encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset); + + // Third chunk: 10 more bytes (total 30, exceeds limit of 25) + const std::string body3(10, 'c'); + Buffer::OwnedImpl buf3(body3); + router_->decodeData(buf3, false); + + EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit"); + EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ + .counter("retry_or_shadow_abandoned") + .value()); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); +} + +// Test request_body_buffer_limit with exactly uint32_t max value behavior +TEST_F(RouterTest, BufferLimitLogicMaxUint32Boundary) { + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(callbacks_, addDecodedData(_, true)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); })); + EXPECT_CALL(callbacks_, decoderBufferLimit()).WillRepeatedly(Return(40)); + + // Test exactly at uint32_t max boundary + const uint64_t large_limit = static_cast(std::numeric_limits::max()) + 100; + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()) + .WillRepeatedly(Return(large_limit)); + + NiceMock encoder1; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, false); + + // Send data that's much smaller than the large limit + const std::string body(1000, 'x'); + Buffer::OwnedImpl buf(body); + EXPECT_CALL(*router_->retry_state_, enabled()).WillRepeatedly(Return(true)); + router_->decodeData(buf, true); + + // Send a successful upstream response to complete the request + Http::ResponseHeaderMapPtr response_headers( + new Http::TestResponseHeaderMapImpl{{":status", "200"}}); + response_decoder->decodeHeaders(std::move(response_headers), true); + + // Should be successful with the large buffer limit + EXPECT_EQ(1000U, decoding_buffer.length()); + EXPECT_TRUE(verifyHostUpstreamStats(1, 0)); +} + // Two requests are sent (slow request + hedged retry) and then global timeout // is hit. Verify everything gets cleaned up. TEST_F(RouterTest, HedgedPerTryTimeoutGlobalTimeout) { @@ -3332,7 +3674,6 @@ TEST_F(RouterTest, RetryNoneHealthy) { EXPECT_CALL(callbacks_, encodeData(_, true)); EXPECT_CALL(callbacks_.stream_info_, setResponseFlag(StreamInfo::CoreResponseFlag::NoHealthyUpstream)); - EXPECT_CALL(callbacks_.route_->route_entry_, finalizeResponseHeaders(_, _)); router_->retry_state_->callback_(); EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); // Pool failure for the first try, so only 1 upstream request was made. @@ -3476,7 +3817,7 @@ TEST_F(RouterTest, NoRetryWithBodyLimit) { expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); // Set a per route body limit which disallows any buffering. - EXPECT_CALL(callbacks_.route_->route_entry_, retryShadowBufferLimit()).WillOnce(Return(0)); + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(0)); Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; HttpTestUtility::addDefaultHeaders(headers); router_->decodeHeaders(headers, false); @@ -3508,7 +3849,7 @@ TEST_F(RouterTest, NoRetryWithBodyLimitWithUpstreamHalfCloseEnabled) { expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10); // Set a per route body limit which disallows any buffering. - EXPECT_CALL(callbacks_.route_->route_entry_, retryShadowBufferLimit()).WillOnce(Return(0)); + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(0)); Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}}; HttpTestUtility::addDefaultHeaders(headers); router_->decodeHeaders(headers, false); @@ -4714,8 +5055,9 @@ makeShadowPolicy(std::string cluster = "", std::string cluster_header = "", class RouterShadowingTest : public RouterTest, public testing::WithParamInterface { public: RouterShadowingTest() : streaming_shadow_(GetParam()) { - scoped_runtime_.mergeValues( - {{"envoy.reloadable_features.streaming_shadow", streaming_shadow_ ? "true" : "false"}}); + // Add default mock for requestBodyBufferLimit which is called during router initialization. + EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()) + .WillRepeatedly(Return(std::numeric_limits::max())); // Recreate router filter so it latches the correct value of streaming shadow. router_ = std::make_unique(config_, config_->default_stats_); router_->setDecoderFilterCallbacks(callbacks_); @@ -4732,63 +5074,6 @@ class RouterShadowingTest : public RouterTest, public testing::WithParamInterfac INSTANTIATE_TEST_SUITE_P(StreamingShadow, RouterShadowingTest, testing::Bool()); -TEST_P(RouterShadowingTest, BufferingShadowWithClusterHeader) { - if (streaming_shadow_) { - GTEST_SKIP(); - } - ShadowPolicyPtr policy = makeShadowPolicy("", "some_header", "bar"); - callbacks_.route_->route_entry_.shadow_policies_.push_back(policy); - ON_CALL(callbacks_, streamId()).WillByDefault(Return(43)); - - NiceMock encoder; - Http::ResponseDecoder* response_decoder = nullptr; - expectNewStreamWithImmediateEncoder(encoder, &response_decoder, Http::Protocol::Http10); - - EXPECT_CALL( - runtime_.snapshot_, - featureEnabled("bar", testing::Matcher(Percent(0)), - 43)) - .WillOnce(Return(true)); - - expectResponseTimerCreate(); - Http::TestRequestHeaderMapImpl headers; - HttpTestUtility::addDefaultHeaders(headers); - headers.addCopy("some_header", "some_cluster"); - - router_->decodeHeaders(headers, false); - - Buffer::InstancePtr body_data(new Buffer::OwnedImpl("hello")); - - EXPECT_CALL(callbacks_, addDecodedData(_, true)); - - EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, router_->decodeData(*body_data, false)); - - Http::TestRequestTrailerMapImpl trailers{{"some", "trailer"}}; - EXPECT_CALL(callbacks_, decodingBuffer()) - .Times(AtLeast(2)) - .WillRepeatedly(Return(body_data.get())); - EXPECT_CALL(*shadow_writer_, shadow_("some_cluster", _, _)) - .WillOnce(Invoke([](const std::string&, Http::RequestMessagePtr& request, - const Http::AsyncClient::RequestOptions& options) -> void { - EXPECT_NE(request->body().length(), 0); - EXPECT_NE(nullptr, request->trailers()); - EXPECT_EQ(absl::optional(10), options.timeout); - EXPECT_TRUE(options.sampled_.value()); - })); - - router_->decodeTrailers(trailers); - EXPECT_EQ(1U, - callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value()); - - EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_.host_->outlier_detector_, - putResult(_, absl::optional(200))); - - Http::ResponseHeaderMapPtr response_headers( - new Http::TestResponseHeaderMapImpl{{":status", "200"}}); - response_decoder->decodeHeaders(std::move(response_headers), true); - EXPECT_TRUE(verifyHostUpstreamStats(1, 0)); -} - TEST_P(RouterShadowingTest, ShadowNoClusterHeaderInHeader) { ShadowPolicyPtr policy = makeShadowPolicy("", "some_header", "bar"); callbacks_.route_->route_entry_.shadow_policies_.push_back(policy); @@ -4811,9 +5096,8 @@ TEST_P(RouterShadowingTest, ShadowNoClusterHeaderInHeader) { router_->decodeHeaders(headers, false); Buffer::InstancePtr body_data(new Buffer::OwnedImpl("hello")); - if (!streaming_shadow_) { - EXPECT_CALL(callbacks_, addDecodedData(_, true)); - } + // With streaming shadows always enabled, we never call addDecodedData. + EXPECT_CALL(callbacks_, addDecodedData(_, true)).Times(0); EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, router_->decodeData(*body_data, false)); Http::TestRequestTrailerMapImpl trailers{{"some", "trailer"}}; @@ -4852,9 +5136,8 @@ TEST_P(RouterShadowingTest, ShadowClusterNameEmptyInHeader) { router_->decodeHeaders(headers, false); Buffer::InstancePtr body_data(new Buffer::OwnedImpl("hello")); - if (!streaming_shadow_) { - EXPECT_CALL(callbacks_, addDecodedData(_, true)); - } + // With streaming shadows always enabled, we never call addDecodedData. + EXPECT_CALL(callbacks_, addDecodedData(_, true)).Times(0); EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, router_->decodeData(*body_data, false)); Http::TestRequestTrailerMapImpl trailers{{"some", "trailer"}}; @@ -4930,73 +5213,8 @@ TEST_P(RouterShadowingTest, StreamingShadow) { Http::TestRequestTrailerMapImpl trailers{{"some", "trailer"}}; EXPECT_CALL(callbacks_, decodingBuffer()).Times(0); - EXPECT_CALL(foo_request, captureAndSendTrailers_(Http::HeaderValueOf("some", "trailer"))); - EXPECT_CALL(fizz_request, captureAndSendTrailers_(Http::HeaderValueOf("some", "trailer"))); - router_->decodeTrailers(trailers); - EXPECT_EQ(1U, - callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value()); - - Http::ResponseHeaderMapPtr response_headers( - new Http::TestResponseHeaderMapImpl{{":status", "200"}}); - response_decoder->decodeHeaders(std::move(response_headers), true); - EXPECT_TRUE(verifyHostUpstreamStats(1, 0)); -} - -TEST_P(RouterShadowingTest, BufferingShadow) { - if (streaming_shadow_) { - GTEST_SKIP(); - } - ShadowPolicyPtr policy = makeShadowPolicy("foo", "", "bar"); - callbacks_.route_->route_entry_.shadow_policies_.push_back(policy); - policy = makeShadowPolicy("fizz", "", "buzz", envoy::type::v3::FractionalPercent(), false); - callbacks_.route_->route_entry_.shadow_policies_.push_back(policy); - ON_CALL(callbacks_, streamId()).WillByDefault(Return(43)); - - NiceMock encoder; - Http::ResponseDecoder* response_decoder = nullptr; - expectNewStreamWithImmediateEncoder(encoder, &response_decoder, Http::Protocol::Http10); - - expectResponseTimerCreate(); - - EXPECT_CALL( - runtime_.snapshot_, - featureEnabled("bar", testing::Matcher(Percent(0)), - 43)) - .WillOnce(Return(true)); - EXPECT_CALL( - runtime_.snapshot_, - featureEnabled("buzz", - testing::Matcher(Percent(0)), 43)) - .WillOnce(Return(true)); - - Http::TestRequestHeaderMapImpl headers; - HttpTestUtility::addDefaultHeaders(headers); - router_->decodeHeaders(headers, false); - - Buffer::InstancePtr body_data(new Buffer::OwnedImpl("hello")); - EXPECT_CALL(callbacks_, addDecodedData(_, true)); - EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, router_->decodeData(*body_data, false)); - - Http::TestRequestTrailerMapImpl trailers{{"some", "trailer"}}; - EXPECT_CALL(callbacks_, decodingBuffer()) - .Times(AtLeast(2)) - .WillRepeatedly(Return(body_data.get())); - EXPECT_CALL(*shadow_writer_, shadow_("foo", _, _)) - .WillOnce(Invoke([](const std::string&, Http::RequestMessagePtr& request, - const Http::AsyncClient::RequestOptions& options) -> void { - EXPECT_NE(request->body().length(), 0); - EXPECT_NE(nullptr, request->trailers()); - EXPECT_EQ(absl::optional(10), options.timeout); - EXPECT_TRUE(options.sampled_.value()); - })); - EXPECT_CALL(*shadow_writer_, shadow_("fizz", _, _)) - .WillOnce(Invoke([](const std::string&, Http::RequestMessagePtr& request, - const Http::AsyncClient::RequestOptions& options) -> void { - EXPECT_NE(request->body().length(), 0); - EXPECT_NE(nullptr, request->trailers()); - EXPECT_EQ(absl::optional(10), options.timeout); - EXPECT_FALSE(options.sampled_.value()); - })); + EXPECT_CALL(foo_request, captureAndSendTrailers_(ContainsHeader("some", "trailer"))); + EXPECT_CALL(fizz_request, captureAndSendTrailers_(ContainsHeader("some", "trailer"))); router_->decodeTrailers(trailers); EXPECT_EQ(1U, callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value()); @@ -5024,12 +5242,7 @@ TEST_P(RouterShadowingTest, NoShadowForConnect) { router_->onDestroy(); } -// If the shadow stream watermark callbacks are invoked in the Router filter destructor, -// it causes a potential use-after-free bug, as the FilterManager may have already been freed. -TEST_P(RouterShadowingTest, ShadowCallbacksNotCalledInDestructor) { - if (!streaming_shadow_) { - GTEST_SKIP(); - } +TEST_P(RouterShadowingTest, ShadowRequestCarriesParentContext) { ShadowPolicyPtr policy = makeShadowPolicy("foo", "", "bar"); callbacks_.route_->route_entry_.shadow_policies_.push_back(policy); ON_CALL(callbacks_, streamId()).WillByDefault(Return(43)); @@ -5047,74 +5260,19 @@ TEST_P(RouterShadowingTest, ShadowCallbacksNotCalledInDestructor) { HttpTestUtility::addDefaultHeaders(headers); NiceMock foo_client; NiceMock foo_request(&foo_client); + EXPECT_CALL(*shadow_writer_, streamingShadow_("foo", _, _)) .WillOnce(Invoke([&](const std::string&, Http::RequestHeaderMapPtr&, const Http::AsyncClient::RequestOptions& options) { - EXPECT_EQ(absl::optional(10), options.timeout); - EXPECT_TRUE(options.sampled_.value()); + EXPECT_NE(options.parent_context.stream_info, nullptr); + EXPECT_EQ(callbacks_.streamInfo().route(), options.parent_context.stream_info->route()); return &foo_request; })); - router_->decodeHeaders(headers, false); - - Buffer::InstancePtr body_data(new Buffer::OwnedImpl("hello")); - EXPECT_CALL(callbacks_, addDecodedData(_, _)).Times(0); - EXPECT_CALL(foo_request, sendData(BufferStringEqual("hello"), false)); - EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, router_->decodeData(*body_data, false)); - - // Guarantee that callbacks are invoked in onDestroy instead of destructor. - { - EXPECT_CALL(foo_request, removeWatermarkCallbacks()); - EXPECT_CALL(foo_request, cancel()); - router_->onDestroy(); - } - EXPECT_CALL(foo_request, removeWatermarkCallbacks()).Times(0); - EXPECT_CALL(foo_request, cancel()).Times(0); -} - -TEST_P(RouterShadowingTest, ShadowRequestCarriesParentContext) { - ShadowPolicyPtr policy = makeShadowPolicy("foo", "", "bar"); - callbacks_.route_->route_entry_.shadow_policies_.push_back(policy); - ON_CALL(callbacks_, streamId()).WillByDefault(Return(43)); - - NiceMock encoder; - Http::ResponseDecoder* response_decoder = nullptr; - expectNewStreamWithImmediateEncoder(encoder, &response_decoder, Http::Protocol::Http10); - - EXPECT_CALL( - runtime_.snapshot_, - featureEnabled("bar", testing::Matcher(Percent(0)), - 43)) - .WillOnce(Return(true)); - Http::TestRequestHeaderMapImpl headers; - HttpTestUtility::addDefaultHeaders(headers); - NiceMock foo_client; - NiceMock foo_request(&foo_client); - if (streaming_shadow_) { - EXPECT_CALL(*shadow_writer_, streamingShadow_("foo", _, _)) - .WillOnce(Invoke([&](const std::string&, Http::RequestHeaderMapPtr&, - const Http::AsyncClient::RequestOptions& options) { - EXPECT_NE(options.parent_context.stream_info, nullptr); - EXPECT_EQ(callbacks_.streamInfo().route(), options.parent_context.stream_info->route()); - return &foo_request; - })); - } else { - EXPECT_CALL(*shadow_writer_, shadow_("foo", _, _)) - .WillOnce(Invoke([&](const std::string&, Http::RequestMessagePtr&, - const Http::AsyncClient::RequestOptions& options) { - EXPECT_NE(options.parent_context.stream_info, nullptr); - EXPECT_EQ(callbacks_.streamInfo().route(), options.parent_context.stream_info->route()); - return &foo_request; - })); - } - - const auto should_end_stream = !streaming_shadow_; - router_->decodeHeaders(headers, should_end_stream); + router_->decodeHeaders(headers, false); - if (streaming_shadow_) { - EXPECT_CALL(foo_request, removeWatermarkCallbacks()); - EXPECT_CALL(foo_request, cancel()); - } + EXPECT_CALL(foo_request, removeWatermarkCallbacks()); + EXPECT_CALL(foo_request, cancel()); router_->onDestroy(); } @@ -5170,7 +5328,7 @@ TEST_F(RouterTest, Redirect) { EXPECT_CALL(direct_response, rewritePathHeader(_, _)); EXPECT_CALL(direct_response, responseCode()).WillRepeatedly(Return(Http::Code::MovedPermanently)); EXPECT_CALL(direct_response, responseBody()).WillOnce(ReturnRef(EMPTY_STRING)); - EXPECT_CALL(direct_response, finalizeResponseHeaders(_, _)); + EXPECT_CALL(direct_response, finalizeResponseHeaders(_, _, _)); EXPECT_CALL(*callbacks_.route_, directResponseEntry()).WillRepeatedly(Return(&direct_response)); Http::TestResponseHeaderMapImpl response_headers{{":status", "301"}, {"location", "hello"}}; @@ -5190,7 +5348,7 @@ TEST_F(RouterTest, RedirectFound) { EXPECT_CALL(direct_response, rewritePathHeader(_, _)); EXPECT_CALL(direct_response, responseCode()).WillRepeatedly(Return(Http::Code::Found)); EXPECT_CALL(direct_response, responseBody()).WillOnce(ReturnRef(EMPTY_STRING)); - EXPECT_CALL(direct_response, finalizeResponseHeaders(_, _)); + EXPECT_CALL(direct_response, finalizeResponseHeaders(_, _, _)); EXPECT_CALL(*callbacks_.route_, directResponseEntry()).WillRepeatedly(Return(&direct_response)); Http::TestResponseHeaderMapImpl response_headers{{":status", "302"}, {"location", "hello"}}; @@ -5701,7 +5859,7 @@ TEST(RouterFilterUtilityTest, FinalTimeout) { } { NiceMock route; - route.retry_policy_.per_try_timeout_ = std::chrono::milliseconds(7); + route.retry_policy_->per_try_timeout_ = std::chrono::milliseconds(7); EXPECT_CALL(route, timeout()).WillOnce(Return(std::chrono::milliseconds(10))); Http::TestRequestHeaderMapImpl headers{{"x-envoy-upstream-rq-timeout-ms", "15"}}; TimeoutData timeout = FilterUtility::finalTimeout(route, headers, true, false, false, false); @@ -5714,7 +5872,7 @@ TEST(RouterFilterUtilityTest, FinalTimeout) { } { NiceMock route; - route.retry_policy_.per_try_timeout_ = std::chrono::milliseconds(10); + route.retry_policy_->per_try_timeout_ = std::chrono::milliseconds(10); EXPECT_CALL(route, timeout()).WillOnce(Return(std::chrono::milliseconds(0))); Http::TestRequestHeaderMapImpl headers; TimeoutData timeout = FilterUtility::finalTimeout(route, headers, true, false, false, false); @@ -5727,7 +5885,7 @@ TEST(RouterFilterUtilityTest, FinalTimeout) { } { NiceMock route; - route.retry_policy_.per_try_timeout_ = std::chrono::milliseconds(7); + route.retry_policy_->per_try_timeout_ = std::chrono::milliseconds(7); EXPECT_CALL(route, timeout()).WillOnce(Return(std::chrono::milliseconds(10))); Http::TestRequestHeaderMapImpl headers{{"x-envoy-upstream-rq-timeout-ms", "15"}, {"x-envoy-upstream-rq-per-try-timeout-ms", "5"}}; @@ -5880,7 +6038,7 @@ TEST(RouterFilterUtilityTest, FinalTimeout) { NiceMock route; EXPECT_CALL(route, maxGrpcTimeout()) .WillRepeatedly(Return(absl::optional(0))); - route.retry_policy_.per_try_timeout_ = std::chrono::milliseconds(7); + route.retry_policy_->per_try_timeout_ = std::chrono::milliseconds(7); Http::TestRequestHeaderMapImpl headers{{"content-type", "application/grpc"}, {"grpc-timeout", "1000m"}, {"x-envoy-upstream-rq-timeout-ms", "15"}}; @@ -5896,7 +6054,7 @@ TEST(RouterFilterUtilityTest, FinalTimeout) { NiceMock route; EXPECT_CALL(route, maxGrpcTimeout()) .WillRepeatedly(Return(absl::optional(0))); - route.retry_policy_.per_try_timeout_ = std::chrono::milliseconds(7); + route.retry_policy_->per_try_timeout_ = std::chrono::milliseconds(7); Http::TestRequestHeaderMapImpl headers{{"content-type", "application/grpc"}, {"grpc-timeout", "1000m"}, {"x-envoy-upstream-rq-timeout-ms", "15"}, @@ -6541,7 +6699,7 @@ TEST_F(RouterTest, Http3DisabledForHttp11Proxies) { TEST_F(RouterTest, ExpectedUpstreamTimeoutUpdatedDuringRetries) { auto retry_options_predicate = std::make_shared(); - callbacks_.route_->route_entry_.retry_policy_.retry_options_predicates_.emplace_back( + callbacks_.route_->route_entry_.retry_policy_->retry_options_predicates_.emplace_back( retry_options_predicate); setIncludeAttemptCountInRequest(true); @@ -6675,7 +6833,7 @@ TEST(RouterFilterUtilityTest, SetTimeoutHeaders) { Http::TestRequestHeaderMapImpl headers; TimeoutData timeout; timeout.global_timeout_ = std::chrono::milliseconds(200); - timeout.per_try_timeout_ = std::chrono::milliseconds(0); + timeout.per_try_timeout_ = std::chrono::milliseconds(150); FilterUtility::setTimeoutHeaders(300, timeout, route, headers, true, false, false); EXPECT_EQ("1", headers.get_("x-envoy-expected-rq-timeout-ms")); // Over time diff --git a/test/common/router/router_test_base.cc b/test/common/router/router_test_base.cc index 144567f98ebca..7f240e8473b39 100644 --- a/test/common/router/router_test_base.cc +++ b/test/common/router/router_test_base.cc @@ -88,8 +88,8 @@ AssertionResult RouterTestBase::verifyHostUpstreamStats(uint64_t success, uint64 } void RouterTestBase::verifyMetadataMatchCriteriaFromRequest(bool route_entry_has_match) { - ProtobufWkt::Struct request_struct, route_struct; - ProtobufWkt::Value val; + Protobuf::Struct request_struct, route_struct; + Protobuf::Value val; // Populate metadata like StreamInfo.setDynamicMetadata() would. auto& fields_map = *request_struct.mutable_fields(); diff --git a/test/common/router/scoped_rds_test.cc b/test/common/router/scoped_rds_test.cc index ef3c7e656b609..4ba7d2b41754b 100644 --- a/test/common/router/scoped_rds_test.cc +++ b/test/common/router/scoped_rds_test.cc @@ -89,8 +89,7 @@ class ScopedRoutesTestBase : public testing::Test { // The delta style API helper. Protobuf::RepeatedPtrField - anyToResource(Protobuf::RepeatedPtrField& resources, - const std::string& version) { + anyToResource(Protobuf::RepeatedPtrField& resources, const std::string& version) { Protobuf::RepeatedPtrField added_resources; for (const auto& resource_any : resources) { auto config = diff --git a/test/common/runtime/runtime_impl_test.cc b/test/common/runtime/runtime_impl_test.cc index 9678cfd650978..3e7875e4c6559 100644 --- a/test/common/runtime/runtime_impl_test.cc +++ b/test/common/runtime/runtime_impl_test.cc @@ -112,7 +112,7 @@ class DiskLoaderImplTest : public LoaderImplTest { EXPECT_TRUE(on_changed_cbs_[layer](Filesystem::Watcher::Events::MovedTo).ok()); } - ProtobufWkt::Struct base_; + Protobuf::Struct base_; }; TEST_F(DiskLoaderImplTest, EmptyKeyTest) { @@ -276,7 +276,7 @@ TEST_F(DiskLoaderImplTest, UintLargeIntegerConversion) { } TEST_F(DiskLoaderImplTest, GetLayers) { - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( foo: whatevs )EOF"); setup(); @@ -478,7 +478,7 @@ TEST_F(DiskLoaderImplTest, MergeValues) { // Validate that admin overrides disk, disk overrides bootstrap. TEST_F(DiskLoaderImplTest, LayersOverride) { - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( some: thing other: thang file2: whatevs @@ -550,7 +550,7 @@ class StaticLoaderImplTest : public LoaderImplTest { loader_ = std::move(loader.value()); } - ProtobufWkt::Struct base_; + Protobuf::Struct base_; }; TEST_F(StaticLoaderImplTest, All) { @@ -574,7 +574,7 @@ TEST_F(StaticLoaderImplTest, QuicheReloadableFlags) { EXPECT_FALSE(GetQuicheReloadableFlag(quic_testonly_default_false)); // Test that Quiche flags can be overwritten via Envoy runtime config. - base_ = TestUtility::parseYaml( + base_ = TestUtility::parseYaml( "envoy.reloadable_features.FLAGS_envoy_quiche_reloadable_flag_quic_testonly_default_true: " "true"); setup(); @@ -583,7 +583,7 @@ TEST_F(StaticLoaderImplTest, QuicheReloadableFlags) { EXPECT_FALSE(GetQuicheReloadableFlag(quic_testonly_default_false)); // Test that Quiche flags can be overwritten again. - base_ = TestUtility::parseYaml( + base_ = TestUtility::parseYaml( "envoy.reloadable_features.FLAGS_envoy_quiche_reloadable_flag_quic_testonly_default_true: " "false"); setup(); @@ -594,20 +594,20 @@ TEST_F(StaticLoaderImplTest, QuicheReloadableFlags) { #endif TEST_F(StaticLoaderImplTest, RemovedFlags) { - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( envoy.reloadable_features.removed_foo: true )EOF"); EXPECT_ENVOY_BUG(setup(), "envoy.reloadable_features.removed_foo"); } TEST_F(StaticLoaderImplTest, ProtoParsingInvalidField) { - base_ = TestUtility::parseYaml("file0:"); + base_ = TestUtility::parseYaml("file0:"); EXPECT_THROW_WITH_MESSAGE(setup(), EnvoyException, "Invalid runtime entry value for file0"); } TEST_F(StaticLoaderImplTest, ProtoParsing) { // Validate proto parsing sanity. - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( file1: hello override file2: world file3: 2 @@ -755,7 +755,7 @@ TEST_F(StaticLoaderImplTest, ProtoParsing) { EXPECT_EQ(2, store_.gauge("runtime.num_layers", Stats::Gauge::ImportMode::NeverImport).value()); // While null values are generally filtered out by walkProtoValue, test manually. - ProtobufWkt::Value empty_value; + Protobuf::Value empty_value; const_cast(dynamic_cast(loader_->snapshot())) .createEntry(empty_value, ""); } @@ -764,7 +764,7 @@ TEST_F(StaticLoaderImplTest, ProtoParsing) { // isn't an actual feature flag. TEST_F(StaticLoaderImplTest, ProtoParsingRuntimeFeaturePrefix) { // Validate proto parsing sanity. - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( envoy.reloadable_features.not_a_feature: true )EOF"); EXPECT_ENVOY_BUG(setup(), @@ -777,7 +777,7 @@ TEST_F(StaticLoaderImplTest, ProtoParsingRuntimeFeaturePrefix) { // in a debug build if the bug was present. TEST_F(StaticLoaderImplTest, ProtoParsingRuntimeFeaturePrefixLegacy) { // Validate proto parsing sanity. - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( envoy.reloadable_features.max_request_headers_count: 2 envoy.reloadable_features.max_response_headers_count: 3 envoy.reloadable_features.max_request_headers_size_kb: 4 @@ -792,7 +792,7 @@ TEST_F(StaticLoaderImplTest, ProtoParsingRuntimeFeaturePrefixLegacy) { } TEST_F(StaticLoaderImplTest, InvalidNumerator) { - base_ = TestUtility::parseYaml(R"EOF( + base_ = TestUtility::parseYaml(R"EOF( invalid_numerator: numerator: 111 denominator: HUNDRED @@ -924,8 +924,7 @@ class RtdsLoaderImplTest : public LoaderImplTest { LoaderImplTest::setup(); envoy::config::bootstrap::v3::LayeredRuntime config; - *config.add_layers()->mutable_static_layer() = - TestUtility::parseYaml(R"EOF( + *config.add_layers()->mutable_static_layer() = TestUtility::parseYaml(R"EOF( foo: whatevs bar: yar )EOF"); diff --git a/test/common/stats/histogram_impl_test.cc b/test/common/stats/histogram_impl_test.cc index 3e00f70d9f951..867863efeb041 100644 --- a/test/common/stats/histogram_impl_test.cc +++ b/test/common/stats/histogram_impl_test.cc @@ -55,6 +55,12 @@ TEST_F(HistogramSettingsImplTest, Sorted) { // Test that only matching configurations are applied. TEST_F(HistogramSettingsImplTest, Matching) { + { + envoy::config::metrics::v3::HistogramBucketSettings setting; + setting.mutable_match()->set_prefix("a"); + setting.mutable_bins()->set_value(5); + buckets_configs_.push_back(setting); + } { envoy::config::metrics::v3::HistogramBucketSettings setting; setting.mutable_match()->set_prefix("a"); @@ -74,6 +80,8 @@ TEST_F(HistogramSettingsImplTest, Matching) { initialize(); EXPECT_EQ(settings_->buckets("abcd"), ConstSupportedBuckets({1, 2})); EXPECT_EQ(settings_->buckets("bcde"), ConstSupportedBuckets({3, 4})); + EXPECT_EQ(settings_->bins("ab"), 5); + EXPECT_EQ(settings_->bins("ba"), absl::nullopt); } // Test that earlier configs take precedence over later configs when both match. @@ -83,6 +91,7 @@ TEST_F(HistogramSettingsImplTest, Priority) { setting.mutable_match()->set_prefix("a"); setting.mutable_buckets()->Add(1); setting.mutable_buckets()->Add(2); + setting.mutable_bins()->set_value(1); buckets_configs_.push_back(setting); } @@ -91,10 +100,13 @@ TEST_F(HistogramSettingsImplTest, Priority) { setting.mutable_match()->set_prefix("ab"); setting.mutable_buckets()->Add(3); setting.mutable_buckets()->Add(4); + setting.mutable_bins()->set_value(2); + buckets_configs_.push_back(setting); } initialize(); EXPECT_EQ(settings_->buckets("abcd"), ConstSupportedBuckets({1, 2})); + EXPECT_EQ(settings_->bins("abcd"), 1); } TEST_F(HistogramSettingsImplTest, ScaledPercent) { diff --git a/test/common/stats/tag_extractor_impl_test.cc b/test/common/stats/tag_extractor_impl_test.cc index 0d2605c6d1666..e5de22ffe61af 100644 --- a/test/common/stats/tag_extractor_impl_test.cc +++ b/test/common/stats/tag_extractor_impl_test.cc @@ -526,7 +526,8 @@ TEST(TagExtractorTest, DefaultTagExtractors) { listener_address.value_ = "0.0.0.0_0"; regex_tester.testRegex( "listener.0.0.0.0_0.ssl.certificate.server_cert.expiration_unix_time_seconds", - "listener.ssl.certificate", {listener_address, certificate_name}); + "listener.ssl.certificate.expiration_unix_time_seconds", + {listener_address, certificate_name}); // Cluster test Tag test_cluster; @@ -534,7 +535,7 @@ TEST(TagExtractorTest, DefaultTagExtractors) { test_cluster.value_ = "test_cluster"; regex_tester.testRegex( "cluster.test_cluster.ssl.certificate.server_cert.expiration_unix_time_seconds", - "cluster.ssl.certificate", {test_cluster, certificate_name}); + "cluster.ssl.certificate.expiration_unix_time_seconds", {test_cluster, certificate_name}); } TEST(TagExtractorTest, ExtAuthzTagExtractors) { diff --git a/test/common/stats/thread_local_store_test.cc b/test/common/stats/thread_local_store_test.cc index e21ad17d3b4f0..9c950ccf3f0f9 100644 --- a/test/common/stats/thread_local_store_test.cc +++ b/test/common/stats/thread_local_store_test.cc @@ -606,6 +606,86 @@ TEST_F(StatsThreadLocalStoreTest, ScopeDelete) { tls_.shutdownThread(); } +TEST_F(StatsThreadLocalStoreTest, Eviction) { + InSequence s; + store_->initializeThreading(main_thread_dispatcher_, tls_); + + ScopeSharedPtr scope = store_->createScope("scope.", true); + ScopeSharedPtr scope1 = store_->createScope("scope.", true); + // References will become invalid, so we create a lexical scope. + { + Counter& c1 = scope->counterFromString("c1"); + EXPECT_EQ(&c1, &scope1->counterFromString("c1")); + c1.add(1); + EXPECT_TRUE(c1.used()); + + Gauge& g1 = scope->gaugeFromString("g1", Gauge::ImportMode::Accumulate); + g1.set(5); + EXPECT_TRUE(g1.used()); + + TextReadout& t1 = scope->textReadoutFromString("t1"); + t1.set("hello"); + EXPECT_TRUE(t1.used()); + + Histogram& h1 = scope->histogramFromString("h1", Histogram::Unit::Unspecified); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 1)); + h1.recordValue(1); + store_->mergeHistograms([]() -> void {}); + + // Eviction only marks unused but does not remove the counters. + store_->evictUnused(); + + EXPECT_EQ(&c1, &scope->counterFromString("c1")); + EXPECT_FALSE(c1.used()); + EXPECT_EQ(1, c1.value()); + EXPECT_EQ(1UL, store_->counters().size()); + + EXPECT_EQ(&g1, &scope->gaugeFromString("g1", Gauge::ImportMode::Accumulate)); + EXPECT_EQ(&g1, &scope1->gaugeFromString("g1", Gauge::ImportMode::Accumulate)); + EXPECT_FALSE(g1.used()); + EXPECT_EQ(5, g1.value()); + EXPECT_EQ(1UL, store_->gauges().size()); + + EXPECT_EQ(&t1, &scope->textReadoutFromString("t1")); + EXPECT_EQ(&t1, &scope1->textReadoutFromString("t1")); + EXPECT_FALSE(t1.used()); + EXPECT_EQ("hello", t1.value()); + EXPECT_EQ(1UL, store_->textReadouts().size()); + + EXPECT_EQ(&h1, &scope->histogramFromString("h1", Histogram::Unit::Unspecified)); + EXPECT_EQ(&h1, &scope1->histogramFromString("h1", Histogram::Unit::Unspecified)); + EXPECT_FALSE(h1.used()); + EXPECT_EQ(1UL, store_->histograms().size()); + } + + // Eviction removes here. + EXPECT_CALL(tls_, runOnAllThreads(_, _)).Times(testing::AtLeast(1)); + store_->evictUnused(); + EXPECT_EQ(0UL, store_->counters().size()); + EXPECT_EQ(0UL, store_->gauges().size()); + EXPECT_EQ(0UL, store_->textReadouts().size()); + EXPECT_EQ(0UL, store_->histograms().size()); + + // Make sure no dangling data is on caches and it is safe to use the same metrics. + { + scope->counterFromString("c1").add(1); + scope1->counterFromString("c1").add(1); + scope->gaugeFromString("g1", Gauge::ImportMode::Accumulate).set(5); + scope1->gaugeFromString("g1", Gauge::ImportMode::Accumulate).set(5); + scope->textReadoutFromString("t1").set("hello"); + scope1->textReadoutFromString("t1").set("hello"); + Histogram& h1 = scope->histogramFromString("h1", Histogram::Unit::Unspecified); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 1)); + h1.recordValue(1); + Histogram& h2 = scope1->histogramFromString("h1", Histogram::Unit::Unspecified); + EXPECT_EQ(&h1, &h2); + } + + tls_.shutdownGlobalThreading(); + store_->shutdownThreading(); + tls_.shutdownThread(); +} + TEST_F(StatsThreadLocalStoreTest, NestedScopes) { InSequence s; store_->initializeThreading(main_thread_dispatcher_, tls_); @@ -1854,6 +1934,126 @@ TEST_F(HistogramTest, ForEachHistogram) { EXPECT_EQ(deleted_histogram.unit(), Histogram::Unit::Unspecified); } +TEST_F(HistogramTest, ForEachSinkedHistogram) { + std::unique_ptr test_sink_predicates = + std::make_unique(); + std::vector> sinked_histograms; + std::vector> unsinked_histograms; + auto scope = store_->rootScope(); + + const size_t num_stats = 11; + // Create some histograms before setting the predicates. + for (size_t idx = 0; idx < num_stats / 2; ++idx) { + auto name = absl::StrCat("histogram.", idx); + StatName stat_name = pool_.add(name); + // sink every 3rd stat + if ((idx + 1) % 3 == 0) { + test_sink_predicates->add(stat_name); + sinked_histograms.emplace_back( + scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); + } else { + unsinked_histograms.emplace_back( + scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); + } + } + + store_->setSinkPredicates(std::move(test_sink_predicates)); + auto& sink_predicates = testSinkPredicatesOrDie(); + + // Create some histograms after setting the predicates. + for (size_t idx = num_stats / 2; idx < num_stats; ++idx) { + auto name = absl::StrCat("histogram.", idx); + StatName stat_name = pool_.add(name); + // sink every 3rd stat + if ((idx + 1) % 3 == 0) { + sink_predicates.add(stat_name); + sinked_histograms.emplace_back( + scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); + } else { + unsinked_histograms.emplace_back( + scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); + } + } + + EXPECT_EQ(sinked_histograms.size(), 3); + EXPECT_EQ(unsinked_histograms.size(), 8); + + size_t num_sinked_histograms = 0; + size_t num_iterations = 0; + store_->forEachSinkedHistogram( + [&num_sinked_histograms](std::size_t size) { num_sinked_histograms = size; }, + [&num_iterations, &sink_predicates](ParentHistogram& histogram) { + EXPECT_TRUE(sink_predicates.has(histogram.statName())); + ++num_iterations; + }); + EXPECT_EQ(num_sinked_histograms, 3); + EXPECT_EQ(num_iterations, 3); + // Verify that rejecting histograms removes them from the sink set. + envoy::config::metrics::v3::StatsConfig stats_config_; + stats_config_.mutable_stats_matcher()->set_reject_all(true); + store_->setStatsMatcher( + std::make_unique(stats_config_, symbol_table_, context_)); + num_sinked_histograms = 0; + num_iterations = 0; + store_->forEachSinkedHistogram( + [&num_sinked_histograms](std::size_t size) { num_sinked_histograms = size; }, + [&num_iterations](ParentHistogram&) { ++num_iterations; }); + EXPECT_EQ(num_sinked_histograms, 0); + EXPECT_EQ(num_iterations, 0); +} + +// Verify that histograms that are not flushed to sinks are merged in the call +// to mergeHistograms +TEST_F(HistogramTest, UnsinkedHistogramsAreMerged) { + store_->setSinkPredicates(std::make_unique()); + auto& sink_predicates = testSinkPredicatesOrDie(); + StatName stat_name = pool_.add("h1"); + sink_predicates.add(stat_name); + auto scope = store_->rootScope(); + + auto& h1 = static_cast( + scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); + stat_name = pool_.add("h2"); + auto& h2 = static_cast( + scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); + + EXPECT_EQ("h1", h1.name()); + EXPECT_EQ("h2", h2.name()); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 5)); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h2), 5)); + + h1.recordValue(5); + h2.recordValue(5); + + EXPECT_THAT(h1.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 0,")); + EXPECT_THAT(h2.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 0,")); + + // Verify that all the histograms have not been merged yet. + EXPECT_EQ(h1.used(), false); + EXPECT_EQ(h2.used(), false); + + store_->mergeHistograms([this, &sink_predicates]() -> void { + size_t num_iterations = 0; + size_t num_sinked_histograms = 0; + store_->forEachSinkedHistogram( + [&num_sinked_histograms](std::size_t size) { num_sinked_histograms = size; }, + [&num_iterations, &sink_predicates](ParentHistogram& histogram) { + EXPECT_TRUE(sink_predicates.has(histogram.statName())); + ++num_iterations; + }); + EXPECT_EQ(num_sinked_histograms, 1); + EXPECT_EQ(num_iterations, 1); + }); + + EXPECT_THAT(h1.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 1,")); + EXPECT_THAT(h2.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 1,")); + EXPECT_EQ(h1.cumulativeStatistics().bucketSummary(), h2.cumulativeStatistics().bucketSummary()); + + // Verify that all the histograms have been merged. + EXPECT_EQ(h1.used(), true); + EXPECT_EQ(h2.used(), true); +} + class OneWorkerThread : public ThreadLocalRealThreadsMixin, public testing::Test { protected: static constexpr uint32_t NumThreads = 1; @@ -2152,193 +2352,5 @@ TEST_F(StatsThreadLocalStoreTest, SetSinkPredicates) { }); EXPECT_EQ(expected_sinked_stats, num_sinked_text_readouts); } - -enum class EnableIncludeHistograms { No = 0, Yes }; -class HistogramParameterisedTest : public HistogramTest, - public ::testing::WithParamInterface { -public: - HistogramParameterisedTest() { local_info_.node_.set_cluster(""); } - -protected: - void SetUp() override { - HistogramTest::SetUp(); - - // Set the feature flag in SetUp as store_ is constructed in HistogramTest::SetUp. - api_ = Api::createApiForTest(*store_); - ProtobufWkt::Struct base = TestUtility::parseYaml( - GetParam() == EnableIncludeHistograms::Yes ? R"EOF( - envoy.reloadable_features.enable_include_histograms: true - )EOF" - : R"EOF( - envoy.reloadable_features.enable_include_histograms: false - )EOF"); - envoy::config::bootstrap::v3::LayeredRuntime layered_runtime; - { - auto* layer = layered_runtime.add_layers(); - layer->set_name("base"); - layer->mutable_static_layer()->MergeFrom(base); - } - { - auto* layer = layered_runtime.add_layers(); - layer->set_name("admin"); - layer->mutable_admin_layer(); - } - absl::StatusOr> loader = - Runtime::LoaderImpl::create(dispatcher_, tls_, layered_runtime, local_info_, *store_, - generator_, validation_visitor_, *api_); - THROW_IF_NOT_OK(loader.status()); - loader_ = std::move(loader.value()); - } - - NiceMock context_; - Event::MockDispatcher dispatcher_; - Api::ApiPtr api_; - NiceMock local_info_; - Random::MockRandomGenerator generator_; - NiceMock validation_visitor_; - std::unique_ptr loader_; -}; - -TEST_P(HistogramParameterisedTest, ForEachSinkedHistogram) { - std::unique_ptr test_sink_predicates = - std::make_unique(); - std::vector> sinked_histograms; - std::vector> unsinked_histograms; - auto scope = store_->rootScope(); - - const size_t num_stats = 11; - // Create some histograms before setting the predicates. - for (size_t idx = 0; idx < num_stats / 2; ++idx) { - auto name = absl::StrCat("histogram.", idx); - StatName stat_name = pool_.add(name); - // sink every 3rd stat - if ((idx + 1) % 3 == 0) { - test_sink_predicates->add(stat_name); - sinked_histograms.emplace_back( - scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); - } else { - unsinked_histograms.emplace_back( - scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); - } - } - - store_->setSinkPredicates(std::move(test_sink_predicates)); - auto& sink_predicates = testSinkPredicatesOrDie(); - - // Create some histograms after setting the predicates. - for (size_t idx = num_stats / 2; idx < num_stats; ++idx) { - auto name = absl::StrCat("histogram.", idx); - StatName stat_name = pool_.add(name); - // sink every 3rd stat - if ((idx + 1) % 3 == 0) { - sink_predicates.add(stat_name); - sinked_histograms.emplace_back( - scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); - } else { - unsinked_histograms.emplace_back( - scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); - } - } - - EXPECT_EQ(sinked_histograms.size(), 3); - EXPECT_EQ(unsinked_histograms.size(), 8); - - size_t num_sinked_histograms = 0; - size_t num_iterations = 0; - store_->forEachSinkedHistogram( - [&num_sinked_histograms](std::size_t size) { num_sinked_histograms = size; }, - [&num_iterations, &sink_predicates](ParentHistogram& histogram) { - if (GetParam() == EnableIncludeHistograms::Yes) { - EXPECT_TRUE(sink_predicates.has(histogram.statName())); - } - ++num_iterations; - }); - if (GetParam() == EnableIncludeHistograms::Yes) { - EXPECT_EQ(num_sinked_histograms, 3); - EXPECT_EQ(num_iterations, 3); - } else { - EXPECT_EQ(num_sinked_histograms, 11); - EXPECT_EQ(num_iterations, 11); - } - // Verify that rejecting histograms removes them from the sink set. - envoy::config::metrics::v3::StatsConfig stats_config_; - stats_config_.mutable_stats_matcher()->set_reject_all(true); - store_->setStatsMatcher( - std::make_unique(stats_config_, symbol_table_, context_)); - num_sinked_histograms = 0; - num_iterations = 0; - store_->forEachSinkedHistogram( - [&num_sinked_histograms](std::size_t size) { num_sinked_histograms = size; }, - [&num_iterations](ParentHistogram&) { ++num_iterations; }); - EXPECT_EQ(num_sinked_histograms, 0); - EXPECT_EQ(num_iterations, 0); -} - -// Verify that histograms that are not flushed to sinks are merged in the call -// to mergeHistograms -TEST_P(HistogramParameterisedTest, UnsinkedHistogramsAreMerged) { - store_->setSinkPredicates(std::make_unique()); - auto& sink_predicates = testSinkPredicatesOrDie(); - StatName stat_name = pool_.add("h1"); - sink_predicates.add(stat_name); - auto scope = store_->rootScope(); - - auto& h1 = static_cast( - scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); - stat_name = pool_.add("h2"); - auto& h2 = static_cast( - scope->histogramFromStatName(stat_name, Histogram::Unit::Unspecified)); - - EXPECT_EQ("h1", h1.name()); - EXPECT_EQ("h2", h2.name()); - EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 5)); - EXPECT_CALL(sink_, onHistogramComplete(Ref(h2), 5)); - - h1.recordValue(5); - h2.recordValue(5); - - EXPECT_THAT(h1.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 0,")); - EXPECT_THAT(h2.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 0,")); - - // Verify that all the histograms have not been merged yet. - EXPECT_EQ(h1.used(), false); - EXPECT_EQ(h2.used(), false); - - store_->mergeHistograms([this, &sink_predicates]() -> void { - size_t num_iterations = 0; - size_t num_sinked_histograms = 0; - store_->forEachSinkedHistogram( - [&num_sinked_histograms](std::size_t size) { num_sinked_histograms = size; }, - [&num_iterations, &sink_predicates](ParentHistogram& histogram) { - if (GetParam() == EnableIncludeHistograms::Yes) { - EXPECT_TRUE(sink_predicates.has(histogram.statName())); - } - ++num_iterations; - }); - if (GetParam() == EnableIncludeHistograms::Yes) { - EXPECT_EQ(num_sinked_histograms, 1); - EXPECT_EQ(num_iterations, 1); - } else { - EXPECT_EQ(num_sinked_histograms, 2); - EXPECT_EQ(num_iterations, 2); - } - }); - - EXPECT_THAT(h1.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 1,")); - EXPECT_THAT(h2.cumulativeStatistics().bucketSummary(), HasSubstr(" B10: 1,")); - EXPECT_EQ(h1.cumulativeStatistics().bucketSummary(), h2.cumulativeStatistics().bucketSummary()); - - // Verify that all the histograms have been merged. - EXPECT_EQ(h1.used(), true); - EXPECT_EQ(h2.used(), true); -} - -INSTANTIATE_TEST_SUITE_P(HistogramParameterisedTestGroup, HistogramParameterisedTest, - testing::Values(EnableIncludeHistograms::Yes, EnableIncludeHistograms::No), - [](const testing::TestParamInfo& info) { - return info.param == EnableIncludeHistograms::No - ? "DisableIncludeHistograms" - : "EnableIncludeHistograms"; - }); } // namespace Stats } // namespace Envoy diff --git a/test/common/stream_info/BUILD b/test/common/stream_info/BUILD index 63d39eb6a350b..c1e6ff8afe6a0 100644 --- a/test/common/stream_info/BUILD +++ b/test/common/stream_info/BUILD @@ -69,7 +69,6 @@ envoy_cc_test( deps = [ "//source/common/stream_info:utility_lib", "//test/mocks/stream_info:stream_info_mocks", - "//test/test_common:test_runtime_lib", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", ], ) diff --git a/test/common/stream_info/stream_info_impl_test.cc b/test/common/stream_info/stream_info_impl_test.cc index 6cbd768df1064..2bd3816b5fdfa 100644 --- a/test/common/stream_info/stream_info_impl_test.cc +++ b/test/common/stream_info/stream_info_impl_test.cc @@ -516,8 +516,8 @@ TEST_F(StreamInfoImplTest, DynamicMetadataTest) { EXPECT_EQ("test_value", Config::Metadata::metadataValue(&stream_info.dynamicMetadata(), "com.test", "test_key") .string_value()); - ProtobufWkt::Struct struct_obj2; - ProtobufWkt::Value val2; + Protobuf::Struct struct_obj2; + Protobuf::Value val2; val2.set_string_value("another_value"); (*struct_obj2.mutable_fields())["another_key"] = val2; stream_info.setDynamicMetadata("com.test", struct_obj2); diff --git a/test/common/stream_info/uint32_accessor_impl_test.cc b/test/common/stream_info/uint32_accessor_impl_test.cc index 62f5b542e233a..d7941d9e5b283 100644 --- a/test/common/stream_info/uint32_accessor_impl_test.cc +++ b/test/common/stream_info/uint32_accessor_impl_test.cc @@ -25,7 +25,7 @@ TEST(UInt32AccessorImplTest, TestProto) { auto message = accessor.serializeAsProto(); EXPECT_NE(nullptr, message); - auto* uint32_struct = dynamic_cast(message.get()); + auto* uint32_struct = dynamic_cast(message.get()); EXPECT_NE(nullptr, uint32_struct); EXPECT_EQ(init_value, uint32_struct->value()); } diff --git a/test/common/stream_info/uint64_accessor_impl_test.cc b/test/common/stream_info/uint64_accessor_impl_test.cc index 8767001919383..4b3b206d16a17 100644 --- a/test/common/stream_info/uint64_accessor_impl_test.cc +++ b/test/common/stream_info/uint64_accessor_impl_test.cc @@ -26,7 +26,7 @@ TEST(UInt64AccessorImplTest, TestProto) { auto message = accessor.serializeAsProto(); EXPECT_NE(nullptr, message); - auto* uint64_struct = dynamic_cast(message.get()); + auto* uint64_struct = dynamic_cast(message.get()); EXPECT_NE(nullptr, uint64_struct); EXPECT_EQ(init_value, uint64_struct->value()); } diff --git a/test/common/stream_info/utility_test.cc b/test/common/stream_info/utility_test.cc index 89be1de3428ae..a8bb517c88423 100644 --- a/test/common/stream_info/utility_test.cc +++ b/test/common/stream_info/utility_test.cc @@ -4,7 +4,6 @@ #include "source/common/stream_info/utility.h" #include "test/mocks/stream_info/mocks.h" -#include "test/test_common/test_runtime.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -364,44 +363,7 @@ TEST(ProxyStatusErrorToString, TestAll) { } } -TEST(ProxyStatusFromStreamInfo, TestAll) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.proxy_status_mapping_more_core_response_flags", "false"}}); - for (const auto& [response_flag, proxy_status_error] : - std::vector>{ - {CoreResponseFlag::FailedLocalHealthCheck, ProxyStatusError::DestinationUnavailable}, - {CoreResponseFlag::NoHealthyUpstream, ProxyStatusError::DestinationUnavailable}, - {CoreResponseFlag::UpstreamRequestTimeout, ProxyStatusError::HttpResponseTimeout}, - {CoreResponseFlag::LocalReset, ProxyStatusError::ConnectionTimeout}, - {CoreResponseFlag::UpstreamRemoteReset, ProxyStatusError::ConnectionTerminated}, - {CoreResponseFlag::UpstreamConnectionFailure, ProxyStatusError::ConnectionRefused}, - {CoreResponseFlag::UpstreamConnectionTermination, - ProxyStatusError::ConnectionTerminated}, - {CoreResponseFlag::UpstreamOverflow, ProxyStatusError::ConnectionLimitReached}, - {CoreResponseFlag::NoRouteFound, ProxyStatusError::DestinationNotFound}, - {CoreResponseFlag::RateLimited, ProxyStatusError::ConnectionLimitReached}, - {CoreResponseFlag::RateLimitServiceError, ProxyStatusError::ConnectionLimitReached}, - {CoreResponseFlag::UpstreamRetryLimitExceeded, ProxyStatusError::DestinationUnavailable}, - {CoreResponseFlag::StreamIdleTimeout, ProxyStatusError::HttpResponseTimeout}, - {CoreResponseFlag::InvalidEnvoyRequestHeaders, ProxyStatusError::HttpRequestError}, - {CoreResponseFlag::DownstreamProtocolError, ProxyStatusError::HttpRequestError}, - {CoreResponseFlag::UpstreamMaxStreamDurationReached, - ProxyStatusError::HttpResponseTimeout}, - {CoreResponseFlag::NoFilterConfigFound, ProxyStatusError::ProxyConfigurationError}, - {CoreResponseFlag::UpstreamProtocolError, ProxyStatusError::HttpProtocolError}, - {CoreResponseFlag::NoClusterFound, ProxyStatusError::DestinationUnavailable}, - {CoreResponseFlag::DnsResolutionFailed, ProxyStatusError::DnsError}}) { - NiceMock stream_info; - ON_CALL(stream_info, hasResponseFlag(response_flag)).WillByDefault(Return(true)); - EXPECT_THAT(ProxyStatusUtils::fromStreamInfo(stream_info), proxy_status_error); - } -} - -TEST(ProxyStatusFromStreamInfo, TestNewAll) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.proxy_status_mapping_more_core_response_flags", "true"}}); +TEST(ProxyStatusFromStreamInfo, TestAllWithExpandedResponseFlags) { for (const auto& [response_flag, proxy_status_error] : std::vector>{ {CoreResponseFlag::FailedLocalHealthCheck, ProxyStatusError::DestinationUnavailable}, diff --git a/test/common/tcp/conn_pool_test.cc b/test/common/tcp/conn_pool_test.cc index 3ab7803a86050..509bd29a8b909 100644 --- a/test/common/tcp/conn_pool_test.cc +++ b/test/common/tcp/conn_pool_test.cc @@ -8,7 +8,6 @@ #include "source/common/upstream/upstream_impl.h" #include "test/common/upstream/utility.h" -#include "test/mocks/common.h" #include "test/mocks/event/mocks.h" #include "test/mocks/network/mocks.h" #include "test/mocks/runtime/mocks.h" @@ -59,7 +58,7 @@ struct ConnPoolCallbacks : public Tcp::ConnectionPool::Callbacks { conn_data_->addUpstreamCallbacks(callbacks_); host_ = host; ssl_ = conn_data_->connection().streamInfo().downstreamAddressProvider().sslConnection(); - pool_ready_.ready(); + mock_pool_ready_cb_.Call(); } void onPoolFailure(ConnectionPool::PoolFailureReason reason, absl::string_view failure_reason, @@ -67,12 +66,12 @@ struct ConnPoolCallbacks : public Tcp::ConnectionPool::Callbacks { reason_ = reason; host_ = host; failure_reason_string_ = std::string(failure_reason); - pool_failure_.ready(); + mock_pool_failure_cb_.Call(); } StrictMock callbacks_; - ReadyWatcher pool_failure_; - ReadyWatcher pool_ready_; + testing::MockFunction mock_pool_failure_cb_; + testing::MockFunction mock_pool_ready_cb_; ConnectionPool::ConnectionDataPtr conn_data_{}; absl::optional reason_; std::string failure_reason_string_; @@ -301,7 +300,7 @@ class TcpConnPoolImplDestructorTest : public Event::TestUsingSimulatedTime, publ EXPECT_NE(nullptr, handle); EXPECT_CALL(*connect_timer_, disableTimer()); - EXPECT_CALL(callbacks_->pool_ready_, ready()); + EXPECT_CALL(callbacks_->mock_pool_ready_cb_, Call); connection_->raiseEvent(Network::ConnectionEvent::Connected); connection_->stream_info_.downstream_connection_info_provider_->setSslConnection(ssl_); } @@ -364,7 +363,7 @@ struct ActiveTestConn { completed_ = true; } - void expectNewConn() { EXPECT_CALL(callbacks_.pool_ready_, ready()); } + void expectNewConn() { EXPECT_CALL(callbacks_.mock_pool_ready_cb_, Call); } void releaseConn() { callbacks_.conn_data_.reset(); } @@ -564,7 +563,7 @@ TEST_F(TcpConnPoolImplTest, VerifyBufferLimitsAndOptions) { EXPECT_CALL(*cluster_, perConnectionBufferLimitBytes()).WillOnce(Return(8192)); EXPECT_CALL(*conn_pool_->test_conns_.back().connection_, setBufferLimits(8192)); - EXPECT_CALL(callbacks.pool_failure_, ready()); + EXPECT_CALL(callbacks.mock_pool_failure_cb_, Call); Tcp::ConnectionPool::Cancellable* handle = conn_pool_->newConnection(callbacks); EXPECT_NE(nullptr, handle); @@ -684,7 +683,7 @@ TEST_F(TcpConnPoolImplTest, MaxPendingRequests) { EXPECT_NE(nullptr, handle); ConnPoolCallbacks callbacks2; - EXPECT_CALL(callbacks2.pool_failure_, ready()); + EXPECT_CALL(callbacks2.mock_pool_failure_cb_, Call); Tcp::ConnectionPool::Cancellable* handle2 = conn_pool_->newConnection(callbacks2); EXPECT_EQ(nullptr, handle2); @@ -712,7 +711,7 @@ TEST_F(TcpConnPoolImplTest, RemoteConnectFailure) { Tcp::ConnectionPool::Cancellable* handle = conn_pool_->newConnection(callbacks); EXPECT_NE(nullptr, handle); - EXPECT_CALL(callbacks.pool_failure_, ready()); + EXPECT_CALL(callbacks.mock_pool_failure_cb_, Call); EXPECT_CALL(*conn_pool_->test_conns_[0].connect_timer_, disableTimer()); EXPECT_CALL(*conn_pool_, onConnDestroyedForTest()); @@ -741,7 +740,7 @@ TEST_F(TcpConnPoolImplTest, LocalConnectFailure) { Tcp::ConnectionPool::Cancellable* handle = conn_pool_->newConnection(callbacks); EXPECT_NE(nullptr, handle); - EXPECT_CALL(callbacks.pool_failure_, ready()); + EXPECT_CALL(callbacks.mock_pool_failure_cb_, Call); EXPECT_CALL(*conn_pool_->test_conns_[0].connect_timer_, disableTimer()); EXPECT_CALL(*conn_pool_, onConnDestroyedForTest()); @@ -766,14 +765,14 @@ TEST_F(TcpConnPoolImplTest, ConnectTimeout) { EXPECT_NE(nullptr, conn_pool_->newConnection(callbacks1)); ConnPoolCallbacks callbacks2; - EXPECT_CALL(callbacks1.pool_failure_, ready()).WillOnce(Invoke([&]() -> void { + EXPECT_CALL(callbacks1.mock_pool_failure_cb_, Call).WillOnce([&]() -> void { conn_pool_->expectConnCreate(); EXPECT_NE(nullptr, conn_pool_->newConnection(callbacks2)); - })); + }); conn_pool_->test_conns_[0].connect_timer_->invokeCallback(); - EXPECT_CALL(callbacks2.pool_failure_, ready()); + EXPECT_CALL(callbacks2.mock_pool_failure_cb_, Call); conn_pool_->test_conns_[1].connect_timer_->invokeCallback(); EXPECT_CALL(*conn_pool_, onConnDestroyedForTest()).Times(2); @@ -838,7 +837,7 @@ TEST_F(TcpConnPoolImplTest, DisconnectWhileBound) { Tcp::ConnectionPool::Cancellable* handle = conn_pool_->newConnection(callbacks); EXPECT_NE(nullptr, handle); - EXPECT_CALL(callbacks.pool_ready_, ready()); + EXPECT_CALL(callbacks.mock_pool_ready_cb_, Call); EXPECT_CALL(callbacks.callbacks_, onEvent(_)); conn_pool_->test_conns_[0].connection_->raiseEvent(Network::ConnectionEvent::Connected); @@ -863,7 +862,7 @@ TEST_F(TcpConnPoolImplTest, DisconnectWhilePending) { EXPECT_NE(nullptr, handle); EXPECT_CALL(*conn_pool_->test_conns_[0].connect_timer_, disableTimer()); - EXPECT_CALL(callbacks.pool_ready_, ready()); + EXPECT_CALL(callbacks.mock_pool_ready_cb_, Call); EXPECT_CALL(callbacks.callbacks_, onEvent(_)); conn_pool_->test_conns_[0].connection_->raiseEvent(Network::ConnectionEvent::Connected); @@ -881,7 +880,7 @@ TEST_F(TcpConnPoolImplTest, DisconnectWhilePending) { // test_conns_[1] is the new connection EXPECT_CALL(*conn_pool_->test_conns_[1].connect_timer_, disableTimer()); - EXPECT_CALL(callbacks2.pool_ready_, ready()); + EXPECT_CALL(callbacks2.mock_pool_ready_cb_, Call); conn_pool_->test_conns_[1].connection_->raiseEvent(Network::ConnectionEvent::Connected); EXPECT_CALL(*conn_pool_, onConnReleasedForTest()); @@ -913,13 +912,13 @@ TEST_F(TcpConnPoolImplTest, MaxConnections) { EXPECT_NE(nullptr, handle); // Connect event will bind to request 1. - EXPECT_CALL(callbacks.pool_ready_, ready()); + EXPECT_CALL(callbacks.mock_pool_ready_cb_, Call); conn_pool_->test_conns_[0].connection_->raiseEvent(Network::ConnectionEvent::Connected); // Finishing request 1 will immediately bind to request 2. EXPECT_CALL(*conn_pool_, onConnReleasedForTest()); conn_pool_->expectEnableUpstreamReady(false); - EXPECT_CALL(callbacks2.pool_ready_, ready()); + EXPECT_CALL(callbacks2.mock_pool_ready_cb_, Call); callbacks.conn_data_.reset(); conn_pool_->expectEnableUpstreamReady(true); @@ -947,7 +946,7 @@ TEST_F(TcpConnPoolImplTest, MaxRequestsPerConnection) { EXPECT_NE(nullptr, handle); - EXPECT_CALL(callbacks.pool_ready_, ready()); + EXPECT_CALL(callbacks.mock_pool_ready_cb_, Call); conn_pool_->test_conns_[0].connection_->raiseEvent(Network::ConnectionEvent::Connected); EXPECT_CALL(*conn_pool_, onConnReleasedForTest()); @@ -1214,7 +1213,7 @@ TEST_F(TcpConnPoolImplDestructorTest, TestPendingConnectionsAreClosed) { ConnectionPool::Cancellable* handle = conn_pool_->newConnection(*callbacks_); EXPECT_NE(nullptr, handle); - EXPECT_CALL(callbacks_->pool_failure_, ready()); + EXPECT_CALL(callbacks_->mock_pool_failure_cb_, Call); EXPECT_CALL(*connection_, close(Network::ConnectionCloseType::NoFlush)); EXPECT_CALL(dispatcher_, clearDeferredDeleteList()); conn_pool_.reset(); diff --git a/test/common/tcp_proxy/BUILD b/test/common/tcp_proxy/BUILD index 332e0fda12511..889ac99975fd2 100644 --- a/test/common/tcp_proxy/BUILD +++ b/test/common/tcp_proxy/BUILD @@ -94,5 +94,7 @@ envoy_cc_test( "//test/mocks/upstream:cluster_manager_mocks", "//test/mocks/upstream:load_balancer_context_mock", "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/request_id/uuid/v3:pkg_cc_proto", ], ) diff --git a/test/common/tcp_proxy/config_test.cc b/test/common/tcp_proxy/config_test.cc index 72403ebfcbb7f..1928cf8ad3b57 100644 --- a/test/common/tcp_proxy/config_test.cc +++ b/test/common/tcp_proxy/config_test.cc @@ -297,7 +297,7 @@ TEST(ConfigTest, WeightedClustersWithMetadataMatchConfig) { Config config_obj(constructConfigFromYaml(yaml, factory_context)); { - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -324,7 +324,7 @@ TEST(ConfigTest, WeightedClustersWithMetadataMatchConfig) { } { - ProtobufWkt::Value v3, v4; + Protobuf::Value v3, v4; v3.set_string_value("v3"); v4.set_string_value("v4"); HashedValue hv3(v3), hv4(v4); @@ -383,14 +383,14 @@ TEST(ConfigTest, WeightedClustersWithMetadataMatchAndTopLevelMetadataMatchConfig NiceMock factory_context; Config config_obj(constructConfigFromYaml(yaml, factory_context)); - ProtobufWkt::Value v00, v01, v04; + Protobuf::Value v00, v01, v04; v00.set_string_value("v00"); v01.set_string_value("v01"); v04.set_string_value("v04"); HashedValue hv00(v00), hv01(v01), hv04(v04); { - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -423,7 +423,7 @@ TEST(ConfigTest, WeightedClustersWithMetadataMatchAndTopLevelMetadataMatchConfig } { - ProtobufWkt::Value v3, v4; + Protobuf::Value v3, v4; v3.set_string_value("v3"); v4.set_string_value("v4"); HashedValue hv3(v3), hv4(v4); @@ -474,7 +474,7 @@ TEST(ConfigTest, WeightedClustersWithTopLevelMetadataMatchConfig) { NiceMock factory_context; Config config_obj(constructConfigFromYaml(yaml, factory_context)); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -513,7 +513,7 @@ TEST(ConfigTest, TopLevelMetadataMatchConfig) { NiceMock factory_context; Config config_obj(constructConfigFromYaml(yaml, factory_context)); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -546,7 +546,7 @@ TEST(ConfigTest, ClusterWithTopLevelMetadataMatchConfig) { NiceMock factory_context; Config config_obj(constructConfigFromYaml(yaml, factory_context)); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -585,7 +585,7 @@ TEST(ConfigTest, PerConnectionClusterWithTopLevelMetadataMatchConfig) { NiceMock factory_context; Config config_obj(constructConfigFromYaml(yaml, factory_context)); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); diff --git a/test/common/tcp_proxy/tcp_proxy_test.cc b/test/common/tcp_proxy/tcp_proxy_test.cc index 8659ac4c11f4e..81247dd5a1ad9 100644 --- a/test/common/tcp_proxy/tcp_proxy_test.cc +++ b/test/common/tcp_proxy/tcp_proxy_test.cc @@ -966,16 +966,16 @@ TEST_P(TcpProxyTest, StreamDecoderFilterCallbacks) { } TEST_P(TcpProxyTest, RouteWithMetadataMatch) { - auto v1 = ProtobufWkt::Value(); + auto v1 = Protobuf::Value(); v1.set_string_value("v1"); - auto v2 = ProtobufWkt::Value(); + auto v2 = Protobuf::Value(); v2.set_number_value(2.0); - auto v3 = ProtobufWkt::Value(); + auto v3 = Protobuf::Value(); v3.set_bool_value(true); std::vector criteria = {{"a", v1}, {"b", v2}, {"c", v3}}; - auto metadata_struct = ProtobufWkt::Struct(); + auto metadata_struct = Protobuf::Struct(); auto mutable_fields = metadata_struct.mutable_fields(); for (const auto& criterion : criteria) { @@ -1032,7 +1032,7 @@ TEST_P(TcpProxyTest, WeightedClusterWithMetadataMatch) { {"cluster1", "cluster2"}); config_ = std::make_shared(constructConfigFromYaml(yaml, factory_context_)); - ProtobufWkt::Value v0, v1, v2; + Protobuf::Value v0, v1, v2; v0.set_string_value("v0"); v1.set_string_value("v1"); v2.set_string_value("v2"); @@ -1105,11 +1105,11 @@ TEST_P(TcpProxyTest, WeightedClusterWithMetadataMatch) { TEST_P(TcpProxyTest, StreamInfoDynamicMetadata) { configure(defaultConfig()); - ProtobufWkt::Value val; + Protobuf::Value val; val.set_string_value("val"); envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Struct& map = + Protobuf::Struct& map = (*metadata.mutable_filter_metadata())[Envoy::Config::MetadataFilters::get().ENVOY_LB]; (*map.mutable_fields())["test"] = val; EXPECT_CALL(filter_callbacks_.connection_.stream_info_, dynamicMetadata()) @@ -1158,14 +1158,14 @@ TEST_P(TcpProxyTest, StreamInfoDynamicMetadataAndConfigMerged) { {"cluster1"}); config_ = std::make_shared(constructConfigFromYaml(yaml, factory_context_)); - ProtobufWkt::Value v0, v1, v2; + Protobuf::Value v0, v1, v2; v0.set_string_value("v0"); v1.set_string_value("from_streaminfo"); // 'v1' is overridden with this value by streamInfo. v2.set_string_value("v2"); HashedValue hv0(v0), hv1(v1), hv2(v2); envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Struct& map = + Protobuf::Struct& map = (*metadata.mutable_filter_metadata())[Envoy::Config::MetadataFilters::get().ENVOY_LB]; (*map.mutable_fields())["k1"] = v1; (*map.mutable_fields())["k2"] = v2; @@ -1283,7 +1283,6 @@ TEST_P(TcpProxyTest, InvalidIdleTimeoutObjectFactory) { TEST_P(TcpProxyTest, IdleTimeoutWithFilterStateOverride) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); - setup(1, config); uint64_t idle_timeout_override = 5000; @@ -1295,6 +1294,9 @@ TEST_P(TcpProxyTest, IdleTimeoutWithFilterStateOverride) { StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::Connection); Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(idle_timeout_override), _)); + setup(1, config); + EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(idle_timeout_override), _)); raiseEventUpstreamConnected(0); @@ -1323,9 +1325,10 @@ TEST_P(TcpProxyTest, IdleTimeoutWithFilterStateOverride) { TEST_P(TcpProxyTest, IdleTimeout) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); + Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); setup(1, config); - Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); raiseEventUpstreamConnected(0); @@ -1353,9 +1356,10 @@ TEST_P(TcpProxyTest, IdleTimeout) { TEST_P(TcpProxyTest, IdleTimerDisabledDownstreamClose) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); + Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); setup(1, config); - Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); raiseEventUpstreamConnected(0); @@ -1367,9 +1371,10 @@ TEST_P(TcpProxyTest, IdleTimerDisabledDownstreamClose) { TEST_P(TcpProxyTest, IdleTimerDisabledUpstreamClose) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); + Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); setup(1, config); - Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); raiseEventUpstreamConnected(0); @@ -1381,9 +1386,10 @@ TEST_P(TcpProxyTest, IdleTimerDisabledUpstreamClose) { TEST_P(TcpProxyTest, IdleTimeoutWithOutstandingDataFlushed) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); + Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); setup(1, config); - Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); EXPECT_CALL(*idle_timer, enableTimer(std::chrono::milliseconds(1000), _)); raiseEventUpstreamConnected(0); @@ -1652,10 +1658,10 @@ TEST_P(TcpProxyTest, UpstreamFlushNoTimeout) { TEST_P(TcpProxyTest, UpstreamFlushTimeoutConfigured) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); + Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(_, _)); setup(1, config); - NiceMock* idle_timer = - new NiceMock(&filter_callbacks_.connection_.dispatcher_); EXPECT_CALL(*idle_timer, enableTimer(_, _)); raiseEventUpstreamConnected(0); @@ -1683,10 +1689,10 @@ TEST_P(TcpProxyTest, UpstreamFlushTimeoutConfigured) { TEST_P(TcpProxyTest, UpstreamFlushTimeoutExpired) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy config = defaultConfig(); config.mutable_idle_timeout()->set_seconds(1); + Event::MockTimer* idle_timer = new Event::MockTimer(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*idle_timer, enableTimer(_, _)); setup(1, config); - NiceMock* idle_timer = - new NiceMock(&filter_callbacks_.connection_.dispatcher_); EXPECT_CALL(*idle_timer, enableTimer(_, _)); raiseEventUpstreamConnected(0); @@ -1700,6 +1706,7 @@ TEST_P(TcpProxyTest, UpstreamFlushTimeoutExpired) { EXPECT_EQ(1U, config_->stats().upstream_flush_active_.value()); EXPECT_CALL(*upstream_connections_.at(0), close(Network::ConnectionCloseType::NoFlush)); + EXPECT_CALL(*idle_timer, disableTimer()); idle_timer->invokeCallback(); EXPECT_EQ(1U, config_->stats().upstream_flush_total_.value()); EXPECT_EQ(0U, config_->stats().upstream_flush_active_.value()); diff --git a/test/common/tcp_proxy/upstream_test.cc b/test/common/tcp_proxy/upstream_test.cc index 3dc669e38c434..323f6789583ca 100644 --- a/test/common/tcp_proxy/upstream_test.cc +++ b/test/common/tcp_proxy/upstream_test.cc @@ -1,5 +1,8 @@ #include +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/extensions/request_id/uuid/v3/uuid.pb.h" + #include "source/common/tcp_proxy/tcp_proxy.h" #include "source/common/tcp_proxy/upstream.h" @@ -239,7 +242,7 @@ class HttpUpstreamRequestEncoderTest : public testing::TestWithParamupstream_->setRequestEncoder(this->encoder_, false); } +TEST_P(HttpUpstreamRequestEncoderTest, RequestIdGeneratedWhenEnabled) { + envoy::extensions::filters::network::http_connection_manager::v3::RequestIDExtension reqid_ext; + envoy::extensions::request_id::uuid::v3::UuidRequestIdConfig uuid_cfg; + reqid_ext.mutable_typed_config()->PackFrom(uuid_cfg); + *this->tcp_proxy_.mutable_tunneling_config()->mutable_request_id_extension() = reqid_ext; + this->setupUpstream(); + + EXPECT_CALL(this->encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool) { + const auto* rid = headers.RequestId(); + EXPECT_NE(rid, nullptr); + if (rid != nullptr) { + EXPECT_FALSE(rid->value().empty()); + } + return Http::okStatus(); + })); + + this->upstream_->setRequestEncoder(this->encoder_, false); +} + +MATCHER(HasNonEmptyTunnelRequestId, "Struct has non-empty tunnel_request_id") { + const Protobuf::Struct& st = arg; + const auto& fields = st.fields(); + auto it = fields.find("tunnel_request_id"); + return it != fields.end() && !it->second.string_value().empty(); +} + +TEST_P(HttpUpstreamRequestEncoderTest, RequestIdStoredInDynamicMetadataWhenEnabled) { + envoy::extensions::filters::network::http_connection_manager::v3::RequestIDExtension reqid_ext; + envoy::extensions::request_id::uuid::v3::UuidRequestIdConfig uuid_cfg; + reqid_ext.mutable_typed_config()->PackFrom(uuid_cfg); + *this->tcp_proxy_.mutable_tunneling_config()->mutable_request_id_extension() = reqid_ext; + this->setupUpstream(); + EXPECT_CALL(this->downstream_stream_info_, + setDynamicMetadata("envoy.filters.network.tcp_proxy", HasNonEmptyTunnelRequestId())); + EXPECT_CALL(this->encoder_, encodeHeaders(_, false)).WillOnce(Return(Http::okStatus())); + this->upstream_->setRequestEncoder(this->encoder_, false); +} + TEST_P(HttpUpstreamRequestEncoderTest, RequestEncoderConnectWithCustomPath) { this->tcp_proxy_.mutable_tunneling_config()->set_use_post(false); this->tcp_proxy_.mutable_tunneling_config()->set_post_path("/test"); @@ -570,6 +612,21 @@ TEST_F(CombinedUpstreamTest, WriteUpstream) { this->upstream_->encodeData(buffer2, true); } +TEST_F(CombinedUpstreamTest, CombinedUpstreamGeneratesRequestIdWhenEnabled) { + envoy::extensions::filters::network::http_connection_manager::v3::RequestIDExtension reqid_ext; + envoy::extensions::request_id::uuid::v3::UuidRequestIdConfig uuid_cfg; + reqid_ext.mutable_typed_config()->PackFrom(uuid_cfg); + *this->tcp_proxy_.mutable_tunneling_config()->mutable_request_id_extension() = reqid_ext; + this->setup(); + auto* headers = this->upstream_->downstreamHeaders(); + ASSERT_NE(headers, nullptr); + const auto* rid = headers->RequestId(); + EXPECT_NE(rid, nullptr); + if (rid != nullptr) { + EXPECT_FALSE(rid->value().empty()); + } +} + TEST_F(CombinedUpstreamTest, WriteDownstream) { this->setup(); EXPECT_CALL(this->callbacks_, onUpstreamData(BufferStringEqual("foo"), false)); diff --git a/test/common/tls/cert_selector/async_cert_selector.h b/test/common/tls/cert_selector/async_cert_selector.h index 425f68bc64aa9..964716cf0d16b 100644 --- a/test/common/tls/cert_selector/async_cert_selector.h +++ b/test/common/tls/cert_selector/async_cert_selector.h @@ -55,9 +55,9 @@ class AsyncTlsCertificateSelectorFactory : public Ssl::TlsCertificateSelectorCon } std::string mode; - const ProtobufWkt::Any* any_config = dynamic_cast(&config); + const Protobuf::Any* any_config = dynamic_cast(&config); if (any_config) { - ProtobufWkt::StringValue string_value; + Protobuf::StringValue string_value; if (any_config->UnpackTo(&string_value)) { mode = string_value.value(); } @@ -76,7 +76,7 @@ class AsyncTlsCertificateSelectorFactory : public Ssl::TlsCertificateSelectorCon } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new ProtobufWkt::StringValue()}; + return ProtobufTypes::MessagePtr{new Protobuf::StringValue()}; } std::string name() const override { return "test-tls-context-provider"; }; diff --git a/test/common/tls/cert_validator/BUILD b/test/common/tls/cert_validator/BUILD index 67ef47884e38c..6176cb47fd79e 100644 --- a/test/common/tls/cert_validator/BUILD +++ b/test/common/tls/cert_validator/BUILD @@ -19,9 +19,9 @@ envoy_cc_test( ], rbe_pool = "6gig", deps = [ + ":test_common", "//source/common/tls/cert_validator:cert_validator_lib", "//test/common/tls:ssl_test_utils", - "//test/common/tls/cert_validator:test_common", "//test/mocks/server:server_factory_context_mocks", "//test/test_common:environment_lib", "//test/test_common:test_runtime_lib", @@ -35,8 +35,8 @@ envoy_cc_test( ], rbe_pool = "6gig", deps = [ + ":test_common", "//source/common/tls/cert_validator:cert_validator_lib", - "//test/common/tls/cert_validator:test_common", ], ) diff --git a/test/common/tls/cert_validator/default_validator_integration_test.cc b/test/common/tls/cert_validator/default_validator_integration_test.cc index c2d82336d9903..b3535d2b4ba38 100644 --- a/test/common/tls/cert_validator/default_validator_integration_test.cc +++ b/test/common/tls/cert_validator/default_validator_integration_test.cc @@ -172,7 +172,7 @@ class TestSanListenerFilterFactory }; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "test.tcp_listener.set_dns_filter_state"; } }; @@ -200,7 +200,7 @@ class CustomSanStringMatcherFactory : public Matchers::StringMatcherExtensionFac std::string name() const override { return "envoy.string_matcher.test_custom_san_matcher"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } }; diff --git a/test/common/tls/context_impl_test.cc b/test/common/tls/context_impl_test.cc index 2a29334c0890a..0a8d2661f807d 100644 --- a/test/common/tls/context_impl_test.cc +++ b/test/common/tls/context_impl_test.cc @@ -1618,12 +1618,13 @@ TEST_F(ClientContextConfigImplTest, UnsupportedCurveEcdsaCert) { *tls_context.mutable_common_tls_context()->add_tls_certificates()); auto client_context_config = *ClientContextConfigImpl::create(tls_context, factory_context_); Stats::IsolatedStoreImpl store; + // Envoy has logic to reject P-224, but newer versions of BoringSSL reject it in `SSL_CTX` + // before Envoy's logic runs. This test expectation is written to accept both paths. EXPECT_THAT(manager_.createSslClientContext(*store.rootScope(), *client_context_config) .status() .message(), testing::ContainsRegex( - "Failed to load certificate chain from .*selfsigned_secp224r1_cert.pem, " - "only P-256, P-384 or P-521 ECDSA certificates are supported")); + "Failed to load certificate chain from .*selfsigned_secp224r1_cert.pem")); } // Multiple TLS certificates are not yet supported. diff --git a/test/common/tls/handshaker_factory_test.cc b/test/common/tls/handshaker_factory_test.cc index 151562f81ca99..2fde1cec99400 100644 --- a/test/common/tls/handshaker_factory_test.cc +++ b/test/common/tls/handshaker_factory_test.cc @@ -65,7 +65,7 @@ class HandshakerFactoryImplForTest std::string name() const override { return kFactoryName; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new ProtobufWkt::StringValue()}; + return ProtobufTypes::MessagePtr{new Protobuf::StringValue()}; } Ssl::HandshakerFactoryCb @@ -103,7 +103,7 @@ class HandshakerFactoryTest : public testing::Test { envoy::config::core::v3::TypedExtensionConfig* custom_handshaker = tls_context_.mutable_common_tls_context()->mutable_custom_handshaker(); custom_handshaker->set_name(HandshakerFactoryImplForTest::kFactoryName); - custom_handshaker->mutable_typed_config()->PackFrom(ProtobufWkt::StringValue()); + custom_handshaker->mutable_typed_config()->PackFrom(Protobuf::StringValue()); } NiceMock server_factory_context_; @@ -213,7 +213,7 @@ class HandshakerFactoryImplForDownstreamTest std::string name() const override { return kFactoryName; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new ProtobufWkt::BoolValue()}; + return ProtobufTypes::MessagePtr{new Protobuf::BoolValue()}; } Ssl::HandshakerFactoryCb @@ -278,7 +278,7 @@ TEST_F(HandshakerFactoryDownstreamTest, ServerHandshakerProvidesCertificates) { envoy::config::core::v3::TypedExtensionConfig* custom_handshaker = tls_context_.mutable_common_tls_context()->mutable_custom_handshaker(); custom_handshaker->set_name(HandshakerFactoryImplForDownstreamTest::kFactoryName); - custom_handshaker->mutable_typed_config()->PackFrom(ProtobufWkt::BoolValue()); + custom_handshaker->mutable_typed_config()->PackFrom(Protobuf::BoolValue()); CustomProcessObjectForTest custom_process_object_for_test( /*cb=*/[](SSL_CTX* ssl_ctx) { SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_TLSv1); }); diff --git a/test/common/tls/test_data/README.md b/test/common/tls/test_data/README.md index 01f782e586a23..5c062426bede7 100644 --- a/test/common/tls/test_data/README.md +++ b/test/common/tls/test_data/README.md @@ -76,3 +76,7 @@ Note that macOS is unable to generate the expired unit test cert starting with its switch from OpenSSL to LibreSSL in High Sierra (10.13). Specifically, that version of the openssl command will not accept a non-positive "-days" parameter. + +`no_extension_cert.pem` can only be generated by a new enough OpenSSL to have +https://github.com/openssl/openssl/issues/28397 fixed. As of writing, the fix +has been approved but not yet merged. diff --git a/test/common/tls/test_data/certs.sh b/test/common/tls/test_data/certs.sh index 6fc0cb3e38b65..0a767f8918b17 100755 --- a/test/common/tls/test_data/certs.sh +++ b/test/common/tls/test_data/certs.sh @@ -154,6 +154,12 @@ generate_x509_cert_no_extension() { openssl x509 -req -days "$days" -in "${1}_cert.csr" -sha256 -CA "${2}_cert.pem" -CAkey \ "${2}_key.pem" -out "${1}_cert.pem" -extensions v3_req -extfile "${1}_cert.cfg" generate_info_header "$1" + # Older OpenSSLs do not correctly generate this certificate. See + # https://github.com/openssl/openssl/issues/28397 + if openssl asn1parse -in "${1}_cert.pem" | grep -F 'cont [ 3 ]' > /dev/null; then + echo "ERROR: ${1}_cert.pem was not generated correctly. Use a newer OpenSSL." + exit 1 + fi } # $1= $2= $3=[days] @@ -446,8 +452,10 @@ generate_rsa_key no_subject generate_x509_cert_nosubject no_subject ca # Generate a certificate with no extensions -generate_rsa_key no_extension -generate_x509_cert_no_extension no_extension ca +# This is skipped for now because OpenSSL cannot generate it correctly. +# See https://github.com/openssl/openssl/issues/28397. +# generate_rsa_key no_extension +# generate_x509_cert_no_extension no_extension ca # Generate unit test certificate generate_rsa_key unittest diff --git a/test/common/tls/test_data/no_extension_cert.pem b/test/common/tls/test_data/no_extension_cert.pem index d5916937b5b5a..33f47d7ed0796 100644 --- a/test/common/tls/test_data/no_extension_cert.pem +++ b/test/common/tls/test_data/no_extension_cert.pem @@ -1,21 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIDaDCCAlCgAwIBAgIURV1cFLzZAOimcNK931D2PYfy5TowDQYJKoZIhvcNAQEL +MIIDZDCCAkygAwIBAgIUbFB1OMClcQjf5PYkiOrDdV3ev9UwDQYJKoZIhvcNAQEL BQAwdjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM DVNhbiBGcmFuY2lzY28xDTALBgNVBAoMBEx5ZnQxGTAXBgNVBAsMEEx5ZnQgRW5n -aW5lZXJpbmcxEDAOBgNVBAMMB1Rlc3QgQ0EwHhcNMjQwOTA3MDU0MzE4WhcNMjYw -OTA3MDU0MzE4WjBiMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEM +aW5lZXJpbmcxEDAOBgNVBAMMB1Rlc3QgQ0EwHhcNMjUwOTA0MjAxNjU1WhcNMjcw +OTA0MjAxNjU1WjBiMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEM MAoGA1UECgwDT3JnMRgwFgYDVQQLDA9PcmcgRW5naW5lZXJpbmcxFjAUBgNVBAMM -DUxlZ2FjeSBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDp -DcgNmR0dVWDPmmpxuJoNlILfPTL7Z5CHuUMzbhhPYI83+9kRIDE7QAYPP4cqSaQL -qQoc+Svb4OV4fTRmmhyaeXbUAxIrJE6qB0XdtAzlSr8UpJsYmB0kHlIryXd3PLSg -hh3g75poufc8swomDiIE+tZzo8ngZHKiOVsqGsHBnbs+83k452nvM0XbGLXRjLCI -WrUyGOM6x6WYNJEs0skP5F0HQn0Q8aQBkMWqYDCu44nafsaxUji2PTj9wo+yGmUR -EDSXqqWm+9aPihq+XHNDWIdpe7Gb07QsmGaEK/+J/VtRbCnyBZTxbYHXyIbV4xkk -Kd6e2j6i7/Cqs4DPK8stAgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBoDP+n -8Vqo4up5BHuXTqZsul3IjpGP8O2AMLC0DXfo59HJkAg3tiSWmxY7UN0YfWotOboV -Jy5TsxoviIOw8eGQbXgyEvtseAPy6Rkl7DBD4PD73buXbHE5oN31cVf9R+E0YBLc -oihpp/wvYpGEl9/PlDBwLQ2W0mGNyQ45M8uCaff8HnloiEUCyfdWaxrpiuN6AXZJ -UCdgfsCdDom9N8eszCIblwrprnG1hgvK+6JQLmqs/6E8zwMXLF56TkHhLK13N+v7 -YkCqnGuMRGuTJVnT3mV9u+lhtPAHh/Pb6g6fwUw9VirrKEOSgaNCxAwKqnkq6+Y5 -B3x8ME8Jm8YxhP4J +DUxlZ2FjeSBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr +42lU62lb1lijMevEO6rSIhlKORHqBXUrZBvXvaJx8tWGZEzxgPXL/+dum0WyKAdz +jxJIkLHHv2tAqcpajcvRtBMONgt6W4DWfRLpVSkyjG7JDkynDc5jyPtO6Bnw3Twl +mapkVnYIW4T6xSNAJBXz/VhD/fiXvO7X9q0JDlD57PMSQ2chVVBkF+Nhac62TwsK +DW3HW1xWviUDErN4hs90IjI+M0Z7NEZK5qJrUPviTdq0LxhJEhTsDetPOlSqHdpz +t83KP+CTKagQZPB3CWNCSNn/z+CdFfvVvvIrwlzZBKSPWH+WRiKLBd/XqGN3eUEA +hqsbDd0LqQL4Ed4cVkiHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEJ6ex48+lmX +PS1TKB59kuMY0QYpYOMqK1XVR43qKfmJUaNm0WHxNovfnxl+3X7SFXmu7VFFynbS +jWhDewzLV0kj8XAZjHYb6WMMx1mnhjDWZ/p5oymkJOnQNVVUfY9w0zQOsygpA+WP +r8bjV3ZnYHEcQ6p/MpeEUhxjXZRT5/+r4yXoFvPtEcJE9TgRQXcIGfSrJyInDGHB +y6KWRyGpDj/f5ljWWs/5lbLQ1E18yEVbtaWdcFpw6Z1NOo3plE3UhvT7kfH9m5vs +yl55kLMnZZhZgrcrh4iIp2+rx4lJNzDLgW0ZzHjaYPJCpH7ihWATlMbJzDl8N0gt +3lgGFeO4t30= -----END CERTIFICATE----- diff --git a/test/common/tls/test_data/no_extension_cert_info.h b/test/common/tls/test_data/no_extension_cert_info.h index 35abdd7556e3b..83b2411db0119 100644 --- a/test/common/tls/test_data/no_extension_cert_info.h +++ b/test/common/tls/test_data/no_extension_cert_info.h @@ -3,9 +3,9 @@ // NOLINT(namespace-envoy) // This file is auto-generated by certs.sh. constexpr char TEST_NO_EXTENSION_CERT_256_HASH[] = - "464d1eb8e3217b515bfb540df3e5f8d136f2e9ea897533c2e296e96c0dcc2585"; -constexpr char TEST_NO_EXTENSION_CERT_1_HASH[] = "a7a954b8a78d7c001cbf56a57db48823e6991a8c"; -constexpr char TEST_NO_EXTENSION_CERT_SPKI[] = "3Fc0C/VBNBl71wdP4oM0/E777sOgEyltsTVeCUPkvBE="; -constexpr char TEST_NO_EXTENSION_CERT_SERIAL[] = "455d5c14bcd900e8a670d2bddf50f63d87f2e53a"; -constexpr char TEST_NO_EXTENSION_CERT_NOT_BEFORE[] = "Sep 7 05:43:18 2024 GMT"; -constexpr char TEST_NO_EXTENSION_CERT_NOT_AFTER[] = "Sep 7 05:43:18 2026 GMT"; + "ed644eb3210685dbf492adc8e1527e2aaf2283b250eccd5d89cda24524aaa39e"; +constexpr char TEST_NO_EXTENSION_CERT_1_HASH[] = "97d501aba5923cdedbcdfda76be0d5f21fc92881"; +constexpr char TEST_NO_EXTENSION_CERT_SPKI[] = "vcaIfQKpeH1I2HoVx2IClqbELHuAJma3cCez596W9KY="; +constexpr char TEST_NO_EXTENSION_CERT_SERIAL[] = "6c507538c0a57108dfe4f62488eac3755ddebfd5"; +constexpr char TEST_NO_EXTENSION_CERT_NOT_BEFORE[] = "Sep 4 20:16:55 2025 GMT"; +constexpr char TEST_NO_EXTENSION_CERT_NOT_AFTER[] = "Sep 4 20:16:55 2027 GMT"; diff --git a/test/common/tls/test_data/no_extension_key.pem b/test/common/tls/test_data/no_extension_key.pem index 1657de4d8b61c..4c7bb9bed2220 100644 --- a/test/common/tls/test_data/no_extension_key.pem +++ b/test/common/tls/test_data/no_extension_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDpDcgNmR0dVWDP -mmpxuJoNlILfPTL7Z5CHuUMzbhhPYI83+9kRIDE7QAYPP4cqSaQLqQoc+Svb4OV4 -fTRmmhyaeXbUAxIrJE6qB0XdtAzlSr8UpJsYmB0kHlIryXd3PLSghh3g75poufc8 -swomDiIE+tZzo8ngZHKiOVsqGsHBnbs+83k452nvM0XbGLXRjLCIWrUyGOM6x6WY -NJEs0skP5F0HQn0Q8aQBkMWqYDCu44nafsaxUji2PTj9wo+yGmUREDSXqqWm+9aP -ihq+XHNDWIdpe7Gb07QsmGaEK/+J/VtRbCnyBZTxbYHXyIbV4xkkKd6e2j6i7/Cq -s4DPK8stAgMBAAECggEACqCDVY68kOHxMkOX5HvMNzUlMEGgZu3enY3BW8xCpVtM -FJEHoXwJZVVX9rnec5DG6fWJPZp6w+yR/NKgFn34XW6PUFTGhJCZL1rPC7Bs4RzC -21Gz38/POQkn6pFl1kT+oI4/uQomopgORyaL3oHeTmyouukGU3+JFE7PZ9CvXUvP -cwfGs6xEfDw27BhHmwznVgzTYm0kH1LcQjfJ1wLiO4D6BB3MqMAHpepTF/b3r/Lu -JkwtF+M8x/Gk9uGdoh5EMNrYxNw1HQgTsIFWHdKjrFhOj6BuRsmpLMVUndRcajME -BoMpy6Cal1lsxQtUVjNPEUi3EoqPAfmUYlMrGSEcMQKBgQD6TxGNG66VdZ5yxO4i -jTfwEIx8WzFVi/nYQx1MJiotjP8Xg12MllTQePSfmKIBZDBAl84i5b081x2dH3tb -mTWRWsna9DaQRWeXAJ82exx53G59Pw2NeqZje5aKfYItn2zigqYLPfoJu/PzApF9 -1Sp3zPe9uOVq7n+8O7xZvCIgsQKBgQDuWkeHUgeGHQwIuLQpxZjtwnvPx4hiNEUh -2s76fKJ2K+DvyToEEgPLzqDfrbmPZISa5nknMtCHAp2//N4dJADKkGpKNys+JNku -ii6xBZ00pa+waOTzhJaioLsKCQyywDPHRDetUPY3U4qjQl8AIk97sL9odK7T/M19 -phQillJRPQKBgQDnYkwRKvO6CZ5M7apMmkqJSmLzWcFDGT/+IBxnFiiLLvloHPFP -UnBYvlczaP7pVloce7f8Hm9OXHRtmHqJ9BjGoyxRkMsXlnDp75M945QxOgmREcZP -cH97GvXQU7EQx3z57lfbsJEAipQ5obgon/LAB+NDqDW7IXlG4dl9AiJyIQKBgDms -mrZBwRRQnwLVPrME3zZY4wCp9XRd1YSVn5O46M7TW0BqXqFxgn2kaAT30njCB9w7 -fIFhqFei6Gz2UQCYH6DkRPPkWZBV9j9urFGlXB7LILH9D7llEdYUMm4BNpNiMqU6 -+oXzm0BT9K4Ad2Be7QCvCgHKiis9drO6phCgcxa5AoGAH00R39pxMG5R4yoOcYuL -kxOpyeOyILQXNlGKTT9eQ78wNK0dtbd/fQeTezqNDSpoY6DsZXn67sfc6gHXl8nv -RaG4fyXBraPWRWozgtLs1Fpn092eR9ufdNyd5IzEhzVDNV8Z20d7N24fcCwiEURl -jOciCXiAcdgLKRhYtDv0Drc= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCr42lU62lb1lij +MevEO6rSIhlKORHqBXUrZBvXvaJx8tWGZEzxgPXL/+dum0WyKAdzjxJIkLHHv2tA +qcpajcvRtBMONgt6W4DWfRLpVSkyjG7JDkynDc5jyPtO6Bnw3TwlmapkVnYIW4T6 +xSNAJBXz/VhD/fiXvO7X9q0JDlD57PMSQ2chVVBkF+Nhac62TwsKDW3HW1xWviUD +ErN4hs90IjI+M0Z7NEZK5qJrUPviTdq0LxhJEhTsDetPOlSqHdpzt83KP+CTKagQ +ZPB3CWNCSNn/z+CdFfvVvvIrwlzZBKSPWH+WRiKLBd/XqGN3eUEAhqsbDd0LqQL4 +Ed4cVkiHAgMBAAECggEAGrbV1IJf1guarAZir5VcZ5swFgaHn7jobG17HE0XNaF5 +iREGmlQiH2nuxJRyQQ2SluWqAEgosTQxTZP15Jv8DOPxQDirEQGupOc8bLI1HGuR +/kJwLFhrdruyPyG4gmRH6EoZHs4HOyZKJRVFdL8HAGwj7zFGFQMilcL7QpiMgkMN +kJX73IMsrbH8L7Dl3jf0RCRIBxvUuaxzq7E/NAwt5QpOioWu+/Gg1iWyBQbEVVdt +q6dQHctv6hcCXU1yxDG26sxkIPt+HfWa9C/2hrO/I6OCeqAW9ACNGT0WCCezPpUf ++CXal3WwHA8NqE3wtJ4pMK+eJdsgeruwJ0f2nTLweQKBgQDUSns3/fr1rJ3vvRLD +WQpZOSlch3H180RrxOKHb6yjoPlUczNSOv61rkXuekD6xDW69BzXXzjwlPxpusjK +4us/Vy4AKMQNwhDH/b0FSIv2RDWsqjUvwmv5VzVeYVlzXOYd5BC7fH2f3gkYLuUl +ckzPdqew9nFOaKJpccIuBoeT2QKBgQDPR19Ql1thSt6HCNXUiDa+6IPzm21k5Ju7 +xx53x8VzKVnP9gxeuhFG+bwoR8EWfbi9tsWwd40RwMU2uUGxv/Jy/C7yTdxPPiJq +bUpXHUqlnyqCxfNStpwO1xqq8gPJ8opPmBhbkvVsOZ6MSMY7oYv052+wYEWvupwP +faMfPDXjXwKBgCZ+yxFAMP3Tq2AJvRlHUCUVxHZO6U9cKZARR7KfgYK6cfvqV+gV +YpK3Y173NElEwyl/kqtLTRvzKEJT6I1B0L7PpDvLKKIGCtz5GgmXOioR/FmvE63x +Z3rzYW4X4QyWT/QjoxUcYftXW/bSqiK8M0l7jrT8O1eoiartQfTuoi8hAoGBAMxx +GRHkN70+mz2U+VMnBthFfeBI7R0WXoRXYTXDVHzBzFPR22GTJHdc2rjgDRKh7hUw +sMvdHsbj26CeGK25JOlE0wkqwqFmJ4vRQAGsYnP5CXTyyYxLkKESiLsS+am2D7Vx +zpSD3o1gR4EWRm+KZwCnRQIx8onhBQxCXyHvwTcBAoGBALzqNRq4r7Q8jMw0tmdw +mWWtuni+8Wf9BQTplDMEUfrrKcrh19QmldtJnKPd/fEZr6NP5jstnA8dtukTg1Dh +Zb62cpVubaZPaCjX8g2I0j42f8SbP7Th1IBeSuAMQr4CBIYN8U2LMTUGgDfUxi0d +E9q7vanEsRQMkqtiVUY5/SK7 -----END PRIVATE KEY----- diff --git a/test/common/tls/test_private_key_method_provider.cc b/test/common/tls/test_private_key_method_provider.cc index 242e634f671e2..48f155997c254 100644 --- a/test/common/tls/test_private_key_method_provider.cc +++ b/test/common/tls/test_private_key_method_provider.cc @@ -314,43 +314,43 @@ int TestPrivateKeyMethodProvider::ecdsaConnectionIndex() { } TestPrivateKeyMethodProvider::TestPrivateKeyMethodProvider( - const ProtobufWkt::Any& typed_config, + const Protobuf::Any& typed_config, Server::Configuration::TransportSocketFactoryContext& factory_context) { std::string private_key_path; - auto config = MessageUtil::anyConvert(typed_config); + auto config = MessageUtil::anyConvert(typed_config); for (auto& value_it : config.fields()) { auto& value = value_it.second; if (value_it.first == "private_key_file" && - value.kind_case() == ProtobufWkt::Value::kStringValue) { + value.kind_case() == Protobuf::Value::kStringValue) { private_key_path = value.string_value(); } - if (value_it.first == "sync_mode" && value.kind_case() == ProtobufWkt::Value::kBoolValue) { + if (value_it.first == "sync_mode" && value.kind_case() == Protobuf::Value::kBoolValue) { test_options_.sync_mode_ = value.bool_value(); } - if (value_it.first == "crypto_error" && value.kind_case() == ProtobufWkt::Value::kBoolValue) { + if (value_it.first == "crypto_error" && value.kind_case() == Protobuf::Value::kBoolValue) { test_options_.crypto_error_ = value.bool_value(); } - if (value_it.first == "method_error" && value.kind_case() == ProtobufWkt::Value::kBoolValue) { + if (value_it.first == "method_error" && value.kind_case() == Protobuf::Value::kBoolValue) { test_options_.method_error_ = value.bool_value(); } - if (value_it.first == "is_available" && value.kind_case() == ProtobufWkt::Value::kBoolValue) { + if (value_it.first == "is_available" && value.kind_case() == Protobuf::Value::kBoolValue) { test_options_.is_available_ = value.bool_value(); } if (value_it.first == "async_method_error" && - value.kind_case() == ProtobufWkt::Value::kBoolValue) { + value.kind_case() == Protobuf::Value::kBoolValue) { test_options_.async_method_error_ = value.bool_value(); } if (value_it.first == "expected_operation" && - value.kind_case() == ProtobufWkt::Value::kStringValue) { + value.kind_case() == Protobuf::Value::kStringValue) { if (value.string_value() == "decrypt") { test_options_.decrypt_expected_ = true; } else if (value.string_value() == "sign") { test_options_.sign_expected_ = true; } } - if (value_it.first == "mode" && value.kind_case() == ProtobufWkt::Value::kStringValue) { + if (value_it.first == "mode" && value.kind_case() == Protobuf::Value::kStringValue) { mode_ = value.string_value(); } } diff --git a/test/common/tls/test_private_key_method_provider.h b/test/common/tls/test_private_key_method_provider.h index 7de910d4a2db5..f31aac5657258 100644 --- a/test/common/tls/test_private_key_method_provider.h +++ b/test/common/tls/test_private_key_method_provider.h @@ -64,7 +64,7 @@ class TestPrivateKeyConnection { class TestPrivateKeyMethodProvider : public virtual Ssl::PrivateKeyMethodProvider { public: TestPrivateKeyMethodProvider( - const ProtobufWkt::Any& typed_config, + const Protobuf::Any& typed_config, Server::Configuration::TransportSocketFactoryContext& factory_context); // Ssl::PrivateKeyMethodProvider void registerPrivateKeyMethod(SSL* ssl, Ssl::PrivateKeyConnectionCallbacks& cb, diff --git a/test/common/tracing/http_tracer_impl_test.cc b/test/common/tracing/http_tracer_impl_test.cc index 4d983b166fd22..9a3e115649a39 100644 --- a/test/common/tracing/http_tracer_impl_test.cc +++ b/test/common/tracing/http_tracer_impl_test.cc @@ -59,17 +59,27 @@ class HttpConnManFinalizerImplTest : public testing::Test { for (const CustomTagCase& cas : cases) { envoy::type::tracing::v3::CustomTag custom_tag; TestUtility::loadFromYaml(cas.custom_tag, custom_tag); - config.custom_tags_.emplace(custom_tag.tag(), CustomTagUtility::createCustomTag(custom_tag)); + custom_tags_.emplace(custom_tag.tag(), CustomTagUtility::createCustomTag(custom_tag)); if (cas.set) { EXPECT_CALL(span, setTag(Eq(custom_tag.tag()), Eq(cas.value))); } else { EXPECT_CALL(span, setTag(Eq(custom_tag.tag()), _)).Times(0); } } + + EXPECT_CALL(config, modifySpan(_)).WillOnce(Invoke([this](Span& span) { + HttpTraceContext trace_context{request_headers_}; + const CustomTagContext ctx{trace_context, stream_info}; + for (const auto& [_, custom_tag] : custom_tags_) { + custom_tag->applySpan(span, ctx); + } + })); } NiceMock span; NiceMock config; + Tracing::CustomTagMap custom_tags_; + Http::TestRequestHeaderMapImpl request_headers_; NiceMock stream_info; std::shared_ptr> cluster_info_{ std::make_shared>()}; @@ -84,11 +94,12 @@ TEST_F(HttpConnManFinalizerImplTest, OriginalAndLongPath) { const auto remote_address = Network::Address::InstanceConstSharedPtr{ new Network::Address::Ipv4Instance(expected_ip, 0, nullptr)}; - Http::TestRequestHeaderMapImpl request_headers{{"x-request-id", "id"}, - {"x-envoy-original-path", path}, - {":method", "GET"}, - {":path", ""}, - {":scheme", "http"}}; + request_headers_ = Http::TestRequestHeaderMapImpl{{"x-request-id", "id"}, + {"x-envoy-original-path", path}, + {":method", "GET"}, + {":path", ""}, + {":scheme", "http"}}; + Http::TestResponseHeaderMapImpl response_headers; Http::TestResponseTrailerMapImpl response_trailers; @@ -106,7 +117,9 @@ TEST_F(HttpConnManFinalizerImplTest, OriginalAndLongPath) { EXPECT_CALL(span, setTag(Eq(Tracing::Tags::get().HttpProtocol), Eq("HTTP/2"))); EXPECT_CALL(span, setTag(Eq(Tracing::Tags::get().PeerAddress), Eq(expected_ip))); - HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers, &response_headers, + expectSetCustomTags({}); + + HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers_, &response_headers, &response_trailers, stream_info, config); } @@ -118,7 +131,7 @@ TEST_F(HttpConnManFinalizerImplTest, NoGeneratedId) { const auto remote_address = Network::Address::InstanceConstSharedPtr{ new Network::Address::Ipv4Instance(expected_ip, 0, nullptr)}; - Http::TestRequestHeaderMapImpl request_headers{ + request_headers_ = Http::TestRequestHeaderMapImpl{ {":path", ""}, {"x-envoy-original-path", path}, {":method", "GET"}, {":scheme", "http"}}; Http::TestResponseHeaderMapImpl response_headers; Http::TestResponseTrailerMapImpl response_trailers; @@ -137,7 +150,9 @@ TEST_F(HttpConnManFinalizerImplTest, NoGeneratedId) { EXPECT_CALL(span, setTag(Eq(Tracing::Tags::get().HttpProtocol), Eq("HTTP/2"))); EXPECT_CALL(span, setTag(Eq(Tracing::Tags::get().PeerAddress), Eq(expected_ip))); - HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers, &response_headers, + expectSetCustomTags({}); + + HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers_, &response_headers, &response_trailers, stream_info, config); } @@ -167,6 +182,8 @@ TEST_F(HttpConnManFinalizerImplTest, Connect) { EXPECT_CALL(span, setTag(Eq(Tracing::Tags::get().HttpProtocol), Eq("HTTP/2"))); EXPECT_CALL(span, setTag(Eq(Tracing::Tags::get().PeerAddress), Eq(expected_ip))); + expectSetCustomTags({}); + HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers, &response_headers, &response_trailers, stream_info, config); } @@ -366,13 +383,13 @@ TEST_F(HttpConnManFinalizerImplTest, UnixDomainSocketPeerAddressTag) { TEST_F(HttpConnManFinalizerImplTest, SpanCustomTags) { TestEnvironment::setEnvVar("E_CC", "c", 1); - Http::TestRequestHeaderMapImpl request_headers{{"x-request-id", "id"}, - {":path", "/test"}, - {":method", "GET"}, - {":scheme", "https"}, - {"x-bb", "b"}}; + request_headers_ = Http::TestRequestHeaderMapImpl{{"x-request-id", "id"}, + {":path", "/test"}, + {":method", "GET"}, + {":scheme", "https"}, + {"x-bb", "b"}}; - ProtobufWkt::Struct fake_struct; + Protobuf::Struct fake_struct; std::string yaml = R"EOF( ree: foo: bar @@ -400,7 +417,6 @@ TEST_F(HttpConnManFinalizerImplTest, SpanCustomTags) { EXPECT_CALL(stream_info, bytesSent()).WillOnce(Return(100)); EXPECT_CALL(*host_, metadata()).WillRepeatedly(Return(host_metadata)); - EXPECT_CALL(config, customTags()); EXPECT_CALL(span, setTag(_, _)).Times(testing::AnyNumber()); expectSetCustomTags( @@ -477,9 +493,9 @@ tag: dd-10, metadata_key: { key: m.host, path: [ { key: not-found } ] })EOF", false, ""}}); - ON_CALL(stream_info, getRequestHeaders()).WillByDefault(Return(&request_headers)); + ON_CALL(stream_info, getRequestHeaders()).WillByDefault(Return(&request_headers_)); - HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers, nullptr, nullptr, stream_info, + HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers_, nullptr, nullptr, stream_info, config); } @@ -688,12 +704,10 @@ TEST_F(HttpConnManFinalizerImplTest, CustomTagOverwritesCommonTag) { EXPECT_CALL(stream_info, responseCode()).WillRepeatedly(ReturnPointee(&response_code)); EXPECT_CALL(stream_info, bytesSent()).WillOnce(Return(100)); - EXPECT_CALL(config, customTags()); - std::string custom_tag_str = "{ tag: component, literal: { value: override_component } }"; envoy::type::tracing::v3::CustomTag custom_tag; TestUtility::loadFromYaml(custom_tag_str, custom_tag); - config.custom_tags_.emplace(custom_tag.tag(), CustomTagUtility::createCustomTag(custom_tag)); + custom_tags_.emplace(custom_tag.tag(), CustomTagUtility::createCustomTag(custom_tag)); EXPECT_CALL(span, setTag(_, _)).Times(testing::AnyNumber()); { @@ -703,6 +717,8 @@ TEST_F(HttpConnManFinalizerImplTest, CustomTagOverwritesCommonTag) { EXPECT_CALL(span, setTag(Eq(custom_tag.tag()), "override_component")); } + expectSetCustomTags({}); + HttpTracerUtility::finalizeDownstreamSpan(span, &request_headers, nullptr, nullptr, stream_info, config); } @@ -757,6 +773,30 @@ TEST(HttpTraceContextTest, HttpTraceContextTest) { // 'host' will be converted to ':authority'. EXPECT_EQ(23, size); } + + { + size_t size = 0; + Http::TestRequestHeaderMapImpl request_headers{{"host", "foo"}, {"bar", "var"}, {"ok", "no"}}; + HttpTraceContext trace_context(request_headers); + trace_context.forEach([&size](absl::string_view key, absl::string_view val) { + size += key.size(); + size += val.size(); + return false; + }); + // 'host' will be converted to ':authority'. + EXPECT_EQ(13, size); + } + + { + Http::TestRequestHeaderMapImpl request_headers; + ReadOnlyHttpTraceContext trace_context(request_headers); + + // No operations for ReadOnlyHttpTraceContext. + trace_context.set("key", "value"); + trace_context.remove("key"); + trace_context.requestHeaders(); + const_cast(trace_context).requestHeaders(); + } } } // namespace diff --git a/test/common/tracing/trace_context_impl_test.cc b/test/common/tracing/trace_context_impl_test.cc index 531112c53a4b0..5a57da0b247db 100644 --- a/test/common/tracing/trace_context_impl_test.cc +++ b/test/common/tracing/trace_context_impl_test.cc @@ -18,22 +18,54 @@ TEST(TraceContextHandlerTest, TraceContextHandlerGetTest) { } TraceContextHandler normal_key("key"); - TraceContextHandler inline_key("content-type"); // This key is inline key for HTTP. + TraceContextHandler inline_key("x-forwarded-for"); // This key is inline key for HTTP. + + TraceContextHandler unknown_normal_key("unknown_normal_key"); + TraceContextHandler unknown_inline_key("x-envoy-original-path"); // Test get. { auto headers = Http::RequestHeaderMapImpl::create(); - headers->setContentType("text/plain"); headers->addCopy(Http::LowerCaseString("key"), "value"); + headers->addCopy(Http::LowerCaseString("x-forwarded-for"), "127.0.0.1"); HttpTraceContext http_tracer_context(*headers); - TestTraceContextImpl trace_context{{"key", "value"}, {"content-type", "text/plain"}}; + TestTraceContextImpl trace_context{{"key", "value"}, {"x-forwarded-for", "127.0.0.1"}}; EXPECT_EQ("value", normal_key.get(trace_context).value()); - EXPECT_EQ("text/plain", inline_key.get(trace_context).value()); + EXPECT_EQ("127.0.0.1", inline_key.get(trace_context).value()); EXPECT_EQ("value", normal_key.get(http_tracer_context).value()); - EXPECT_EQ("text/plain", inline_key.get(http_tracer_context).value()); + EXPECT_EQ("127.0.0.1", inline_key.get(http_tracer_context).value()); + } + + // Test get all. + { + + Http::TestRequestHeaderMapImpl headers{{"key", "value1"}, + {"key", "value2"}, + {"x-forwarded-for", "127.0.0.1"}, + {"x-forwarded-for", "127.0.0.2"}, + {"other", "other_value"}}; + HttpTraceContext http_tracer_context(headers); + TestTraceContextImpl trace_context{{"key", "value"}, {"x-forwarded-for", "127.0.0.1"}}; + + EXPECT_EQ(normal_key.getAll(trace_context)[0], "value"); + EXPECT_EQ(inline_key.getAll(trace_context)[0], "127.0.0.1"); + + auto multiple_values_of_normal_key = normal_key.getAll(http_tracer_context); + EXPECT_EQ(multiple_values_of_normal_key.size(), 2); + EXPECT_EQ(multiple_values_of_normal_key[0], "value1"); + EXPECT_EQ(multiple_values_of_normal_key[1], "value2"); + + auto multiple_values_of_inline_key = inline_key.getAll(http_tracer_context); + EXPECT_EQ(multiple_values_of_inline_key.size(), 1); + EXPECT_EQ(multiple_values_of_inline_key[0], "127.0.0.1,127.0.0.2"); + + EXPECT_TRUE(unknown_normal_key.getAll(http_tracer_context).empty()); + EXPECT_TRUE(unknown_inline_key.getAll(http_tracer_context).empty()); + EXPECT_TRUE(!unknown_normal_key.get(trace_context).has_value()); + EXPECT_TRUE(!unknown_inline_key.get(trace_context).has_value()); } } diff --git a/test/common/tracing/tracer_config_impl_test.cc b/test/common/tracing/tracer_config_impl_test.cc index 85f49f594fe1c..8c65c327c517b 100644 --- a/test/common/tracing/tracer_config_impl_test.cc +++ b/test/common/tracing/tracer_config_impl_test.cc @@ -23,7 +23,7 @@ TEST(ConnectionManagerTracingConfigImplTest, SimpleTest) { custom_tag->set_tag("foo"); custom_tag->mutable_literal()->set_value("bar"); - ConnectionManagerTracingConfigImpl config(traffic_direction, tracing_config); + ConnectionManagerTracingConfig config(traffic_direction, tracing_config); EXPECT_EQ(Tracing::OperationName::Ingress, config.operationName()); EXPECT_EQ(true, config.verbose()); @@ -45,7 +45,7 @@ TEST(ConnectionManagerTracingConfigImplTest, SimpleTest) { custom_tag->set_tag("foo"); custom_tag->mutable_literal()->set_value("bar"); - ConnectionManagerTracingConfigImpl config(traffic_direction, tracing_config); + ConnectionManagerTracingConfig config(traffic_direction, tracing_config); EXPECT_EQ(Tracing::OperationName::Egress, config.operationName()); EXPECT_EQ(true, config.verbose()); diff --git a/test/common/tracing/tracer_impl_test.cc b/test/common/tracing/tracer_impl_test.cc index a6c70556b0a4b..2bd6f069d58ab 100644 --- a/test/common/tracing/tracer_impl_test.cc +++ b/test/common/tracing/tracer_impl_test.cc @@ -107,17 +107,26 @@ class FinalizerImplTest : public testing::Test { for (const CustomTagCase& cas : cases) { envoy::type::tracing::v3::CustomTag custom_tag; TestUtility::loadFromYaml(cas.custom_tag, custom_tag); - config.custom_tags_.emplace(custom_tag.tag(), CustomTagUtility::createCustomTag(custom_tag)); + custom_tags.emplace(custom_tag.tag(), CustomTagUtility::createCustomTag(custom_tag)); if (cas.set) { EXPECT_CALL(span, setTag(Eq(custom_tag.tag()), Eq(cas.value))); } else { EXPECT_CALL(span, setTag(Eq(custom_tag.tag()), _)).Times(0); } } + + EXPECT_CALL(config, modifySpan(_)).WillOnce(Invoke([this](Span& span) { + const CustomTagContext ctx{trace_context, stream_info}; + for (const auto& [_, custom_tag] : custom_tags) { + custom_tag->applySpan(span, ctx); + } + })); } NiceMock span; NiceMock config; + Tracing::CustomTagMap custom_tags; + Tracing::TestTraceContextImpl trace_context; NiceMock stream_info; std::shared_ptr> cluster_info_{ std::make_shared>()}; @@ -127,7 +136,7 @@ class FinalizerImplTest : public testing::Test { TEST_F(FinalizerImplTest, TestAll) { TestEnvironment::setEnvVar("E_CC", "c", 1); - Tracing::TestTraceContextImpl trace_context{{"x-request-id", "id"}, {"x-bb", "b"}}; + trace_context.context_map_ = {{"x-request-id", "id"}, {"x-bb", "b"}}; trace_context.context_host_ = "test.com"; trace_context.context_method_ = "method"; trace_context.context_path_ = "TestService"; @@ -197,7 +206,7 @@ TEST_F(FinalizerImplTest, TestAll) { {"{ tag: cc-3, environment: { name: E_CC_NOT_FOUND} }", false, ""}, }); - TracerUtility::finalizeSpan(span, trace_context, stream_info, config, true); + TracerUtility::finalizeSpan(span, stream_info, config, true); } { @@ -232,7 +241,7 @@ TEST_F(FinalizerImplTest, TestAll) { {"{ tag: cc-3, environment: { name: E_CC_NOT_FOUND} }", false, ""}, }); - TracerUtility::finalizeSpan(span, trace_context, stream_info, config, false); + TracerUtility::finalizeSpan(span, stream_info, config, false); } } @@ -240,9 +249,10 @@ TEST(EgressConfigImplTest, EgressConfigImplTest) { EgressConfigImpl config_impl; EXPECT_EQ(OperationName::Egress, config_impl.operationName()); - EXPECT_EQ(nullptr, config_impl.customTags()); EXPECT_EQ(false, config_impl.verbose()); EXPECT_EQ(Tracing::DefaultMaxPathTagLength, config_impl.maxPathTagLength()); + NiceMock span; + config_impl.modifySpan(span); } TEST(NullTracerTest, BasicFunctionality) { diff --git a/test/common/tracing/tracer_manager_impl_test.cc b/test/common/tracing/tracer_manager_impl_test.cc index 6520fc2919d0e..8a29f403fa50b 100644 --- a/test/common/tracing/tracer_manager_impl_test.cc +++ b/test/common/tracing/tracer_manager_impl_test.cc @@ -40,7 +40,7 @@ class SampleTracerFactory : public Server::Configuration::TracerFactory { std::string name() const override { return "envoy.tracers.sample"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; @@ -66,7 +66,7 @@ TEST_F(TracerManagerImplTest, ShouldReturnWhenNoTracingProviderHasBeenConfigured TEST_F(TracerManagerImplTest, ShouldUseProperTracerFactory) { envoy::config::trace::v3::Tracing_Http tracing_config; tracing_config.set_name("envoy.tracers.sample"); - tracing_config.mutable_typed_config()->PackFrom(ProtobufWkt::Struct()); + tracing_config.mutable_typed_config()->PackFrom(Protobuf::Struct()); auto tracer = tracer_manager_.getOrCreateTracer(&tracing_config); @@ -120,7 +120,7 @@ TEST_F(TracerManagerImplTest, ShouldCacheTracersBasedOnFullConfig) { TEST_F(TracerManagerImplTest, ShouldFailIfTracerProviderIsUnknown) { envoy::config::trace::v3::Tracing_Http tracing_config; tracing_config.set_name("invalid"); - tracing_config.mutable_typed_config()->PackFrom(ProtobufWkt::Value()); + tracing_config.mutable_typed_config()->PackFrom(Protobuf::Value()); EXPECT_THROW_WITH_MESSAGE(tracer_manager_.getOrCreateTracer(&tracing_config), EnvoyException, "Didn't find a registered implementation for 'invalid' " @@ -132,7 +132,7 @@ TEST_F(TracerManagerImplTest, ShouldFailIfProviderSpecificConfigIsNotValid) { tracing_config.set_name("envoy.tracers.sample"); tracing_config.mutable_typed_config()->PackFrom(ValueUtil::stringValue("value")); - ProtobufWkt::Any expected_any_proto; + Protobuf::Any expected_any_proto; expected_any_proto.PackFrom(ValueUtil::stringValue("value")); EXPECT_THROW_WITH_MESSAGE(tracer_manager_.getOrCreateTracer(&tracing_config), EnvoyException, "Didn't find a registered implementation for 'envoy.tracers.sample' " diff --git a/test/common/upstream/BUILD b/test/common/upstream/BUILD index 7d4d3a773b6af..1f3548d4d2616 100644 --- a/test/common/upstream/BUILD +++ b/test/common/upstream/BUILD @@ -21,9 +21,30 @@ envoy_cc_test( "//envoy/config:subscription_interface", "//source/common/stats:isolated_store_lib", "//source/common/upstream:od_cds_api_lib", + "//test/mocks/config:xds_manager_mocks", + "//test/mocks/protobuf:protobuf_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/upstream:cluster_manager_mocks", + "//test/mocks/upstream:missing_cluster_notifier_mocks", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "xdstp_od_cds_api_impl_test", + srcs = ["xdstp_od_cds_api_impl_test.cc"], + rbe_pool = "6gig", + deps = [ + "//envoy/config:subscription_interface", + "//source/common/stats:isolated_store_lib", + "//source/common/upstream:od_cds_api_lib", + "//test/mocks/config:xds_manager_mocks", "//test/mocks/protobuf:protobuf_mocks", + "//test/mocks/server:server_mocks", "//test/mocks/upstream:cluster_manager_mocks", "//test/mocks/upstream:missing_cluster_notifier_mocks", + "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -122,11 +143,11 @@ envoy_cc_test( ":test_cluster_manager", "//envoy/config:config_validator_interface", "//source/common/router:context_lib", + "//source/common/upstream:load_balancer_factory_base_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/eds:eds_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", "//source/extensions/clusters/original_dst:original_dst_cluster_lib", "//source/extensions/clusters/static:static_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/config_subscription/grpc:grpc_collection_subscription_lib", "//source/extensions/config_subscription/grpc:grpc_subscription_lib", "//source/extensions/health_checkers/http:health_checker_lib", @@ -171,8 +192,8 @@ envoy_cc_test( srcs = ["cluster_manager_lifecycle_test.cc"], deps = [ ":cluster_manager_impl_test_common", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/static:static_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/load_balancing_policies/ring_hash:config", "//source/extensions/network/dns_resolver/cares:config", "//test/mocks/upstream:cds_api_mocks", @@ -386,6 +407,7 @@ envoy_cc_test( srcs = ["load_stats_reporter_test.cc"], rbe_pool = "6gig", deps = [ + "//source/common/network:address_lib", "//source/common/stats:stats_lib", "//source/common/upstream:load_stats_reporter_lib", "//test/common/upstream:utility_lib", @@ -533,7 +555,6 @@ envoy_cc_test( "//source/extensions/clusters/eds:eds_lib", # TODO(mattklein123): Split this into 2 tests for each cluster. "//source/extensions/clusters/static:static_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/transport_sockets/raw_buffer:config", "//source/extensions/transport_sockets/http_11_proxy:upstream_config", "//source/extensions/transport_sockets/tls:config", @@ -541,7 +562,6 @@ envoy_cc_test( "//source/extensions/upstreams/tcp:config", "//source/server:transport_socket_config_lib", "//test/common/stats:stat_test_utility_lib", - "//test/mocks:common_lib", "//test/mocks/local_info:local_info_mocks", "//test/mocks/network:network_mocks", "//test/mocks/protobuf:protobuf_mocks", diff --git a/test/common/upstream/cds_api_impl_test.cc b/test/common/upstream/cds_api_impl_test.cc index 15614d01c07d3..6cbc51f1b10a8 100644 --- a/test/common/upstream/cds_api_impl_test.cc +++ b/test/common/upstream/cds_api_impl_test.cc @@ -37,10 +37,10 @@ MATCHER_P(WithName, expectedName, "") { return arg.name() == expectedName; } class CdsApiImplTest : public testing::Test { protected: - void setup() { + void setup(bool support_multi_ads_sources = false) { envoy::config::core::v3::ConfigSource cds_config; cds_ = *CdsApiImpl::create(cds_config, nullptr, cm_, *scope_.rootScope(), validation_visitor_, - server_factory_context_); + server_factory_context_, support_multi_ads_sources); cds_->setInitializedCb([this]() -> void { initialized_.ready(); }); EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_)); @@ -382,6 +382,101 @@ TEST_F(CdsApiImplTest, FailureSubscription) { EXPECT_EQ("", cds_->versionInfo()); } +// Tests that when a SotW update happens, a cluster that was added by another +// source is not removed. +TEST_F(CdsApiImplTest, MultiAdsSourcesEnabledSotW) { + InSequence s; + setup(true); + + // 1. Initial SotW update introduces "sotw_cluster_1". + const std::string response1_yaml = R"EOF( +version_info: '0' +resources: +- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster + name: sotw_cluster_1 +)EOF"; + auto response1 = + TestUtility::parseYaml(response1_yaml); + const auto decoded_resources1 = + TestUtility::decodeResources(response1); + + expectAdd("sotw_cluster_1", "0"); + EXPECT_CALL(initialized_, ready()); + EXPECT_TRUE( + cds_callbacks_->onConfigUpdate(decoded_resources1.refvec_, response1.version_info()).ok()); + EXPECT_EQ("0", cds_->versionInfo()); + + // 2. A second SotW update removes "sotw_cluster_1" and adds "sotw_cluster_2". + // We also imagine an on-demand cluster "od_cluster_1" now exists. + const std::string response2_yaml = R"EOF( +version_info: '1' +resources: +- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster + name: sotw_cluster_2 +)EOF"; + auto response2 = + TestUtility::parseYaml(response2_yaml); + const auto decoded_resources2 = + TestUtility::decodeResources(response2); + + // The update should add the new cluster. + expectAdd("sotw_cluster_2", "1"); + // Crucially, it should ONLY remove the cluster it knew about ("sotw_cluster_1"). + // "od_cluster_1" should NOT be removed. + EXPECT_CALL(cm_, removeCluster("sotw_cluster_1", false)); + EXPECT_CALL(cm_, removeCluster("od_cluster_1", false)).Times(0); + + EXPECT_TRUE( + cds_callbacks_->onConfigUpdate(decoded_resources2.refvec_, response2.version_info()).ok()); + EXPECT_EQ("1", cds_->versionInfo()); +} + +// Tests that if a SotW update contains all the clusters it previously managed, +// no clusters are removed, even if other on-demand clusters exist. +TEST_F(CdsApiImplTest, MultiAdsSourcesEnabledNoRemoval) { + InSequence s; + setup(true); + + // 1. Initial SotW update introduces "sotw_cluster_1". + const std::string response1_yaml = R"EOF( +version_info: '0' +resources: +- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster + name: sotw_cluster_1 +)EOF"; + auto response1 = + TestUtility::parseYaml(response1_yaml); + const auto decoded_resources1 = + TestUtility::decodeResources(response1); + + expectAdd("sotw_cluster_1", "0"); + EXPECT_CALL(initialized_, ready()); + EXPECT_TRUE( + cds_callbacks_->onConfigUpdate(decoded_resources1.refvec_, response1.version_info()).ok()); + + // 2. A second SotW update still contains "sotw_cluster_1". + // An on-demand cluster "od_cluster_1" has also been added. + const std::string response2_yaml = R"EOF( +version_info: '1' +resources: +- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster + name: sotw_cluster_1 +)EOF"; + auto response2 = + TestUtility::parseYaml(response2_yaml); + const auto decoded_resources2 = + TestUtility::decodeResources(response2); + + // The existing cluster is updated. + expectAdd("sotw_cluster_1", "1"); + // No clusters should be removed. + EXPECT_CALL(cm_, removeCluster(_, false)).Times(0); + + EXPECT_TRUE( + cds_callbacks_->onConfigUpdate(decoded_resources2.refvec_, response2.version_info()).ok()); + EXPECT_EQ("1", cds_->versionInfo()); +} + } // namespace } // namespace Upstream } // namespace Envoy diff --git a/test/common/upstream/cluster_manager_impl_test.cc b/test/common/upstream/cluster_manager_impl_test.cc index c82c5728f8837..f3b69d6b1b3cb 100644 --- a/test/common/upstream/cluster_manager_impl_test.cc +++ b/test/common/upstream/cluster_manager_impl_test.cc @@ -74,7 +74,7 @@ class AlpnTestConfigFactory return std::make_unique(); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; @@ -183,8 +183,9 @@ TEST_F(ClusterManagerImplTest, OutlierEventLog) { } )EOF"; - EXPECT_CALL(log_manager_, createAccessLog(Filesystem::FilePathAndType{ - Filesystem::DestinationType::File, "foo"})); + EXPECT_CALL( + factory_.server_context_.access_log_manager_, + createAccessLog(Filesystem::FilePathAndType{Filesystem::DestinationType::File, "foo"})); create(parseBootstrapFromV3Json(json)); } @@ -193,7 +194,7 @@ TEST_F(ClusterManagerImplTest, AdsCluster) { // can be set on. std::shared_ptr> ads_mux = std::make_shared>(); - ON_CALL(xds_manager_, adsMux()).WillByDefault(Return(ads_mux)); + ON_CALL(factory_.server_context_.xds_manager_, adsMux()).WillByDefault(Return(ads_mux)); const std::string yaml = R"EOF( dynamic_resources: @@ -229,7 +230,7 @@ TEST_F(ClusterManagerImplTest, AdsClusterStartsMuxOnlyOnce) { // can be set on. std::shared_ptr> ads_mux = std::make_shared>(); - ON_CALL(xds_manager_, adsMux()).WillByDefault(Return(ads_mux)); + ON_CALL(factory_.server_context_.xds_manager_, adsMux()).WillByDefault(Return(ads_mux)); const std::string yaml = R"EOF( dynamic_resources: @@ -1773,7 +1774,7 @@ class TestUpstreamNetworkFilterConfigFactory ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.filter"; } }; @@ -1821,8 +1822,8 @@ class ClusterManagerInitHelperTest : public testing::Test { public: MOCK_METHOD(void, onClusterInit, (ClusterManagerCluster & cluster)); - NiceMock cm_; - ClusterManagerInitHelper init_helper_{cm_, [this](ClusterManagerCluster& cluster) { + NiceMock xds_manager_; + ClusterManagerInitHelper init_helper_{xds_manager_, [this](ClusterManagerCluster& cluster) { onClusterInit(cluster); return absl::OkStatus(); }}; diff --git a/test/common/upstream/cluster_manager_impl_test_common.cc b/test/common/upstream/cluster_manager_impl_test_common.cc index 9a5c3f12b316b..32418735c8764 100644 --- a/test/common/upstream/cluster_manager_impl_test_common.cc +++ b/test/common/upstream/cluster_manager_impl_test_common.cc @@ -36,18 +36,13 @@ class MockedUpdatedClusterManagerImpl : public TestClusterManagerImpl { public: using TestClusterManagerImpl::TestClusterManagerImpl; - MockedUpdatedClusterManagerImpl( - const envoy::config::bootstrap::v3::Bootstrap& bootstrap, ClusterManagerFactory& factory, - Server::Configuration::CommonFactoryContext& factory_context, Stats::Store& stats, - ThreadLocal::Instance& tls, Runtime::Loader& runtime, const LocalInfo::LocalInfo& local_info, - AccessLog::AccessLogManager& log_manager, Event::Dispatcher& main_thread_dispatcher, - Server::Admin& admin, Api::Api& api, MockLocalClusterUpdate& local_cluster_update, - MockLocalHostsRemoved& local_hosts_removed, Http::Context& http_context, - Grpc::Context& grpc_context, Router::Context& router_context, Server::Instance& server, - Config::XdsManager& xds_manager, absl::Status& creation_status) - : TestClusterManagerImpl(bootstrap, factory, factory_context, stats, tls, runtime, local_info, - log_manager, main_thread_dispatcher, admin, api, http_context, - grpc_context, router_context, server, xds_manager, creation_status), + MockedUpdatedClusterManagerImpl(const envoy::config::bootstrap::v3::Bootstrap& bootstrap, + ClusterManagerFactory& factory, + Server::Configuration::ServerFactoryContext& factory_context, + MockLocalClusterUpdate& local_cluster_update, + MockLocalHostsRemoved& local_hosts_removed, + absl::Status& creation_status) + : TestClusterManagerImpl(bootstrap, factory, factory_context, creation_status), local_cluster_update_(local_cluster_update), local_hosts_removed_(local_hosts_removed) {} protected: @@ -67,22 +62,16 @@ class MockedUpdatedClusterManagerImpl : public TestClusterManagerImpl { MockLocalHostsRemoved& local_hosts_removed_; }; -ClusterManagerImplTest::ClusterManagerImplTest() - : http_context_(factory_.stats_.symbolTable()), grpc_context_(factory_.stats_.symbolTable()), - router_context_(factory_.stats_.symbolTable()), - registered_dns_factory_(dns_resolver_factory_) { +ClusterManagerImplTest::ClusterManagerImplTest() : registered_dns_factory_(dns_resolver_factory_) { // Using the NullGrpcMuxImpl by default making the calls a no-op. - ON_CALL(xds_manager_, adsMux()) + ON_CALL(factory_.server_context_.xds_manager_, adsMux()) .WillByDefault(Return(std::make_shared())); } void ClusterManagerImplTest::create(const Bootstrap& bootstrap) { // Override the bootstrap used by the mock Server::Instance object. - server_.bootstrap_.CopyFrom(bootstrap); - cluster_manager_ = TestClusterManagerImpl::createTestClusterManager( - bootstrap, factory_, factory_.server_context_, factory_.stats_, factory_.tls_, - factory_.runtime_, factory_.local_info_, log_manager_, factory_.dispatcher_, admin_, - *factory_.api_, http_context_, grpc_context_, router_context_, server_, xds_manager_); + cluster_manager_ = TestClusterManagerImpl::createTestClusterManager(bootstrap, factory_, + factory_.server_context_); ON_CALL(factory_.server_context_, clusterManager()).WillByDefault(ReturnRef(*cluster_manager_)); THROW_IF_NOT_OK(cluster_manager_->initialize(bootstrap)); @@ -150,11 +139,9 @@ void ClusterManagerImplTest::createWithLocalClusterUpdate(const bool enable_merg const auto& bootstrap = parseBootstrapFromV3Yaml(yaml); absl::Status creation_status = absl::OkStatus(); cluster_manager_ = std::make_unique( - bootstrap, factory_, factory_.server_context_, factory_.stats_, factory_.tls_, - factory_.runtime_, factory_.local_info_, log_manager_, factory_.dispatcher_, admin_, - *factory_.api_, local_cluster_update_, local_hosts_removed_, http_context_, grpc_context_, - router_context_, server_, xds_manager_, creation_status); - THROW_IF_NOT_OK(creation_status); + bootstrap, factory_, factory_.server_context_, local_cluster_update_, local_hosts_removed_, + creation_status); + THROW_IF_NOT_OK_REF(creation_status); THROW_IF_NOT_OK(cluster_manager_->initialize(bootstrap)); } @@ -175,7 +162,9 @@ void ClusterManagerImplTest::checkStats(uint64_t added, uint64_t modified, uint6 void ClusterManagerImplTest::checkConfigDump(const std::string& expected_dump_yaml, const Matchers::StringMatcher& name_matcher) { - auto message_ptr = admin_.config_tracker_.config_tracker_callbacks_["clusters"](name_matcher); + auto message_ptr = + factory_.server_context_.admin_.config_tracker_.config_tracker_callbacks_["clusters"]( + name_matcher); const auto& clusters_config_dump = dynamic_cast(*message_ptr); diff --git a/test/common/upstream/cluster_manager_impl_test_common.h b/test/common/upstream/cluster_manager_impl_test_common.h index 679517814dd02..82af10104e511 100644 --- a/test/common/upstream/cluster_manager_impl_test_common.h +++ b/test/common/upstream/cluster_manager_impl_test_common.h @@ -83,16 +83,9 @@ class ClusterManagerImplTest : public testing::Test { Event::SimulatedTimeSystem time_system_; NiceMock factory_; - NiceMock xds_manager_; std::unique_ptr cluster_manager_; - AccessLog::MockAccessLogManager log_manager_; - NiceMock admin_; MockLocalClusterUpdate local_cluster_update_; MockLocalHostsRemoved local_hosts_removed_; - Http::ContextImpl http_context_; - Grpc::ContextImpl grpc_context_; - Router::ContextImpl router_context_; - NiceMock server_; NiceMock dns_resolver_factory_; Registry::InjectFactory registered_dns_factory_; }; diff --git a/test/common/upstream/cluster_manager_lifecycle_test.cc b/test/common/upstream/cluster_manager_lifecycle_test.cc index 0c0aa6f44ca31..ed02a278c22df 100644 --- a/test/common/upstream/cluster_manager_lifecycle_test.cc +++ b/test/common/upstream/cluster_manager_lifecycle_test.cc @@ -110,8 +110,7 @@ TEST_P(ClusterManagerLifecycleTest, InitializeOrder) { "envoy.load_balancing_policies.ring_hash"); auto proto_message = cluster2->info_->lb_factory_->createEmptyConfigProto(); cluster2->info_->typed_lb_config_ = - cluster2->info_->lb_factory_->loadConfig(*server_.server_factory_context_, *proto_message) - .value(); + cluster2->info_->lb_factory_->loadConfig(factory_.server_context_, *proto_message).value(); // This part tests static init. InSequence s; @@ -914,7 +913,7 @@ TEST_P(ClusterManagerLifecycleTest, HostsPostedToTlsCluster) { cluster1->priority_set_.updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, hosts, {}, - 123, true, 100); + true, 100); auto* tls_cluster = cluster_manager_->getThreadLocalCluster(cluster1->info_->name()); @@ -2037,7 +2036,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdates) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_merge_cancelled").value()); @@ -2048,12 +2047,12 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdates) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); cluster.prioritySet().updateHosts( 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_merge_cancelled").value()); @@ -2071,7 +2070,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdates) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(2, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_merge_cancelled").value()); @@ -2084,21 +2083,21 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdates) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); (*hosts)[0]->healthFlagSet(Host::HealthFlag::FAILED_EDS_HEALTH); cluster.prioritySet().updateHosts( 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); (*hosts)[0]->weight(100); cluster.prioritySet().updateHosts( 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); // Updates not delivered yet. EXPECT_EQ(2, factory_.stats_.counter("cluster_manager.cluster_updated").value()); @@ -2111,7 +2110,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdates) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(3, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); @@ -2154,7 +2153,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdatesOutOfWindow) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.update_out_of_merge_window").value()); @@ -2189,7 +2188,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdatesInsideWindow) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_out_of_merge_window").value()); @@ -2231,7 +2230,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdatesOutOfWindowDisabled) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_out_of_merge_window").value()); @@ -2308,7 +2307,7 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdatesDestroyedOnUpdate) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_merge_cancelled").value()); @@ -2319,12 +2318,12 @@ TEST_P(ClusterManagerLifecycleTest, MergedUpdatesDestroyedOnUpdate) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); cluster.prioritySet().updateHosts( 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_merge_cancelled").value()); @@ -2430,7 +2429,7 @@ TEST_P(ClusterManagerLifecycleTest, CrossPriorityHostMapSyncTest) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); @@ -2447,7 +2446,7 @@ TEST_P(ClusterManagerLifecycleTest, CrossPriorityHostMapSyncTest) { 0, updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 123, absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); EXPECT_EQ(2, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.update_merge_cancelled").value()); @@ -2482,7 +2481,7 @@ TEST_P(ClusterManagerLifecycleTest, DrainConnectionsPredicate) { // Sending non-mergeable updates. cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, hosts, {}, - 123, absl::nullopt, 100); + absl::nullopt, 100); // Using RR LB get a pool for each host. EXPECT_CALL(factory_, allocateConnPool_(_, _, _, _, _, _, _)) @@ -2560,7 +2559,7 @@ TEST_P(ClusterManagerLifecycleTest, ConnPoolsDrainedOnHostSetChange) { // Sending non-mergeable updates. cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, hosts, {}, - 123, absl::nullopt, 100); + absl::nullopt, 100); EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.cluster_updated").value()); EXPECT_EQ(0, factory_.stats_.counter("cluster_manager.cluster_updated_via_merge").value()); @@ -2625,7 +2624,7 @@ TEST_P(ClusterManagerLifecycleTest, ConnPoolsDrainedOnHostSetChange) { // This update should drain all connection pools (host1, host2). cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, {}, - hosts_removed, 123, absl::nullopt, 100); + hosts_removed, absl::nullopt, 100); // Recreate connection pool for host1. cp1 = HttpPoolDataPeer::getPool( @@ -2656,7 +2655,7 @@ TEST_P(ClusterManagerLifecycleTest, ConnPoolsDrainedOnHostSetChange) { // Adding host3 should drain connection pool for host1. cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, - hosts_added, {}, 123, absl::nullopt, 100); + hosts_added, {}, absl::nullopt, 100); } TEST_P(ClusterManagerLifecycleTest, ConnPoolsNotDrainedOnHostSetChange) { @@ -2691,7 +2690,7 @@ TEST_P(ClusterManagerLifecycleTest, ConnPoolsNotDrainedOnHostSetChange) { // Sending non-mergeable updates. cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, hosts, {}, - 123, absl::nullopt, 100); + absl::nullopt, 100); EXPECT_CALL(factory_, allocateConnPool_(_, _, _, _, _, _, _)) .Times(1) @@ -2727,7 +2726,7 @@ TEST_P(ClusterManagerLifecycleTest, ConnPoolsNotDrainedOnHostSetChange) { // No connection pools should be drained. cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, - hosts_added, {}, 123, absl::nullopt, 100); + hosts_added, {}, absl::nullopt, 100); } TEST_P(ClusterManagerLifecycleTest, ConnPoolsIdleDeleted) { @@ -2764,7 +2763,7 @@ TEST_P(ClusterManagerLifecycleTest, ConnPoolsIdleDeleted) { // Sending non-mergeable updates. cluster.prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, hosts, {}, - 123, absl::nullopt, 100); + absl::nullopt, 100); { auto* cp1 = new NiceMock(); diff --git a/test/common/upstream/cluster_manager_misc_test.cc b/test/common/upstream/cluster_manager_misc_test.cc index 237a6cc5adcf2..84b3f20a3c0da 100644 --- a/test/common/upstream/cluster_manager_misc_test.cc +++ b/test/common/upstream/cluster_manager_misc_test.cc @@ -131,8 +131,7 @@ class ClusterManagerImplThreadAwareLbTest : public ClusterManagerImplTest { Config::Utility::getFactoryByName(factory_name); auto proto_message = cluster1->info_->lb_factory_->createEmptyConfigProto(); cluster1->info_->typed_lb_config_ = - cluster1->info_->lb_factory_->loadConfig(*server_.server_factory_context_, *proto_message) - .value(); + cluster1->info_->lb_factory_->loadConfig(factory_.server_context_, *proto_message).value(); InSequence s; EXPECT_CALL(factory_, clusterFromProto_(_, _, _)) @@ -185,7 +184,7 @@ class MetadataWriterLbImpl : public Upstream::ThreadAwareLoadBalancer { Upstream::HostSelectionResponse chooseHost(Upstream::LoadBalancerContext* context) override { if (context && context->requestStreamInfo()) { - ProtobufWkt::Struct value; + Protobuf::Struct value; (*value.mutable_fields())["foo"] = ValueUtil::stringValue("bar"); context->requestStreamInfo()->setDynamicMetadata("envoy.load_balancers.metadata_writer", value); @@ -1040,7 +1039,7 @@ class PreconnectTest : public ClusterManagerImplTest { // Sending non-mergeable updates. cluster_->prioritySet().updateHosts( 0, HostSetImpl::partitionHosts(hosts_ptr, HostsPerLocalityImpl::empty()), nullptr, hosts, - {}, 123, absl::nullopt, 100); + {}, absl::nullopt, 100); } Cluster* cluster_{}; diff --git a/test/common/upstream/deferred_cluster_initialization_test.cc b/test/common/upstream/deferred_cluster_initialization_test.cc index cc05ec547a005..fb713f3087ce4 100644 --- a/test/common/upstream/deferred_cluster_initialization_test.cc +++ b/test/common/upstream/deferred_cluster_initialization_test.cc @@ -21,8 +21,6 @@ namespace Envoy { namespace Upstream { namespace { -using testing::_; - using ClusterType = absl::variant; @@ -59,19 +57,15 @@ envoy::config::cluster::v3::Cluster parseClusterFromV3Yaml(const std::string& ya class DeferredClusterInitializationTest : public testing::TestWithParam { protected: DeferredClusterInitializationTest() - : ads_mux_(std::make_shared>()), - http_context_(factory_.stats_.symbolTable()), grpc_context_(factory_.stats_.symbolTable()), - router_context_(factory_.stats_.symbolTable()) {} + : ads_mux_(std::make_shared>()) {} void create(const envoy::config::bootstrap::v3::Bootstrap& bootstrap) { // Replace the adsMux to have mocked GrpcMux object that will allow invoking // methods when creating the cluster-manager. - ON_CALL(xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); + ON_CALL(factory_.server_context_.xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); - cluster_manager_ = TestClusterManagerImpl::createTestClusterManager( - bootstrap, factory_, factory_.server_context_, factory_.stats_, factory_.tls_, - factory_.runtime_, factory_.local_info_, log_manager_, factory_.dispatcher_, admin_, - *factory_.api_, http_context_, grpc_context_, router_context_, server_, xds_manager_); + cluster_manager_ = TestClusterManagerImpl::createTestClusterManager(bootstrap, factory_, + factory_.server_context_); ON_CALL(factory_.server_context_, clusterManager()).WillByDefault(ReturnRef(*cluster_manager_)); THROW_IF_NOT_OK(cluster_manager_->initialize(bootstrap)); @@ -121,14 +115,7 @@ class DeferredClusterInitializationTest : public testing::TestWithParam { NiceMock factory_; NiceMock validation_context_; std::shared_ptr> ads_mux_; - NiceMock xds_manager_; std::unique_ptr cluster_manager_; - AccessLog::MockAccessLogManager log_manager_; - NiceMock admin_; - Http::ContextImpl http_context_; - Grpc::ContextImpl grpc_context_; - Router::ContextImpl router_context_; - NiceMock server_; }; class StaticClusterTest : public DeferredClusterInitializationTest {}; @@ -438,7 +425,7 @@ class EdsTest : public DeferredClusterInitializationTest { const envoy::config::endpoint::v3::ClusterLoadAssignment& cluster_load_assignment) { const auto decoded_resources = TestUtility::decodeResources({cluster_load_assignment}, "cluster_name"); - EXPECT_TRUE(xds_manager_.subscription_factory_.callbacks_ + EXPECT_TRUE(factory_.server_context_.xds_manager_.subscription_factory_.callbacks_ ->onConfigUpdate(decoded_resources.refvec_, {}, "") .ok()); } diff --git a/test/common/upstream/health_checker_impl_test.cc b/test/common/upstream/health_checker_impl_test.cc index 18aa228dff752..3cb2753c28cd6 100644 --- a/test/common/upstream/health_checker_impl_test.cc +++ b/test/common/upstream/health_checker_impl_test.cc @@ -4173,6 +4173,34 @@ TEST(PayloadMatcher, loadJsonBytes) { } } +TEST(PayloadMatcher, loadSinglePayload) { + // Test the single payload overload with text. + { + envoy::config::core::v3::HealthCheck::Payload single_payload; + single_payload.set_text("39000000"); + + PayloadMatcher::MatchSegments segments = PayloadMatcher::loadProtoBytes(single_payload).value(); + EXPECT_EQ(1U, segments.size()); + } + + // Test the single payload overload with binary. + { + envoy::config::core::v3::HealthCheck::Payload single_payload; + single_payload.set_binary(std::string({0x01, 0x02})); + + PayloadMatcher::MatchSegments segments = PayloadMatcher::loadProtoBytes(single_payload).value(); + EXPECT_EQ(1U, segments.size()); + } + + // Test the single payload overload with invalid hex. + { + envoy::config::core::v3::HealthCheck::Payload single_payload; + single_payload.set_text("gg"); + + EXPECT_FALSE(PayloadMatcher::loadProtoBytes(single_payload).status().ok()); + } +} + static void addUint8(Buffer::Instance& buffer, uint8_t addend) { buffer.add(&addend, sizeof(addend)); } @@ -6825,6 +6853,402 @@ TEST(HealthCheckProto, Validation) { } } +// Tests for HTTP health check payload functionality. +TEST_F(HttpHealthCheckerImplTest, PayloadPostMethod) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: POST + send: + text: "48656C6C6F20576F726C64" # "Hello World" in hex + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; + cluster_->info_->trafficStats()->upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + + // Verify that POST method is used and body is sent. + Buffer::OwnedImpl expected_payload; + expected_payload.add("Hello World"); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool end_stream) -> Http::Status { + EXPECT_EQ(headers.getMethodValue(), "POST"); + EXPECT_EQ(headers.getPathValue(), "/healthcheck"); + EXPECT_EQ(headers.getContentLengthValue(), "11"); // "Hello World" is 11 bytes + EXPECT_FALSE(end_stream); // Should not end stream yet as we have body to send + return Http::okStatus(); + })); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeData(_, true)) + .WillOnce(Invoke([&](Buffer::Instance& data, bool end_stream) -> void { + EXPECT_EQ(data.toString(), "Hello World"); + EXPECT_TRUE(end_stream); // Should end stream after sending body + })); + + health_checker_->start(); + + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.max_interval", _)); + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.min_interval", _)) + .WillOnce(Return(45000)); + EXPECT_CALL(*test_sessions_[0]->interval_timer_, + enableTimer(std::chrono::milliseconds(45000), _)); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); + respond(0, "200", false, false, true); + EXPECT_EQ(Host::Health::Healthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->coarseHealth()); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadPutMethod) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /api/health + method: PUT + send: + text: "7B0A20202270696E67223A20226F6B220A7D" # {"ping": "ok"} in hex + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; + cluster_->info_->trafficStats()->upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + + // Verify that PUT method is used and JSON body is sent. + const std::string expected_json = "{\n \"ping\": \"ok\"\n}"; + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool end_stream) -> Http::Status { + EXPECT_EQ(headers.getMethodValue(), "PUT"); + EXPECT_EQ(headers.getPathValue(), "/api/health"); + EXPECT_EQ(headers.getContentLengthValue(), std::to_string(expected_json.length())); + EXPECT_FALSE(end_stream); + return Http::okStatus(); + })); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeData(_, true)) + .WillOnce(Invoke([&](Buffer::Instance& data, bool end_stream) -> void { + EXPECT_EQ(data.toString(), expected_json); + EXPECT_TRUE(end_stream); + })); + + health_checker_->start(); + + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.max_interval", _)); + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.min_interval", _)) + .WillOnce(Return(45000)); + EXPECT_CALL(*test_sessions_[0]->interval_timer_, + enableTimer(std::chrono::milliseconds(45000), _)); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); + respond(0, "200", false, false, true); + EXPECT_EQ(Host::Health::Healthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->coarseHealth()); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadGetMethodThrowsError) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: GET + send: + text: "48656C6C6F" # "Hello" in hex + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + allocHealthChecker(yaml), EnvoyException, + "HTTP health check cannot specify a request payload with method 'GET'. " + "Only methods that support a request body (POST, PUT, PATCH, OPTIONS) can " + "be used with payload."); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadHeadMethodThrowsError) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: HEAD + send: + text: "48656C6C6F" # "Hello" in hex + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + allocHealthChecker(yaml), EnvoyException, + "HTTP health check cannot specify a request payload with method 'HEAD'. " + "Only methods that support a request body (POST, PUT, PATCH, OPTIONS) can be used with " + "payload."); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadDeleteMethodThrowsError) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: DELETE + send: + text: "48656C6C6F" # "Hello" in hex + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + allocHealthChecker(yaml), EnvoyException, + "HTTP health check cannot specify a request payload with method 'DELETE'. " + "Only methods that support a request body (POST, PUT, PATCH, OPTIONS) can be used with " + "payload."); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadTraceMethodThrowsError) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: TRACE + send: + text: "48656C6C6F" # "Hello" in hex + )EOF"; + + EXPECT_THROW_WITH_MESSAGE( + allocHealthChecker(yaml), EnvoyException, + "HTTP health check cannot specify a request payload with method 'TRACE'. " + "Only methods that support a request body (POST, PUT, PATCH, OPTIONS) can be used with " + "payload."); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadOptionsMethodSuccess) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: OPTIONS + send: + text: "48656C6C6F" # "Hello" in hex + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; + cluster_->info_->trafficStats()->upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool end_stream) -> Http::Status { + EXPECT_EQ(headers.getMethodValue(), "OPTIONS"); + EXPECT_EQ(headers.getContentLengthValue(), "5"); // "Hello" is 5 bytes + EXPECT_FALSE(end_stream); + return Http::okStatus(); + })); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeData(_, true)) + .WillOnce(Invoke([&](Buffer::Instance& data, bool end_stream) -> void { + EXPECT_EQ(data.toString(), "Hello"); + EXPECT_TRUE(end_stream); + })); + + health_checker_->start(); + + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.max_interval", _)); + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.min_interval", _)) + .WillOnce(Return(45000)); + EXPECT_CALL(*test_sessions_[0]->interval_timer_, + enableTimer(std::chrono::milliseconds(45000), _)); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); + respond(0, "200", false, false, true); + EXPECT_EQ(Host::Health::Healthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->coarseHealth()); +} + +TEST_F(HttpHealthCheckerImplTest, PayloadPatchMethodSuccess) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: PATCH + send: + text: "7B2274657374223A2274727565227D" # {"test":"true"} in hex + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; + cluster_->info_->trafficStats()->upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool end_stream) -> Http::Status { + EXPECT_EQ(headers.getMethodValue(), "PATCH"); + EXPECT_EQ(headers.getContentLengthValue(), "15"); // {"test":"true"} is 15 bytes + EXPECT_FALSE(end_stream); + return Http::okStatus(); + })); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeData(_, true)) + .WillOnce(Invoke([&](Buffer::Instance& data, bool end_stream) -> void { + EXPECT_EQ(data.toString(), "{\"test\":\"true\"}"); + EXPECT_TRUE(end_stream); + })); + + health_checker_->start(); + + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.max_interval", _)); + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.min_interval", _)) + .WillOnce(Return(45000)); + EXPECT_CALL(*test_sessions_[0]->interval_timer_, + enableTimer(std::chrono::milliseconds(45000), _)); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); + respond(0, "200", false, false, true); + EXPECT_EQ(Host::Health::Healthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->coarseHealth()); +} + +TEST_F(HttpHealthCheckerImplTest, NoPayloadGetMethodDefault) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; + cluster_->info_->trafficStats()->upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + + // Verify that when no payload is specified, GET method works normally and no body is sent. + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeHeaders(_, true)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool end_stream) -> Http::Status { + EXPECT_EQ(headers.getMethodValue(), "GET"); + EXPECT_EQ(headers.getPathValue(), "/healthcheck"); + EXPECT_EQ(headers.ContentLength(), nullptr); // No Content-Length header should be set + EXPECT_TRUE(end_stream); // Should end stream as there's no body to send + return Http::okStatus(); + })); + + health_checker_->start(); + + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.max_interval", _)); + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.min_interval", _)) + .WillOnce(Return(45000)); + EXPECT_CALL(*test_sessions_[0]->interval_timer_, + enableTimer(std::chrono::milliseconds(45000), _)); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); + respond(0, "200", false, false, true); + EXPECT_EQ(Host::Health::Healthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->coarseHealth()); +} + +TEST_F(HttpHealthCheckerImplTest, MinimalPayloadPostMethod) { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /healthcheck + method: POST + send: + text: "31" # "1" in hex - minimal valid payload + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; + cluster_->info_->trafficStats()->upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + + // Verify that minimal payload is sent correctly. + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([&](const Http::RequestHeaderMap& headers, bool end_stream) -> Http::Status { + EXPECT_EQ(headers.getMethodValue(), "POST"); + EXPECT_EQ(headers.getContentLengthValue(), "1"); // "1" is 1 byte + EXPECT_FALSE(end_stream); + return Http::okStatus(); + })); + + EXPECT_CALL(test_sessions_[0]->request_encoder_, encodeData(_, true)) + .WillOnce(Invoke([&](Buffer::Instance& data, bool end_stream) -> void { + EXPECT_EQ(data.toString(), "1"); + EXPECT_TRUE(end_stream); + })); + + health_checker_->start(); + + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.max_interval", _)); + EXPECT_CALL(runtime_.snapshot_, getInteger("health_check.min_interval", _)) + .WillOnce(Return(45000)); + EXPECT_CALL(*test_sessions_[0]->interval_timer_, + enableTimer(std::chrono::milliseconds(45000), _)); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); + respond(0, "200", false, false, true); + EXPECT_EQ(Host::Health::Healthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->coarseHealth()); +} + } // namespace } // namespace Upstream } // namespace Envoy diff --git a/test/common/upstream/load_balancer_simulation_test.cc b/test/common/upstream/load_balancer_simulation_test.cc index 1c957fe415406..43d2104bd0090 100644 --- a/test/common/upstream/load_balancer_simulation_test.cc +++ b/test/common/upstream/load_balancer_simulation_test.cc @@ -101,7 +101,7 @@ void leastRequestLBWeightTest(LRLBTestParams params) { updateHostsParams(updated_hosts, updated_locality_hosts, std::make_shared(*updated_hosts), updated_locality_hosts), - {}, hosts, {}, random.random(), absl::nullopt); + {}, hosts, {}, absl::nullopt); Stats::IsolatedStoreImpl stats_store; ClusterLbStatNames stat_names(stats_store.symbolTable()); @@ -264,7 +264,7 @@ class DISABLED_SimulationTest : public testing::Test { // NOLINT(readability-ide updateHostsParams(originating_hosts, per_zone_local_shared, std::make_shared(*originating_hosts), per_zone_local_shared), - {}, empty_vector_, empty_vector_, random_.random(), absl::nullopt); + {}, empty_vector_, empty_vector_, absl::nullopt); HostConstSharedPtr selected = lb.chooseHost(nullptr).host; hits[selected->address()->asString()]++; diff --git a/test/common/upstream/load_stats_reporter_test.cc b/test/common/upstream/load_stats_reporter_test.cc index effafb0a79476..949b42a00e426 100644 --- a/test/common/upstream/load_stats_reporter_test.cc +++ b/test/common/upstream/load_stats_reporter_test.cc @@ -3,6 +3,7 @@ #include "envoy/config/endpoint/v3/load_report.pb.h" #include "envoy/service/load_stats/v3/lrs.pb.h" +#include "source/common/network/address_impl.h" #include "source/common/upstream/load_stats_reporter.h" #include "test/common/upstream/utility.h" @@ -63,10 +64,12 @@ class LoadStatsReporterTest : public testing::Test { sendMessageRaw_(Grpc::ProtoBufferEqIgnoreRepeatedFieldOrdering(expected_request), false)); } - void deliverLoadStatsResponse(const std::vector& cluster_names) { + void deliverLoadStatsResponse(const std::vector& cluster_names, + bool report_endpoint_granularity = false) { std::unique_ptr response( new envoy::service::load_stats::v3::LoadStatsResponse()); response->mutable_load_reporting_interval()->set_seconds(42); + response->set_report_endpoint_granularity(report_endpoint_granularity); std::copy(cluster_names.begin(), cluster_names.end(), Protobuf::RepeatedPtrFieldBackInserter(response->mutable_clusters())); @@ -74,6 +77,16 @@ class LoadStatsReporterTest : public testing::Test { load_stats_reporter_->onReceiveMessage(std::move(response)); } + void + addEndpointStatExpectation(envoy::config::endpoint::v3::UpstreamEndpointStats* endpoint_stats, + const std::string& metric_name, uint64_t rq_count, + double total_value) { + auto* metric = endpoint_stats->add_load_metric_stats(); + metric->set_metric_name(metric_name); + metric->set_num_requests_finished_with_metric(rq_count); + metric->set_total_metric_value(total_value); + } + void setDropOverload(envoy::config::endpoint::v3::ClusterStats& cluster_stats, uint64_t count) { auto* dropped_request = cluster_stats.add_dropped_requests(); dropped_request->set_category("drop_overload"); @@ -250,6 +263,10 @@ HostSharedPtr makeTestHost(const std::string& hostname, const auto host = std::make_shared>(); ON_CALL(*host, hostname()).WillByDefault(::testing::ReturnRef(hostname)); ON_CALL(*host, locality()).WillByDefault(::testing::ReturnRef(locality)); + + // Use a concrete Ipv4Instance instead of a mock address + auto address = std::make_shared("127.0.0.1", 80); + ON_CALL(*host, address()).WillByDefault(::testing::Return(address)); return host; } @@ -279,6 +296,136 @@ void addStatExpectation(envoy::config::endpoint::v3::UpstreamLocalityStats* stat metric->set_total_metric_value(total_metric_value); } +// This test validates that the LoadStatsReporter correctly handles and reports +// endpoint-level granularity load metrics when the feature is enabled. It sets +// up a cluster with a host, simulates load metrics, and ensures that the +// generated load report includes the expected endpoint-level statistics. +TEST_F(LoadStatsReporterTest, EndpointLevelLoadStatsReporting) { + // Enable endpoint granularity + EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); + expectSendMessage({}); + createLoadStatsReporter(); + time_system_.setMonotonicTime(std::chrono::microseconds(100)); + + NiceMock cluster; + MockHostSet& host_set = *cluster.prioritySet().getMockHostSet(0); + ::envoy::config::core::v3::Locality locality; + locality.set_region("test_region"); + + // Create two hosts with different metric values + HostSharedPtr host1 = makeTestHost("host1", locality); + HostSharedPtr host2 = makeTestHost("host2", locality); + host_set.hosts_per_locality_ = makeHostsPerLocality({{host1, host2}}); + addStats(host1, 10.0); // metric_a = 10.0 + addStats(host2, 20.0); // metric_a = 20.0 + + cluster.info_->eds_service_name_ = "eds_service_for_foo"; + + ON_CALL(cm_, getActiveCluster("foo")) + .WillByDefault(Return(OptRef(cluster))); + deliverLoadStatsResponse({"foo"}, true); + time_system_.setMonotonicTime(std::chrono::microseconds(101)); + { + envoy::config::endpoint::v3::ClusterStats expected_cluster_stats; + + expected_cluster_stats.set_cluster_name("foo"); + expected_cluster_stats.set_cluster_service_name("eds_service_for_foo"); + expected_cluster_stats.mutable_load_report_interval()->MergeFrom( + Protobuf::util::TimeUtil::MicrosecondsToDuration(1)); + + auto* expected_locality_stats = expected_cluster_stats.add_upstream_locality_stats(); + expected_locality_stats->mutable_locality()->MergeFrom(locality); + expected_locality_stats->set_priority(0); + expected_locality_stats->set_total_successful_requests(2); + expected_locality_stats->set_total_issued_requests(2); + // Locality metric is the sum + addStatExpectation(expected_locality_stats, "metric_a", 2, 30.0); + + // Endpoint 1 + auto* endpoint_stats1 = expected_locality_stats->add_upstream_endpoint_stats(); + endpoint_stats1->mutable_address()->mutable_socket_address()->set_address("127.0.0.1"); + endpoint_stats1->mutable_address()->mutable_socket_address()->set_port_value(80); + endpoint_stats1->set_total_successful_requests(1); + endpoint_stats1->set_total_issued_requests(1); + addEndpointStatExpectation(endpoint_stats1, "metric_a", 1, 10.0); + + // Endpoint 2 + auto* endpoint_stats2 = expected_locality_stats->add_upstream_endpoint_stats(); + endpoint_stats2->mutable_address()->mutable_socket_address()->set_address("127.0.0.1"); + endpoint_stats2->mutable_address()->mutable_socket_address()->set_port_value(80); + endpoint_stats2->set_total_successful_requests(1); + endpoint_stats2->set_total_issued_requests(1); + addEndpointStatExpectation(endpoint_stats2, "metric_a", 1, 20.0); + + std::vector expected_cluster_stats_vector = { + expected_cluster_stats}; + + expectSendMessage(expected_cluster_stats_vector); + } + EXPECT_CALL(*response_timer_, enableTimer(std::chrono::milliseconds(42000), _)); + response_timer_cb_(); +} + +// This test validates that endpoint stats are not reported if the endpoint has no load stat +// updates. +TEST_F(LoadStatsReporterTest, EndpointLevelLoadStatsReportingNoUpdate) { + // Enable endpoint granularity + EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); + expectSendMessage({}); + createLoadStatsReporter(); + time_system_.setMonotonicTime(std::chrono::microseconds(100)); + + NiceMock cluster; + MockHostSet& host_set = *cluster.prioritySet().getMockHostSet(0); + ::envoy::config::core::v3::Locality locality; + locality.set_region("test_region"); + + // Create two hosts, but only one will have stats. + HostSharedPtr host1 = makeTestHost("host1", locality); + HostSharedPtr host2 = makeTestHost("host2", locality); + host_set.hosts_per_locality_ = makeHostsPerLocality({{host1, host2}}); + addStats(host1, 10.0); + // Host2 has no updates. Its stats are all 0 and will be latched as such. + + cluster.info_->eds_service_name_ = "eds_service_for_foo"; + + ON_CALL(cm_, getActiveCluster("foo")) + .WillByDefault(Return(OptRef(cluster))); + deliverLoadStatsResponse({"foo"}, true); + time_system_.setMonotonicTime(std::chrono::microseconds(101)); + { + envoy::config::endpoint::v3::ClusterStats expected_cluster_stats; + + expected_cluster_stats.set_cluster_name("foo"); + expected_cluster_stats.set_cluster_service_name("eds_service_for_foo"); + expected_cluster_stats.mutable_load_report_interval()->MergeFrom( + Protobuf::util::TimeUtil::MicrosecondsToDuration(1)); + + auto* expected_locality_stats = expected_cluster_stats.add_upstream_locality_stats(); + expected_locality_stats->mutable_locality()->MergeFrom(locality); + expected_locality_stats->set_priority(0); + // Locality stats should only reflect host1. + expected_locality_stats->set_total_successful_requests(1); + expected_locality_stats->set_total_issued_requests(1); + addStatExpectation(expected_locality_stats, "metric_a", 1, 10.0); + + // Only Endpoint 1 should be in the report. + auto* endpoint_stats1 = expected_locality_stats->add_upstream_endpoint_stats(); + endpoint_stats1->mutable_address()->mutable_socket_address()->set_address("127.0.0.1"); + endpoint_stats1->mutable_address()->mutable_socket_address()->set_port_value(80); + endpoint_stats1->set_total_successful_requests(1); + endpoint_stats1->set_total_issued_requests(1); + addEndpointStatExpectation(endpoint_stats1, "metric_a", 1, 10.0); + + std::vector expected_cluster_stats_vector = { + expected_cluster_stats}; + + expectSendMessage(expected_cluster_stats_vector); + } + EXPECT_CALL(*response_timer_, enableTimer(std::chrono::milliseconds(42000), _)); + response_timer_cb_(); +} + class LoadStatsReporterTestWithRqTotal : public LoadStatsReporterTest, public testing::WithParamInterface { public: diff --git a/test/common/upstream/local_address_selector_integration_test.cc b/test/common/upstream/local_address_selector_integration_test.cc index 22b0ce57426a5..4f5ef84940dc8 100644 --- a/test/common/upstream/local_address_selector_integration_test.cc +++ b/test/common/upstream/local_address_selector_integration_test.cc @@ -57,7 +57,7 @@ TEST_P(HttpProtocolIntegrationTest, CustomUpstreamLocalAddressSelector) { uint32_t const port_value = 1234; auto bind_config = bootstrap.mutable_cluster_manager()->mutable_upstream_bind_config(); auto local_address_selector_config = bind_config->mutable_local_address_selector(); - ProtobufWkt::Empty empty; + Protobuf::Empty empty; local_address_selector_config->mutable_typed_config()->PackFrom(empty); local_address_selector_config->set_name("mock.upstream.local.address.selector"); bind_config->mutable_source_address()->set_address("::1"); @@ -131,7 +131,7 @@ TEST_P(HttpProtocolIntegrationTest, BindConfigOverride) { bind_config->mutable_source_address()->set_address( version_ == Network::Address::IpVersion::v4 ? "127.0.0.2" : "::1"); bind_config->mutable_source_address()->set_port_value(port_value_1); - ProtobufWkt::Empty empty; + Protobuf::Empty empty; auto address_selector_config = bind_config->mutable_local_address_selector(); address_selector_config->mutable_typed_config()->PackFrom(empty); address_selector_config->set_name("test.upstream.local.address.selector"); diff --git a/test/common/upstream/od_cds_api_impl_test.cc b/test/common/upstream/od_cds_api_impl_test.cc index e2cc8a07d24a9..82b1d915fc6f9 100644 --- a/test/common/upstream/od_cds_api_impl_test.cc +++ b/test/common/upstream/od_cds_api_impl_test.cc @@ -4,7 +4,9 @@ #include "source/common/stats/isolated_store_impl.h" #include "source/common/upstream/od_cds_api_impl.h" +#include "test/mocks/config/xds_manager.h" #include "test/mocks/protobuf/mocks.h" +#include "test/mocks/server/mocks.h" #include "test/mocks/upstream/cluster_manager.h" #include "test/mocks/upstream/missing_cluster_notifier.h" @@ -24,14 +26,17 @@ class OdCdsApiImplTest : public testing::Test { void SetUp() override { envoy::config::core::v3::ConfigSource odcds_config; OptRef null_locator; - odcds_ = *OdCdsApiImpl::create(odcds_config, null_locator, cm_, notifier_, *store_.rootScope(), - validation_visitor_); + odcds_ = + *OdCdsApiImpl::create(odcds_config, null_locator, xds_manager_, cm_, notifier_, + *store_.rootScope(), validation_visitor_, server_factory_context_); odcds_callbacks_ = cm_.subscription_factory_.callbacks_; } + NiceMock xds_manager_; NiceMock cm_; Stats::IsolatedStoreImpl store_; MockMissingClusterNotifier notifier_; + NiceMock server_factory_context_; OdCdsApiSharedPtr odcds_; Config::SubscriptionCallbacks* odcds_callbacks_ = nullptr; NiceMock validation_visitor_; diff --git a/test/common/upstream/test_cluster_manager.h b/test/common/upstream/test_cluster_manager.h index fa5df9d518403..8998d0c5f340c 100644 --- a/test/common/upstream/test_cluster_manager.h +++ b/test/common/upstream/test_cluster_manager.h @@ -63,9 +63,11 @@ namespace Upstream { // the expectations when needed. class TestClusterManagerFactory : public ClusterManagerFactory { public: - TestClusterManagerFactory() : api_(Api::createApiForTest(stats_, random_)) { - + TestClusterManagerFactory() + : api_(Api::createApiForTest(server_context_.store_, server_context_.api_.random_)) { ON_CALL(server_context_, api()).WillByDefault(testing::ReturnRef(*api_)); + ON_CALL(server_context_, sslContextManager()).WillByDefault(ReturnRef(ssl_context_manager_)); + ON_CALL(*this, clusterFromProto_(_, _, _)) .WillByDefault(Invoke( [&](const envoy::config::cluster::v3::Cluster& cluster, @@ -83,7 +85,7 @@ class TestClusterManagerFactory : public ClusterManagerFactory { })); } - ~TestClusterManagerFactory() override { dispatcher_.to_delete_.clear(); } + ~TestClusterManagerFactory() override { server_context_.dispatcher_.to_delete_.clear(); } Http::ConnectionPool::InstancePtr allocateConnPool( Event::Dispatcher&, HostConstSharedPtr host, ResourcePriority, std::vector&, @@ -95,7 +97,7 @@ class TestClusterManagerFactory : public ClusterManagerFactory { OptRef network_observer_registry) override { return Http::ConnectionPool::InstancePtr{ allocateConnPool_(host, alternate_protocol_options, options, transport_socket_options, - state, network_observer_registry, overload_manager_)}; + state, network_observer_registry, server_context_.overloadManager())}; } Tcp::ConnectionPool::InstancePtr @@ -116,8 +118,8 @@ class TestClusterManagerFactory : public ClusterManagerFactory { } absl::StatusOr createCds(const envoy::config::core::v3::ConfigSource&, - const xds::core::v3::ResourceLocator*, - ClusterManager&) override { + const xds::core::v3::ResourceLocator*, ClusterManager&, + bool) override { return CdsApiPtr{createCds_()}; } @@ -145,38 +147,27 @@ class TestClusterManagerFactory : public ClusterManagerFactory { NiceMock server_context_; Stats::TestUtil::TestStore& stats_ = server_context_.store_; NiceMock& tls_ = server_context_.thread_local_; + NiceMock& dispatcher_ = server_context_.dispatcher_; + testing::NiceMock& random_ = server_context_.api_.random_; + std::shared_ptr> dns_resolver_{ new NiceMock}; - NiceMock& runtime_ = server_context_.runtime_loader_; - NiceMock& dispatcher_ = server_context_.dispatcher_; Extensions::TransportSockets::Tls::ContextManagerImpl ssl_context_manager_{server_context_}; - NiceMock& local_info_ = server_context_.local_info_; - NiceMock& admin_ = server_context_.admin_; - NiceMock& log_manager_ = server_context_.access_log_manager_; - NiceMock validation_visitor_; - NiceMock random_; Api::ApiPtr api_; - Server::MockOptions& options_ = server_context_.options_; - NiceMock overload_manager_; }; // A test version of ClusterManagerImpl that provides a way to get a non-const handle to the // clusters, which is necessary in order to call updateHosts on the priority set. class TestClusterManagerImpl : public ClusterManagerImpl { public: - static std::unique_ptr createTestClusterManager( - const envoy::config::bootstrap::v3::Bootstrap& bootstrap, ClusterManagerFactory& factory, - Server::Configuration::CommonFactoryContext& context, Stats::Store& stats, - ThreadLocal::Instance& tls, Runtime::Loader& runtime, const LocalInfo::LocalInfo& local_info, - AccessLog::AccessLogManager& log_manager, Event::Dispatcher& main_thread_dispatcher, - Server::Admin& admin, Api::Api& api, Http::Context& http_context, Grpc::Context& grpc_context, - Router::Context& router_context, Server::Instance& server, Config::XdsManager& xds_manager) { + static std::unique_ptr + createTestClusterManager(const envoy::config::bootstrap::v3::Bootstrap& bootstrap, + ClusterManagerFactory& factory, + Server::Configuration::ServerFactoryContext& context) { absl::Status creation_status = absl::OkStatus(); - auto cluster_manager = std::unique_ptr{new TestClusterManagerImpl( - bootstrap, factory, context, stats, tls, runtime, local_info, log_manager, - main_thread_dispatcher, admin, api, http_context, grpc_context, router_context, server, - xds_manager, creation_status)}; - THROW_IF_NOT_OK(creation_status); + auto cluster_manager = std::unique_ptr{ + new TestClusterManagerImpl(bootstrap, factory, context, creation_status)}; + THROW_IF_NOT_OK_REF(creation_status); return cluster_manager; } @@ -209,17 +200,9 @@ class TestClusterManagerImpl : public ClusterManagerImpl { TestClusterManagerImpl(const envoy::config::bootstrap::v3::Bootstrap& bootstrap, ClusterManagerFactory& factory, - Server::Configuration::CommonFactoryContext& context, Stats::Store& stats, - ThreadLocal::Instance& tls, Runtime::Loader& runtime, - const LocalInfo::LocalInfo& local_info, - AccessLog::AccessLogManager& log_manager, - Event::Dispatcher& main_thread_dispatcher, Server::Admin& admin, - Api::Api& api, Http::Context& http_context, Grpc::Context& grpc_context, - Router::Context& router_context, Server::Instance& server, - Config::XdsManager& xds_manager, absl::Status& creation_status) - : ClusterManagerImpl(bootstrap, factory, context, stats, tls, runtime, local_info, - log_manager, main_thread_dispatcher, admin, api, http_context, - grpc_context, router_context, server, xds_manager, creation_status) {} + Server::Configuration::ServerFactoryContext& context, + absl::Status& creation_status) + : ClusterManagerImpl(bootstrap, factory, context, creation_status) {} }; } // namespace Upstream diff --git a/test/common/upstream/test_local_address_selector.h b/test/common/upstream/test_local_address_selector.h index a4b7407a6a602..501114c0d9c32 100644 --- a/test/common/upstream/test_local_address_selector.h +++ b/test/common/upstream/test_local_address_selector.h @@ -48,7 +48,7 @@ class TestUpstreamLocalAddressSelectorFactory : public UpstreamLocalAddressSelec } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "test.upstream.local.address.selector"; } diff --git a/test/common/upstream/upstream_impl_test.cc b/test/common/upstream/upstream_impl_test.cc index 6e114d0fecd19..862052e490ff4 100644 --- a/test/common/upstream/upstream_impl_test.cc +++ b/test/common/upstream/upstream_impl_test.cc @@ -27,8 +27,8 @@ #include "source/common/protobuf/utility.h" #include "source/common/singleton/manager_impl.h" #include "source/extensions/clusters/common/dns_cluster_backcompat.h" +#include "source/extensions/clusters/dns/dns_cluster.h" #include "source/extensions/clusters/static/static_cluster.h" -#include "source/extensions/clusters/strict_dns/strict_dns_cluster.h" #include "source/extensions/load_balancing_policies/least_request/config.h" #include "source/extensions/load_balancing_policies/round_robin/config.h" #include "source/server/transport_socket_config_impl.h" @@ -36,7 +36,6 @@ #include "test/common/stats/stat_test_utility.h" #include "test/common/upstream/test_local_address_selector.h" #include "test/common/upstream/utility.h" -#include "test/mocks/common.h" #include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" #include "test/mocks/protobuf/mocks.h" @@ -63,6 +62,7 @@ using testing::_; using testing::AnyNumber; using testing::ContainerEq; using testing::Invoke; +using testing::MockFunction; using testing::NiceMock; using testing::Return; using testing::ReturnRef; @@ -70,6 +70,10 @@ using testing::ReturnRef; namespace Envoy { namespace Upstream { +using MockInitializeCallback = MockFunction; +using MockPriorityUpdateCallback = + MockFunction; + class UpstreamImplTestBase { protected: UpstreamImplTestBase() { ON_CALL(server_context_, api()).WillByDefault(ReturnRef(*api_)); } @@ -82,7 +86,7 @@ class UpstreamImplTestBase { return std::dynamic_pointer_cast(status_or_cluster->first); } - absl::StatusOr> + absl::StatusOr> createStrictDnsCluster(const envoy::config::cluster::v3::Cluster& cluster_config, ClusterFactoryContext& factory_context, std::shared_ptr dns_resolver) { @@ -96,7 +100,7 @@ class UpstreamImplTestBase { if (!status_or_cluster.ok()) { return status_or_cluster.status(); } - return (std::dynamic_pointer_cast(status_or_cluster->first)); + return (std::dynamic_pointer_cast(status_or_cluster->first)); } NiceMock server_context_; @@ -117,20 +121,6 @@ std::list hostListToAddresses(const HostVector& hosts) { return addresses; } -template -std::shared_ptr -makeHostsFromHostsPerLocality(HostsPerLocalityConstSharedPtr hosts_per_locality) { - HostVector hosts; - - for (const auto& locality_hosts : hosts_per_locality->get()) { - for (const auto& host : locality_hosts) { - hosts.emplace_back(host); - } - } - - return std::make_shared(hosts); -} - struct ResolverData { ResolverData(Network::MockDnsResolver& dns_resolver, Event::MockDispatcher& dispatcher) { timer_ = new Event::MockTimer(&dispatcher); @@ -153,35 +143,39 @@ struct ResolverData { }; using StrictDnsConfigTuple = - std::tuple>; + std::tuple, std::string>; std::vector generateStrictDnsParams() { std::vector dns_config; { std::string family_yaml(""); Network::DnsLookupFamily family(Network::DnsLookupFamily::Auto); std::list dns_response{"127.0.0.1", "127.0.0.2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); } { std::string family_yaml(R"EOF(dns_lookup_family: v4_only )EOF"); Network::DnsLookupFamily family(Network::DnsLookupFamily::V4Only); std::list dns_response{"127.0.0.1", "127.0.0.2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); } { std::string family_yaml(R"EOF(dns_lookup_family: v6_only )EOF"); Network::DnsLookupFamily family(Network::DnsLookupFamily::V6Only); std::list dns_response{"::1", "::2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); } { std::string family_yaml(R"EOF(dns_lookup_family: auto )EOF"); Network::DnsLookupFamily family(Network::DnsLookupFamily::Auto); std::list dns_response{"127.0.0.1", "127.0.0.2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); } return dns_config; } @@ -190,8 +184,10 @@ class StrictDnsParamTest : public testing::TestWithParam, public UpstreamImplTestBase { public: void dropOverloadRuntimeTest(uint64_t numerator, float drop_ratio) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -229,8 +225,10 @@ INSTANTIATE_TEST_SUITE_P(DnsParam, StrictDnsParamTest, testing::ValuesIn(generateStrictDnsParams())); TEST_P(StrictDnsParamTest, ImmediateResolve) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -247,7 +245,6 @@ TEST_P(StrictDnsParamTest, ImmediateResolve) { address: foo.bar.com port_value: 443 )EOF"; - EXPECT_CALL(initialized, ready()); EXPECT_CALL(*dns_resolver, resolve("foo.bar.com", std::get<1>(GetParam()), _)) .WillOnce(Invoke([&](const std::string&, Network::DnsLookupFamily, Network::DnsResolver::ResolveCb cb) -> Network::ActiveDnsQuery* { @@ -261,17 +258,21 @@ TEST_P(StrictDnsParamTest, ImmediateResolve) { false); auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver); - cluster->initialize([&]() -> absl::Status { - initialized.ready(); - return absl::OkStatus(); - }); + { + MockInitializeCallback initialize_cb; + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); + cluster->initialize(initialize_cb.AsStdFunction()); + // initialize_cb going out of scope ensures it was called here. + } EXPECT_EQ(2UL, cluster->prioritySet().hostSetsPerPriority()[0]->hosts().size()); EXPECT_EQ(2UL, cluster->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); } TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBasicMillion) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -296,8 +297,10 @@ TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBasicMillion) { } TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBasicTenThousand) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -322,8 +325,10 @@ TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBasicTenThousand) { } TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBadDenominator) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -349,8 +354,10 @@ TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBadDenominator) { } TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBadNumerator) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -377,8 +384,10 @@ TEST_P(StrictDnsParamTest, DropOverLoadConfigTestBadNumerator) { } TEST_P(StrictDnsParamTest, DropOverLoadConfigTestMultipleCategory) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); auto dns_resolver = std::make_shared>(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -428,8 +437,18 @@ class StrictDnsClusterImplTest : public testing::Test, public UpstreamImplTestBa std::make_shared(); }; -TEST_F(StrictDnsClusterImplTest, ZeroHostsIsInializedImmediately) { - ReadyWatcher initialized; +class StrictDnsClusterImplParamTest : public StrictDnsClusterImplTest, + public testing::WithParamInterface { +public: + TestScopedRuntime scoped_runtime; +}; + +INSTANTIATE_TEST_SUITE_P(DnsImplementations, StrictDnsClusterImplParamTest, + testing::ValuesIn({"true", "false"})); + +TEST_P(StrictDnsClusterImplParamTest, ZeroHostsIsInializedImmediately) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string yaml = R"EOF( name: name @@ -446,18 +465,21 @@ TEST_F(StrictDnsClusterImplTest, ZeroHostsIsInializedImmediately) { false); auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_); - EXPECT_CALL(initialized, ready()); - cluster->initialize([&]() -> absl::Status { - initialized.ready(); - return absl::OkStatus(); - }); + + { + MockInitializeCallback initialize_cb; + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); + cluster->initialize(initialize_cb.AsStdFunction()); + // initialize_cb going out of scope ensures it was called here. + } EXPECT_EQ(0UL, cluster->prioritySet().hostSetsPerPriority()[0]->hosts().size()); EXPECT_EQ(0UL, cluster->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); } // Resolve zero hosts, while using health checking. -TEST_F(StrictDnsClusterImplTest, ZeroHostsHealthChecker) { - ReadyWatcher initialized; +TEST_P(StrictDnsClusterImplParamTest, ZeroHostsHealthChecker) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string yaml = R"EOF( name: name @@ -485,20 +507,22 @@ TEST_F(StrictDnsClusterImplTest, ZeroHostsHealthChecker) { EXPECT_CALL(*health_checker, start()); EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)); cluster->setHealthChecker(health_checker); - cluster->initialize([&]() -> absl::Status { - initialized.ready(); - return absl::OkStatus(); - }); + + MockInitializeCallback initialize_cb; + cluster->initialize(initialize_cb.AsStdFunction()); EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)); - EXPECT_CALL(initialized, ready()); + // We expect initialize_cb to only be called on resolve, not during initialize. + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*resolver.timer_, enableTimer(_, _)); resolver.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", {}); EXPECT_EQ(0UL, cluster->prioritySet().hostSetsPerPriority()[0]->hosts().size()); EXPECT_EQ(0UL, cluster->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); } -TEST_F(StrictDnsClusterImplTest, DontWaitForDNSOnInit) { +TEST_P(StrictDnsClusterImplParamTest, DontWaitForDNSOnInit) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -527,30 +551,26 @@ TEST_F(StrictDnsClusterImplTest, DontWaitForDNSOnInit) { false); auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_); + { + MockInitializeCallback initialize_cb; + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); + cluster->initialize(initialize_cb.AsStdFunction()); + // initialize_cb going out of scope validates that it was called here. + } - ReadyWatcher initialized; - - // Initialized without completing DNS resolution. - EXPECT_CALL(initialized, ready()); - cluster->initialize([&]() -> absl::Status { - initialized.ready(); - return absl::OkStatus(); - }); - - ReadyWatcher membership_updated; - auto priority_update_cb = cluster->prioritySet().addPriorityUpdateCb( - [&](uint32_t, const HostVector&, const HostVector&) { - membership_updated.ready(); - return absl::OkStatus(); - }); + MockPriorityUpdateCallback priority_update_cb; + auto priority_update_handle = + cluster->prioritySet().addPriorityUpdateCb(priority_update_cb.AsStdFunction()); EXPECT_CALL(*resolver.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.2", "127.0.0.1"})); } -TEST_F(StrictDnsClusterImplTest, Basic) { +TEST_P(StrictDnsClusterImplParamTest, Basic) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); // gmock matches in LIFO order which is why these are swapped. ResolverData resolver2(*dns_resolver_, server_context_.dispatcher_); ResolverData resolver1(*dns_resolver_, server_context_.dispatcher_); @@ -644,18 +664,15 @@ TEST_F(StrictDnsClusterImplTest, Basic) { EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.maintenance_mode.name", 0)); EXPECT_FALSE(cluster->info()->maintenanceMode()); - ReadyWatcher membership_updated; - auto priority_update_cb = cluster->prioritySet().addPriorityUpdateCb( - [&](uint32_t, const HostVector&, const HostVector&) { - membership_updated.ready(); - return absl::OkStatus(); - }); + MockPriorityUpdateCallback priority_update_cb; + auto priority_update_handle = + cluster->prioritySet().addPriorityUpdateCb(priority_update_cb.AsStdFunction()); cluster->initialize([] { return absl::OkStatus(); }); resolver1.expectResolve(*dns_resolver_); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"})); EXPECT_THAT( @@ -684,7 +701,7 @@ TEST_F(StrictDnsClusterImplTest, Basic) { resolver1.timer_->invokeCallback(); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.3"})); EXPECT_THAT( @@ -693,7 +710,7 @@ TEST_F(StrictDnsClusterImplTest, Basic) { // Make sure we de-dup the same address. EXPECT_CALL(*resolver2.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver2.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"10.0.0.1", "10.0.0.1"})); EXPECT_THAT( @@ -715,7 +732,7 @@ TEST_F(StrictDnsClusterImplTest, Basic) { resolver1.expectResolve(*dns_resolver_); resolver1.timer_->invokeCallback(); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({})); EXPECT_THAT( @@ -754,7 +771,10 @@ TEST_F(StrictDnsClusterImplTest, Basic) { // Verifies that host removal works correctly when hosts are being health checked // but the cluster is configured to always remove hosts -TEST_F(StrictDnsClusterImplTest, HostRemovalActiveHealthSkipped) { +TEST_P(StrictDnsClusterImplParamTest, HostRemovalActiveHealthSkipped) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -814,7 +834,10 @@ TEST_F(StrictDnsClusterImplTest, HostRemovalActiveHealthSkipped) { // Verify that a host is not removed if it is removed from DNS but still passing active health // checking. -TEST_F(StrictDnsClusterImplTest, HostRemovalAfterHcFail) { +TEST_P(StrictDnsClusterImplParamTest, HostRemovalAfterHcFail) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -842,11 +865,9 @@ TEST_F(StrictDnsClusterImplTest, HostRemovalAfterHcFail) { EXPECT_CALL(*health_checker, start()); EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)); cluster->setHealthChecker(health_checker); - ReadyWatcher initialized; - cluster->initialize([&]() -> absl::Status { - initialized.ready(); - return absl::OkStatus(); - }); + + MockInitializeCallback initialize_cb; + cluster->initialize(initialize_cb.AsStdFunction()); EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)); EXPECT_CALL(*resolver.timer_, enableTimer(_, _)).Times(2); @@ -865,7 +886,8 @@ TEST_F(StrictDnsClusterImplTest, HostRemovalAfterHcFail) { hosts[i]->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); hosts[i]->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); if (i == 1) { - EXPECT_CALL(initialized, ready()); + // We only expect initialize_cb to be called on the second time around this loop. + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); } health_checker->runCallbacks(hosts[i], HealthTransition::Changed, HealthState::Healthy); } @@ -893,7 +915,10 @@ TEST_F(StrictDnsClusterImplTest, HostRemovalAfterHcFail) { } } -TEST_F(StrictDnsClusterImplTest, HostUpdateWithDisabledACEndpoint) { +TEST_P(StrictDnsClusterImplParamTest, HostUpdateWithDisabledACEndpoint) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -923,12 +948,10 @@ TEST_F(StrictDnsClusterImplTest, HostUpdateWithDisabledACEndpoint) { EXPECT_CALL(*health_checker, start()); EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)); cluster->setHealthChecker(health_checker); - ReadyWatcher initialized; - cluster->initialize([&]() -> absl::Status { - initialized.ready(); - return absl::OkStatus(); - }); - EXPECT_CALL(initialized, ready()); + MockInitializeCallback initialize_cb; + cluster->initialize(initialize_cb.AsStdFunction()); + // initialize_cb should only be called during dns_callback_; + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)); EXPECT_CALL(*resolver.timer_, enableTimer(_, _)).Times(2); @@ -961,7 +984,10 @@ TEST_F(StrictDnsClusterImplTest, HostUpdateWithDisabledACEndpoint) { } } -TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { +TEST_P(StrictDnsClusterImplParamTest, LoadAssignmentBasic) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + // gmock matches in LIFO order which is why these are swapped. ResolverData resolver3(*dns_resolver_, server_context_.dispatcher_); ResolverData resolver2(*dns_resolver_, server_context_.dispatcher_); @@ -1062,18 +1088,15 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.maintenance_mode.name", 0)); EXPECT_FALSE(cluster->info()->maintenanceMode()); - ReadyWatcher membership_updated; - auto priority_update_cb = cluster->prioritySet().addPriorityUpdateCb( - [&](uint32_t, const HostVector&, const HostVector&) { - membership_updated.ready(); - return absl::OkStatus(); - }); + MockPriorityUpdateCallback priority_update_cb; + auto priority_update_handle = + cluster->prioritySet().addPriorityUpdateCb(priority_update_cb.AsStdFunction()); cluster->initialize([] { return absl::OkStatus(); }); resolver1.expectResolve(*dns_resolver_); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"})); EXPECT_THAT( @@ -1120,7 +1143,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { EXPECT_EQ(2UL, stats_.counter("cluster.name.update_no_rebuild").value()); EXPECT_CALL(*resolver2.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver2.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"10.0.0.1", "10.0.0.1"})); @@ -1138,7 +1161,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { resolver1.timer_->invokeCallback(); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.3"})); EXPECT_THAT( @@ -1161,7 +1184,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { // Make sure that we *don't* de-dup between resolve targets. EXPECT_CALL(*resolver3.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver3.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"10.0.0.1"})); @@ -1198,12 +1221,12 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { }); EXPECT_CALL(*resolver2.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver2.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({})); EXPECT_CALL(*resolver3.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver3.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({})); @@ -1226,7 +1249,10 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { cancel(Network::ActiveDnsQuery::CancelReason::QueryAbandoned)); } -TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasicMultiplePriorities) { +TEST_P(StrictDnsClusterImplParamTest, LoadAssignmentBasicMultiplePriorities) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver3(*dns_resolver_, server_context_.dispatcher_); ResolverData resolver2(*dns_resolver_, server_context_.dispatcher_); ResolverData resolver1(*dns_resolver_, server_context_.dispatcher_); @@ -1278,18 +1304,15 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasicMultiplePriorities) { auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_); - ReadyWatcher membership_updated; - auto priority_update_cb = cluster->prioritySet().addPriorityUpdateCb( - [&](uint32_t, const HostVector&, const HostVector&) { - membership_updated.ready(); - return absl::OkStatus(); - }); + MockPriorityUpdateCallback priority_update_cb; + auto priority_update_handle = + cluster->prioritySet().addPriorityUpdateCb(priority_update_cb.AsStdFunction()); cluster->initialize([] { return absl::OkStatus(); }); resolver1.expectResolve(*dns_resolver_); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"})); EXPECT_THAT( @@ -1318,7 +1341,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasicMultiplePriorities) { resolver1.timer_->invokeCallback(); EXPECT_CALL(*resolver1.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver1.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.3"})); EXPECT_THAT( @@ -1327,7 +1350,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasicMultiplePriorities) { // Make sure we de-dup the same address. EXPECT_CALL(*resolver2.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver2.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"10.0.0.1", "10.0.0.1"})); EXPECT_THAT( @@ -1344,7 +1367,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasicMultiplePriorities) { } EXPECT_CALL(*resolver3.timer_, enableTimer(std::chrono::milliseconds(4000), _)); - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); resolver3.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"192.168.1.1", "192.168.1.2"})); @@ -1370,7 +1393,10 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasicMultiplePriorities) { } // Verifies that specifying a custom resolver when using STRICT_DNS fails -TEST_F(StrictDnsClusterImplTest, CustomResolverFails) { +TEST_P(StrictDnsClusterImplParamTest, CustomResolverFails) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -1393,12 +1419,23 @@ TEST_F(StrictDnsClusterImplTest, CustomResolverFails) { Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr, false); - EXPECT_THROW_WITH_MESSAGE( - auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_), - EnvoyException, "STRICT_DNS clusters must NOT have a custom resolver name set"); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + auto cluster_or_error = createStrictDnsCluster(cluster_config, factory_context, dns_resolver_); + EXPECT_FALSE(cluster_or_error.ok()); + EXPECT_EQ(cluster_or_error.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_EQ(cluster_or_error.status().message(), + "STRICT_DNS clusters must NOT have a custom resolver name set"); + } else { + EXPECT_THROW_WITH_MESSAGE( + auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_), + EnvoyException, "STRICT_DNS clusters must NOT have a custom resolver name set"); + } } -TEST_F(StrictDnsClusterImplTest, FailureRefreshRateBackoffResetsWhenSuccessHappens) { +TEST_P(StrictDnsClusterImplParamTest, FailureRefreshRateBackoffResetsWhenSuccessHappens) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -1447,7 +1484,10 @@ TEST_F(StrictDnsClusterImplTest, FailureRefreshRateBackoffResetsWhenSuccessHappe TestUtility::makeDnsResponse({})); } -TEST_F(StrictDnsClusterImplTest, ClusterTypeConfig) { +TEST_P(StrictDnsClusterImplParamTest, ClusterTypeConfig) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -1486,7 +1526,10 @@ TEST_F(StrictDnsClusterImplTest, ClusterTypeConfig) { TestUtility::makeDnsResponse({"192.168.1.1", "192.168.1.2"}, std::chrono::seconds(30))); } -TEST_F(StrictDnsClusterImplTest, ClusterTypeConfig2) { +TEST_P(StrictDnsClusterImplParamTest, ClusterTypeConfig2) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -1528,7 +1571,7 @@ TEST_F(StrictDnsClusterImplTest, ClusterTypeConfig2) { TestUtility::makeDnsResponse({"192.168.1.1", "192.168.1.2"}, std::chrono::seconds(30))); } -TEST_F(StrictDnsClusterImplTest, ClusterTypeConfigTypedDnsResolverConfig) { +TEST_P(StrictDnsClusterImplParamTest, ClusterTypeConfigTypedDnsResolverConfig) { NiceMock dns_resolver_factory; Registry::InjectFactory registered_dns_factory(dns_resolver_factory); EXPECT_CALL(dns_resolver_factory, createDnsResolver(_, _, _)).WillOnce(Return(dns_resolver_)); @@ -1568,7 +1611,10 @@ TEST_F(StrictDnsClusterImplTest, ClusterTypeConfigTypedDnsResolverConfig) { auto cluster = *createStrictDnsCluster(cluster_config, factory_context, nullptr); } -TEST_F(StrictDnsClusterImplTest, TtlAsDnsRefreshRateNoJitter) { +TEST_P(StrictDnsClusterImplParamTest, TtlAsDnsRefreshRateNoJitter) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -1595,24 +1641,21 @@ TEST_F(StrictDnsClusterImplTest, TtlAsDnsRefreshRateNoJitter) { auto cluster = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_); - ReadyWatcher membership_updated; - auto priority_update_cb = cluster->prioritySet().addPriorityUpdateCb( - [&](uint32_t, const HostVector&, const HostVector&) { - membership_updated.ready(); - return absl::OkStatus(); - }); + MockPriorityUpdateCallback priority_update_cb; + auto priority_update_handle = + cluster->prioritySet().addPriorityUpdateCb(priority_update_cb.AsStdFunction()); cluster->initialize([] { return absl::OkStatus(); }); // TTL is recorded when the DNS response is successful and not empty - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*resolver.timer_, enableTimer(std::chrono::milliseconds(5000), _)); resolver.dns_callback_( Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"192.168.1.1", "192.168.1.2"}, std::chrono::seconds(5))); // If the response is successful but empty, the cluster uses the cluster configured refresh rate. - EXPECT_CALL(membership_updated, ready()); + EXPECT_CALL(priority_update_cb, Call).WillOnce(Return(absl::OkStatus())); EXPECT_CALL(*resolver.timer_, enableTimer(std::chrono::milliseconds(4000), _)); resolver.dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({}, std::chrono::seconds(5))); @@ -1623,7 +1666,10 @@ TEST_F(StrictDnsClusterImplTest, TtlAsDnsRefreshRateNoJitter) { TestUtility::makeDnsResponse({}, std::chrono::seconds(5))); } -TEST_F(StrictDnsClusterImplTest, NegativeDnsJitter) { +TEST_P(StrictDnsClusterImplParamTest, NegativeDnsJitter) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name type: STRICT_DNS @@ -1646,7 +1692,11 @@ TEST_F(StrictDnsClusterImplTest, NegativeDnsJitter) { auto x = *createStrictDnsCluster(cluster_config, factory_context, dns_resolver_), EnvoyException, "(?s)Invalid duration: Expected positive duration:.*seconds: -1\n"); } -TEST_F(StrictDnsClusterImplTest, TtlAsDnsRefreshRateYesJitter) { + +TEST_P(StrictDnsClusterImplParamTest, TtlAsDnsRefreshRateYesJitter) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -1688,7 +1738,10 @@ TEST_F(StrictDnsClusterImplTest, TtlAsDnsRefreshRateYesJitter) { TestUtility::makeDnsResponse({"192.168.1.1", "192.168.1.2"}, std::chrono::seconds(ttl_s))); } -TEST_F(StrictDnsClusterImplTest, ExtremeJitter) { +TEST_P(StrictDnsClusterImplParamTest, ExtremeJitter) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + ResolverData resolver(*dns_resolver_, server_context_.dispatcher_); const std::string yaml = R"EOF( @@ -1722,7 +1775,10 @@ TEST_F(StrictDnsClusterImplTest, ExtremeJitter) { } // Ensures that HTTP/2 user defined SETTINGS parameter validation is enforced on clusters. -TEST_F(StrictDnsClusterImplTest, Http2UserDefinedSettingsParametersValidation) { +TEST_P(StrictDnsClusterImplParamTest, Http2UserDefinedSettingsParametersValidation) { + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -1983,8 +2039,6 @@ TEST_F(HostImplTest, ProxyOverridesHappyEyeballs) { } TEST_F(HostImplTest, CreateConnectionHappyEyeballsWithConfig) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.use_config_in_happy_eyeballs", "true"}}); MockClusterMockPrioritySet cluster; // pass in custom happy_eyeballs_config @@ -2039,12 +2093,10 @@ TEST_F(HostImplTest, CreateConnectionHappyEyeballsWithConfig) { } TEST_F(HostImplTest, CreateConnectionHappyEyeballsWithEmptyConfig) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.use_config_in_happy_eyeballs", "true"}}); MockClusterMockPrioritySet cluster; // pass in empty happy_eyeballs_config - // a default config will be created when flag turned on + // a default config will be created EXPECT_CALL(*(cluster.info_), happyEyeballsConfig()).WillRepeatedly(Return(absl::nullopt)); envoy::config::core::v3::Metadata metadata; @@ -2215,6 +2267,21 @@ TEST_F(HostImplTest, HealthPipeAddress) { "Invalid host configuration: non-zero port for non-IP address"); } +// Test that a network namespace specified for a host is invalid. +TEST_F(HostImplTest, NetnsInvalid) { + std::shared_ptr info{new NiceMock()}; + envoy::config::endpoint::v3::Endpoint::HealthCheckConfig config; + config.set_port_value(8000); + auto dest_addr = + Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:9999", true, "/netns/filepath"); + EXPECT_EQ( + HostDescriptionImpl::create(info, "", dest_addr, nullptr, nullptr, + envoy::config::core::v3::Locality().default_instance(), config, 1) + .status() + .message(), + "Invalid host configuration: hosts cannot specify network namespaces with their address"); +} + TEST_F(HostImplTest, HostAddressList) { MockClusterMockPrioritySet cluster; HostSharedPtr host = makeTestHost(cluster.info_, "tcp://10.0.0.1:1234", 1); @@ -2302,6 +2369,49 @@ TEST_F(StaticClusterImplTest, LoadAssignmentEmptyHostname) { EXPECT_FALSE(cluster->info()->addedViaApi()); } +TEST_F(StaticClusterImplTest, UpstreamBindConfigWithNetns) { + const std::string yaml_base = R"EOF( + name: staticcluster_with_ns_bind + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 10.0.0.1 + port_value: 443 + upstream_bind_config: + source_address: + address: 1.2.3.4 + port_value: 5678 + network_namespace_filepath: "/var/run/netns/test_ns" + )EOF"; + + envoy::config::cluster::v3::Cluster cluster_config = parseClusterFromV3Yaml(yaml_base); + Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr, + false); + +#if defined(__linux__) + constexpr bool is_linux = true; +#else + constexpr bool is_linux = false; +#endif + + if (is_linux) { + // On Linux, this configuration should be valid. + std::shared_ptr cluster = createCluster(cluster_config, factory_context); + EXPECT_NE(nullptr, cluster); + } else { + // On non-Linux, this should fail validation. + EXPECT_THAT_THROWS_MESSAGE( + createCluster(cluster_config, factory_context), EnvoyException, + testing::HasSubstr("network namespace filepaths, but the OS is not Linux")); + } +} + TEST_F(StaticClusterImplTest, LoadAssignmentNonEmptyHostname) { const std::string yaml = R"EOF( name: staticcluster @@ -2738,11 +2848,8 @@ TEST_F(StaticClusterImplTest, HealthyStat) { std::shared_ptr health_checker(new NiceMock()); cluster->setHealthChecker(health_checker); - ReadyWatcher initialized; - cluster->initialize([&initialized] { - initialized.ready(); - return absl::OkStatus(); - }); + MockInitializeCallback initialize_cb; + cluster->initialize(initialize_cb.AsStdFunction()); EXPECT_EQ(2UL, cluster->prioritySet().hostSetsPerPriority()[0]->hosts().size()); EXPECT_EQ(0UL, cluster->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); @@ -2755,7 +2862,7 @@ TEST_F(StaticClusterImplTest, HealthyStat) { HealthTransition::Changed, HealthState::Healthy); cluster->prioritySet().hostSetsPerPriority()[0]->hosts()[1]->healthFlagClear( Host::HealthFlag::FAILED_ACTIVE_HC); - EXPECT_CALL(initialized, ready()); + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); health_checker->runCallbacks(cluster->prioritySet().hostSetsPerPriority()[0]->hosts()[1], HealthTransition::Changed, HealthState::Healthy); @@ -2881,11 +2988,8 @@ TEST_F(StaticClusterImplTest, InitialHostsDisableHC) { std::shared_ptr health_checker(new NiceMock()); cluster->setHealthChecker(health_checker); - ReadyWatcher initialized; - cluster->initialize([&initialized] { - initialized.ready(); - return absl::OkStatus(); - }); + MockInitializeCallback initialize_cb; + cluster->initialize(initialize_cb.AsStdFunction()); // The endpoint with disabled active health check should not be set FAILED_ACTIVE_HC // at beginning. @@ -2901,7 +3005,7 @@ TEST_F(StaticClusterImplTest, InitialHostsDisableHC) { EXPECT_EQ(0UL, cluster->info()->endpointStats().membership_degraded_.value()); // Perform a health check for the second host, and then the initialization is finished. - EXPECT_CALL(initialized, ready()); + EXPECT_CALL(initialize_cb, Call).WillOnce(Return(absl::OkStatus())); cluster->prioritySet().hostSetsPerPriority()[0]->hosts()[1]->healthFlagClear( Host::HealthFlag::FAILED_ACTIVE_HC); health_checker->runCallbacks(cluster->prioritySet().hostSetsPerPriority()[0]->hosts()[0], @@ -3842,7 +3946,7 @@ TEST_F(StaticClusterImplTest, CustomUpstreamLocalAddressSelector) { envoy::config::cluster::v3::Cluster config; config.set_name("staticcluster"); config.mutable_connect_timeout(); - ProtobufWkt::Empty empty; + Protobuf::Empty empty; auto address_selector_config = server_context_.cluster_manager_.mutableBindConfig().mutable_local_address_selector(); address_selector_config->mutable_typed_config()->PackFrom(empty); @@ -3915,7 +4019,6 @@ class ClusterImplTest : public testing::Test, public UpstreamImplTestBase {}; // configured. TEST_F(ClusterImplTest, CloseConnectionsOnHostHealthFailure) { auto dns_resolver = std::make_shared(); - ReadyWatcher initialized; const std::string yaml = R"EOF( name: name @@ -3957,7 +4060,7 @@ class TestBatchUpdateCb : public PrioritySet::BatchUpdateCb { updateHostsParams(hosts_, hosts_per_locality_, std::make_shared(*hosts_), hosts_per_locality_), - {}, hosts_added, hosts_removed, random_.random(), absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); } // Remove the host from P1. @@ -3970,7 +4073,7 @@ class TestBatchUpdateCb : public PrioritySet::BatchUpdateCb { updateHostsParams(empty_hosts, HostsPerLocalityImpl::empty(), std::make_shared(*empty_hosts), HostsPerLocalityImpl::empty()), - {}, hosts_added, hosts_removed, random_.random(), absl::nullopt, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt); } } @@ -4026,12 +4129,11 @@ TEST(PrioritySet, Extend) { HostVector hosts_added{hosts->front()}; HostVector hosts_removed{}; - priority_set.updateHosts(1, - updateHostsParams(hosts, hosts_per_locality, - std::make_shared(*hosts), - hosts_per_locality), - {}, hosts_added, hosts_removed, 0, absl::nullopt, absl::nullopt, - fake_cross_priority_host_map); + priority_set.updateHosts( + 1, + updateHostsParams(hosts, hosts_per_locality, + std::make_shared(*hosts), hosts_per_locality), + {}, hosts_added, hosts_removed, absl::nullopt, absl::nullopt, fake_cross_priority_host_map); } EXPECT_EQ(1, priority_changes); EXPECT_EQ(1, membership_changes); @@ -4096,7 +4198,7 @@ TEST(PrioritySet, MainPrioritySetTest) { updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 0, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt); } // Only mutable host map can be updated directly. Read only host map will not be updated before @@ -4117,7 +4219,7 @@ TEST(PrioritySet, MainPrioritySetTest) { updateHostsParams(hosts, hosts_per_locality, std::make_shared(*hosts), hosts_per_locality), - {}, hosts_added, hosts_removed, 0, absl::nullopt); + {}, hosts_added, hosts_removed, absl::nullopt); } // New mutable host map will be created and all update will be applied to new mutable host map. @@ -4137,16 +4239,16 @@ class ClusterInfoImplTest : public testing::Test, public UpstreamImplTestBase { public: ClusterInfoImplTest() { ON_CALL(server_context_, api()).WillByDefault(ReturnRef(*api_)); } - std::shared_ptr makeCluster(const std::string& yaml) { + std::shared_ptr makeCluster(const std::string& yaml) { cluster_config_ = parseClusterFromV3Yaml(yaml); Envoy::Upstream::ClusterFactoryContextImpl factory_context( server_context_, [this]() { return dns_resolver_; }, nullptr, false); - StrictDnsClusterFactory factory{}; + DnsClusterFactory factory{}; auto status_or_cluster = factory.create(cluster_config_, factory_context); THROW_IF_NOT_OK_REF(status_or_cluster.status()); - return std::dynamic_pointer_cast(status_or_cluster->first); + return std::dynamic_pointer_cast(status_or_cluster->first); } class RetryBudgetTestClusterInfo : public ClusterInfoImpl { @@ -4162,13 +4264,19 @@ class ClusterInfoImplTest : public testing::Test, public UpstreamImplTestBase { NiceMock random_; Api::ApiPtr api_ = Api::createApiForTest(stats_, random_); NiceMock& runtime_ = server_context_.runtime_loader_; + TestScopedRuntime scoped_runtime_; std::shared_ptr dns_resolver_{new NiceMock()}; - ReadyWatcher initialized_; envoy::config::cluster::v3::Cluster cluster_config_; }; +class ParametrizedClusterInfoImplTest : public ClusterInfoImplTest, + public testing::WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P(DnsImplementations, ParametrizedClusterInfoImplTest, + testing::ValuesIn({"true", "false"})); + struct Foo : public Envoy::Config::TypedMetadata::Object {}; struct Baz : public Envoy::Config::TypedMetadata::Object { @@ -4181,7 +4289,7 @@ class BazFactory : public ClusterTypedMetadataFactory { std::string name() const override { return "baz"; } // Returns nullptr (conversion failure) if d is empty. std::unique_ptr - parse(const ProtobufWkt::Struct& d) const override { + parse(const Protobuf::Struct& d) const override { if (d.fields().find("name") != d.fields().end()) { return std::make_unique(d.fields().at("name").string_value()); } @@ -4189,13 +4297,15 @@ class BazFactory : public ClusterTypedMetadataFactory { } std::unique_ptr - parse(const ProtobufWkt::Any&) const override { + parse(const Protobuf::Any&) const override { return nullptr; } }; // Cluster metadata and common config retrieval. -TEST_F(ClusterInfoImplTest, Metadata) { +TEST_P(ParametrizedClusterInfoImplTest, Metadata) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4230,7 +4340,7 @@ TEST_F(ClusterInfoImplTest, Metadata) { } // Verify retry budget default values are honored. -TEST_F(ClusterInfoImplTest, RetryBudgetDefaultPopulation) { +TEST_P(ParametrizedClusterInfoImplTest, RetryBudgetDefaultPopulation) { std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4293,7 +4403,7 @@ TEST_F(ClusterInfoImplTest, RetryBudgetDefaultPopulation) { EXPECT_EQ(min_retry_concurrency, 123UL); } -TEST_F(ClusterInfoImplTest, LoadStatsConflictWithPerEndpointStats) { +TEST_P(ParametrizedClusterInfoImplTest, LoadStatsConflictWithPerEndpointStats) { std::string yaml = R"EOF( name: name type: STRICT_DNS @@ -4309,7 +4419,7 @@ TEST_F(ClusterInfoImplTest, LoadStatsConflictWithPerEndpointStats) { "load_stats_config can be specified"); } -TEST_F(ClusterInfoImplTest, UnsupportedPerHostFields) { +TEST_P(ParametrizedClusterInfoImplTest, UnsupportedPerHostFields) { std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4329,7 +4439,7 @@ TEST_F(ClusterInfoImplTest, UnsupportedPerHostFields) { } // Eds service_name is populated. -TEST_F(ClusterInfoImplTest, EdsServiceNamePopulation) { +TEST_P(ParametrizedClusterInfoImplTest, EdsServiceNamePopulation) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4376,7 +4486,7 @@ TEST_F(ClusterInfoImplTest, EdsServiceNamePopulation) { } // Typed metadata loading throws exception. -TEST_F(ClusterInfoImplTest, BrokenTypedMetadata) { +TEST_P(ParametrizedClusterInfoImplTest, BrokenTypedMetadata) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4404,7 +4514,7 @@ TEST_F(ClusterInfoImplTest, BrokenTypedMetadata) { } // Cluster extension protocol options fails validation when configured for an unregistered filter. -TEST_F(ClusterInfoImplTest, ExtensionProtocolOptionsForUnknownFilter) { +TEST_P(ParametrizedClusterInfoImplTest, ExtensionProtocolOptionsForUnknownFilter) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4430,7 +4540,7 @@ TEST_F(ClusterInfoImplTest, ExtensionProtocolOptionsForUnknownFilter) { "protocol options implementation for name: 'no_such_filter'"); } -TEST_F(ClusterInfoImplTest, TypedExtensionProtocolOptionsForUnknownFilter) { +TEST_P(ParametrizedClusterInfoImplTest, TypedExtensionProtocolOptionsForUnknownFilter) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4454,7 +4564,7 @@ TEST_F(ClusterInfoImplTest, TypedExtensionProtocolOptionsForUnknownFilter) { "protocol options implementation for name: 'no_such_filter'"); } -TEST_F(ClusterInfoImplTest, TestTrackRequestResponseSizesNotSetInConfig) { +TEST_P(ParametrizedClusterInfoImplTest, TestTrackRequestResponseSizesNotSetInConfig) { const std::string yaml_disabled = R"EOF( name: name connect_timeout: 0.25s @@ -4489,7 +4599,7 @@ TEST_F(ClusterInfoImplTest, TestTrackRequestResponseSizesNotSetInConfig) { EXPECT_FALSE(cluster->info()->requestResponseSizeStats().has_value()); } -TEST_F(ClusterInfoImplTest, TestTrackRequestResponseSizes) { +TEST_P(ParametrizedClusterInfoImplTest, TestTrackRequestResponseSizes) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4510,7 +4620,7 @@ TEST_F(ClusterInfoImplTest, TestTrackRequestResponseSizes) { EXPECT_EQ(Stats::Histogram::Unit::Bytes, req_resp_stats.upstream_rs_body_size_.unit()); } -TEST_F(ClusterInfoImplTest, TestTrackRemainingResourcesGauges) { +TEST_P(ParametrizedClusterInfoImplTest, TestTrackRemainingResourcesGauges) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4557,7 +4667,7 @@ TEST_F(ClusterInfoImplTest, TestTrackRemainingResourcesGauges) { EXPECT_EQ(4U, high_remaining_retries.value()); } -TEST_F(ClusterInfoImplTest, DefaultConnectTimeout) { +TEST_P(ParametrizedClusterInfoImplTest, DefaultConnectTimeout) { const std::string yaml = R"EOF( name: cluster1 type: STRICT_DNS @@ -4571,7 +4681,7 @@ TEST_F(ClusterInfoImplTest, DefaultConnectTimeout) { EXPECT_EQ(std::chrono::seconds(5), cluster->info()->connectTimeout()); } -TEST_F(ClusterInfoImplTest, MaxConnectionDurationTest) { +TEST_P(ParametrizedClusterInfoImplTest, MaxConnectionDurationTest) { constexpr absl::string_view yaml_base = R"EOF( name: {} type: STRICT_DNS @@ -4600,7 +4710,7 @@ TEST_F(ClusterInfoImplTest, MaxConnectionDurationTest) { EXPECT_EQ(absl::nullopt, cluster3->info()->maxConnectionDuration()); } -TEST_F(ClusterInfoImplTest, Timeouts) { +TEST_P(ParametrizedClusterInfoImplTest, Timeouts) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4691,7 +4801,7 @@ TEST_F(ClusterInfoImplTest, Timeouts) { } } -TEST_F(ClusterInfoImplTest, TcpPoolIdleTimeout) { +TEST_P(ParametrizedClusterInfoImplTest, TcpPoolIdleTimeout) { constexpr absl::string_view yaml_base = R"EOF( name: {} type: STRICT_DNS @@ -4717,7 +4827,7 @@ TEST_F(ClusterInfoImplTest, TcpPoolIdleTimeout) { EXPECT_EQ(absl::nullopt, cluster3->info()->tcpPoolIdleTimeout()); } -TEST_F(ClusterInfoImplTest, TestTrackTimeoutBudgetsNotSetInConfig) { +TEST_P(ParametrizedClusterInfoImplTest, TestTrackTimeoutBudgetsNotSetInConfig) { // Check that without the flag specified, the histogram is null. const std::string yaml_disabled = R"EOF( name: name @@ -4753,7 +4863,7 @@ TEST_F(ClusterInfoImplTest, TestTrackTimeoutBudgetsNotSetInConfig) { EXPECT_FALSE(cluster->info()->timeoutBudgetStats().has_value()); } -TEST_F(ClusterInfoImplTest, TestTrackTimeoutBudgets) { +TEST_P(ParametrizedClusterInfoImplTest, TestTrackTimeoutBudgets) { // Check that with the flag, the histogram is created. const std::string yaml = R"EOF( name: name @@ -4774,7 +4884,7 @@ TEST_F(ClusterInfoImplTest, TestTrackTimeoutBudgets) { tb_stats.upstream_rq_timeout_budget_per_try_percent_used_.unit()); } -TEST_F(ClusterInfoImplTest, DEPRECATED_FEATURE_TEST(TestTrackTimeoutBudgetsOld)) { +TEST_P(ParametrizedClusterInfoImplTest, DEPRECATED_FEATURE_TEST(TestTrackTimeoutBudgetsOld)) { // Check that without the flag specified, the histogram is null. const std::string yaml_disabled = R"EOF( name: name @@ -4808,7 +4918,7 @@ TEST_F(ClusterInfoImplTest, DEPRECATED_FEATURE_TEST(TestTrackTimeoutBudgetsOld)) } // Validates HTTP2 SETTINGS config. -TEST_F(ClusterInfoImplTest, Http2ProtocolOptions) { +TEST_P(ParametrizedClusterInfoImplTest, Http2ProtocolOptions) { const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -4998,9 +5108,9 @@ TEST_F(ClusterInfoImplTest, ExtensionProtocolOptionsForFilterWithOptions) { auto protocol_options = std::make_shared(); TestFilterConfigFactoryBase factoryBase( - []() -> ProtobufTypes::MessagePtr { return std::make_unique(); }, + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); }, [&](const Protobuf::Message& msg) -> Upstream::ProtocolOptionsConfigConstSharedPtr { - const auto& msg_struct = dynamic_cast(msg); + const auto& msg_struct = dynamic_cast(msg); EXPECT_TRUE(msg_struct.fields().find("option") != msg_struct.fields().end()); return protocol_options; @@ -5048,7 +5158,7 @@ TEST_F(ClusterInfoImplTest, ExtensionProtocolOptionsForFilterWithOptions) { // This vector is used to gather clusters with extension_protocol_options from the different // types of extension factories (network, http). - std::vector> clusters; + std::vector> clusters; { // Get the cluster with extension_protocol_options for a network filter factory. @@ -5882,343 +5992,6 @@ TEST_F(HostsWithLocalityImpl, Filter) { } } -class HostSetImplLocalityTest : public Event::TestUsingSimulatedTime, public testing::Test { -public: - LocalityWeightsConstSharedPtr locality_weights_; - HostSetImpl host_set_{0, false, kDefaultOverProvisioningFactor}; - std::shared_ptr info_{new NiceMock()}; -}; - -// When no locality weights belong to the host set, there's an empty pick. -TEST_F(HostSetImplLocalityTest, Empty) { - EXPECT_EQ(nullptr, host_set_.localityWeights()); - EXPECT_FALSE(host_set_.chooseHealthyLocality().has_value()); -} - -// When no hosts are healthy we should fail to select a locality -TEST_F(HostSetImplLocalityTest, AllUnhealthy) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - envoy::config::core::v3::Locality zone_c; - zone_c.set_zone("C"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; - - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1, 1}}; - auto hosts_const_shared = std::make_shared(hosts); - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality), locality_weights, - {}, {}, 0, absl::nullopt); - EXPECT_FALSE(host_set_.chooseHealthyLocality().has_value()); -} - -// When a locality has endpoints that have not yet been warmed, weight calculation should ignore -// these hosts. -TEST_F(HostSetImplLocalityTest, NotWarmedHostsLocality) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:83", zone_b), - makeTestHost(info_, "tcp://127.0.0.1:84", zone_b)}; - - // We have two localities with 3 hosts in A, 2 hosts in B. Two of the hosts in A are not - // warmed yet, so even though they are unhealthy we should not adjust the locality weight. - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0], hosts[1], hosts[2]}, {hosts[3], hosts[4]}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1}}; - auto hosts_const_shared = std::make_shared(hosts); - HostsPerLocalitySharedPtr healthy_hosts_per_locality = - makeHostsPerLocality({{hosts[0]}, {hosts[3], hosts[4]}}); - HostsPerLocalitySharedPtr excluded_hosts_per_locality = - makeHostsPerLocality({{hosts[1], hosts[2]}, {}}); - - host_set_.updateHosts( - HostSetImpl::updateHostsParams( - hosts_const_shared, hosts_per_locality, - makeHostsFromHostsPerLocality(healthy_hosts_per_locality), - healthy_hosts_per_locality, std::make_shared(), - HostsPerLocalityImpl::empty(), - makeHostsFromHostsPerLocality(excluded_hosts_per_locality), - excluded_hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - // We should RR between localities with equal weight. - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); -} - -// When a locality has zero hosts, it should be treated as if it has zero healthy. -TEST_F(HostSetImplLocalityTest, EmptyLocality) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_a)}; - - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0], hosts[1], hosts[2]}, {}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1}}; - auto hosts_const_shared = std::make_shared(hosts); - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, - std::make_shared(hosts), - hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - // Verify that we are not RRing between localities. - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); -} - -// When all locality weights are zero we should fail to select a locality. -TEST_F(HostSetImplLocalityTest, AllZeroWeights) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_b)}; - - HostsPerLocalitySharedPtr hosts_per_locality = makeHostsPerLocality({{hosts[0]}, {hosts[1]}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{0, 0}}; - auto hosts_const_shared = std::make_shared(hosts); - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, - std::make_shared(hosts), - hosts_per_locality), - locality_weights, {}, {}, 0); - EXPECT_FALSE(host_set_.chooseHealthyLocality().has_value()); -} - -// When all locality weights are the same we have unweighted RR behavior. -TEST_F(HostSetImplLocalityTest, Unweighted) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - envoy::config::core::v3::Locality zone_c; - zone_c.set_zone("C"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; - - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1, 1}}; - auto hosts_const_shared = std::make_shared(hosts); - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, - std::make_shared(hosts), - hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(2, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(2, host_set_.chooseHealthyLocality().value()); -} - -// When locality weights differ, we have weighted RR behavior. -TEST_F(HostSetImplLocalityTest, Weighted) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_b)}; - - HostsPerLocalitySharedPtr hosts_per_locality = makeHostsPerLocality({{hosts[0]}, {hosts[1]}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 2}}; - auto hosts_const_shared = std::make_shared(hosts); - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, - std::make_shared(hosts), - hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(1, host_set_.chooseHealthyLocality().value()); -} - -// Localities with no weight assignment are never picked. -TEST_F(HostSetImplLocalityTest, MissingWeight) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - envoy::config::core::v3::Locality zone_c; - zone_c.set_zone("C"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; - - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 0, 1}}; - auto hosts_const_shared = std::make_shared(hosts); - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, - std::make_shared(hosts), - hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(2, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(2, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(0, host_set_.chooseHealthyLocality().value()); - EXPECT_EQ(2, host_set_.chooseHealthyLocality().value()); -} - -// Validates that with weighted initialization all localities are chosen -// proportionally to their weight. -TEST_F(HostSetImplLocalityTest, WeightedAllChosen) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - envoy::config::core::v3::Locality zone_c; - zone_b.set_zone("C"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; - - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); - // Set weights of 10%, 60% and 30% to the three zones. - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 6, 3}}; - - // Keep track of how many times each locality is picked, initialized to 0. - uint32_t locality_picked_count[] = {0, 0, 0}; - - // Create the load-balancer 10 times, each with a different seed number (from - // 0 to 10), do a single pick, and validate that the number of picks equals - // to the weights assigned to the localities. - auto hosts_const_shared = std::make_shared(hosts); - for (uint32_t i = 0; i < 10; ++i) { - host_set_.updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, - std::make_shared(hosts), - hosts_per_locality), - locality_weights, {}, {}, i, absl::nullopt); - locality_picked_count[host_set_.chooseHealthyLocality().value()]++; - } - EXPECT_EQ(locality_picked_count[0], 1); - EXPECT_EQ(locality_picked_count[1], 6); - EXPECT_EQ(locality_picked_count[2], 3); -} - -// Gentle failover between localities as health diminishes. -TEST_F(HostSetImplLocalityTest, UnhealthyFailover) { - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:81", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:82", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:83", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:84", zone_a), - makeTestHost(info_, "tcp://127.0.0.1:85", zone_b)}; - - const auto setHealthyHostCount = [this, hosts](uint32_t host_count) { - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 2}}; - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0], hosts[1], hosts[2], hosts[3], hosts[4]}, {hosts[5]}}); - HostVector healthy_hosts; - for (uint32_t i = 0; i < host_count; ++i) { - healthy_hosts.emplace_back(hosts[i]); - } - HostsPerLocalitySharedPtr healthy_hosts_per_locality = - makeHostsPerLocality({healthy_hosts, {hosts[5]}}); - - auto hosts = makeHostsFromHostsPerLocality(hosts_per_locality); - host_set_.updateHosts(updateHostsParams(hosts, hosts_per_locality, - makeHostsFromHostsPerLocality( - healthy_hosts_per_locality), - healthy_hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - }; - - const auto expectPicks = [this](uint32_t locality_0_picks, uint32_t locality_1_picks) { - uint32_t count[2] = {0, 0}; - for (uint32_t i = 0; i < 100; ++i) { - const uint32_t locality_index = host_set_.chooseHealthyLocality().value(); - ASSERT_LT(locality_index, 2); - ++count[locality_index]; - } - ENVOY_LOG_MISC(debug, "Locality picks {} {}", count[0], count[1]); - EXPECT_EQ(locality_0_picks, count[0]); - EXPECT_EQ(locality_1_picks, count[1]); - }; - - setHealthyHostCount(5); - expectPicks(33, 67); - setHealthyHostCount(4); - expectPicks(33, 67); - setHealthyHostCount(3); - expectPicks(29, 71); - setHealthyHostCount(2); - expectPicks(22, 78); - setHealthyHostCount(1); - expectPicks(12, 88); - setHealthyHostCount(0); - expectPicks(0, 100); -} - -TEST(OverProvisioningFactorTest, LocalityPickChanges) { - auto setUpHostSetWithOPFAndTestPicks = [](const uint32_t overprovisioning_factor, - const uint32_t pick_0, const uint32_t pick_1) { - HostSetImpl host_set(0, false, overprovisioning_factor); - std::shared_ptr cluster_info{new NiceMock()}; - auto time_source = std::make_unique>(); - envoy::config::core::v3::Locality zone_a; - zone_a.set_zone("A"); - envoy::config::core::v3::Locality zone_b; - zone_b.set_zone("B"); - HostVector hosts{makeTestHost(cluster_info, "tcp://127.0.0.1:80", zone_a), - makeTestHost(cluster_info, "tcp://127.0.0.1:81", zone_a), - makeTestHost(cluster_info, "tcp://127.0.0.1:82", zone_b)}; - LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1}}; - HostsPerLocalitySharedPtr hosts_per_locality = - makeHostsPerLocality({{hosts[0], hosts[1]}, {hosts[2]}}); - // Healthy ratio: (1/2, 1). - HostsPerLocalitySharedPtr healthy_hosts_per_locality = - makeHostsPerLocality({{hosts[0]}, {hosts[2]}}); - auto healthy_hosts = - makeHostsFromHostsPerLocality(healthy_hosts_per_locality); - host_set.updateHosts(updateHostsParams(std::make_shared(hosts), - hosts_per_locality, healthy_hosts, - healthy_hosts_per_locality), - locality_weights, {}, {}, 0, absl::nullopt); - uint32_t cnts[] = {0, 0}; - for (uint32_t i = 0; i < 100; ++i) { - absl::optional locality_index = host_set.chooseHealthyLocality(); - if (!locality_index.has_value()) { - // It's possible locality scheduler is nullptr (when factor is 0). - continue; - } - ASSERT_LT(locality_index.value(), 2); - ++cnts[locality_index.value()]; - } - EXPECT_EQ(pick_0, cnts[0]); - EXPECT_EQ(pick_1, cnts[1]); - }; - - // NOTE: effective locality weight: weight * min(1, factor * healthy-ratio). - - // Picks in localities match to weight(1) * healthy-ratio when - // overprovisioning factor is 1. - setUpHostSetWithOPFAndTestPicks(100, 33, 67); - // Picks in localities match to weights as factor * healthy-ratio > 1. - setUpHostSetWithOPFAndTestPicks(200, 50, 50); -}; - // Verifies that partitionHosts correctly splits hosts based on their health flags. TEST(HostPartitionTest, PartitionHosts) { std::shared_ptr info{new NiceMock()}; @@ -6395,11 +6168,9 @@ TEST_F(PriorityStateManagerTest, LocalityClusterUpdate) { cluster->initialize([] { return absl::OkStatus(); }); EXPECT_EQ(1UL, cluster->prioritySet().hostSetsPerPriority()[0]->hosts().size()); - NiceMock random; // Make priority state manager and fill it with the initial state of the cluster and the added // hosts - PriorityStateManager priority_state_manager(*cluster, server_context_.local_info_, nullptr, - random); + PriorityStateManager priority_state_manager(*cluster, server_context_.local_info_, nullptr); auto current_hosts = cluster->prioritySet().hostSetsPerPriority()[0]->hosts(); HostVector hosts_added{makeTestHost(cluster->info(), "tcp://127.0.0.1:81", zone_b), diff --git a/test/common/upstream/utility.h b/test/common/upstream/utility.h index 2be50f3309ce0..a374f9734907e 100644 --- a/test/common/upstream/utility.h +++ b/test/common/upstream/utility.h @@ -193,6 +193,20 @@ inline HostsPerLocalitySharedPtr makeHostsPerLocality(std::vector&& std::move(locality_hosts), !force_no_local_locality && !locality_hosts.empty()); } +template +std::shared_ptr +makeHostsFromHostsPerLocality(HostsPerLocalityConstSharedPtr hosts_per_locality) { + HostVector hosts; + + for (const auto& locality_hosts : hosts_per_locality->get()) { + for (const auto& host : locality_hosts) { + hosts.emplace_back(host); + } + } + + return std::make_shared(hosts); +} + inline LocalityWeightsSharedPtr makeLocalityWeights(std::initializer_list locality_weights) { return std::make_shared(locality_weights); diff --git a/test/common/upstream/xdstp_od_cds_api_impl_test.cc b/test/common/upstream/xdstp_od_cds_api_impl_test.cc new file mode 100644 index 0000000000000..7a449d341b12b --- /dev/null +++ b/test/common/upstream/xdstp_od_cds_api_impl_test.cc @@ -0,0 +1,329 @@ +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/config_source.pb.h" +#include "envoy/config/subscription.h" + +#include "source/common/config/decoded_resource_impl.h" +#include "source/common/stats/isolated_store_impl.h" +#include "source/common/upstream/od_cds_api_impl.h" + +#include "test/mocks/config/xds_manager.h" +#include "test/mocks/protobuf/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" +#include "test/mocks/upstream/missing_cluster_notifier.h" +#include "test/test_common/test_runtime.h" +#include "test/test_common/utility.h" + +#include "fmt/core.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Upstream { +namespace { + +using ::testing::ElementsAre; +using ::testing::InSequence; +using ::testing::UnorderedElementsAre; + +class XdstpOdCdsApiImplTest : public testing::Test { +public: + void SetUp() override { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.xdstp_based_config_singleton_subscriptions", "true"}}); + envoy::config::core::v3::ConfigSource odcds_config; + OptRef null_locator; + odcds_ = *XdstpOdCdsApiImpl::create(odcds_config, null_locator, xds_manager_, cm_, notifier_, + *store_.rootScope(), validation_visitor_, + server_factory_context_); + + ON_CALL(xds_manager_, subscriptionFactory()) + .WillByDefault(ReturnRef(cm_.subscription_factory_)); + } + + void expectSingletonSubscription(absl::string_view resource_name) { + EXPECT_CALL(xds_manager_, subscribeToSingletonResource(resource_name, _, _, _, _, _, _)) + .WillOnce(Invoke( + [this](absl::string_view, OptRef, + absl::string_view, Stats::Scope&, Config::SubscriptionCallbacks& callbacks, + Config::OpaqueResourceDecoderSharedPtr, + const Config::SubscriptionOptions&) -> absl::StatusOr { + auto ret = std::make_unique>(); + subscription_ = ret.get(); + odcds_callbacks_ = &callbacks; + return ret; + })); + } + + TestScopedRuntime scoped_runtime_; + NiceMock xds_manager_; + NiceMock cm_; + Stats::IsolatedStoreImpl store_; + MockMissingClusterNotifier notifier_; + NiceMock server_factory_context_; + OdCdsApiSharedPtr odcds_; + Config::SubscriptionCallbacks* odcds_callbacks_ = nullptr; + NiceMock validation_visitor_; + Config::MockSubscription* subscription_; +}; + +// Check that a subscription is created when the odcds is updated. +TEST_F(XdstpOdCdsApiImplTest, SuccesfulSubscriptionToCluster) { + InSequence s; + + expectSingletonSubscription("fake_cluster"); + odcds_->updateOnDemand("fake_cluster"); +} + +// Check that a subscription is created when the odcds is updated, +// and if the same cluster is requested again, another subscription will not be created. +TEST_F(XdstpOdCdsApiImplTest, SingleSubscriptionToSomeCluster) { + InSequence s; + + expectSingletonSubscription("fake_cluster"); + odcds_->updateOnDemand("fake_cluster"); + + EXPECT_CALL(xds_manager_, subscribeToSingletonResource(_, _, _, _, _, _, _)).Times(0); + odcds_->updateOnDemand("fake_cluster"); +} + +// Check that a subscription is created when the odcds is updated, +// and if a different cluster is requested, a new subscription will be created. +TEST_F(XdstpOdCdsApiImplTest, TwoSubscriptionsToDifferectClusters) { + InSequence s; + + expectSingletonSubscription("fake_cluster"); + odcds_->updateOnDemand("fake_cluster"); + + expectSingletonSubscription("fake_cluster2"); + odcds_->updateOnDemand("fake_cluster2"); +} + +// Tests a successful subscription and cluster addition. +TEST_F(XdstpOdCdsApiImplTest, SuccessfulClusterAddition) { + InSequence s; + + const std::string cluster_name = "fake_cluster"; + expectSingletonSubscription(cluster_name); + odcds_->updateOnDemand(cluster_name); + + ASSERT_NE(odcds_callbacks_, nullptr); + + const auto cluster = + TestUtility::parseYaml(fmt::format(R"EOF( + name: {} + connect_timeout: 1.250s + lb_policy: ROUND_ROBIN + type: STATIC + )EOF", + cluster_name)); + + const std::string version = "v1"; + EXPECT_CALL(cm_, addOrUpdateCluster(ProtoEq(cluster), version, false)); + + Config::DecodedResourceImpl decoded_resource( + std::make_unique(cluster), "fake_cluster", {}, version); + std::vector resources; + resources.emplace_back(decoded_resource); + + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate(resources, version).ok()); +} + +// Tests cluster removal. +TEST_F(XdstpOdCdsApiImplTest, ClusterRemoval) { + InSequence s; + + const std::string cluster_name = "fake_cluster"; + expectSingletonSubscription(cluster_name); + odcds_->updateOnDemand(cluster_name); + + ASSERT_NE(odcds_callbacks_, nullptr); + + const auto cluster = + TestUtility::parseYaml(fmt::format(R"EOF( + name: {} + connect_timeout: 1.250s + lb_policy: ROUND_ROBIN + type: STATIC + )EOF", + cluster_name)); + + const std::string version = "v1"; + EXPECT_CALL(cm_, addOrUpdateCluster(ProtoEq(cluster), version, false)); + + Config::DecodedResourceImpl decoded_resource( + std::make_unique(cluster), "fake_cluster", {}, version); + std::vector resources; + resources.emplace_back(decoded_resource); + + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate(resources, version).ok()); + + // Now remove the cluster. + const std::string version2 = "v2"; + EXPECT_CALL(notifier_, notifyMissingCluster(cluster_name)); + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate({}, version2).ok()); +} + +// Tests that a config update failure is handled correctly. +TEST_F(XdstpOdCdsApiImplTest, SubscriptionFailure) { + InSequence s; + + const std::string cluster_name = "fake_cluster"; + expectSingletonSubscription(cluster_name); + odcds_->updateOnDemand(cluster_name); + + ASSERT_NE(odcds_callbacks_, nullptr); + + EXPECT_CALL(cm_, addOrUpdateCluster(_, _, _)).Times(0); + EXPECT_CALL(notifier_, notifyMissingCluster(cluster_name)); + + EnvoyException e("rejecting update"); + odcds_callbacks_->onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, + &e); +} + +// Tests that an existing cluster is updated. +TEST_F(XdstpOdCdsApiImplTest, ClusterUpdate) { + InSequence s; + + const std::string cluster_name = "fake_cluster"; + expectSingletonSubscription(cluster_name); + odcds_->updateOnDemand(cluster_name); + + ASSERT_NE(odcds_callbacks_, nullptr); + + const auto cluster = + TestUtility::parseYaml(fmt::format(R"EOF( + name: {} + connect_timeout: 1.250s + lb_policy: ROUND_ROBIN + type: STATIC + )EOF", + cluster_name)); + + const std::string version = "v1"; + EXPECT_CALL(cm_, addOrUpdateCluster(ProtoEq(cluster), version, false)); + + Config::DecodedResourceImpl decoded_resource( + std::make_unique(cluster), "fake_cluster", {}, version); + std::vector resources; + resources.emplace_back(decoded_resource); + + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate(resources, version).ok()); + + // Now update the cluster. + const auto updated_cluster = + TestUtility::parseYaml(fmt::format(R"EOF( + name: {} + connect_timeout: 2.250s + lb_policy: ROUND_ROBIN + type: STATIC + )EOF", + cluster_name)); + const std::string version2 = "v2"; + EXPECT_CALL(cm_, addOrUpdateCluster(ProtoEq(updated_cluster), version2, false)); + + Config::DecodedResourceImpl decoded_resource2( + std::make_unique(updated_cluster), "fake_cluster", {}, + version2); + std::vector resources2; + resources2.emplace_back(decoded_resource2); + + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate(resources2, version2).ok()); +} + +// Tests that multiple subscriptions are handled independently. +TEST_F(XdstpOdCdsApiImplTest, MultipleSubscriptions) { + InSequence s; + + // Subscription for fake_cluster1 succeeds. + const std::string cluster_name1 = "fake_cluster1"; + Config::SubscriptionCallbacks* callbacks1 = nullptr; + EXPECT_CALL(xds_manager_, subscribeToSingletonResource(cluster_name1, _, _, _, _, _, _)) + .WillOnce(Invoke( + [&callbacks1](absl::string_view, OptRef, + absl::string_view, Stats::Scope&, Config::SubscriptionCallbacks& callbacks, + Config::OpaqueResourceDecoderSharedPtr, const Config::SubscriptionOptions&) + -> absl::StatusOr { + callbacks1 = &callbacks; + return std::make_unique>(); + })); + odcds_->updateOnDemand(cluster_name1); + ASSERT_NE(callbacks1, nullptr); + + // Subscription for fake_cluster2 fails. + const std::string cluster_name2 = "fake_cluster2"; + Config::SubscriptionCallbacks* callbacks2 = nullptr; + EXPECT_CALL(xds_manager_, subscribeToSingletonResource(cluster_name2, _, _, _, _, _, _)) + .WillOnce(Invoke( + [&callbacks2](absl::string_view, OptRef, + absl::string_view, Stats::Scope&, Config::SubscriptionCallbacks& callbacks, + Config::OpaqueResourceDecoderSharedPtr, const Config::SubscriptionOptions&) + -> absl::StatusOr { + callbacks2 = &callbacks; + return std::make_unique>(); + })); + odcds_->updateOnDemand(cluster_name2); + ASSERT_NE(callbacks2, nullptr); + + // Verify that the successful subscription works as expected. + const auto cluster = + TestUtility::parseYaml(fmt::format(R"EOF( + name: {} + connect_timeout: 1.250s + lb_policy: ROUND_ROBIN + type: STATIC + )EOF", + cluster_name1)); + const std::string version = "v1"; + EXPECT_CALL(cm_, addOrUpdateCluster(ProtoEq(cluster), version, false)); + Config::DecodedResourceImpl decoded_resource( + std::make_unique(cluster), cluster_name1, {}, version); + std::vector resources; + resources.emplace_back(decoded_resource); + EXPECT_TRUE(callbacks1->onConfigUpdate(resources, version).ok()); + + // Verify that the failed subscription works as expected. + EXPECT_CALL(cm_, addOrUpdateCluster(_, _, _)).Times(0); + EXPECT_CALL(notifier_, notifyMissingCluster(cluster_name2)); + EnvoyException e("rejecting update"); + callbacks2->onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, &e); +} + +// Tests cluster removal via a delta update. +TEST_F(XdstpOdCdsApiImplTest, ClusterRemovalViaDeltaUpdate) { + InSequence s; + + const std::string cluster_name = "fake_cluster"; + expectSingletonSubscription(cluster_name); + odcds_->updateOnDemand(cluster_name); + + ASSERT_NE(odcds_callbacks_, nullptr); + + // First, add the cluster. + const auto cluster = + TestUtility::parseYaml(fmt::format(R"EOF( + name: {} + connect_timeout: 1.250s + lb_policy: ROUND_ROBIN + type: STATIC + )EOF", + cluster_name)); + const std::string version = "v1"; + EXPECT_CALL(cm_, addOrUpdateCluster(ProtoEq(cluster), version, false)); + Config::DecodedResourceImpl decoded_resource( + std::make_unique(cluster), cluster_name, {}, version); + std::vector resources; + resources.emplace_back(decoded_resource); + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate(resources, version).ok()); + + // Now, remove the cluster using a delta update. + const std::string version2 = "v2"; + Protobuf::RepeatedPtrField removed_resources; + removed_resources.Add(std::string(cluster_name)); + EXPECT_CALL(notifier_, notifyMissingCluster(cluster_name)); + EXPECT_TRUE(odcds_callbacks_->onConfigUpdate({}, removed_resources, version2).ok()); +} +} // namespace +} // namespace Upstream +} // namespace Envoy diff --git a/test/config/utility.cc b/test/config/utility.cc index 381a9a31adf02..f66e096ca92f0 100644 --- a/test/config/utility.cc +++ b/test/config/utility.cc @@ -773,7 +773,7 @@ ConfigHelper::ConfigHelper(const Network::Address::IpVersion version, } } -void ConfigHelper::addListenerTypedMetadata(absl::string_view key, ProtobufWkt::Any& packed_value) { +void ConfigHelper::addListenerTypedMetadata(absl::string_view key, Protobuf::Any& packed_value) { RELEASE_ASSERT(!finalized_, ""); auto* static_resources = bootstrap_.mutable_static_resources(); ASSERT_TRUE(static_resources->listeners_size() > 0); @@ -786,7 +786,7 @@ void ConfigHelper::addClusterFilterMetadata(absl::string_view metadata_yaml, absl::string_view cluster_name) { #ifdef ENVOY_ENABLE_YAML RELEASE_ASSERT(!finalized_, ""); - ProtobufWkt::Struct cluster_metadata; + Protobuf::Struct cluster_metadata; TestUtility::loadFromYaml(std::string(metadata_yaml), cluster_metadata); auto* static_resources = bootstrap_.mutable_static_resources(); @@ -796,7 +796,7 @@ void ConfigHelper::addClusterFilterMetadata(absl::string_view metadata_yaml, continue; } for (const auto& kvp : cluster_metadata.fields()) { - ASSERT_TRUE(kvp.second.kind_case() == ProtobufWkt::Value::KindCase::kStructValue); + ASSERT_TRUE(kvp.second.kind_case() == Protobuf::Value::KindCase::kStructValue); cluster->mutable_metadata()->mutable_filter_metadata()->insert( {kvp.first, kvp.second.struct_value()}); } @@ -1728,7 +1728,7 @@ void ConfigHelper::setLds(absl::string_view version_info) { envoy::service::discovery::v3::DiscoveryResponse lds; lds.set_version_info(std::string(version_info)); for (auto& listener : bootstrap_.static_resources().listeners()) { - ProtobufWkt::Any* resource = lds.add_resources(); + Protobuf::Any* resource = lds.add_resources(); resource->PackFrom(listener); } @@ -1775,6 +1775,62 @@ void ConfigHelper::setUpstreamOutboundFramesLimits(uint32_t max_all_frames, }); } +void ConfigHelper::setDownstreamHttp2MaxConcurrentStreams(uint32_t max_streams) { + auto filter = getFilterFromListener("http"); + if (filter) { + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager + hcm_config; + loadHttpConnectionManager(hcm_config); + if (hcm_config.codec_type() == envoy::extensions::filters::network::http_connection_manager:: + v3::HttpConnectionManager::HTTP2) { + auto* options = hcm_config.mutable_http2_protocol_options(); + options->mutable_max_concurrent_streams()->set_value(max_streams); + storeHttpConnectionManager(hcm_config); + } + } +} + +void ConfigHelper::setUpstreamHttp2MaxConcurrentStreams(uint32_t max_streams) { + addConfigModifier([max_streams](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + auto* http_protocol_options = + protocol_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + http_protocol_options->mutable_max_concurrent_streams()->set_value(max_streams); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); +} + +void ConfigHelper::setDownstreamHttp2WindowSize(uint32_t stream_window, + uint32_t connection_window) { + auto filter = getFilterFromListener("http"); + if (filter) { + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager + hcm_config; + loadHttpConnectionManager(hcm_config); + if (hcm_config.codec_type() == envoy::extensions::filters::network::http_connection_manager:: + v3::HttpConnectionManager::HTTP2) { + auto* options = hcm_config.mutable_http2_protocol_options(); + options->mutable_initial_stream_window_size()->set_value(stream_window); + options->mutable_initial_connection_window_size()->set_value(connection_window); + storeHttpConnectionManager(hcm_config); + } + } +} + +void ConfigHelper::setUpstreamHttp2WindowSize(uint32_t stream_window, uint32_t connection_window) { + addConfigModifier([stream_window, + connection_window](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + auto* http_protocol_options = + protocol_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + http_protocol_options->mutable_initial_stream_window_size()->set_value(stream_window); + http_protocol_options->mutable_initial_connection_window_size()->set_value(connection_window); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); +} + void ConfigHelper::setLocalReply( const envoy::extensions::filters::network::http_connection_manager::v3::LocalReplyConfig& config) { @@ -1823,7 +1879,7 @@ void CdsHelper::setCds(const std::vector& c // Write to file the DiscoveryResponse and trigger inotify watch. envoy::service::discovery::v3::DiscoveryResponse cds_response; cds_response.set_version_info(std::to_string(cds_version_++)); - cds_response.set_type_url(Config::TypeUrl::get().Cluster); + cds_response.set_type_url(Config::TestTypeUrl::get().Cluster); for (const auto& cluster : clusters) { cds_response.add_resources()->PackFrom(cluster); } @@ -1845,7 +1901,7 @@ void EdsHelper::setEds(const std::vectorPackFrom(cluster_load_assignment); } diff --git a/test/config/utility.h b/test/config/utility.h index 1221f1cc853a1..29491a55118ed 100644 --- a/test/config/utility.h +++ b/test/config/utility.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -415,6 +416,14 @@ class ConfigHelper { // Set limits on pending upstream outbound frames. void setUpstreamOutboundFramesLimits(uint32_t max_all_frames, uint32_t max_control_frames); + // Set limits on HTTP/2 concurrent streams. + void setDownstreamHttp2MaxConcurrentStreams(uint32_t max_streams); + void setUpstreamHttp2MaxConcurrentStreams(uint32_t max_streams); + + // Set limits on HTTP/2 window sizes. + void setDownstreamHttp2WindowSize(uint32_t stream_window, uint32_t connection_window); + void setUpstreamHttp2WindowSize(uint32_t stream_window, uint32_t connection_window); + // Return the bootstrap configuration for hand-off to Envoy. const envoy::config::bootstrap::v3::Bootstrap& bootstrap() { return bootstrap_; } @@ -437,7 +446,7 @@ class ConfigHelper { void addRuntimeOverride(absl::string_view key, absl::string_view value); // Add typed_filter_metadata to the first listener. - void addListenerTypedMetadata(absl::string_view key, ProtobufWkt::Any& packed_value); + void addListenerTypedMetadata(absl::string_view key, Protobuf::Any& packed_value); // Add filter_metadata to a cluster with the given name void addClusterFilterMetadata(absl::string_view metadata_yaml, diff --git a/test/config_test/config_test.cc b/test/config_test/config_test.cc index 7d811c65ed991..beb105ba50baa 100644 --- a/test/config_test/config_test.cc +++ b/test/config_test/config_test.cc @@ -61,9 +61,8 @@ class ConfigTest { ConfigTest(OptionsImplBase& options) : api_(Api::createApiForTest(time_system_)), ads_mux_(std::make_shared>()), options_(options) { - ON_CALL(server_.server_factory_context_->xds_manager_, adsMux()) - .WillByDefault(Return(ads_mux_)); - ON_CALL(*server_.server_factory_context_, api()).WillByDefault(ReturnRef(server_.api_)); + ON_CALL(server_, serverFactoryContext()).WillByDefault(ReturnRef(server_factory_context_)); + ON_CALL(server_.xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); ON_CALL(server_, options()).WillByDefault(ReturnRef(options_)); ON_CALL(server_, sslContextManager()).WillByDefault(ReturnRef(ssl_context_manager_)); ON_CALL(server_.api_, fileSystem()).WillByDefault(ReturnRef(file_system_)); @@ -124,10 +123,9 @@ class ConfigTest { } cluster_manager_factory_ = std::make_unique( - *server_.server_factory_context_, server_.stats(), server_.threadLocal(), - server_.httpContext(), + server_factory_context_, [this]() -> Network::DnsResolverSharedPtr { return this->server_.dnsResolver(); }, - ssl_context_manager_, server_.quic_stat_names_, server_); + server_.quic_stat_names_); ON_CALL(server_, clusterManager()).WillByDefault(Invoke([&]() -> Upstream::ClusterManager& { return *main_config.clusterManager(); @@ -158,7 +156,6 @@ class ConfigTest { return Server::ProdListenerComponentFactory::createUdpListenerFilterFactoryListImpl( filters, context); })); - ON_CALL(server_, serverFactoryContext()).WillByDefault(ReturnRef(server_factory_context_)); try { THROW_IF_NOT_OK(main_config.initialize(bootstrap, server_, *cluster_manager_factory_)); diff --git a/test/coverage.yaml b/test/coverage.yaml index 236ae348bd744..af05a8249083e 100644 --- a/test/coverage.yaml +++ b/test/coverage.yaml @@ -6,10 +6,8 @@ directories: source/common: 96.4 source/common/api: 95.3 # some syscalls require sandboxing source/common/api/posix: 94.9 # setns requires Linux CAP_NET_ADMIN privileges - source/common/common/posix: 96.2 # flaky due to posix: be careful adjusting - source/common/config: 96.5 source/common/crypto: 95.5 - source/common/event: 98.3 # Emulated edge events guards don't report LCOV + source/common/event: 96.5 # Emulated edge events guards don't report LCOV and setns requires Linux CAP_NET_ADMIN privileges. source/common/filesystem/posix: 96.4 # FileReadToEndNotReadable fails in some env; createPath can't test all failure branches. source/common/http: 96.5 source/common/http/http1: 93.4 # To be removed when http_inspector_use_balsa_parser is retired. @@ -22,14 +20,15 @@ directories: source/common/quic: 93.2 source/common/signal: 87.4 # Death tests don't report LCOV source/common/thread: 0.0 # Death tests don't report LCOV - source/common/tls: 94.4 # FIPS code paths impossible to trigger on non-FIPS builds and vice versa + source/common/tls: 94.5 # FIPS code paths impossible to trigger on non-FIPS builds and vice versa source/common/tls/cert_validator: 95.0 source/common/tls/private_key: 88.9 - source/common/tracing: 95.4 + source/common/tracing: 95.8 source/common/watchdog: 60.0 # Death tests don't report LCOV source/exe: 94.4 # increased by #32346, need coverage for terminate_handler and hot restart failures source/extensions/api_listeners: 55.0 # Many IS_ENVOY_BUG are not covered. - source/extensions/api_listeners/default_api_listener: 55.0 # Many IS_ENVOY_BUG are not covered. + source/extensions/api_listeners/default_api_listener: 59.0 # Many IS_ENVOY_BUG are not covered. + source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface: 95.6 source/extensions/common/aws: 98.5 source/extensions/common/aws/credential_providers: 100.0 source/extensions/common/proxy_protocol: 94.6 # Adjusted for security patch @@ -38,12 +37,12 @@ directories: source/extensions/common/wasm/ext: 100.0 source/extensions/filters/common: 97.1 source/extensions/filters/common/fault: 94.5 - source/extensions/filters/common/rbac: 93.2 + source/extensions/filters/common/rbac: 95.4 source/extensions/filters/common/lua: 95.6 source/extensions/filters/http/cache: 95.9 - source/extensions/filters/http/dynamic_forward_proxy: 94.3 + source/extensions/filters/http/dynamic_forward_proxy: 94.8 source/extensions/filters/http/decompressor: 95.9 - source/extensions/filters/http/ext_proc: 96.5 + source/extensions/filters/http/ext_proc: 96.4 # might be flaky: be careful adjusting source/extensions/filters/http/grpc_json_reverse_transcoder: 94.8 source/extensions/filters/http/grpc_json_transcoder: 94.0 # TODO(#28232) source/extensions/filters/http/ip_tagging: 95.9 @@ -51,22 +50,25 @@ directories: source/extensions/filters/http/oauth2: 97.6 source/extensions/filters/listener: 96.5 source/extensions/filters/listener/original_src: 92.1 - source/extensions/filters/listener/tls_inspector: 94.0 + source/extensions/filters/listener/tls_inspector: 94.1 source/extensions/filters/network/dubbo_proxy: 96.2 source/extensions/filters/network/mongo_proxy: 96.1 + source/extensions/filters/network/reverse_tunnel: 91.0 source/extensions/filters/network/sni_cluster: 88.9 + source/extensions/formatter/cel: 100.0 source/extensions/internal_redirect: 86.2 source/extensions/internal_redirect/safe_cross_scheme: 81.3 source/extensions/internal_redirect/allow_listed_routes: 85.7 source/extensions/internal_redirect/previous_routes: 89.3 - source/extensions/load_balancing_policies/maglev: 94.8 + source/extensions/load_balancing_policies/common: 96.3 + source/extensions/load_balancing_policies/maglev: 94.9 source/extensions/load_balancing_policies/round_robin: 96.4 source/extensions/load_balancing_policies/ring_hash: 96.9 source/extensions/rate_limit_descriptors: 95.0 - source/extensions/rate_limit_descriptors/expr: 95.2 + source/extensions/rate_limit_descriptors/expr: 88.2 source/extensions/stat_sinks/graphite_statsd: 82.8 # Death tests don't report LCOV source/extensions/stat_sinks/statsd: 85.2 # Death tests don't report LCOV - source/extensions/tracers/zipkin: 95.9 + source/extensions/tracers/zipkin: 95.3 source/extensions/transport_sockets/proxy_protocol: 96.2 source/extensions/wasm_runtime/wamr: 0.0 # Not enabled in coverage build source/extensions/wasm_runtime/wasmtime: 0.0 # Not enabled in coverage build @@ -75,9 +77,9 @@ directories: source/extensions/listener_managers/validation_listener_manager: 77.3 source/extensions/watchdog/profile_action: 86.1 source/server: 91.0 # flaky: be careful adjusting. See https://github.com/envoyproxy/envoy/issues/15239 - source/server/config_validation: 92.3 + source/server/config_validation: 93.1 source/extensions/health_checkers: 96.1 - source/extensions/health_checkers/http: 94.2 + source/extensions/health_checkers/http: 94.6 source/extensions/health_checkers/grpc: 92.3 source/extensions/config_subscription/rest: 94.9 - source/extensions/matching/input_matchers/cel_matcher: 91.3 # Death tests don't report LCOV + source/extensions/matching/input_matchers/cel_matcher: 100.0 diff --git a/test/exe/BUILD b/test/exe/BUILD index ef1d7e6f9657e..680088fbbad0d 100644 --- a/test/exe/BUILD +++ b/test/exe/BUILD @@ -75,7 +75,6 @@ envoy_cc_test_library( "//source/common/stats:isolated_store_lib", "//source/exe:envoy_main_common_with_core_extensions_lib", "//source/exe:platform_impl_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", "//test/mocks/runtime:runtime_mocks", "//test/test_common:contention_lib", "//test/test_common:environment_lib", @@ -95,7 +94,7 @@ envoy_cc_test( "//source/common/formatter:formatter_extension_lib", "//source/exe:envoy_main_common_with_core_extensions_lib", "//source/exe:platform_impl_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//test/mocks/runtime:runtime_mocks", "//test/test_common:contention_lib", "//test/test_common:environment_lib", @@ -114,7 +113,7 @@ envoy_cc_test( "//source/common/formatter:formatter_extension_lib", "//source/exe:envoy_main_common_with_core_extensions_lib", "//source/exe:platform_impl_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//test/mocks/runtime:runtime_mocks", "//test/test_common:contention_lib", "//test/test_common:environment_lib", diff --git a/test/exe/all_extensions_build_test.cc b/test/exe/all_extensions_build_test.cc index 3e57d90e3cf38..60390f768c4a3 100644 --- a/test/exe/all_extensions_build_test.cc +++ b/test/exe/all_extensions_build_test.cc @@ -10,7 +10,7 @@ namespace Envoy { namespace { -std::vector stringsFromListValue(const ProtobufWkt::Value& value) { +std::vector stringsFromListValue(const Protobuf::Value& value) { std::vector strings; for (const auto& elt : value.list_value().values()) { strings.push_back(elt.string_value()); @@ -30,8 +30,8 @@ TEST(CheckExtensionsAgainstRegistry, CorrectMetadata) { const std::string manifest_path = TestEnvironment::runfilesPath("source/extensions/extensions_metadata.yaml"); const std::string manifest = TestEnvironment::readFileToStringForTest(manifest_path); - ProtobufWkt::Value value = ValueUtil::loadFromYaml(manifest); - ASSERT_EQ(ProtobufWkt::Value::kStructValue, value.kind_case()); + Protobuf::Value value = ValueUtil::loadFromYaml(manifest); + ASSERT_EQ(Protobuf::Value::kStructValue, value.kind_case()); const auto& json = value.struct_value(); for (const auto& ext : Registry::FactoryCategoryRegistry::registeredFactories()) { @@ -45,7 +45,7 @@ TEST(CheckExtensionsAgainstRegistry, CorrectMetadata) { ENVOY_LOG_MISC(warn, "Missing extension '{}' from category '{}'.", name, ext.first); continue; } - ASSERT_EQ(ProtobufWkt::Value::kStructValue, it->second.kind_case()) + ASSERT_EQ(Protobuf::Value::kStructValue, it->second.kind_case()) << "Malformed extension metadata for: " << name; const auto& extension_fields = it->second.struct_value().fields(); diff --git a/test/extensions/access_loggers/common/grpc_access_logger_test.cc b/test/extensions/access_loggers/common/grpc_access_logger_test.cc index 9d06bc250d92a..ae31325695b29 100644 --- a/test/extensions/access_loggers/common/grpc_access_logger_test.cc +++ b/test/extensions/access_loggers/common/grpc_access_logger_test.cc @@ -44,9 +44,9 @@ const Protobuf::MethodDescriptor& mockMethodDescriptor() { // need to use a proto type because the ByteSizeLong() is used to determine the log size, so we use // standard Struct and Empty protos. class MockGrpcAccessLoggerImpl - : public Common::GrpcAccessLogger, - public Grpc::AsyncRequestCallbacks { + : public Common::GrpcAccessLogger, + public Grpc::AsyncRequestCallbacks { public: MockGrpcAccessLoggerImpl( const Grpc::RawAsyncClientSharedPtr& client, @@ -56,18 +56,17 @@ class MockGrpcAccessLoggerImpl : GrpcAccessLogger(config, dispatcher, scope, access_log_prefix, createGrpcAccessLoggClient(stream, client, service_method, config)) {} - std::unique_ptr> + std::unique_ptr> createGrpcAccessLoggClient( bool stream, const Grpc::RawAsyncClientSharedPtr& client, const Protobuf::MethodDescriptor& service_method, const envoy::extensions::access_loggers::grpc::v3::CommonGrpcAccessLogConfig& config) { if (stream) { return std::make_unique< - Common::StreamingGrpcAccessLogClient>( + Common::StreamingGrpcAccessLogClient>( client, service_method, GrpcCommon::optionalRetryPolicy(config)); } - return std::make_unique< - Common::UnaryGrpcAccessLogClient>( + return std::make_unique>( client, service_method, GrpcCommon::optionalRetryPolicy(config), [this]() -> MockGrpcAccessLoggerImpl& { return *this; }); } @@ -76,7 +75,7 @@ class MockGrpcAccessLoggerImpl int numClears() const { return num_clears_; } - void onSuccess(Grpc::ResponsePtr&&, Tracing::Span&) override {} + void onSuccess(Grpc::ResponsePtr&&, Tracing::Span&) override {} void onCreateInitialMetadata(Http::RequestHeaderMap&) override {} void onFailure(Grpc::Status::GrpcStatus, const std::string&, Tracing::Span&) override {} @@ -84,7 +83,7 @@ class MockGrpcAccessLoggerImpl private: void mockAddEntry(const std::string& key) { if (!message_.fields().contains(key)) { - ProtobufWkt::Value default_value; + Protobuf::Value default_value; default_value.set_number_value(0); message_.mutable_fields()->insert({key, default_value}); } @@ -97,12 +96,12 @@ class MockGrpcAccessLoggerImpl // it's up to each logger implementation. We test whether they were called in the regular flow of // logging or not. For example, we count how many entries were added, but don't add the log entry // itself to the message. - void addEntry(ProtobufWkt::Struct&& entry) override { + void addEntry(Protobuf::Struct&& entry) override { (void)entry; mockAddEntry(MOCK_HTTP_LOG_FIELD_NAME); } - void addEntry(ProtobufWkt::Empty&& entry) override { + void addEntry(Protobuf::Empty&& entry) override { (void)entry; mockAddEntry(MOCK_TCP_LOG_FIELD_NAME); } @@ -123,13 +122,13 @@ class MockGrpcAccessLoggerImpl class StreamingGrpcAccessLogTest : public testing::Test { public: using MockAccessLogStream = Grpc::MockAsyncStream; - using AccessLogCallbacks = Grpc::AsyncStreamCallbacks; + using AccessLogCallbacks = Grpc::AsyncStreamCallbacks; // We log a non empty entry (even though not used) so that we can trigger buffering mechanisms, // which are based on the entry size. - ProtobufWkt::Struct mockHttpEntry() { - ProtobufWkt::Struct entry; - entry.mutable_fields()->insert({"test-key", ProtobufWkt::Value()}); + Protobuf::Struct mockHttpEntry() { + Protobuf::Struct entry; + entry.mutable_fields()->insert({"test-key", Protobuf::Value()}); return entry; } @@ -160,7 +159,7 @@ class StreamingGrpcAccessLogTest : public testing::Test { EXPECT_CALL(stream, isAboveWriteBufferHighWatermark()).WillOnce(Return(false)); EXPECT_CALL(stream, sendMessageRaw_(_, false)) .WillOnce(Invoke([key, count](Buffer::InstancePtr& request, bool) { - ProtobufWkt::Struct message; + Protobuf::Struct message; Buffer::ZeroCopyInputStreamImpl request_stream(std::move(request)); EXPECT_TRUE(message.ParseFromZeroCopyStream(&request_stream)); EXPECT_TRUE(message.fields().contains(key)); @@ -195,14 +194,14 @@ TEST_F(StreamingGrpcAccessLogTest, BasicFlow) { // Log a TCP entry. expectFlushedLogEntriesCount(stream, MOCK_TCP_LOG_FIELD_NAME, 1); - logger_->log(ProtobufWkt::Empty()); + logger_->log(Protobuf::Empty()); EXPECT_EQ(2, logger_->numClears()); // TCP logging doesn't change the logs_written counter. EXPECT_EQ(1, TestUtility::findCounter(stats_store_, "mock_access_log_prefix.logs_written")->value()); // Verify that sending an empty response message doesn't do anything bad. - callbacks->onReceiveMessage(std::make_unique()); + callbacks->onReceiveMessage(std::make_unique()); // Close the stream and make sure we make a new one. callbacks->onRemoteClose(Grpc::Status::Internal, "bad"); @@ -323,9 +322,9 @@ TEST_F(StreamingGrpcAccessLogTest, Batching) { // Logging an entry that's bigger than the buffer size should trigger another flush. expectFlushedLogEntriesCount(stream, MOCK_HTTP_LOG_FIELD_NAME, 1); - ProtobufWkt::Struct big_entry = mockHttpEntry(); + Protobuf::Struct big_entry = mockHttpEntry(); const std::string big_key(max_buffer_size, 'a'); - big_entry.mutable_fields()->insert({big_key, ProtobufWkt::Value()}); + big_entry.mutable_fields()->insert({big_key, Protobuf::Value()}); logger_->log(std::move(big_entry)); EXPECT_EQ(2, logger_->numClears()); } @@ -357,13 +356,13 @@ TEST_F(StreamingGrpcAccessLogTest, Flushing) { class UnaryGrpcAccessLogTest : public testing::Test { public: using MockAccessLogStream = Grpc::MockAsyncStream; - using AccessLogCallbacks = Grpc::AsyncRequestCallbacks; + using AccessLogCallbacks = Grpc::AsyncRequestCallbacks; // We log a non empty entry (even though not used) so that we can trigger buffering mechanisms, // which are based on the entry size. - ProtobufWkt::Struct mockHttpEntry() { - ProtobufWkt::Struct entry; - entry.mutable_fields()->insert({"test-key", ProtobufWkt::Value()}); + Protobuf::Struct mockHttpEntry() { + Protobuf::Struct entry; + entry.mutable_fields()->insert({"test-key", Protobuf::Value()}); return entry; } @@ -385,7 +384,7 @@ class UnaryGrpcAccessLogTest : public testing::Test { Invoke([key, count](absl::string_view, absl::string_view, Buffer::InstancePtr&& request, Grpc::RawAsyncRequestCallbacks&, Tracing::Span&, const Http::AsyncClient::RequestOptions&) { - ProtobufWkt::Struct message; + Protobuf::Struct message; Buffer::ZeroCopyInputStreamImpl request_stream(std::move(request)); EXPECT_TRUE(message.ParseFromZeroCopyStream(&request_stream)); EXPECT_TRUE(message.fields().contains(key)); @@ -416,7 +415,7 @@ TEST_F(UnaryGrpcAccessLogTest, BasicFlow) { // Log a TCP entry. expectFlushedLogEntriesCount(MOCK_TCP_LOG_FIELD_NAME, 1); - logger_->log(ProtobufWkt::Empty()); + logger_->log(Protobuf::Empty()); // Message should be initialized and cleared every time a request is sent. EXPECT_EQ(2, logger_->numInits()); EXPECT_EQ(2, logger_->numClears()); @@ -463,9 +462,9 @@ TEST_F(UnaryGrpcAccessLogTest, Batching) { // Logging an entry that's bigger than the buffer size should trigger another flush. expectFlushedLogEntriesCount(MOCK_HTTP_LOG_FIELD_NAME, 1); - ProtobufWkt::Struct big_entry = mockHttpEntry(); + Protobuf::Struct big_entry = mockHttpEntry(); const std::string big_key(max_buffer_size, 'a'); - big_entry.mutable_fields()->insert({big_key, ProtobufWkt::Value()}); + big_entry.mutable_fields()->insert({big_key, Protobuf::Value()}); logger_->log(std::move(big_entry)); EXPECT_EQ(2, logger_->numClears()); } diff --git a/test/extensions/access_loggers/fluentd/substitution_formatter_test.cc b/test/extensions/access_loggers/fluentd/substitution_formatter_test.cc index 962d60a1ce4a2..865646f017203 100644 --- a/test/extensions/access_loggers/fluentd/substitution_formatter_test.cc +++ b/test/extensions/access_loggers/fluentd/substitution_formatter_test.cc @@ -19,7 +19,7 @@ namespace Fluentd { namespace { TEST(FluentdFormatterImplTest, FormatMsgpack) { - ProtobufWkt::Struct log_struct; + Protobuf::Struct log_struct; (*log_struct.mutable_fields())["Message"].set_string_value("SomeValue"); (*log_struct.mutable_fields())["LogType"].set_string_value("%ACCESS_LOG_TYPE%"); diff --git a/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc b/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc index b3982539ada8b..870a0e44e6c5a 100644 --- a/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc +++ b/test/extensions/access_loggers/grpc/grpc_access_log_utils_test.cc @@ -89,7 +89,7 @@ TEST(UtilityExtractCommonAccessLogPropertiesTest, FilterStateFromDownstream) { ASSERT_EQ(common_access_log.mutable_filter_state_objects()->count("downstream_peer"), 1); ASSERT_EQ(common_access_log.mutable_filter_state_objects()->size(), 1); auto any = (*(common_access_log.mutable_filter_state_objects()))["downstream_peer"]; - ProtobufWkt::BytesValue gotState; + Protobuf::BytesValue gotState; any.UnpackTo(&gotState); EXPECT_EQ(gotState.value(), "value_from_downstream_peer"); } @@ -120,7 +120,7 @@ TEST(UtilityExtractCommonAccessLogPropertiesTest, FilterStateFromUpstream) { ASSERT_EQ(common_access_log.mutable_filter_state_objects()->count("upstream_peer"), 1); ASSERT_EQ(common_access_log.mutable_filter_state_objects()->size(), 1); auto any = (*(common_access_log.mutable_filter_state_objects()))["upstream_peer"]; - ProtobufWkt::BytesValue gotState; + Protobuf::BytesValue gotState; any.UnpackTo(&gotState); EXPECT_EQ(gotState.value(), "value_from_upstream_peer"); } @@ -160,7 +160,7 @@ TEST(UtilityExtractCommonAccessLogPropertiesTest, ASSERT_EQ(common_access_log.mutable_filter_state_objects()->count("same_key"), 1); ASSERT_EQ(common_access_log.mutable_filter_state_objects()->size(), 1); auto any = (*(common_access_log.mutable_filter_state_objects()))["same_key"]; - ProtobufWkt::BytesValue gotState; + Protobuf::BytesValue gotState; any.UnpackTo(&gotState); EXPECT_EQ(gotState.value(), "value_from_downstream_peer"); } diff --git a/test/extensions/access_loggers/grpc/http_grpc_access_log_impl_test.cc b/test/extensions/access_loggers/grpc/http_grpc_access_log_impl_test.cc index 0f03f55222a3a..b372c753b8362 100644 --- a/test/extensions/access_loggers/grpc/http_grpc_access_log_impl_test.cc +++ b/test/extensions/access_loggers/grpc/http_grpc_access_log_impl_test.cc @@ -170,8 +170,8 @@ response: {{}} class TestSerializedFilterState : public StreamInfo::FilterState::Object { public: ProtobufTypes::MessagePtr serializeAsProto() const override { - auto any = std::make_unique(); - ProtobufWkt::Duration value; + auto any = std::make_unique(); + Protobuf::Duration value; value.set_seconds(10); any->PackFrom(value); return any; @@ -195,7 +195,7 @@ TEST_F(HttpGrpcAccessLogTest, Marshalling) { ASSERT(timing.lastDownstreamTxByteSent().has_value()); stream_info.downstream_connection_info_provider_->setLocalAddress( *Network::Address::PipeInstance::create("/foo")); - (*stream_info.metadata_.mutable_filter_metadata())["foo"] = ProtobufWkt::Struct(); + (*stream_info.metadata_.mutable_filter_metadata())["foo"] = Protobuf::Struct(); stream_info.filter_state_->setData("string_accessor", std::make_unique("test_value"), StreamInfo::FilterState::StateType::ReadOnly, @@ -765,7 +765,7 @@ response: {} ASSERT(timing.lastDownstreamTxByteSent().has_value()); stream_info.downstream_connection_info_provider_->setLocalAddress( *Network::Address::PipeInstance::create("/foo")); - (*stream_info.metadata_.mutable_filter_metadata())["foo"] = ProtobufWkt::Struct(); + (*stream_info.metadata_.mutable_filter_metadata())["foo"] = Protobuf::Struct(); stream_info.filter_state_->setData("string_accessor", std::make_unique("test_value"), StreamInfo::FilterState::StateType::ReadOnly, @@ -1080,7 +1080,7 @@ tag: mtag std::shared_ptr> host( new NiceMock()); auto metadata = std::make_shared(); - metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( "foo", MessageUtil::keyValueStruct("bar", "baz"))); ON_CALL(*host, metadata()).WillByDefault(Return(metadata)); stream_info.upstreamInfo()->setUpstreamHost(host); diff --git a/test/extensions/access_loggers/open_telemetry/access_log_impl_test.cc b/test/extensions/access_loggers/open_telemetry/access_log_impl_test.cc index db253e668004c..9c65513b242c4 100644 --- a/test/extensions/access_loggers/open_telemetry/access_log_impl_test.cc +++ b/test/extensions/access_loggers/open_telemetry/access_log_impl_test.cc @@ -53,7 +53,7 @@ class MockGrpcAccessLogger : public GrpcAccessLogger { public: // GrpcAccessLogger MOCK_METHOD(void, log, (LogRecord && entry)); - MOCK_METHOD(void, log, (ProtobufWkt::Empty && entry)); + MOCK_METHOD(void, log, (Protobuf::Empty && entry)); }; class MockGrpcAccessLoggerCache : public GrpcAccessLoggerCache { diff --git a/test/extensions/access_loggers/open_telemetry/grpc_access_log_impl_test.cc b/test/extensions/access_loggers/open_telemetry/grpc_access_log_impl_test.cc index e9b99bef9a5af..0013291aed670 100644 --- a/test/extensions/access_loggers/open_telemetry/grpc_access_log_impl_test.cc +++ b/test/extensions/access_loggers/open_telemetry/grpc_access_log_impl_test.cc @@ -139,7 +139,7 @@ TEST_F(GrpcAccessLoggerImplTest, Log) { .value(), 1); // TCP logging shouldn't do anything. - logger_->log(ProtobufWkt::Empty()); + logger_->log(Protobuf::Empty()); EXPECT_EQ(stats_store_.findCounterByString("access_logs.open_telemetry_access_log.logs_written") .value() .get() diff --git a/test/extensions/access_loggers/open_telemetry/substitution_formatter_test.cc b/test/extensions/access_loggers/open_telemetry/substitution_formatter_test.cc index 8127d8ce9be00..f22fd74a01749 100644 --- a/test/extensions/access_loggers/open_telemetry/substitution_formatter_test.cc +++ b/test/extensions/access_loggers/open_telemetry/substitution_formatter_test.cc @@ -41,7 +41,7 @@ class TestSerializedStructFilterState : public StreamInfo::FilterState::Object { (*struct_.mutable_fields())["inner_key"] = ValueUtil::stringValue("inner_value"); } - explicit TestSerializedStructFilterState(const ProtobufWkt::Struct& s) : use_struct_(true) { + explicit TestSerializedStructFilterState(const Protobuf::Struct& s) : use_struct_(true) { struct_.CopyFrom(s); } @@ -51,20 +51,20 @@ class TestSerializedStructFilterState : public StreamInfo::FilterState::Object { ProtobufTypes::MessagePtr serializeAsProto() const override { if (use_struct_) { - auto s = std::make_unique(); + auto s = std::make_unique(); s->CopyFrom(struct_); return s; } - auto d = std::make_unique(); + auto d = std::make_unique(); d->CopyFrom(duration_); return d; } private: const bool use_struct_{false}; - ProtobufWkt::Struct struct_; - ProtobufWkt::Duration duration_; + Protobuf::Struct struct_; + Protobuf::Duration duration_; }; // Class used to test serializeAsString and serializeAsProto of FilterState @@ -75,7 +75,7 @@ class TestSerializedStringFilterState : public StreamInfo::FilterState::Object { return raw_string_ + " By PLAIN"; } ProtobufTypes::MessagePtr serializeAsProto() const override { - auto message = std::make_unique(); + auto message = std::make_unique(); message->set_value(raw_string_ + " By TYPED"); return message; } @@ -89,12 +89,12 @@ class TestSerializedStringFilterState : public StreamInfo::FilterState::Object { * "com.test": {"test_key":"test_value","test_obj":{"inner_key":"inner_value"}} */ void populateMetadataTestData(envoy::config::core::v3::Metadata& metadata) { - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; auto& fields_map = *struct_obj.mutable_fields(); fields_map["test_key"] = ValueUtil::stringValue("test_value"); - ProtobufWkt::Struct struct_inner; + Protobuf::Struct struct_inner; (*struct_inner.mutable_fields())["inner_key"] = ValueUtil::stringValue("inner_value"); - ProtobufWkt::Value val; + Protobuf::Value val; *val.mutable_struct_value() = struct_inner; fields_map["test_obj"] = val; (*metadata.mutable_filter_metadata())["com.test"] = struct_obj; diff --git a/test/extensions/access_loggers/wasm/config_test.cc b/test/extensions/access_loggers/wasm/config_test.cc index d068451056348..4054ff93124ee 100644 --- a/test/extensions/access_loggers/wasm/config_test.cc +++ b/test/extensions/access_loggers/wasm/config_test.cc @@ -109,7 +109,7 @@ TEST_P(WasmAccessLogConfigTest, CreateWasmFromWASM) { config.mutable_config()->mutable_vm_config()->mutable_code()->mutable_local()->set_inline_bytes( code); // Test Any configuration. - ProtobufWkt::Struct some_proto; + Protobuf::Struct some_proto; config.mutable_config()->mutable_vm_config()->mutable_configuration()->PackFrom(some_proto); AccessLog::FilterPtr filter; diff --git a/test/extensions/bootstrap/reverse_tunnel/common/BUILD b/test/extensions/bootstrap/reverse_tunnel/common/BUILD new file mode 100644 index 0000000000000..01739aedda6ce --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/common/BUILD @@ -0,0 +1,22 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "reverse_connection_utility_test", + size = "medium", + srcs = ["reverse_connection_utility_test.cc"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/common/network:connection_lib", + "//source/extensions/bootstrap/reverse_tunnel/common:reverse_connection_utility_lib", + "//test/mocks/network:network_mocks", + "//test/test_common:test_runtime_lib", + ], +) diff --git a/test/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility_test.cc b/test/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility_test.cc new file mode 100644 index 0000000000000..dc9b849efc318 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility_test.cc @@ -0,0 +1,236 @@ +#include "source/common/buffer/buffer_impl.h" +#include "source/common/network/connection_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" + +#include "test/mocks/network/mocks.h" +#include "test/test_common/test_runtime.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ReverseConnectionUtilityTest : public testing::Test { +protected: + ReverseConnectionUtilityTest() = default; +}; + +// Test isPingMessage functionality +TEST_F(ReverseConnectionUtilityTest, IsPingMessageEmptyData) { + // Test with empty data + EXPECT_FALSE(ReverseConnectionUtility::isPingMessage("")); + EXPECT_FALSE(ReverseConnectionUtility::isPingMessage(absl::string_view())); +} + +TEST_F(ReverseConnectionUtilityTest, IsPingMessageExactMatch) { + // Test with exact RPING match + EXPECT_TRUE(ReverseConnectionUtility::isPingMessage("RPING")); + EXPECT_TRUE(ReverseConnectionUtility::isPingMessage(absl::string_view("RPING"))); +} + +TEST_F(ReverseConnectionUtilityTest, IsPingMessageInvalidData) { + // Test with non-RPING data. isPingMessage should return false for these cases. + EXPECT_FALSE(ReverseConnectionUtility::isPingMessage("PING")); + EXPECT_FALSE(ReverseConnectionUtility::isPingMessage("RPIN")); + EXPECT_FALSE(ReverseConnectionUtility::isPingMessage("RPINGG")); + EXPECT_FALSE(ReverseConnectionUtility::isPingMessage("Hello World")); +} + +// Test createPingResponse functionality +TEST_F(ReverseConnectionUtilityTest, CreatePingResponse) { + auto ping_buffer = ReverseConnectionUtility::createPingResponse(); + + EXPECT_NE(ping_buffer, nullptr); + EXPECT_EQ(ping_buffer->toString(), "RPING"); + EXPECT_EQ(ping_buffer->length(), 5); +} + +// Test sendPingResponse with Connection +TEST_F(ReverseConnectionUtilityTest, SendPingResponseConnection) { + auto connection = std::make_unique>(); + + // Set up mock expectations + EXPECT_CALL(*connection, write(_, false)); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = ReverseConnectionUtility::sendPingResponse(*connection); + + EXPECT_TRUE(result); +} + +// Test sendPingResponse with IoHandle +TEST_F(ReverseConnectionUtilityTest, SendPingResponseIoHandleSuccess) { + auto io_handle = std::make_unique>(); + + EXPECT_CALL(*io_handle, write(_)) + .WillOnce(Return(Api::IoCallUint64Result{5, Api::IoError::none()})); + + Api::IoCallUint64Result result = ReverseConnectionUtility::sendPingResponse(*io_handle); + + EXPECT_TRUE(result.ok()); + EXPECT_EQ(result.return_value_, 5); + EXPECT_EQ(result.err_, nullptr); +} + +TEST_F(ReverseConnectionUtilityTest, SendPingResponseIoHandleFailure) { + auto io_handle = std::make_unique>(); + + // Set up mock expectations for failed write + EXPECT_CALL(*io_handle, write(_)) + .WillOnce(Return(Api::IoCallUint64Result{0, Network::IoSocketError::create(ECONNRESET)})); + + Api::IoCallUint64Result result = ReverseConnectionUtility::sendPingResponse(*io_handle); + + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.return_value_, 0); + EXPECT_NE(result.err_, nullptr); +} + +// Test handlePingMessage functionality +TEST_F(ReverseConnectionUtilityTest, HandlePingMessageValidPing) { + auto connection = std::make_unique>(); + + // should call sendPingResponse and return true since it is a valid RPING message + EXPECT_CALL(*connection, write(_, false)); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = ReverseConnectionUtility::handlePingMessage("RPING", *connection); + + EXPECT_TRUE(result); +} + +TEST_F(ReverseConnectionUtilityTest, HandlePingMessageInvalidData) { + auto connection = std::make_unique>(); + + // Should not call sendPingResponse for invalid data + EXPECT_CALL(*connection, write(_, _)).Times(0); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = ReverseConnectionUtility::handlePingMessage("INVALID", *connection); + + EXPECT_FALSE(result); +} + +TEST_F(ReverseConnectionUtilityTest, HandlePingMessageEmptyData) { + auto connection = std::make_unique>(); + + // Should not call sendPingResponse for empty data + EXPECT_CALL(*connection, write(_, _)).Times(0); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = ReverseConnectionUtility::handlePingMessage("", *connection); + + EXPECT_FALSE(result); +} + +// Test extractPingFromHttpData functionality +TEST_F(ReverseConnectionUtilityTest, ExtractPingFromHttpDataValid) { + // Test with RPING in HTTP response body + EXPECT_TRUE(ReverseConnectionUtility::extractPingFromHttpData("HTTP/1.1 200 OK\r\n\r\nRPING")); + EXPECT_TRUE(ReverseConnectionUtility::extractPingFromHttpData( + "GET /ping HTTP/1.1\r\nHost: example.com\r\n\r\nRPING")); + EXPECT_TRUE(ReverseConnectionUtility::extractPingFromHttpData( + "POST /data HTTP/1.1\r\nContent-Length: 5\r\n\r\nRPING")); +} + +TEST_F(ReverseConnectionUtilityTest, ExtractPingFromHttpDataInvalid) { + // Test with no RPING in HTTP data + EXPECT_FALSE(ReverseConnectionUtility::extractPingFromHttpData("HTTP/1.1 200 OK\r\n\r\nHello")); + EXPECT_FALSE(ReverseConnectionUtility::extractPingFromHttpData( + "GET /ping HTTP/1.1\r\nHost: example.com\r\n\r\nPING")); + EXPECT_FALSE(ReverseConnectionUtility::extractPingFromHttpData("")); +} + +// Test ReverseConnectionMessageHandlerFactory functionality +TEST_F(ReverseConnectionUtilityTest, CreatePingHandler) { + auto handler = ReverseConnectionMessageHandlerFactory::createPingHandler(); + + EXPECT_NE(handler, nullptr); + EXPECT_EQ(handler->getPingCount(), 0); +} + +// Test PingMessageHandler functionality +TEST_F(ReverseConnectionUtilityTest, PingMessageHandlerProcessValidPing) { + auto handler = ReverseConnectionMessageHandlerFactory::createPingHandler(); + auto connection = std::make_unique>(); + + // Set up mock expectations + EXPECT_CALL(*connection, write(_, false)); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = handler->processPingMessage("RPING", *connection); + + EXPECT_TRUE(result); + EXPECT_EQ(handler->getPingCount(), 1); +} + +TEST_F(ReverseConnectionUtilityTest, PingMessageHandlerProcessInvalidPing) { + auto handler = ReverseConnectionMessageHandlerFactory::createPingHandler(); + auto connection = std::make_unique>(); + + // Set up mock expectations - should not call write for invalid data + EXPECT_CALL(*connection, write(_, _)).Times(0); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = handler->processPingMessage("INVALID", *connection); + + EXPECT_FALSE(result); + EXPECT_EQ(handler->getPingCount(), 0); +} + +TEST_F(ReverseConnectionUtilityTest, PingMessageHandlerProcessMultiplePings) { + auto handler = ReverseConnectionMessageHandlerFactory::createPingHandler(); + auto connection = std::make_unique>(); + + // Set up mock expectations for multiple writes + EXPECT_CALL(*connection, write(_, false)).Times(3); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + // Process multiple valid pings + EXPECT_TRUE(handler->processPingMessage("RPING", *connection)); + EXPECT_TRUE(handler->processPingMessage("RPING", *connection)); + EXPECT_TRUE(handler->processPingMessage("RPING", *connection)); + + EXPECT_EQ(handler->getPingCount(), 3); +} + +TEST_F(ReverseConnectionUtilityTest, PingMessageHandlerProcessEmptyPing) { + auto handler = ReverseConnectionMessageHandlerFactory::createPingHandler(); + auto connection = std::make_unique>(); + + // Set up mock expectations - should not call write for empty data + EXPECT_CALL(*connection, write(_, _)).Times(0); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + bool result = handler->processPingMessage("", *connection); + + EXPECT_FALSE(result); + EXPECT_EQ(handler->getPingCount(), 0); +} + +TEST_F(ReverseConnectionUtilityTest, PingMessageHandlerGetPingCount) { + auto handler = ReverseConnectionMessageHandlerFactory::createPingHandler(); + + // Initially should be 0 + EXPECT_EQ(handler->getPingCount(), 0); + + // After processing a ping, should be 1 + auto connection = std::make_unique>(); + EXPECT_CALL(*connection, write(_, false)); + EXPECT_CALL(*connection, id()).WillRepeatedly(Return(12345)); + + handler->processPingMessage("RPING", *connection); + EXPECT_EQ(handler->getPingCount(), 1); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/BUILD b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/BUILD new file mode 100644 index 0000000000000..e0ab471193d60 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/BUILD @@ -0,0 +1,132 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "reverse_tunnel_initiator_test", + size = "large", + srcs = ["reverse_tunnel_initiator_test.cc"], + extension_names = ["envoy.bootstrap.reverse_tunnel.downstream_socket_interface"], + deps = [ + "//source/common/network:address_lib", + "//source/common/network:utility_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_address_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_io_handle_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:logging_lib", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "reverse_tunnel_initiator_extension_test", + size = "medium", + srcs = ["reverse_tunnel_initiator_extension_test.cc"], + deps = [ + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_extension_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "reverse_connection_io_handle_test", + size = "large", + srcs = ["reverse_connection_io_handle_test.cc"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_io_handle_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_extension_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "downstream_reverse_connection_io_handle_test", + size = "medium", + srcs = ["downstream_reverse_connection_io_handle_test.cc"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_io_handle_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_extension_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "reverse_connection_address_test", + size = "medium", + srcs = ["reverse_connection_address_test.cc"], + deps = [ + "//source/common/network:address_lib", + "//source/common/network:default_socket_interface_lib", + "//source/common/singleton:threadsafe_singleton", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_address_lib", + "//test/mocks/network:network_mocks", + "//test/test_common:registry_lib", + "//test/test_common:test_runtime_lib", + ], +) + +envoy_cc_test( + name = "reverse_connection_resolver_test", + size = "medium", + srcs = ["reverse_connection_resolver_test.cc"], + deps = [ + "//source/common/network:address_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_resolver_lib", + "//test/mocks/network:network_mocks", + "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "rc_connection_wrapper_test", + size = "large", + srcs = ["rc_connection_wrapper_test.cc"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_io_handle_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_extension_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:upstream_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle_test.cc new file mode 100644 index 0000000000000..c9456cf26663a --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle_test.cc @@ -0,0 +1,547 @@ +#include +#include +#include + +#include + +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/server/factory_context.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/io_socket_error_impl.h" +#include "source/common/network/socket_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/common/reverse_connection_utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/downstream_reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// Base test class for ReverseConnectionIOHandle (minimal version for +// DownstreamReverseConnectionIOHandleTest) +class ReverseConnectionIOHandleTestBase : public testing::Test { +protected: + ReverseConnectionIOHandleTestBase() { + // Set up the stats scope. + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context. + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + + // Create the socket interface. + socket_interface_ = std::make_unique(context_); + + // Create the extension. + extension_ = std::make_unique(context_, config_); + + // Set up mock dispatcher with default expectations. + EXPECT_CALL(dispatcher_, createTimer_(_)) + .WillRepeatedly(testing::ReturnNew>()); + EXPECT_CALL(dispatcher_, createFileEvent_(_, _, _, _)) + .WillRepeatedly(testing::ReturnNew>()); + } + + void TearDown() override { + io_handle_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + // Helper to create a ReverseConnectionIOHandle with specified configuration. + std::unique_ptr + createTestIOHandle(const ReverseConnectionSocketConfig& config) { + // Create a test socket file descriptor. + int test_fd = ::socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(test_fd, 0); + + // Create the IO handle. + return std::make_unique(test_fd, config, cluster_manager_, + extension_.get(), *stats_scope_); + } + + // Helper to create a default test configuration. + ReverseConnectionSocketConfig createDefaultTestConfig() { + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.enable_circuit_breaker = true; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + return config; + } + + NiceMock context_; + NiceMock thread_local_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + std::unique_ptr io_handle_; + + // Mock cluster manager. + NiceMock cluster_manager_; + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); + + // Mock socket for testing. + std::unique_ptr mock_socket_; +}; + +/** + * Test class for DownstreamReverseConnectionIOHandle. + */ +class DownstreamReverseConnectionIOHandleTest : public ReverseConnectionIOHandleTestBase { +protected: + void SetUp() override { + ReverseConnectionIOHandleTestBase::SetUp(); + + // Initialize io_handle_ for testing. + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create a mock socket for testing. + mock_socket_ = std::make_unique>(); + auto mock_io_handle_unique = std::make_unique>(); + mock_io_handle_ = mock_io_handle_unique.get(); + + // Set up basic mock expectations. + EXPECT_CALL(*mock_io_handle_, fdDoNotUse()).WillRepeatedly(Return(42)); // Arbitrary FD + EXPECT_CALL(*mock_socket_, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_)); + } + + void TearDown() override { + mock_socket_.reset(); + ReverseConnectionIOHandleTestBase::TearDown(); + } + + // Helper to create a DownstreamReverseConnectionIOHandle. + std::unique_ptr + createHandle(ReverseConnectionIOHandle* parent = nullptr, + const std::string& connection_key = "test_connection_key") { + // Create a new mock socket for each handle to avoid releasing the shared one. + auto new_mock_socket = std::make_unique>(); + auto new_mock_io_handle = std::make_unique>(); + + // Store the raw pointer before moving + mock_io_handle_ = new_mock_io_handle.get(); + + // Set up basic mock expectations. + EXPECT_CALL(*mock_io_handle_, fdDoNotUse()).WillRepeatedly(Return(42)); // Arbitrary FD + EXPECT_CALL(*new_mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_)); + + auto socket_ptr = std::unique_ptr(new_mock_socket.release()); + return std::make_unique(std::move(socket_ptr), parent, + connection_key); + } + + // Test fixtures. + std::unique_ptr> mock_socket_; + NiceMock* mock_io_handle_; // Raw pointer, managed by socket +}; + +// Test constructor and destructor. +TEST_F(DownstreamReverseConnectionIOHandleTest, Setup) { + // Test constructor with parent. + { + auto handle = createHandle(io_handle_.get(), "test_key_1"); + EXPECT_NE(handle, nullptr); + // Test fdDoNotUse() before any other operations. + EXPECT_EQ(handle->fdDoNotUse(), 42); + } // Destructor called here + + // Test constructor without parent. + { + auto handle = createHandle(nullptr, "test_key_2"); + EXPECT_NE(handle, nullptr); + // Test fdDoNotUse() before any other operations. + EXPECT_EQ(handle->fdDoNotUse(), 42); + } // Destructor called here +} + +// Test close() method and all edge cases. +TEST_F(DownstreamReverseConnectionIOHandleTest, CloseMethod) { + // Test with parent - should notify parent and reset socket. + { + auto handle = createHandle(io_handle_.get(), "test_key"); + + // Verify that parent is set correctly. + EXPECT_NE(io_handle_.get(), nullptr); + + // First close - should notify parent and reset owned_socket. + auto result1 = handle->close(); + EXPECT_EQ(result1.err_, nullptr); + + // Second close - should return immediately without notifying parent (fd < 0). + auto result2 = handle->close(); + EXPECT_EQ(result2.err_, nullptr); + } +} + +// Test getSocket() method. +TEST_F(DownstreamReverseConnectionIOHandleTest, GetSocket) { + auto handle = createHandle(io_handle_.get(), "test_key"); + + // Test getSocket() returns the owned socket. + const auto& socket = handle->getSocket(); + EXPECT_NE(&socket, nullptr); + + // Test getSocket() works on const object. + const auto const_handle = createHandle(io_handle_.get(), "test_key"); + const auto& const_socket = const_handle->getSocket(); + EXPECT_NE(&const_socket, nullptr); + + // Test that getSocket() works before close() is called. + EXPECT_EQ(handle->fdDoNotUse(), 42); +} + +// Test ignoreCloseAndShutdown() functionality. +TEST_F(DownstreamReverseConnectionIOHandleTest, IgnoreCloseAndShutdown) { + auto handle = createHandle(io_handle_.get(), "test_key"); + + // Initially, close and shutdown should work normally + // Test shutdown before ignoring - we don't check the result since it depends on base + // implementation + handle->shutdown(SHUT_RDWR); + + // Now enable ignore mode + handle->ignoreCloseAndShutdown(); + + // Test that close() is ignored when flag is set + auto close_result = handle->close(); + EXPECT_EQ(close_result.err_, nullptr); // Should return success but do nothing + + // Test that shutdown() is ignored when flag is set + auto shutdown_result2 = handle->shutdown(SHUT_RDWR); + EXPECT_EQ(shutdown_result2.return_value_, 0); + EXPECT_EQ(shutdown_result2.errno_, 0); + + // Test different shutdown modes are all ignored + auto shutdown_rd = handle->shutdown(SHUT_RD); + EXPECT_EQ(shutdown_rd.return_value_, 0); + EXPECT_EQ(shutdown_rd.errno_, 0); + + auto shutdown_wr = handle->shutdown(SHUT_WR); + EXPECT_EQ(shutdown_wr.return_value_, 0); + EXPECT_EQ(shutdown_wr.errno_, 0); +} + +// Test read() method with real socket pairs to test RPING handling. +TEST_F(DownstreamReverseConnectionIOHandleTest, ReadRpingEchoScenarios) { + const std::string rping_msg = std::string(ReverseConnectionUtility::PING_MESSAGE); + + // Complete RPING message - should be echoed and drained. + { + // Create a socket pair. + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + // Create a mock socket with real file descriptor + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + // Store the io handle in the socket. + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + // Create handle with the socket. + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key"); + + // Write RPING to the other end of the socket pair. + ssize_t written = write(fds[1], rping_msg.data(), rping_msg.size()); + ASSERT_EQ(written, static_cast(rping_msg.size())); + + // Read should process RPING and return the size (indicating RPING was handled). + Buffer::OwnedImpl buffer; + auto result = handle->read(buffer, absl::nullopt); + + EXPECT_EQ(result.return_value_, rping_msg.size()); + EXPECT_EQ(result.err_, nullptr); + EXPECT_EQ(buffer.length(), 0); // RPING should be drained. + + // Verify RPING echo was sent back. + char echo_buffer[10]; + ssize_t echo_read = read(fds[1], echo_buffer, sizeof(echo_buffer)); + EXPECT_EQ(echo_read, static_cast(rping_msg.size())); + EXPECT_EQ(std::string(echo_buffer, echo_read), rping_msg); + + close(fds[1]); + } + + // RPING + application data - echo RPING, keep app data, disable echo. + { + // Create another socket pair. + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key2"); + + const std::string app_data = "GET /path HTTP/1.1\r\n"; + const std::string combined = rping_msg + app_data; + + // Write combined data to socket. + ssize_t written = write(fds[1], combined.data(), combined.size()); + ASSERT_EQ(written, static_cast(combined.size())); + + // Read should process RPING and return only app data size. + Buffer::OwnedImpl buffer; + auto result = handle->read(buffer, absl::nullopt); + + EXPECT_EQ(result.return_value_, app_data.size()); // Only app data size + EXPECT_EQ(result.err_, nullptr); + EXPECT_EQ(buffer.toString(), app_data); // Only app data remains + + // Verify RPING echo was sent back. + char echo_buffer[10]; + ssize_t echo_read = read(fds[1], echo_buffer, sizeof(echo_buffer)); + EXPECT_EQ(echo_read, static_cast(rping_msg.size())); + EXPECT_EQ(std::string(echo_buffer, echo_read), rping_msg); + + close(fds[1]); + } + + // Non-RPING data should disable echo and pass through. + { + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key3"); + + const std::string http_data = "GET /path HTTP/1.1\r\n"; + + // Write HTTP data to socket. + ssize_t written = write(fds[1], http_data.data(), http_data.size()); + ASSERT_EQ(written, static_cast(http_data.size())); + + // Read should return all HTTP data without processing. + Buffer::OwnedImpl buffer; + auto result = handle->read(buffer, absl::nullopt); + + EXPECT_EQ(result.return_value_, http_data.size()); + EXPECT_EQ(result.err_, nullptr); + EXPECT_EQ(buffer.toString(), http_data); + + // Verify no echo was sent back. + char echo_buffer[10]; + // Set socket to non-blocking to avoid hanging. + int flags = fcntl(fds[1], F_GETFL, 0); + fcntl(fds[1], F_SETFL, flags | O_NONBLOCK); + ssize_t echo_read = read(fds[1], echo_buffer, sizeof(echo_buffer)); + EXPECT_EQ(echo_read, -1); + EXPECT_EQ(errno, EAGAIN); // No data available + + close(fds[1]); + } +} + +// Test read() method with partial data handling using real sockets. +TEST_F(DownstreamReverseConnectionIOHandleTest, ReadPartialDataAndStateTransitions) { + const std::string rping_msg = std::string(ReverseConnectionUtility::PING_MESSAGE); + + // Test partial RPING - should pass through and wait for more data. + { + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key"); + + // Write partial RPING (first 3 bytes). + const std::string partial_rping = rping_msg.substr(0, 3); + ssize_t written = write(fds[1], partial_rping.data(), partial_rping.size()); + ASSERT_EQ(written, static_cast(partial_rping.size())); + + // Read should return the partial data as-is. + Buffer::OwnedImpl buffer; + auto result = handle->read(buffer, absl::nullopt); + + EXPECT_EQ(result.return_value_, 3); + EXPECT_EQ(result.err_, nullptr); + EXPECT_EQ(buffer.toString(), partial_rping); + + close(fds[1]); + } + + // Test non-RPING data - should disable echo permanently. + { + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key2"); + + const std::string http_data = "GET /path"; + + // Write HTTP data. + ssize_t written = write(fds[1], http_data.data(), http_data.size()); + ASSERT_EQ(written, static_cast(http_data.size())); + + // Read should return HTTP data and disable echo. + Buffer::OwnedImpl buffer; + auto result = handle->read(buffer, absl::nullopt); + + EXPECT_EQ(result.return_value_, http_data.size()); + EXPECT_EQ(result.err_, nullptr); + EXPECT_EQ(buffer.toString(), http_data); + + // Verify no echo was sent. + char echo_buffer[10]; + int flags = fcntl(fds[1], F_GETFL, 0); + fcntl(fds[1], F_SETFL, flags | O_NONBLOCK); + ssize_t echo_read = read(fds[1], echo_buffer, sizeof(echo_buffer)); + EXPECT_EQ(echo_read, -1); + EXPECT_EQ(errno, EAGAIN); + + close(fds[1]); + } +} + +// Test read() method in scenarios where echo is disabled. +TEST_F(DownstreamReverseConnectionIOHandleTest, ReadEchoDisabledAndErrorHandling) { + const std::string rping_msg = std::string(ReverseConnectionUtility::PING_MESSAGE); + + // Test that after echo is disabled, RPING passes through without processing. + { + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key"); + + // First, disable echo by sending HTTP data. + const std::string http_data = "HTTP/1.1"; + ssize_t written = write(fds[1], http_data.data(), http_data.size()); + ASSERT_EQ(written, static_cast(http_data.size())); + + Buffer::OwnedImpl buffer1; + handle->read(buffer1, absl::nullopt); + EXPECT_EQ(buffer1.toString(), http_data); + + // Now send RPING - it should pass through without echo. + written = write(fds[1], rping_msg.data(), rping_msg.size()); + ASSERT_EQ(written, static_cast(rping_msg.size())); + + Buffer::OwnedImpl buffer2; + auto result = handle->read(buffer2, absl::nullopt); + + EXPECT_EQ(result.return_value_, rping_msg.size()); + EXPECT_EQ(result.err_, nullptr); + EXPECT_EQ(buffer2.toString(), rping_msg); // RPING data preserved + + // Verify no echo was sent. + char echo_buffer[10]; + int flags = fcntl(fds[1], F_GETFL, 0); + fcntl(fds[1], F_SETFL, flags | O_NONBLOCK); + ssize_t echo_read = read(fds[1], echo_buffer, sizeof(echo_buffer)); + EXPECT_EQ(echo_read, -1); + EXPECT_EQ(errno, EAGAIN); + + close(fds[1]); + } + + // Test EOF scenario - close the write end. + { + int fds[2]; + ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + + auto mock_socket = std::make_unique>(); + auto mock_io_handle = std::make_unique(fds[0]); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + auto* io_handle_ptr = mock_io_handle.release(); + mock_socket->io_handle_.reset(io_handle_ptr); + + auto socket_ptr = Network::ConnectionSocketPtr(mock_socket.release()); + auto handle = std::make_unique( + std::move(socket_ptr), io_handle_.get(), "test_key2"); + + // Close write end to simulate EOF. + close(fds[1]); + + Buffer::OwnedImpl buffer; + auto result = handle->read(buffer, absl::nullopt); + + EXPECT_EQ(result.return_value_, 0); // EOF + EXPECT_EQ(result.err_, nullptr); // No error, just EOF + EXPECT_EQ(buffer.length(), 0); + } +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper_test.cc new file mode 100644 index 0000000000000..a5857252077c1 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper_test.cc @@ -0,0 +1,1064 @@ +#include + +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/server/factory_context.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/http/header_map_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// RCConnectionWrapper Tests. + +class RCConnectionWrapperTest : public testing::Test { +protected: + void SetUp() override { + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + extension_ = std::make_unique(context_, config_); + setupThreadLocalSlot(); + io_handle_ = createTestIOHandle(createDefaultTestConfig()); + } + + void TearDown() override { + io_handle_.reset(); + extension_.reset(); + } + + void setupThreadLocalSlot() { + thread_local_registry_ = + std::make_shared(dispatcher_, *stats_scope_); + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + } + + ReverseConnectionSocketConfig createDefaultTestConfig() { + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.enable_circuit_breaker = true; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + return config; + } + + std::unique_ptr + createTestIOHandle(const ReverseConnectionSocketConfig& config) { + int test_fd = ::socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(test_fd, 0); + return std::make_unique(test_fd, config, cluster_manager_, + extension_.get(), *stats_scope_); + } + + // Connection Management Helpers. + + bool initiateOneReverseConnection(const std::string& cluster_name, + const std::string& host_address, + Upstream::HostConstSharedPtr host) { + return io_handle_->initiateOneReverseConnection(cluster_name, host_address, host); + } + + // Data Access Helpers. + + const std::vector>& getConnectionWrappers() const { + return io_handle_->connection_wrappers_; + } + + const absl::flat_hash_map& getConnWrapperToHostMap() const { + return io_handle_->conn_wrapper_to_host_map_; + } + + // Test Data Setup Helpers. + + void addHostConnectionInfo(const std::string& host_address, const std::string& cluster_name, + uint32_t target_count) { + io_handle_->host_to_conn_info_map_[host_address] = + ReverseConnectionIOHandle::HostConnectionInfo{ + host_address, + cluster_name, + {}, // connection_keys - empty set initially + target_count, // target_connection_count + 0, // failure_count + // last_failure_time + std::chrono::steady_clock::now(), // NO_CHECK_FORMAT(real_time) + // backoff_until + std::chrono::steady_clock::now(), // NO_CHECK_FORMAT(real_time) + {} // connection_states + }; + } + + // Helper to create a mock host. + Upstream::HostConstSharedPtr createMockHost(const std::string& address) { + auto mock_host = std::make_shared>(); + auto mock_address = std::make_shared(address, 8080); + EXPECT_CALL(*mock_host, address()).WillRepeatedly(Return(mock_address)); + return mock_host; + } + + // Helper method to set up mock connection with proper socket expectations. + std::unique_ptr> setupMockConnection() { + auto mock_connection = std::make_unique>(); + + // Create a mock socket for the connection. + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle = std::make_unique>(); + + // Set up IO handle expectations. + EXPECT_CALL(*mock_io_handle, resetFileEvents()).WillRepeatedly(Return()); + EXPECT_CALL(*mock_io_handle, isOpen()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_io_handle, duplicate()).WillRepeatedly(Invoke([]() { + auto duplicated_handle = std::make_unique>(); + EXPECT_CALL(*duplicated_handle, isOpen()).WillRepeatedly(Return(true)); + return duplicated_handle; + })); + + // Set up socket expectations. + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(true)); + + // Store the mock_io_handle in the socket before casting. + mock_socket_ptr->io_handle_ = std::move(mock_io_handle); + + // Cast the mock to the base ConnectionSocket type and store it in member variable. + mock_socket_ = std::unique_ptr(mock_socket_ptr.release()); + + // Set up connection expectations for getSocket() + EXPECT_CALL(*mock_connection, getSocket()).WillRepeatedly(ReturnRef(mock_socket_)); + + return mock_connection; + } + + // Test fixtures. + NiceMock context_; + NiceMock thread_local_; + NiceMock cluster_manager_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + std::unique_ptr extension_; + std::unique_ptr io_handle_; + std::unique_ptr> tls_slot_; + std::shared_ptr thread_local_registry_; + + // Mock socket for testing. + std::unique_ptr mock_socket_; +}; + +// Test RCConnectionWrapper::connect() method with HTTP/1.1 handshake success +TEST_F(RCConnectionWrapperTest, ConnectHttpHandshakeSuccess) { + // Create a mock connection. + auto mock_connection = std::make_unique>(); + + // Set up connection expectations. + EXPECT_CALL(*mock_connection, addConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, addReadFilter(_)); + EXPECT_CALL(*mock_connection, connect()); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12345)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + + // Set up socket expectations for address info. + auto mock_address = std::make_shared("192.168.1.1", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12345); + + // Set up connection info provider expectations directly on the mock connection. + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke([mock_address, + mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = + std::make_unique(mock_local_address, mock_address); + return *mock_provider; + })); + + // Create a mock host. + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Call connect() method. + std::string result = wrapper.connect("test-tenant", "test-cluster", "test-node"); + + // Verify connect() returns the local address. + EXPECT_EQ(result, "127.0.0.1:12345"); +} + +// Test RCConnectionWrapper::connect() method with HTTP proxy (internal address) scenario. +TEST_F(RCConnectionWrapperTest, ConnectHttpHandshakeWithHttpProxy) { + // Create a mock connection. + auto mock_connection = std::make_unique>(); + + // Set up connection expectations. + EXPECT_CALL(*mock_connection, addConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, addReadFilter(_)); + EXPECT_CALL(*mock_connection, connect()); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12345)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + + // Set up socket expectations for internal address (HTTP proxy scenario). + auto mock_internal_address = std::make_shared( + "internal_listener_name", "endpoint_id_123"); + auto mock_local_address = std::make_shared("127.0.0.1", 12345); + + // Set up connection info provider expectations with internal address. + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke( + [mock_internal_address, mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = std::make_unique( + mock_local_address, mock_internal_address); + return *mock_provider; + })); + + // Capture the written buffer to verify HTTP request and simulate kernel drain. + Buffer::OwnedImpl captured_buffer; + EXPECT_CALL(*mock_connection, write(_, _)) + .WillOnce(Invoke([&captured_buffer](Buffer::Instance& buffer, bool) { + captured_buffer.add(buffer); + buffer.drain(buffer.length()); + })); + + // Create a mock host. + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Call connect() method. + std::string result = wrapper.connect("test-tenant", "test-cluster", "test-node"); + + // Verify connect() returns the local address. + EXPECT_EQ(result, "127.0.0.1:12345"); +} + +// Test RCConnectionWrapper::connect() method with connection write failure. +TEST_F(RCConnectionWrapperTest, ConnectHttpHandshakeWriteFailure) { + // Create a mock connection that fails to write. + auto mock_connection = std::make_unique>(); + + // Set up connection expectations. + EXPECT_CALL(*mock_connection, addConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, addReadFilter(_)); + EXPECT_CALL(*mock_connection, connect()); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12345)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + EXPECT_CALL(*mock_connection, write(_, _)).WillOnce(Invoke([](Buffer::Instance&, bool) -> void { + throw EnvoyException("Write failed"); + })); + + // Set up socket expectations. + auto mock_address = std::make_shared("192.168.1.1", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12345); + + // Set up connection info provider expectations directly on the mock connection. + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke([mock_address, + mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = + std::make_unique(mock_local_address, mock_address); + return *mock_provider; + })); + + // Create a mock host. + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Call connect() method - should handle the write failure gracefully. + // The method should not throw but should handle the exception internally. + std::string result; + try { + result = wrapper.connect("test-tenant", "test-cluster", "test-node"); + } catch (const EnvoyException& e) { + // The connect() method doesn't handle exceptions, so we expect it to throw. + // This is the current behavior - the method should be updated to handle exceptions. + EXPECT_STREQ(e.what(), "Write failed"); + return; // Exit test early since exception was thrown + } + + // If no exception was thrown, verify connect() still returns the local address. + EXPECT_EQ(result, "127.0.0.1:12345"); +} + +// Test RCConnectionWrapper::onHandshakeSuccess method. +TEST_F(RCConnectionWrapperTest, OnHandshakeSuccess) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper and add it to the map. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Get initial stats before onHandshakeSuccess. + auto initial_stats = extension_->getCrossWorkerStatMap(); + std::string host_stat_name = "test_scope.reverse_connections.host.192.168.1.1.connected"; + std::string cluster_stat_name = "test_scope.reverse_connections.cluster.test-cluster.connected"; + + // Call onHandshakeSuccess. + wrapper_ptr->onHandshakeSuccess(); + + // Get stats after onHandshakeSuccess. + auto final_stats = extension_->getCrossWorkerStatMap(); + + // Verify that connected stats were incremented. + EXPECT_EQ(final_stats[host_stat_name], initial_stats[host_stat_name] + 1); + EXPECT_EQ(final_stats[cluster_stat_name], initial_stats[cluster_stat_name] + 1); +} + +// Test RCConnectionWrapper::onHandshakeFailure method. +TEST_F(RCConnectionWrapperTest, OnHandshakeFailure) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper and add it to the map. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Get initial stats before onHandshakeFailure. + auto initial_stats = extension_->getCrossWorkerStatMap(); + std::string host_failed_stat_name = "test_scope.reverse_connections.host.192.168.1.1.failed"; + std::string cluster_failed_stat_name = + "test_scope.reverse_connections.cluster.test-cluster.failed"; + + // Call onHandshakeFailure with an error message. + std::string error_message = "Handshake failed due to authentication error"; + wrapper_ptr->onHandshakeFailure(error_message); + + // Get stats after onHandshakeFailure. + auto final_stats = extension_->getCrossWorkerStatMap(); + + // Verify that failed stats were incremented. + EXPECT_EQ(final_stats[host_failed_stat_name], initial_stats[host_failed_stat_name] + 1); + EXPECT_EQ(final_stats[cluster_failed_stat_name], initial_stats[cluster_failed_stat_name] + 1); +} + +// Test RCConnectionWrapper::onEvent method with RemoteClose event. +TEST_F(RCConnectionWrapperTest, OnEventRemoteClose) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper and add it to the map. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Get initial stats before onEvent. + auto initial_stats = extension_->getCrossWorkerStatMap(); + std::string host_connected_stat_name = + "test_scope.reverse_connections.host.192.168.1.1.connected"; + std::string cluster_connected_stat_name = + "test_scope.reverse_connections.cluster.test-cluster.connected"; + + // Call onEvent with RemoteClose event. + wrapper_ptr->onEvent(Network::ConnectionEvent::RemoteClose); + + // Get stats after onEvent. + auto final_stats = extension_->getCrossWorkerStatMap(); + + // Verify that the connection closure was handled gracefully. +} + +// Test RCConnectionWrapper::onEvent method with Connected event (should be ignored) +TEST_F(RCConnectionWrapperTest, OnEventConnected) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper and add it to the map. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Get initial stats before onEvent. + auto initial_stats = extension_->getCrossWorkerStatMap(); + + // Call onEvent with Connected event (should be ignored) + wrapper_ptr->onEvent(Network::ConnectionEvent::Connected); + + // Get stats after onEvent. + auto final_stats = extension_->getCrossWorkerStatMap(); + + // Verify that Connected event doesn't change stats (it should be ignored) + // The stats should remain the same. + EXPECT_EQ(final_stats, initial_stats); +} + +// Test RCConnectionWrapper::onEvent method with null connection. +TEST_F(RCConnectionWrapperTest, OnEventWithNullConnection) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper and add it to the map. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Get initial stats before onEvent. + auto initial_stats = extension_->getCrossWorkerStatMap(); + + // Call onEvent with RemoteClose event. + wrapper_ptr->onEvent(Network::ConnectionEvent::RemoteClose); + + // Get stats after onEvent. + auto final_stats = extension_->getCrossWorkerStatMap(); + + // Verify that the event was handled gracefully even with connection closure. + // The exact behavior depends on the implementation, but it should not crash. +} + +// Test decodeHeaders handles HTTP 200 status by calling success path. +TEST_F(RCConnectionWrapperTest, DecodeHeadersOk) { + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + Http::ResponseHeaderMapPtr headers = Http::ResponseHeaderMapImpl::create(); + headers->setStatus(200); + + wrapper.decodeHeaders(std::move(headers), true); +} + +// Test decodeHeaders handles non-200 status by calling failure path. +TEST_F(RCConnectionWrapperTest, DecodeHeadersNonOk) { + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + Http::ResponseHeaderMapPtr headers = Http::ResponseHeaderMapImpl::create(); + headers->setStatus(404); + + wrapper.decodeHeaders(std::move(headers), true); +} + +// Test dispatchHttp1 error path by initializing codec via connect() and +// then feeding invalid bytes to the parser. +TEST_F(RCConnectionWrapperTest, DispatchHttp1ErrorPath) { + auto mock_connection = std::make_unique>(); + + EXPECT_CALL(*mock_connection, addConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, addReadFilter(_)); + EXPECT_CALL(*mock_connection, connect()); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(42)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + // Allow writes made by the HTTP/1 encoder and drain them to simulate kernel behavior. + EXPECT_CALL(*mock_connection, write(_, _)) + .WillRepeatedly( + Invoke([](Buffer::Instance& buffer, bool) { buffer.drain(buffer.length()); })); + + // Provide connection info provider. + auto mock_remote = std::make_shared("10.0.0.1", 80); + auto mock_local = std::make_shared("127.0.0.1", 10001); + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke([mock_remote, mock_local]() -> const Network::ConnectionInfoProvider& { + static auto provider = + std::make_unique(mock_local, mock_remote); + return *provider; + })); + + auto mock_host = std::make_shared>(); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + // Initialize codec inside the wrapper. + (void)wrapper.connect("tenant", "cluster", "node"); + + // Feed clearly invalid/non-HTTP bytes to exercise error log path. + Buffer::OwnedImpl invalid_bytes("\x00\x01garbage"); + wrapper.dispatchHttp1(invalid_bytes); +} + +// Test that destructor invokes shutdown when not already called. +TEST_F(RCConnectionWrapperTest, DestructorInvokesShutdown) { + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + EXPECT_CALL(*mock_connection, removeConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::FlushWrite)); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(777)); + + { + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + // No explicit shutdown; leaving scope should run destructor which calls shutdown. + } +} + +// Test RCConnectionWrapper::releaseConnection method. +TEST_F(RCConnectionWrapperTest, ReleaseConnection) { + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Verify connection exists before release. + EXPECT_NE(wrapper.getConnection(), nullptr); + + // Release the connection. + auto released_connection = wrapper.releaseConnection(); + + // Verify connection was released. + EXPECT_NE(released_connection, nullptr); + EXPECT_EQ(wrapper.getConnection(), nullptr); +} + +// Test RCConnectionWrapper::getConnection method. +TEST_F(RCConnectionWrapperTest, GetConnection) { + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Get the connection. + auto* connection = wrapper.getConnection(); + + // Verify connection is returned. + EXPECT_NE(connection, nullptr); + + // Test after release. + wrapper.releaseConnection(); + EXPECT_EQ(wrapper.getConnection(), nullptr); +} + +// Test RCConnectionWrapper::getHost method. +TEST_F(RCConnectionWrapperTest, GetHost) { + // Create a mock connection and host with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Get the host. + auto host = wrapper.getHost(); + + // Verify host is returned. + EXPECT_EQ(host, mock_host); +} + +// Test RCConnectionWrapper::onAboveWriteBufferHighWatermark method (no-op) +TEST_F(RCConnectionWrapperTest, OnAboveWriteBufferHighWatermark) { + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Call onAboveWriteBufferHighWatermark - should be a no-op. + wrapper.onAboveWriteBufferHighWatermark(); +} + +// Test RCConnectionWrapper::onBelowWriteBufferLowWatermark method (no-op) +TEST_F(RCConnectionWrapperTest, OnBelowWriteBufferLowWatermark) { + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Call onBelowWriteBufferLowWatermark - should be a no-op. + wrapper.onBelowWriteBufferLowWatermark(); +} + +// Test RCConnectionWrapper::shutdown method. +TEST_F(RCConnectionWrapperTest, Shutdown) { + // Test 1: Shutdown with open connection. + { + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Set up connection expectations for open connection. + EXPECT_CALL(*mock_connection, removeConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::FlushWrite)); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12345)); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + EXPECT_NE(wrapper.getConnection(), nullptr); + wrapper.shutdown(); + EXPECT_EQ(wrapper.getConnection(), nullptr); + } + // Test 2: Shutdown with already closed connection. + { + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Set up connection expectations for closed connection. + EXPECT_CALL(*mock_connection, state()) + .WillRepeatedly(Return(Network::Connection::State::Closed)); + EXPECT_CALL(*mock_connection, close(_)) + .Times(0); // Should not call close on already closed connection + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12346)); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + EXPECT_NE(wrapper.getConnection(), nullptr); + wrapper.shutdown(); + EXPECT_EQ(wrapper.getConnection(), nullptr); + } + + // Test 3: Shutdown with closing connection. + { + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Set up connection expectations for closing connection. + EXPECT_CALL(*mock_connection, removeConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, state()) + .WillRepeatedly(Return(Network::Connection::State::Closing)); + EXPECT_CALL(*mock_connection, close(_)) + .Times(0); // Should not call close on already closing connection + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12347)); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + EXPECT_NE(wrapper.getConnection(), nullptr); + wrapper.shutdown(); + EXPECT_EQ(wrapper.getConnection(), nullptr); + } + // Test 4: Shutdown with null connection (should be safe) + { + auto mock_host = std::make_shared>(); + + // Create wrapper with null connection. + RCConnectionWrapper wrapper(*io_handle_, nullptr, mock_host, "test-cluster"); + + EXPECT_EQ(wrapper.getConnection(), nullptr); + wrapper.shutdown(); // Should not crash + EXPECT_EQ(wrapper.getConnection(), nullptr); + } + // Test 5: Multiple shutdown calls (should be safe) + { + auto mock_connection = std::make_unique>(); + auto mock_host = std::make_shared>(); + + // Set up connection expectations. + EXPECT_CALL(*mock_connection, removeConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::FlushWrite)); + EXPECT_CALL(*mock_connection, id()).WillRepeatedly(Return(12348)); + + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + EXPECT_NE(wrapper.getConnection(), nullptr); + + // First shutdown. + wrapper.shutdown(); + EXPECT_EQ(wrapper.getConnection(), nullptr); + + // Second shutdown (should be safe) + wrapper.shutdown(); + EXPECT_EQ(wrapper.getConnection(), nullptr); + } +} + +// Test SimpleConnReadFilter::onData method. +class SimpleConnReadFilterTest : public testing::Test { +protected: + void SetUp() override { + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Create a mock IO handle. + auto mock_io_handle = std::make_unique>(); + io_handle_ = std::make_unique( + 7, // dummy fd + ReverseConnectionSocketConfig{}, cluster_manager_, + nullptr, // extension + *stats_scope_); // Use the created scope + } + + void TearDown() override { io_handle_.reset(); } + + // Helper to create a mock RCConnectionWrapper. + std::unique_ptr createMockWrapper() { + auto mock_connection = std::make_unique>(); + auto mock_host = std::make_shared>(); + return std::make_unique(*io_handle_, std::move(mock_connection), mock_host, + "test-cluster"); + } + + // Helper to create SimpleConnReadFilter. + std::unique_ptr createFilter(void* parent) { + return std::make_unique(parent); + } + + NiceMock cluster_manager_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + std::unique_ptr io_handle_; +}; + +TEST_F(SimpleConnReadFilterTest, OnDataWithNullParent) { + // Create filter with null parent. + auto filter = createFilter(nullptr); + + // Create a buffer with some data. + Buffer::OwnedImpl buffer("HTTP/1.1 200 OK\r\n\r\n"); + + // Call onData - should return StopIteration when parent is null. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithHttp200Response) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with HTTP 200 response. + Buffer::OwnedImpl buffer("HTTP/1.1 200 OK\r\n\r\n"); + + // Call onData - the filter always stops iteration after dispatch. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithHttp2Response) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with HTTP/2 response but invalid protobuf. + Buffer::OwnedImpl buffer("HTTP/2 200\r\n\r\nACCEPTED"); + + // Call onData - should return StopIteration for invalid response format. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithIncompleteHeaders) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with incomplete HTTP headers. + Buffer::OwnedImpl buffer("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n"); + + // Call onData - the filter always stops iteration after dispatch. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithEmptyResponseBody) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with HTTP 200 but empty body. + Buffer::OwnedImpl buffer("HTTP/1.1 200 OK\r\n\r\n"); + + // Call onData - the filter always stops iteration after dispatch. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithNon200Response) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with HTTP 404 response. + Buffer::OwnedImpl buffer("HTTP/1.1 404 Not Found\r\n\r\n"); + + // Call onData - should return StopIteration for error response. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithHttp2ErrorResponse) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with HTTP/2 error response. + Buffer::OwnedImpl buffer("HTTP/2 500\r\n\r\n"); + + // Call onData - should return StopIteration for error response. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +TEST_F(SimpleConnReadFilterTest, OnDataWithPartialData) { + // Create wrapper and filter. + auto wrapper = createMockWrapper(); + auto filter = createFilter(wrapper.get()); + + // Create a buffer with partial data (no HTTP response yet) + Buffer::OwnedImpl buffer("partial data"); + + // Call onData - the filter always stops iteration after dispatch. + auto result = filter->onData(buffer, false); + EXPECT_EQ(result, Network::FilterStatus::StopIteration); +} + +// Test all no-op methods in RCConnectionWrapper. +TEST_F(RCConnectionWrapperTest, NoOpMethods) { + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Test Network::ConnectionCallbacks no-op methods + wrapper.onAboveWriteBufferHighWatermark(); + wrapper.onBelowWriteBufferLowWatermark(); + + // Test Http::ResponseDecoder no-op methods + wrapper.decode1xxHeaders(nullptr); + + Buffer::OwnedImpl data("test data"); + wrapper.decodeData(data, false); + wrapper.decodeData(data, true); + + wrapper.decodeTrailers(nullptr); + wrapper.decodeMetadata(nullptr); + + std::ostringstream output; + wrapper.dumpState(output, 0); + wrapper.dumpState(output, 2); + + // Test Http::ConnectionCallbacks no-op methods + wrapper.onGoAway(Http::GoAwayErrorCode::NoError); + wrapper.onGoAway(Http::GoAwayErrorCode::Other); + + NiceMock settings; + wrapper.onSettings(settings); + + wrapper.onMaxStreamsChanged(0); + wrapper.onMaxStreamsChanged(100); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address_test.cc new file mode 100644 index 0000000000000..a48204e9cba83 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address_test.cc @@ -0,0 +1,306 @@ +#include "source/common/network/io_socket_handle_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/common/singleton/threadsafe_singleton.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" + +#include "test/mocks/network/mocks.h" +#include "test/test_common/registry.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ReverseConnectionAddressTest : public testing::Test { +protected: + void SetUp() override {} + + // Helper function to create a test config. + ReverseConnectionAddress::ReverseConnectionConfig createTestConfig() { + return ReverseConnectionAddress::ReverseConnectionConfig{ + "test-node-123", "test-cluster-456", "test-tenant-789", "remote-cluster-abc", 5}; + } + + // Helper function to create a test address. + ReverseConnectionAddress createTestAddress() { + return ReverseConnectionAddress(createTestConfig()); + } + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); +}; + +// Test constructor and basic properties. +TEST_F(ReverseConnectionAddressTest, BasicSetup) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + // Test that the address string is set correctly. + EXPECT_EQ(address.asString(), "127.0.0.1:0"); + EXPECT_EQ(address.asStringView(), "127.0.0.1:0"); + + // Test that the logical name is formatted correctly. + std::string expected_logical_name = + "rc://test-node-123:test-cluster-456:test-tenant-789@remote-cluster-abc:5"; + EXPECT_EQ(address.logicalName(), expected_logical_name); + + // Test address type. + EXPECT_EQ(address.type(), Network::Address::Type::Ip); + EXPECT_EQ(address.addressType(), "reverse_connection"); +} + +// Test equality operator. +TEST_F(ReverseConnectionAddressTest, EqualityOperator) { + auto config1 = createTestConfig(); + auto config2 = createTestConfig(); + + ReverseConnectionAddress address1(config1); + ReverseConnectionAddress address2(config2); + + // Same config should be equal. + EXPECT_TRUE(address1 == address2); + EXPECT_TRUE(address2 == address1); + + // Different configs should not be equal. + config2.src_node_id = "different-node"; + ReverseConnectionAddress address3(config2); + EXPECT_FALSE(address1 == address3); + EXPECT_FALSE(address3 == address1); +} + +// Test equality with different address types. +TEST_F(ReverseConnectionAddressTest, EqualityWithDifferentTypes) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + // Create a regular IPv4 address. + auto regular_address = std::make_shared("127.0.0.1", 8080); + + // Should not be equal to different address types. + EXPECT_FALSE(address == *regular_address); + EXPECT_FALSE(*regular_address == address); +} + +// Test reverse connection config accessor. +TEST_F(ReverseConnectionAddressTest, ReverseConnectionConfig) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + const auto& retrieved_config = address.reverseConnectionConfig(); + + EXPECT_EQ(retrieved_config.src_node_id, config.src_node_id); + EXPECT_EQ(retrieved_config.src_cluster_id, config.src_cluster_id); + EXPECT_EQ(retrieved_config.src_tenant_id, config.src_tenant_id); + EXPECT_EQ(retrieved_config.remote_cluster, config.remote_cluster); + EXPECT_EQ(retrieved_config.connection_count, config.connection_count); +} + +// Test IP address properties. +TEST_F(ReverseConnectionAddressTest, IpAddressProperties) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + // Should have IP address. + EXPECT_NE(address.ip(), nullptr); + EXPECT_EQ(address.ip()->addressAsString(), "127.0.0.1"); + EXPECT_EQ(address.ip()->port(), 0); + + // Should not have pipe or envoy internal address. + EXPECT_EQ(address.pipe(), nullptr); + EXPECT_EQ(address.envoyInternalAddress(), nullptr); +} + +// Test socket address properties. +TEST_F(ReverseConnectionAddressTest, SocketAddressProperties) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + const sockaddr* sock_addr = address.sockAddr(); + EXPECT_NE(sock_addr, nullptr); + + socklen_t addr_len = address.sockAddrLen(); + EXPECT_EQ(addr_len, sizeof(struct sockaddr_in)); + + // Verify the sockaddr structure. + const struct sockaddr_in* addr_in = reinterpret_cast(sock_addr); + EXPECT_EQ(addr_in->sin_family, AF_INET); + EXPECT_EQ(addr_in->sin_port, htons(0)); // Port 0 + EXPECT_EQ(addr_in->sin_addr.s_addr, htonl(INADDR_LOOPBACK)); // 127.0.0.1 +} + +// Test network namespace. +TEST_F(ReverseConnectionAddressTest, NetworkNamespace) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + // Should not have a network namespace. + auto namespace_opt = address.networkNamespace(); + EXPECT_FALSE(namespace_opt.has_value()); +} + +// Test socket interface. +TEST_F(ReverseConnectionAddressTest, SocketInterface) { + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + // Should return the default socket interface. + const auto& socket_interface = address.socketInterface(); + EXPECT_NE(&socket_interface, nullptr); +} + +// Test socket interface with registered reverse connection interface. +TEST_F(ReverseConnectionAddressTest, SocketInterfaceWithReverseInterface) { + // Create a mock socket interface that extends SocketInterfaceBase and registers itself + class TestReverseSocketInterface : public Network::SocketInterfaceBase { + public: + TestReverseSocketInterface() = default; + + // Network::SocketInterface + Network::IoHandlePtr socket(Network::Socket::Type socket_type, Network::Address::Type addr_type, + Network::Address::IpVersion version, bool socket_v6only, + const Network::SocketCreationOptions& options) const override { + UNREFERENCED_PARAMETER(socket_v6only); + UNREFERENCED_PARAMETER(options); + // Create a regular socket for testing + if (socket_type == Network::Socket::Type::Stream && addr_type == Network::Address::Type::Ip) { + int domain = (version == Network::Address::IpVersion::v4) ? AF_INET : AF_INET6; + int sock_fd = ::socket(domain, SOCK_STREAM, 0); + if (sock_fd == -1) { + return nullptr; + } + return std::make_unique(sock_fd); + } + return nullptr; + } + + Network::IoHandlePtr socket(Network::Socket::Type socket_type, + const Network::Address::InstanceConstSharedPtr addr, + const Network::SocketCreationOptions& options) const override { + // Delegate to the other socket method + return socket(socket_type, addr->type(), + addr->ip() ? addr->ip()->version() : Network::Address::IpVersion::v4, false, + options); + } + + bool ipFamilySupported(int domain) override { return domain == AF_INET || domain == AF_INET6; } + + // Server::Configuration::BootstrapExtensionFactory + Server::BootstrapExtensionPtr + createBootstrapExtension(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override { + UNREFERENCED_PARAMETER(config); + UNREFERENCED_PARAMETER(context); + return nullptr; + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { return nullptr; } + + std::string name() const override { + return "envoy.bootstrap.reverse_tunnel.downstream_socket_interface"; + } + + std::set configTypes() override { return {}; } + }; + + // Register the test interface in the registry + TestReverseSocketInterface test_interface; + Registry::InjectFactory registered_factory( + test_interface); + + auto config = createTestConfig(); + ReverseConnectionAddress address(config); + + // Should return the registered test socket interface. + const auto& socket_interface = address.socketInterface(); + EXPECT_EQ(&socket_interface, &test_interface); +} + +// Test with empty configuration values. +TEST_F(ReverseConnectionAddressTest, EmptyConfigValues) { + ReverseConnectionAddress::ReverseConnectionConfig config; + config.src_node_id = ""; + config.src_cluster_id = ""; + config.src_tenant_id = ""; + config.remote_cluster = ""; + config.connection_count = 0; + + ReverseConnectionAddress address(config); + + // Should still work with empty values. + EXPECT_EQ(address.asString(), "127.0.0.1:0"); + EXPECT_EQ(address.logicalName(), "rc://::@:0"); + + const auto& retrieved_config = address.reverseConnectionConfig(); + EXPECT_EQ(retrieved_config.src_node_id, ""); + EXPECT_EQ(retrieved_config.src_cluster_id, ""); + EXPECT_EQ(retrieved_config.src_tenant_id, ""); + EXPECT_EQ(retrieved_config.remote_cluster, ""); + EXPECT_EQ(retrieved_config.connection_count, 0); +} + +// Test multiple instances with different configurations. +TEST_F(ReverseConnectionAddressTest, MultipleInstances) { + ReverseConnectionAddress::ReverseConnectionConfig config1; + config1.src_node_id = "node1"; + config1.src_cluster_id = "cluster1"; + config1.src_tenant_id = "tenant1"; + config1.remote_cluster = "remote1"; + config1.connection_count = 1; + + ReverseConnectionAddress::ReverseConnectionConfig config2; + config2.src_node_id = "node2"; + config2.src_cluster_id = "cluster2"; + config2.src_tenant_id = "tenant2"; + config2.remote_cluster = "remote2"; + config2.connection_count = 2; + + ReverseConnectionAddress address1(config1); + ReverseConnectionAddress address2(config2); + + // Should not be equal. + EXPECT_FALSE(address1 == address2); + EXPECT_FALSE(address2 == address1); + + // Should have different logical names. + EXPECT_NE(address1.logicalName(), address2.logicalName()); + + // Should have same address string (both use 127.0.0.1:0) + EXPECT_EQ(address1.asString(), address2.asString()); +} + +// Test copy constructor and assignment (if implemented). +TEST_F(ReverseConnectionAddressTest, CopyAndAssignment) { + auto config = createTestConfig(); + ReverseConnectionAddress original(config); + + // Test copy constructor. + ReverseConnectionAddress copied(original); + EXPECT_TRUE(original == copied); + EXPECT_EQ(original.logicalName(), copied.logicalName()); + EXPECT_EQ(original.asString(), copied.asString()); + + // Test assignment operator. + ReverseConnectionAddress::ReverseConnectionConfig config2; + config2.src_node_id = "different-node"; + config2.src_cluster_id = "different-cluster"; + config2.src_tenant_id = "different-tenant"; + config2.remote_cluster = "different-remote"; + config2.connection_count = 10; + + ReverseConnectionAddress assigned(config2); + assigned = original; + EXPECT_TRUE(original == assigned); + EXPECT_EQ(original.logicalName(), assigned.logicalName()); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle_test.cc new file mode 100644 index 0000000000000..94419077a73bd --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle_test.cc @@ -0,0 +1,2882 @@ +#include +#include + +#include + +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/server/factory_context.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/network/address_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// ReverseConnectionIOHandle Test Class. + +class ReverseConnectionIOHandleTest : public testing::Test { +protected: + ReverseConnectionIOHandleTest() { + // Set up the stats scope. + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context. + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + + // Create the socket interface. + socket_interface_ = std::make_unique(context_); + + // Create the extension. + extension_ = std::make_unique(context_, config_); + + // Set up mock dispatcher with default expectations. + EXPECT_CALL(dispatcher_, createTimer_(_)) + .WillRepeatedly(testing::ReturnNew>()); + EXPECT_CALL(dispatcher_, createFileEvent_(_, _, _, _)) + .WillRepeatedly(testing::ReturnNew>()); + } + + void TearDown() override { + io_handle_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + // Helper to create a ReverseConnectionIOHandle with specified configuration. + std::unique_ptr + createTestIOHandle(const ReverseConnectionSocketConfig& config) { + // Create a test socket file descriptor. + int test_fd = ::socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(test_fd, 0); + + // Create the IO handle. + return std::make_unique(test_fd, config, cluster_manager_, + extension_.get(), *stats_scope_); + } + + // Helper to create a default test configuration. + ReverseConnectionSocketConfig createDefaultTestConfig() { + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.enable_circuit_breaker = true; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + return config; + } + + NiceMock context_; + NiceMock thread_local_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + std::unique_ptr io_handle_; + + // Mock cluster manager. + NiceMock cluster_manager_; + + // Thread local components for testing. + std::unique_ptr> tls_slot_; + std::shared_ptr thread_local_registry_; + std::unique_ptr> another_tls_slot_; + std::shared_ptr another_thread_local_registry_; + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); + + // Mock socket for testing. + std::unique_ptr mock_socket_; + + // Thread Local Setup Helpers. + + // Helper function to set up thread local slot for tests. + void setupThreadLocalSlot() { + // Create a thread local registry. + thread_local_registry_ = + std::make_shared(dispatcher_, *stats_scope_); + + // Create the actual TypedSlot. + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the slot to return our registry. + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Set the slot in the extension using the test-only method. + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + } + + // Multi-Thread Local Setup Helpers. + + void setupAnotherThreadLocalSlot() { + // Create a thread local registry for the other dispatcher. + another_thread_local_registry_ = + std::make_shared(dispatcher_, *stats_scope_); + + // Create the actual TypedSlot. + another_tls_slot_ = + ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the slot to return our registry. + another_tls_slot_->set( + [registry = another_thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Set the slot in the extension using the test-only method. + extension_->setTestOnlyTLSRegistry(std::move(another_tls_slot_)); + } + + // Trigger Pipe Management Helpers. + + bool isTriggerPipeReady() const { return io_handle_->isTriggerPipeReady(); } + + void createTriggerPipe() { io_handle_->createTriggerPipe(); } + + int getTriggerPipeReadFd() const { return io_handle_->trigger_pipe_read_fd_; } + + int getTriggerPipeWriteFd() const { return io_handle_->trigger_pipe_write_fd_; } + + // Connection Management Helpers. + + void addConnectionToEstablishedQueue(Network::ClientConnectionPtr connection) { + io_handle_->established_connections_.push(std::move(connection)); + } + + bool initiateOneReverseConnection(const std::string& cluster_name, + const std::string& host_address, + Upstream::HostConstSharedPtr host) { + return io_handle_->initiateOneReverseConnection(cluster_name, host_address, host); + } + + void maintainReverseConnections() { io_handle_->maintainReverseConnections(); } + + void maintainClusterConnections(const std::string& cluster_name, + const RemoteClusterConnectionConfig& cluster_config) { + io_handle_->maintainClusterConnections(cluster_name, cluster_config); + } + + // Host Management Helpers. + + void maybeUpdateHostsMappingsAndConnections(const std::string& cluster_id, + const std::vector& hosts) { + io_handle_->maybeUpdateHostsMappingsAndConnections(cluster_id, hosts); + } + + bool shouldAttemptConnectionToHost(const std::string& host_address, + const std::string& cluster_name) { + return io_handle_->shouldAttemptConnectionToHost(host_address, cluster_name); + } + + void trackConnectionFailure(const std::string& host_address, const std::string& cluster_name) { + io_handle_->trackConnectionFailure(host_address, cluster_name); + } + + void resetHostBackoff(const std::string& host_address) { + io_handle_->resetHostBackoff(host_address); + } + + // Data Access Helpers. + + const absl::flat_hash_map& + getHostToConnInfoMap() const { + return io_handle_->host_to_conn_info_map_; + } + + const ReverseConnectionIOHandle::HostConnectionInfo& + getHostConnectionInfo(const std::string& host_address) const { + auto it = io_handle_->host_to_conn_info_map_.find(host_address); + EXPECT_NE(it, io_handle_->host_to_conn_info_map_.end()) + << "Host " << host_address << " not found in host_to_conn_info_map_"; + return it->second; + } + + ReverseConnectionIOHandle::HostConnectionInfo& + getMutableHostConnectionInfo(const std::string& host_address) { + auto it = io_handle_->host_to_conn_info_map_.find(host_address); + EXPECT_NE(it, io_handle_->host_to_conn_info_map_.end()) + << "Host " << host_address << " not found in host_to_conn_info_map_"; + return it->second; + } + + const std::vector>& getConnectionWrappers() const { + return io_handle_->connection_wrappers_; + } + + const absl::flat_hash_map& getConnWrapperToHostMap() const { + return io_handle_->conn_wrapper_to_host_map_; + } + + // Test Data Setup Helpers. + + void addHostConnectionInfo(const std::string& host_address, const std::string& cluster_name, + uint32_t target_count) { + io_handle_->host_to_conn_info_map_[host_address] = + ReverseConnectionIOHandle::HostConnectionInfo{ + host_address, + cluster_name, + {}, // connection_keys - empty set initially + target_count, // target_connection_count + 0, // failure_count + // last_failure_time + std::chrono::steady_clock::now(), // NO_CHECK_FORMAT(real_time) + // backoff_until - set to epoch start so host is not in backoff initially + std::chrono::steady_clock::time_point{}, // NO_CHECK_FORMAT(real_time) + {} // connection_states + }; + } + + // Helper to create a mock host. + Upstream::HostConstSharedPtr createMockHost(const std::string& address) { + auto mock_host = std::make_shared>(); + auto mock_address = std::make_shared(address, 8080); + EXPECT_CALL(*mock_host, address()).WillRepeatedly(Return(mock_address)); + return mock_host; + } + + // Helper to create a mock host with a pipe address (no IP/port). + Upstream::HostConstSharedPtr createMockPipeHost(const std::string& path) { + auto mock_host = std::make_shared>(); + auto status_or_pipe = Network::Address::PipeInstance::create(path); + auto owned = std::move(status_or_pipe.value()); + std::shared_ptr shared_pipe(std::move(owned)); + Network::Address::InstanceConstSharedPtr mock_address = shared_pipe; + EXPECT_CALL(*mock_host, address()).WillRepeatedly(Return(mock_address)); + return mock_host; + } + + // Helper method to set up mock connection with proper socket expectations. + std::unique_ptr> setupMockConnection() { + auto mock_connection = std::make_unique>(); + + // Create a mock socket for the connection. + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle = std::make_unique>(); + + // Set up IO handle expectations. + EXPECT_CALL(*mock_io_handle, resetFileEvents()).WillRepeatedly(Return()); + EXPECT_CALL(*mock_io_handle, isOpen()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_io_handle, duplicate()).WillRepeatedly(Invoke([]() { + auto duplicated_handle = std::make_unique>(); + EXPECT_CALL(*duplicated_handle, isOpen()).WillRepeatedly(Return(true)); + return duplicated_handle; + })); + + // Set up socket expectations. + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(true)); + + // Store the mock_io_handle in the socket before casting. + mock_socket_ptr->io_handle_ = std::move(mock_io_handle); + + // Cast the mock to the base ConnectionSocket type and store it in member variable. + mock_socket_ = std::unique_ptr(mock_socket_ptr.release()); + + // Set up connection expectations for getSocket() + EXPECT_CALL(*mock_connection, getSocket()).WillRepeatedly(ReturnRef(mock_socket_)); + + return mock_connection; + } + + // Helper to access private members for testing. + void addWrapperToHostMap(RCConnectionWrapper* wrapper, const std::string& host_address) { + io_handle_->conn_wrapper_to_host_map_[wrapper] = host_address; + } + + void cleanup() { io_handle_->cleanup(); } + + void removeStaleHostAndCloseConnections(const std::string& host) { + io_handle_->removeStaleHostAndCloseConnections(host); + } + + // Helper to get the established connections queue size (if accessible) + size_t getEstablishedConnectionsSize() const { + return io_handle_->established_connections_.size(); + } +}; + +// Test getClusterManager returns correct reference. +TEST_F(ReverseConnectionIOHandleTest, GetClusterManager) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Verify that getClusterManager returns the correct reference. + EXPECT_EQ(&io_handle_->getClusterManager(), &cluster_manager_); +} + +// Basic setup. +TEST_F(ReverseConnectionIOHandleTest, BasicSetup) { + // Test that constructor doesn't crash and creates a valid instance. + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Verify the IO handle has a valid file descriptor. + EXPECT_GE(io_handle_->fdDoNotUse(), 0); +} + +// listen() is a no-op for the initiator +TEST_F(ReverseConnectionIOHandleTest, ListenNoOp) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Test that listen() returns success (0) with no error. + auto result = io_handle_->listen(10); + EXPECT_EQ(result.return_value_, 0); + EXPECT_EQ(result.errno_, 0); +} + +// Test isTriggerPipeReady() behavior. +TEST_F(ReverseConnectionIOHandleTest, IsTriggerPipeReady) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Initially, trigger pipe should not be ready. + EXPECT_FALSE(isTriggerPipeReady()); + + // Create the trigger pipe. + createTriggerPipe(); + + // Now trigger pipe should be ready. + EXPECT_TRUE(isTriggerPipeReady()); + + // Verify the file descriptors are valid. + EXPECT_GE(getTriggerPipeReadFd(), 0); + EXPECT_GE(getTriggerPipeWriteFd(), 0); +} + +// Test createTriggerPipe() basic pipe creation. +TEST_F(ReverseConnectionIOHandleTest, CreateTriggerPipe) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Initially, trigger pipe should not be ready. + EXPECT_FALSE(isTriggerPipeReady()); + + // Manually call createTriggerPipe. + createTriggerPipe(); + + // Verify that the trigger pipe was created successfully. + EXPECT_TRUE(isTriggerPipeReady()); + EXPECT_GE(getTriggerPipeReadFd(), 0); + EXPECT_GE(getTriggerPipeWriteFd(), 0); + + // Verify getPipeMonitorFd returns the correct file descriptor. + EXPECT_EQ(io_handle_->getPipeMonitorFd(), getTriggerPipeReadFd()); + + // Verify the file descriptors are different. + EXPECT_NE(getTriggerPipeReadFd(), getTriggerPipeWriteFd()); +} + +// Test initializeFileEvent() creates trigger pipe. +TEST_F(ReverseConnectionIOHandleTest, InitializeFileEventCreatesTriggerPipe) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Initially, trigger pipe should not be ready. + EXPECT_FALSE(isTriggerPipeReady()); + + // Mock file event callback. + Event::FileReadyCb mock_callback = [](uint32_t) -> absl::Status { return absl::OkStatus(); }; + + // Call initializeFileEvent - this should create the trigger pipe. + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); + + // Verify that the trigger pipe was created successfully. + EXPECT_TRUE(isTriggerPipeReady()); + EXPECT_GE(getTriggerPipeReadFd(), 0); + EXPECT_GE(getTriggerPipeWriteFd(), 0); + + // Verify getPipeMonitorFd returns the correct file descriptor. + EXPECT_EQ(io_handle_->getPipeMonitorFd(), getTriggerPipeReadFd()); +} + +// Test that subsequent calls to initializeFileEvent do not create new pipes. +TEST_F(ReverseConnectionIOHandleTest, InitializeFileEventDoesNotCreateNewPipes) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Initially, trigger pipe should not be ready. + EXPECT_FALSE(isTriggerPipeReady()); + + // Mock file event callback. + Event::FileReadyCb mock_callback = [](uint32_t) -> absl::Status { return absl::OkStatus(); }; + + // First call to initializeFileEvent - should create the trigger pipe. + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); + + // Verify that the trigger pipe was created. + EXPECT_TRUE(isTriggerPipeReady()); + int first_read_fd = getTriggerPipeReadFd(); + int first_write_fd = getTriggerPipeWriteFd(); + EXPECT_GE(first_read_fd, 0); + EXPECT_GE(first_write_fd, 0); + + // Second call to initializeFileEvent - should NOT create new pipes because. + // is_reverse_conn_started_ is true + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); + + // Verify that the same file descriptors are still used (no new pipes created) + EXPECT_TRUE(isTriggerPipeReady()); + EXPECT_EQ(getTriggerPipeReadFd(), first_read_fd); + EXPECT_EQ(getTriggerPipeWriteFd(), first_write_fd); + + // Verify getPipeMonitorFd still returns the correct file descriptor. + EXPECT_EQ(io_handle_->getPipeMonitorFd(), first_read_fd); +} + +// Test that we do NOT update stats for the cluster if src_node_id is empty. +TEST_F(ReverseConnectionIOHandleTest, EmptySrcNodeIdNoStatsUpdate) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Create config with empty src_node_id. + ReverseConnectionSocketConfig empty_node_config; + empty_node_config.src_cluster_id = "test-cluster"; + empty_node_config.src_node_id = ""; // Empty node ID + empty_node_config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + + io_handle_ = createTestIOHandle(empty_node_config); + EXPECT_NE(io_handle_, nullptr); + + // Call maintainReverseConnections - should return early due to empty src_node_id. + maintainReverseConnections(); + + // Verify that no stats were updated. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map.size(), 0); // No stats should be created +} + +// Test that rev_conn_retry_timer_ gets created and enabled upon calling initializeFileEvent. +TEST_F(ReverseConnectionIOHandleTest, RetryTimerEnabled) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Mock timer expectations. + auto mock_timer = new NiceMock(); + EXPECT_CALL(dispatcher_, createTimer_(_)).WillOnce(Return(mock_timer)); + EXPECT_CALL(*mock_timer, enableTimer(_, _)); + + // Mock file event callback. + Event::FileReadyCb mock_callback = [](uint32_t) -> absl::Status { return absl::OkStatus(); }; + + // Call initializeFileEvent - this should create and enable the retry timer. + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); +} + +// Test that rev_conn_retry_timer_ is properly managed when reverse connection is started. +TEST_F(ReverseConnectionIOHandleTest, RetryTimerWhenReverseConnStarted) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Mock timer expectations. + auto mock_timer = new NiceMock(); + EXPECT_CALL(dispatcher_, createTimer_(_)).WillOnce(Return(mock_timer)); + EXPECT_CALL(*mock_timer, enableTimer(_, _)); + + // Mock file event callback. + Event::FileReadyCb mock_callback = [](uint32_t) -> absl::Status { return absl::OkStatus(); }; + + // Call initializeFileEvent to create the timer. + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); + + // Call initializeFileEvent again to ensure the timer is not created again. + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); +} + +// Test that we do not initiate reverse tunnels when thread local cluster is not present. +TEST_F(ReverseConnectionIOHandleTest, NoThreadLocalClusterCannotConnect) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up cluster manager to return nullptr for non-existent cluster. + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("non-existent-cluster")) + .WillOnce(Return(nullptr)); + + // Call maintainClusterConnections with non-existent cluster. + RemoteClusterConnectionConfig cluster_config("non-existent-cluster", 2); + maintainClusterConnections("non-existent-cluster", cluster_config); + + // Verify that CannotConnect gauge was updated for the cluster. + auto stat_map = extension_->getCrossWorkerStatMap(); + + for (const auto& stat : stat_map) { + std::cout << stat.first << " " << stat.second << std::endl; + } + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.non-existent-cluster.cannot_connect"], + 1); +} + +// Test that we do not initiate reverse tunnels when cluster has no hosts. +TEST_F(ReverseConnectionIOHandleTest, NoHostsInClusterCannotConnect) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster with empty host map. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("empty-cluster")) + .WillOnce(Return(mock_thread_local_cluster.get())); + + // Set up empty priority set. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Set up empty cross priority host map. + auto empty_host_map = std::make_shared(); + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(empty_host_map)); + + // Call maintainClusterConnections with empty cluster. + RemoteClusterConnectionConfig cluster_config("empty-cluster", 2); + maintainClusterConnections("empty-cluster", cluster_config); + + // Verify that CannotConnect gauge was updated for the cluster. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.empty-cluster.cannot_connect"], 1); +} + +// Test maybeUpdateHostsMappingsAndConnections with valid hosts. +TEST_F(ReverseConnectionIOHandleTest, MaybeUpdateHostsMappingsValidHosts) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with some hosts. + auto host_map = std::make_shared(); + auto mock_host1 = createMockHost("192.168.1.1"); + auto mock_host2 = createMockHost("192.168.1.2"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host1); + (*host_map)["192.168.1.2"] = std::const_pointer_cast(mock_host2); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Call maintainClusterConnections which will create HostConnectionInfo entries and call. + // maybeUpdateHostsMappingsAndConnections + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Verify that hosts were added to the mapping. + const auto& host_to_conn_info_map = getHostToConnInfoMap(); + EXPECT_EQ(host_to_conn_info_map.size(), 2); + EXPECT_NE(host_to_conn_info_map.find("192.168.1.1"), host_to_conn_info_map.end()); + EXPECT_NE(host_to_conn_info_map.find("192.168.1.2"), host_to_conn_info_map.end()); +} + +// Test maybeUpdateHostsMappingsAndConnections with no new hosts. +TEST_F(ReverseConnectionIOHandleTest, MaybeUpdateHostsMappingsNoNewHosts) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with multiple hosts. + auto host_map = std::make_shared(); + auto mock_host1 = createMockHost("192.168.1.1"); + auto mock_host2 = createMockHost("192.168.1.2"); + auto mock_host3 = createMockHost("192.168.1.3"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host1); + (*host_map)["192.168.1.2"] = std::const_pointer_cast(mock_host2); + (*host_map)["192.168.1.3"] = std::const_pointer_cast(mock_host3); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Call maintainClusterConnections which will create HostConnectionInfo entries and call. + // maybeUpdateHostsMappingsAndConnections + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Verify that all three host entries exist after maintainClusterConnections. + const auto& host_to_conn_info_map = getHostToConnInfoMap(); + EXPECT_EQ(host_to_conn_info_map.size(), 3); + EXPECT_NE(host_to_conn_info_map.find("192.168.1.1"), host_to_conn_info_map.end()); + EXPECT_NE(host_to_conn_info_map.find("192.168.1.2"), host_to_conn_info_map.end()); + EXPECT_NE(host_to_conn_info_map.find("192.168.1.3"), host_to_conn_info_map.end()); + + // Now test partial host removal by calling maybeUpdateHostsMappingsAndConnections with fewer. + // hosts + std::vector reduced_host_addresses = {"192.168.1.1", "192.168.1.3"}; + maybeUpdateHostsMappingsAndConnections("test-cluster", reduced_host_addresses); + + // Verify that the removed host was cleaned up but others remain. + const auto& updated_host_to_conn_info_map = getHostToConnInfoMap(); + EXPECT_EQ(updated_host_to_conn_info_map.size(), 2); + EXPECT_NE(updated_host_to_conn_info_map.find("192.168.1.1"), updated_host_to_conn_info_map.end()); + EXPECT_EQ(updated_host_to_conn_info_map.find("192.168.1.2"), + updated_host_to_conn_info_map.end()); // Should be removed + EXPECT_NE(updated_host_to_conn_info_map.find("192.168.1.3"), updated_host_to_conn_info_map.end()); +} + +// Test shouldAttemptConnectionToHost with valid host and no existing connections. +TEST_F(ReverseConnectionIOHandleTest, ShouldAttemptConnectionToHostValidHost) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Call maintainClusterConnections to create HostConnectionInfo entries. + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Test with valid host and no existing connections. + bool should_attempt = shouldAttemptConnectionToHost("192.168.1.1", "test-cluster"); + EXPECT_TRUE(should_attempt); + + // Test circuit breaker disabled scenario - should always return true regardless of backoff state. + // First, put the host in backoff by tracking a failure. + trackConnectionFailure("192.168.1.1", "test-cluster"); + + // Verify host is in backoff with circuit breaker enabled (default). + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.1.1", "test-cluster")); + + // Now create a new IO handle with circuit breaker disabled. + auto config_disabled = createDefaultTestConfig(); + config_disabled.enable_circuit_breaker = false; + auto io_handle_disabled = createTestIOHandle(config_disabled); + EXPECT_NE(io_handle_disabled, nullptr); + + // Set up the same thread local cluster for the new IO handle. + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Call maintainClusterConnections to create HostConnectionInfo entries in the new IO handle. + maintainClusterConnections("test-cluster", cluster_config); + + // Put the host in backoff in the new IO handle. + io_handle_disabled->trackConnectionFailure("192.168.1.1", "test-cluster"); + + // With circuit breaker disabled, shouldAttemptConnectionToHost should always return true. + EXPECT_TRUE(io_handle_disabled->shouldAttemptConnectionToHost("192.168.1.1", "test-cluster")); +} + +// Test trackConnectionFailure puts host in backoff. +TEST_F(ReverseConnectionIOHandleTest, TrackConnectionFailurePutsHostInBackoff) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // First call maintainClusterConnections to create HostConnectionInfo entries. + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Verify host is initially not in backoff. + bool should_attempt_before = shouldAttemptConnectionToHost("192.168.1.1", "test-cluster"); + EXPECT_TRUE(should_attempt_before); + + // Call trackConnectionFailure to put host in backoff. + trackConnectionFailure("192.168.1.1", "test-cluster"); + + // Verify host is now in backoff. + bool should_attempt_after = shouldAttemptConnectionToHost("192.168.1.1", "test-cluster"); + EXPECT_FALSE(should_attempt_after); + + // Verify stat gauges - should show backoff state. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.backoff"], 1); + + // Test that trackConnectionFailure returns if host_to_conn_info_map_ does not have an entry. + // Call trackConnectionFailure with a host that doesn't exist in host_to_conn_info_map_ + trackConnectionFailure("non-existent-host", "test-cluster"); + + // Verify that no stats were updated since the host doesn't exist. + auto stat_map_after_non_existent = extension_->getCrossWorkerStatMap(); + EXPECT_EQ( + stat_map_after_non_existent["test_scope.reverse_connections.host.non-existent-host.backoff"], + 0); + + // Test that maintainClusterConnections skips hosts in backoff. + // Call maintainClusterConnections again - should skip the host in backoff. + // and not attempt any new connections + maintainClusterConnections("test-cluster", cluster_config); + + // Verify that the host is still in backoff state. + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.1.1", "test-cluster")); +} + +// Test resetHostBackoff resets the backoff. +TEST_F(ReverseConnectionIOHandleTest, ResetHostBackoff) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // First call maintainClusterConnections to create HostConnectionInfo entries. + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Verify host is initially not in backoff. + bool should_attempt_before = shouldAttemptConnectionToHost("192.168.1.1", "test-cluster"); + EXPECT_TRUE(should_attempt_before); + + // Call trackConnectionFailure to put host in backoff. + trackConnectionFailure("192.168.1.1", "test-cluster"); + + // Verify host is now in backoff. + bool should_attempt_after_failure = shouldAttemptConnectionToHost("192.168.1.1", "test-cluster"); + EXPECT_FALSE(should_attempt_after_failure); + + // Verify stat gauges - should show backoff state. + auto stat_map_after_failure = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map_after_failure["test_scope.reverse_connections.host.192.168.1.1.backoff"], 1); + + // Call resetHostBackoff to reset the backoff. + resetHostBackoff("192.168.1.1"); + + // Verify host is no longer in backoff. + bool should_attempt_after_reset = shouldAttemptConnectionToHost("192.168.1.1", "test-cluster"); + EXPECT_TRUE(should_attempt_after_reset); + + // Verify stat gauges - should show recovered state. + auto stat_map_after_reset = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map_after_reset["test_scope.reverse_connections.host.192.168.1.1.backoff"], 0); + EXPECT_EQ(stat_map_after_reset["test_scope.reverse_connections.host.192.168.1.1.recovered"], 1); +} + +// Test resetHostBackoff returns if host_to_conn_info_map_ does not have an entry. +TEST_F(ReverseConnectionIOHandleTest, ResetHostBackoffReturnsIfHostNotFound) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Call resetHostBackoff with a host that doesn't exist in host_to_conn_info_map_ + // This should not crash and should return early. + resetHostBackoff("non-existent-host"); + + // Verify that no stats were updated since the host doesn't exist. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.non-existent-host.recovered"], 0); +} + +// Test trackConnectionFailure exponential backoff. +TEST_F(ReverseConnectionIOHandleTest, TrackConnectionFailureExponentialBackoff) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // First call maintainClusterConnections to create HostConnectionInfo entries. + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Get initial host info. + const auto& host_info_initial = getHostConnectionInfo("192.168.1.1"); + EXPECT_EQ(host_info_initial.failure_count, 0); + + // First failure - should have 1 second backoff (1000ms) + trackConnectionFailure("192.168.1.1", "test-cluster"); + const auto& host_info_1 = getHostConnectionInfo("192.168.1.1"); + EXPECT_EQ(host_info_1.failure_count, 1); + // Verify backoff_until is set to a future time (approximately current_time + 1000ms) + auto backoff_duration_1 = + host_info_1.backoff_until - std::chrono::steady_clock::now(); // NO_CHECK_FORMAT(real_time) + // backoff_delay_ms = 1000 * 2^(1-1) = 1000 * 2^0 = 1000 * 1 = 1000ms + auto backoff_ms_1 = + std::chrono::duration_cast(backoff_duration_1).count(); + EXPECT_GE(backoff_ms_1, 900); // Should be at least 900ms (allowing for small timing variations) + EXPECT_LE(backoff_ms_1, 1100); // Should be at most 1100ms + + // Second failure - should have 2 second backoff (2000ms) + trackConnectionFailure("192.168.1.1", "test-cluster"); + const auto& host_info_2 = getHostConnectionInfo("192.168.1.1"); + EXPECT_EQ(host_info_2.failure_count, 2); + // backoff_delay_ms = 1000 * 2^(2-1) = 1000 * 2^1 = 1000 * 2 = 2000ms + auto backoff_duration_2 = + host_info_2.backoff_until - std::chrono::steady_clock::now(); // NO_CHECK_FORMAT(real_time) + auto backoff_ms_2 = + std::chrono::duration_cast(backoff_duration_2).count(); + EXPECT_GE(backoff_ms_2, 1900); // Should be at least 1900ms + EXPECT_LE(backoff_ms_2, 2100); // Should be at most 2100ms + + // Third failure - should have 4 second backoff (4000ms) + trackConnectionFailure("192.168.1.1", "test-cluster"); + const auto& host_info_3 = getHostConnectionInfo("192.168.1.1"); + EXPECT_EQ(host_info_3.failure_count, 3); + // backoff_delay_ms = 1000 * 2^(3-1) = 1000 * 2^2 = 1000 * 4 = 4000ms + auto backoff_duration_3 = + host_info_3.backoff_until - std::chrono::steady_clock::now(); // NO_CHECK_FORMAT(real_time) + auto backoff_ms_3 = + std::chrono::duration_cast(backoff_duration_3).count(); + EXPECT_GE(backoff_ms_3, 3900); // Should be at least 3900ms + EXPECT_LE(backoff_ms_3, 4100); // Should be at most 4100ms + + // Verify that shouldAttemptConnectionToHost returns false during backoff. + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.1.1", "test-cluster")); +} + +// Test host mapping and backoff integration. +TEST_F(ReverseConnectionIOHandleTest, HostMappingAndBackoffIntegration) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster for cluster-A. + auto mock_thread_local_cluster_a = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("cluster-A")) + .WillRepeatedly(Return(mock_thread_local_cluster_a.get())); + + // Set up priority set with hosts for cluster-A. + auto mock_priority_set_a = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster_a, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set_a)); + + // Create host map for cluster-A with hosts A1, A2, A3. + auto host_map_a = std::make_shared(); + auto mock_host_a1 = createMockHost("192.168.1.1"); + auto mock_host_a2 = createMockHost("192.168.1.2"); + auto mock_host_a3 = createMockHost("192.168.1.3"); + (*host_map_a)["192.168.1.1"] = std::const_pointer_cast(mock_host_a1); + (*host_map_a)["192.168.1.2"] = std::const_pointer_cast(mock_host_a2); + (*host_map_a)["192.168.1.3"] = std::const_pointer_cast(mock_host_a3); + + EXPECT_CALL(*mock_priority_set_a, crossPriorityHostMap()).WillRepeatedly(Return(host_map_a)); + + // Set up mock thread local cluster for cluster-B. + auto mock_thread_local_cluster_b = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("cluster-B")) + .WillRepeatedly(Return(mock_thread_local_cluster_b.get())); + + // Set up priority set with hosts for cluster-B. + auto mock_priority_set_b = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster_b, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set_b)); + + // Create host map for cluster-B with hosts B1, B2. + auto host_map_b = std::make_shared(); + auto mock_host_b1 = createMockHost("192.168.2.1"); + auto mock_host_b2 = createMockHost("192.168.2.2"); + (*host_map_b)["192.168.2.1"] = std::const_pointer_cast(mock_host_b1); + (*host_map_b)["192.168.2.2"] = std::const_pointer_cast(mock_host_b2); + + EXPECT_CALL(*mock_priority_set_b, crossPriorityHostMap()).WillRepeatedly(Return(host_map_b)); + + // Step 1: Create initial host mappings for cluster-A. + RemoteClusterConnectionConfig cluster_config_a("cluster-A", 2); + maintainClusterConnections("cluster-A", cluster_config_a); + + // Step 2: Create initial host mappings for cluster-B. + RemoteClusterConnectionConfig cluster_config_b("cluster-B", 2); + maintainClusterConnections("cluster-B", cluster_config_b); + + // Verify all hosts exist initially. + const auto& host_to_conn_info_map_initial = getHostToConnInfoMap(); + EXPECT_EQ(host_to_conn_info_map_initial.size(), + 5); // 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.2.1, 192.168.2.2 + EXPECT_NE(host_to_conn_info_map_initial.find("192.168.1.1"), host_to_conn_info_map_initial.end()); + EXPECT_NE(host_to_conn_info_map_initial.find("192.168.1.2"), host_to_conn_info_map_initial.end()); + EXPECT_NE(host_to_conn_info_map_initial.find("192.168.1.3"), host_to_conn_info_map_initial.end()); + EXPECT_NE(host_to_conn_info_map_initial.find("192.168.2.1"), host_to_conn_info_map_initial.end()); + EXPECT_NE(host_to_conn_info_map_initial.find("192.168.2.2"), host_to_conn_info_map_initial.end()); + + // Step 3: Put some hosts in backoff. + trackConnectionFailure("192.168.1.1", "cluster-A"); // 192.168.1.1 in backoff + trackConnectionFailure("192.168.2.1", "cluster-B"); // 192.168.2.1 in backoff + // 192.168.1.2, 192.168.1.3, 192.168.2.2 remain normal + + // Verify backoff states. + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.1.1", "cluster-A")); // In backoff + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.2.1", "cluster-B")); // In backoff + EXPECT_TRUE(shouldAttemptConnectionToHost("192.168.1.2", "cluster-A")); // Normal + EXPECT_TRUE(shouldAttemptConnectionToHost("192.168.1.3", "cluster-A")); // Normal + EXPECT_TRUE(shouldAttemptConnectionToHost("192.168.2.2", "cluster-B")); // Normal + + // Step 4: Update host mappings. + // - Move 192.168.1.2 from cluster-A to cluster-B + // - Remove 192.168.1.3 from cluster-A + // - Add new host 192.168.1.4 to cluster-A + maybeUpdateHostsMappingsAndConnections( + "cluster-A", {"192.168.1.1", "192.168.1.4"}); // 192.168.1.2, 192.168.1.3 removed + maybeUpdateHostsMappingsAndConnections( + "cluster-B", {"192.168.2.1", "192.168.2.2", "192.168.1.2"}); // 192.168.1.2 added + + // Step 5: Verify backoff states are preserved for existing hosts. + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.1.1", "cluster-A")); // Still in backoff + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.2.1", "cluster-B")); // Still in backoff + + // Step 6: Verify moved host has clean state. + EXPECT_TRUE(shouldAttemptConnectionToHost("192.168.1.2", "cluster-B")); // Moved, no backoff + + // Step 7: Verify removed host is cleaned up. + const auto& host_to_conn_info_map_after = getHostToConnInfoMap(); + EXPECT_EQ(host_to_conn_info_map_after.find("192.168.1.3"), + host_to_conn_info_map_after.end()); // Removed + + // Step 8: Verify stats are updated correctly. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.backoff"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.2.1.backoff"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.2.backoff"], + 0); // Reset when moved +} + +// Test initiateOneReverseConnection when connection establishment fails. +TEST_F(ReverseConnectionIOHandleTest, InitiateOneReverseConnectionFailure) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // First call maintainClusterConnections to create HostConnectionInfo entries. + RemoteClusterConnectionConfig cluster_config("test-cluster", 2); + maintainClusterConnections("test-cluster", cluster_config); + + // Mock tcpConn to return null connection (simulating connection failure) + Upstream::MockHost::MockCreateConnectionData failed_conn_data; + failed_conn_data.connection_ = nullptr; // Connection creation failed + failed_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(failed_conn_data)); + + // Call initiateOneReverseConnection - should fail. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_FALSE(result); + + // Verify that CannotConnect stats are set. + // Calculation: 3 increments total. + // - 2 increments from maintainClusterConnections (target_connection_count = 2) + // - 1 increment from our direct call to initiateOneReverseConnection + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.cannot_connect"], 3); +} + +// Test initiateOneReverseConnection when connection establishment is successful. +TEST_F(ReverseConnectionIOHandleTest, InitiateOneReverseConnectionSuccess) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry using helper method. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Set up mock for successful connection. + auto mock_connection = std::make_unique>(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection - should succeed. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify that Connecting stats are set. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], 1); + + // Verify that connection wrapper is added to the map. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); +} + +// Test that reverse connection initiation works with custom stat scope. +TEST_F(ReverseConnectionIOHandleTest, InitiateReverseConnectionWithCustomScope) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Create config with custom stat prefix. + ReverseConnectionSocketConfig custom_prefix_config; + custom_prefix_config.src_cluster_id = "test-cluster"; + custom_prefix_config.src_node_id = "test-node"; + custom_prefix_config.remote_clusters.push_back(RemoteClusterConnectionConfig("test-cluster", 1)); + + // Create a new extension with custom stat prefix. + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface custom_config; + custom_config.set_stat_prefix("custom_stats"); + + auto custom_extension = + std::make_unique(context_, custom_config); + custom_extension->setTestOnlyTLSRegistry(std::move(tls_slot_)); + + // Replace the class member io_handle_ with our custom one for this test + auto original_io_handle = std::move(io_handle_); + io_handle_ = std::make_unique(8, // dummy fd + custom_prefix_config, cluster_manager_, + custom_extension.get(), *stats_scope_); + + // Initialize the file event to set up worker_dispatcher_ properly. + Event::FileReadyCb mock_callback = [](uint32_t) { return absl::OkStatus(); }; + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry using helper method. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Set up mock for successful connection. + auto mock_connection = std::make_unique>(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection using the helper method - should succeed. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify that Connecting stats are set with custom stat prefix. + auto stat_map = custom_extension->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.custom_stats.host.192.168.1.1.connecting"], 1); + + // Restore the original io_handle_ + io_handle_ = std::move(original_io_handle); +} + +// Test maintainClusterConnections skips hosts that already have enough connections. +TEST_F(ReverseConnectionIOHandleTest, MaintainClusterConnectionsSkipsHostsWithEnoughConnections) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // First call maintainClusterConnections to create HostConnectionInfo entries. + RemoteClusterConnectionConfig cluster_config("test-cluster", 1); // Only need 1 connection + maintainClusterConnections("test-cluster", cluster_config); + + // Manually add a connection key to simulate having enough connections. + const auto& host_info = getHostConnectionInfo("192.168.1.1"); + EXPECT_EQ(host_info.connection_keys.size(), 0); // Initially no connections + + // Manually add a connection key to the host info to simulate having enough connections. + auto& mutable_host_info = getMutableHostConnectionInfo("192.168.1.1"); + mutable_host_info.connection_keys.insert("fake-connection-key"); + + // Verify we now have enough connections. + EXPECT_EQ(getHostConnectionInfo("192.168.1.1").connection_keys.size(), 1); + + // Call maintainClusterConnections again - should skip the host since it has enough connections. + maintainClusterConnections("test-cluster", cluster_config); + + // Verify that no additional connection attempts were made. + // The host should still have exactly 1 connection. + EXPECT_EQ(getHostConnectionInfo("192.168.1.1").connection_keys.size(), 1); +} + +// Test initiateOneReverseConnection with empty host address. +TEST_F(ReverseConnectionIOHandleTest, InitiateOneReverseConnectionEmptyHostAddress) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Call initiateOneReverseConnection with empty host address - should fail. + bool result = initiateOneReverseConnection("test-cluster", "", nullptr); + EXPECT_FALSE(result); + + // When host address is empty, only cluster stats are updated. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.cannot_connect"], 1); +} + +// Test initiateOneReverseConnection with non-existent cluster. +TEST_F(ReverseConnectionIOHandleTest, InitiateOneReverseConnectionNonExistentCluster) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up cluster manager to return nullptr for non-existent cluster. + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("non-existent-cluster")) + .WillOnce(Return(nullptr)); + + // Call initiateOneReverseConnection with non-existent cluster - should fail. + bool result = initiateOneReverseConnection("non-existent-cluster", "192.168.1.1", nullptr); + EXPECT_FALSE(result); + + // When cluster is not found, both host and cluster stats are updated. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.cannot_connect"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.non-existent-cluster.cannot_connect"], + 1); + + // No wrapper should be created since the cluster doesn't exist. + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 0); +} + +// Test mixed success and failure scenarios for multiple connection attempts. +TEST_F(ReverseConnectionIOHandleTest, InitiateMultipleConnectionsMixedResults) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with hosts. + auto host_map = std::make_shared(); + auto mock_host1 = createMockHost("192.168.1.1"); + auto mock_host2 = createMockHost("192.168.1.2"); + auto mock_host3 = createMockHost("192.168.1.3"); + + // MockHostDescription already has a cluster_ member that's returned by cluster(). + // We don't need to set up expectations for it. + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host1); + (*host_map)["192.168.1.2"] = std::const_pointer_cast(mock_host2); + (*host_map)["192.168.1.3"] = std::const_pointer_cast(mock_host3); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entries for all hosts with target count of 3. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); // Host 1 + addHostConnectionInfo("192.168.1.2", "test-cluster", 1); // Host 2 + addHostConnectionInfo("192.168.1.3", "test-cluster", 1); // Host 3 + + // Set up connection outcomes in sequence: + // 1. First host: successful connection + // 2. Second host: null connection (failure) + // 3. Third host: successful connection + + // Prepare mock connections that will be transferred to the wrappers. + auto mock_connection1 = std::make_unique>(); + auto mock_connection3 = std::make_unique>(); + + // Set up connection info for the connections. + auto local_address = std::make_shared("10.0.0.2", 40000); + auto remote_address1 = std::make_shared("192.168.1.1", 8080); + auto remote_address3 = std::make_shared("192.168.1.3", 8080); + + // Set up local/remote addresses for connections using the stream_info_. + mock_connection1->stream_info_.downstream_connection_info_provider_->setLocalAddress( + local_address); + mock_connection1->stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address1); + + mock_connection3->stream_info_.downstream_connection_info_provider_->setLocalAddress( + local_address); + mock_connection3->stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address3); + + // Set up expectations on the mock connections before they're moved. + // Set up connection expectations for mock_connection1. + EXPECT_CALL(*mock_connection1, id()).WillRepeatedly(Return(1)); + EXPECT_CALL(*mock_connection1, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + EXPECT_CALL(*mock_connection1, addConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection1, connect()); + EXPECT_CALL(*mock_connection1, addReadFilter(_)); + EXPECT_CALL(*mock_connection1, write(_, _)) + .Times(1) + .WillOnce(Invoke([](Buffer::Instance& buffer, bool) -> void { + // Drain the buffer to simulate actual write. + buffer.drain(buffer.length()); + })); + // Expect calls during shutdown. + EXPECT_CALL(*mock_connection1, removeConnectionCallbacks(_)).Times(testing::AtMost(1)); + EXPECT_CALL(*mock_connection1, close(_)).Times(testing::AtMost(1)); + + // Set up connection expectations for mock_connection3. + EXPECT_CALL(*mock_connection3, id()).WillRepeatedly(Return(3)); + EXPECT_CALL(*mock_connection3, state()).WillRepeatedly(Return(Network::Connection::State::Open)); + EXPECT_CALL(*mock_connection3, addConnectionCallbacks(_)); + EXPECT_CALL(*mock_connection3, connect()); + EXPECT_CALL(*mock_connection3, addReadFilter(_)); + EXPECT_CALL(*mock_connection3, write(_, _)) + .Times(1) + .WillOnce(Invoke([](Buffer::Instance& buffer, bool) -> void { + // Drain the buffer to simulate actual write. + buffer.drain(buffer.length()); + })); + // Expect calls during shutdown. + EXPECT_CALL(*mock_connection3, removeConnectionCallbacks(_)).Times(testing::AtMost(1)); + EXPECT_CALL(*mock_connection3, close(_)).Times(testing::AtMost(1)); + + // Set up connection attempts with host-specific expectations. + // We need to transfer ownership of the connections properly. + // The lambda will be called multiple times, so we use a counter to track which call we're on. + int call_count = 0; + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)) + .WillRepeatedly( + testing::Invoke([&call_count, &mock_connection1, &mock_connection3, mock_host1, + mock_host2, mock_host3](Upstream::LoadBalancerContext* context) + -> Upstream::MockHost::MockCreateConnectionData { + auto* reverse_context = + dynamic_cast(context); + EXPECT_NE(reverse_context, nullptr); + + auto override_host = reverse_context->overrideHostToSelect(); + EXPECT_TRUE(override_host.has_value()); + + std::string host_address = std::string(override_host->first); + + Upstream::MockHost::MockCreateConnectionData result; + if (host_address == "192.168.1.1") { + // First host: success - transfer ownership of mock_connection1 + // Transfer ownership only on the first call for this host. + if (mock_connection1) { + result.connection_ = mock_connection1.release(); + } else { + result.connection_ = nullptr; // Already used. + } + result.host_description_ = mock_host1; + } else if (host_address == "192.168.1.2") { + // Second host: failure - no connection + result.connection_ = nullptr; + result.host_description_ = mock_host2; + } else if (host_address == "192.168.1.3") { + // Third host: success - transfer ownership of mock_connection3 + // Transfer ownership only on the first call for this host. + if (mock_connection3) { + result.connection_ = mock_connection3.release(); + } else { + result.connection_ = nullptr; // Already used. + } + result.host_description_ = mock_host3; + } else { + // Unexpected host. + EXPECT_TRUE(false) << "Unexpected host address: " << host_address; + result.connection_ = nullptr; + result.host_description_ = mock_host2; + } + call_count++; + return result; + })); + + // Create 1 connection per host. + RemoteClusterConnectionConfig cluster_config("test-cluster", 1); + + // Call maintainClusterConnections which will attempt connections to all hosts. + maintainClusterConnections("test-cluster", cluster_config); + + // Verify final stats. + auto stat_map = extension_->getCrossWorkerStatMap(); + + // Verify connecting stats for successful connections. + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], 1); // Success + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.3.connecting"], 1); // Success + + // Verify cannot_connect stats for failed connection. + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.2.cannot_connect"], + 1); // Failed + + // Verify cluster-level stats for test-cluster. + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connecting"], + 2); // 2 successful connections + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.cannot_connect"], + 1); // 1 failed connection + + // Verify that only 2 connection wrappers were created (for successful connections) + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 2); + + // Verify that wrappers are mapped to successful hosts only. + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 2); + + // Count hosts in the mapping. + std::set mapped_hosts; + for (const auto& [wrapper, host] : wrapper_to_host_map) { + mapped_hosts.insert(host); + } + EXPECT_EQ(mapped_hosts.size(), 2); // Should have 2 successful hosts + EXPECT_NE(mapped_hosts.find("192.168.1.1"), mapped_hosts.end()); // Success + EXPECT_EQ(mapped_hosts.find("192.168.1.2"), mapped_hosts.end()); // Failed - not in map + EXPECT_NE(mapped_hosts.find("192.168.1.3"), mapped_hosts.end()); // Success +} + +// Test removeStaleHostAndCloseConnections removes host and closes connections. +TEST_F(ReverseConnectionIOHandleTest, RemoveStaleHostAndCloseConnections) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with multiple hosts. + auto host_map = std::make_shared(); + auto mock_host1 = createMockHost("192.168.1.1"); + auto mock_host2 = createMockHost("192.168.1.2"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host1); + (*host_map)["192.168.1.2"] = std::const_pointer_cast(mock_host2); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Set up successful connections for both hosts. + auto mock_connection1 = std::make_unique>(); + Upstream::MockHost::MockCreateConnectionData success_conn_data1; + success_conn_data1.connection_ = mock_connection1.get(); + success_conn_data1.host_description_ = mock_host1; + + auto mock_connection2 = std::make_unique>(); + Upstream::MockHost::MockCreateConnectionData success_conn_data2; + success_conn_data2.connection_ = mock_connection2.get(); + success_conn_data2.host_description_ = mock_host2; + + // Set up connection attempts with host-specific expectations. + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)) + .WillRepeatedly(testing::Invoke([&](Upstream::LoadBalancerContext* context) { + // Cast to our custom context to get the host address. + auto* reverse_context = + dynamic_cast(context); + EXPECT_NE(reverse_context, nullptr); + + auto override_host = reverse_context->overrideHostToSelect(); + EXPECT_TRUE(override_host.has_value()); + + std::string host_address = std::string(override_host->first); + + if (host_address == "192.168.1.1") { + return success_conn_data1; // First host: success + } else if (host_address == "192.168.1.2") { + return success_conn_data2; // Second host: success + } else { + // Unexpected host. + EXPECT_TRUE(false) << "Unexpected host address: " << host_address; + return success_conn_data1; // Default fallback + } + })); + + mock_connection1.release(); + mock_connection2.release(); + + // First call maintainClusterConnections to create HostConnectionInfo entries and connection. + // wrappers + RemoteClusterConnectionConfig cluster_config("test-cluster", 1); + maintainClusterConnections("test-cluster", cluster_config); + + // Verify both hosts are initially present. + EXPECT_EQ(getHostToConnInfoMap().size(), 2); + EXPECT_NE(getHostToConnInfoMap().find("192.168.1.1"), getHostToConnInfoMap().end()); + EXPECT_NE(getHostToConnInfoMap().find("192.168.1.2"), getHostToConnInfoMap().end()); + + // Verify that connection wrappers were created by maintainClusterConnections. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 2); // One wrapper per host + EXPECT_EQ(getConnWrapperToHostMap().size(), 2); + + // Call removeStaleHostAndCloseConnections to remove host 192.168.1.1 + removeStaleHostAndCloseConnections("192.168.1.1"); + + // Verify that host 192.168.1.1 is still in host_to_conn_info_map_ + // (removeStaleHostAndCloseConnections doesn't remove it) + EXPECT_EQ(getHostToConnInfoMap().size(), 2); + EXPECT_NE(getHostToConnInfoMap().find("192.168.1.1"), getHostToConnInfoMap().end()); + EXPECT_NE(getHostToConnInfoMap().find("192.168.1.2"), getHostToConnInfoMap().end()); + + // Verify that connection wrappers for the removed host are removed. + EXPECT_EQ(getConnectionWrappers().size(), 1); // Only host 192.168.1.2's wrapper remains + EXPECT_EQ(getConnWrapperToHostMap().size(), 1); // Only host 192.168.1.2's mapping remains + + // Verify that host 192.168.1.2's wrapper is still present and unaffected + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + EXPECT_EQ(wrapper_to_host_map.begin()->second, "192.168.1.2"); // Only 192.168.1.2 should remain +} + +// Test read() method - should delegate to base class. +TEST_F(ReverseConnectionIOHandleTest, ReadMethod) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create a buffer to read into. + Buffer::OwnedImpl buffer; + + // Call read() - should delegate to base class implementation. + auto result = io_handle_->read(buffer, absl::optional(100)); + + // Should return a valid result. + EXPECT_NE(result.err_, nullptr); +} + +// Test write() method - should delegate to base class. +TEST_F(ReverseConnectionIOHandleTest, WriteMethod) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create a buffer to write from. + Buffer::OwnedImpl buffer; + buffer.add("test data"); + + // Call write() - should delegate to base class implementation. + auto result = io_handle_->write(buffer); + + // Should return a valid result. + EXPECT_NE(result.err_, nullptr); +} + +// Test connect() method - should delegate to base class. +TEST_F(ReverseConnectionIOHandleTest, ConnectMethod) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create a mock address. + auto address = std::make_shared("127.0.0.1", 8080); + + // Call connect() - should delegate to base class implementation. + auto result = io_handle_->connect(address); + + // Should return a valid result. + EXPECT_NE(result.errno_, 0); // Should fail since we're not actually connecting +} + +// Test onEvent() method - should delegate to base class. +TEST_F(ReverseConnectionIOHandleTest, OnEventMethod) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Call onEvent() with a mock event - no-op. + io_handle_->onEvent(Network::ConnectionEvent::LocalClose); +} + +// Test RCConnectionWrapper::onEvent with null connection. +TEST_F(ReverseConnectionIOHandleTest, RCConnectionWrapperOnEventWithNullConnection) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + auto mock_host = std::make_shared>(); + + // Create RCConnectionWrapper with the mock connection. + RCConnectionWrapper wrapper(*io_handle_, std::move(mock_connection), mock_host, "test-cluster"); + + // Release the connection to make it null. + wrapper.releaseConnection(); + + // Call onEvent with RemoteClose event - should handle null connection gracefully. + wrapper.onEvent(Network::ConnectionEvent::RemoteClose); +} + +// onConnectionDone Unit Tests + +// Early returns in onConnectionDone without calling initiateOneReverseConnection. +TEST_F(ReverseConnectionIOHandleTest, OnConnectionDoneEarlyReturns) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Test 1.1: Null wrapper - should return early + io_handle_->onConnectionDone("test error", nullptr, false); + + // Verify no stats were updated. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map.size(), 0); + + // Test 1.2: Empty conn_wrapper_to_host_map_ - should return early + // Create a dummy wrapper pointer (we can't easily mock RCConnectionWrapper directly) + RCConnectionWrapper* wrapper_ptr = reinterpret_cast(0x12345678); + + // Verify the map is empty. + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 0); + + io_handle_->onConnectionDone("test error", wrapper_ptr, false); + + // Verify no stats were updated. + stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map.size(), 0); + + // Test 1.3: Empty host_to_conn_info_map_ - should return early after finding wrapper + // First add wrapper to the map but no host info. + addWrapperToHostMap(wrapper_ptr, "192.168.1.1"); + + // Verify host info map is empty. + const auto& host_to_conn_info_map = getHostToConnInfoMap(); + EXPECT_EQ(host_to_conn_info_map.size(), 0); + + io_handle_->onConnectionDone("test error", wrapper_ptr, false); + + // Verify wrapper was removed from map but no stats updated. + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map.size(), 0); +} + +// Connection success scenario - test stats and wrapper creation and mapping. +TEST_F(ReverseConnectionIOHandleTest, OnConnectionDoneSuccess) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create trigger pipe BEFORE initiating connection to ensure it's ready. + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Create a successful connection. + auto mock_connection = setupMockConnection(); + + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Verify initial state - no established connections yet. + EXPECT_EQ(getEstablishedConnectionsSize(), 0); + + // Call onConnectionDone to simulate successful connection completion. + io_handle_->onConnectionDone("reverse connection accepted", wrapper_ptr, false); + + // Verify wrapper was removed from tracking (cleanup should happen) + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + EXPECT_EQ(getConnectionWrappers().size(), 0); + + // Verify that connection was pushed to established_connections_ + EXPECT_EQ(getEstablishedConnectionsSize(), 1); + + // Verify that trigger mechanism was executed. + // Read 1 byte from the pipe to verify the trigger was written. + char trigger_byte; + int pipe_read_fd = getTriggerPipeReadFd(); + EXPECT_GE(pipe_read_fd, 0); + + ssize_t bytes_read = ::read(pipe_read_fd, &trigger_byte, 1); + EXPECT_EQ(bytes_read, 1) << "Expected to read 1 byte from trigger pipe, got " << bytes_read; + EXPECT_EQ(trigger_byte, 1) << "Expected trigger byte to be 1, got " + << static_cast(trigger_byte); +} + +// Success path where trigger write fails: still enqueues connection and cleans up wrapper. +TEST_F(ReverseConnectionIOHandleTest, OnConnectionDoneSuccessTriggerWriteFailure) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Prepare trigger pipe, then close write end so ::write fails. + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + ::close(getTriggerPipeWriteFd()); + + // Mock cluster and single host. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + mock_connection.release(); + + // Create wrapper via initiation, then complete as success. + EXPECT_TRUE(initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host)); + RCConnectionWrapper* wrapper_ptr = getConnectionWrappers()[0].get(); + io_handle_->onConnectionDone("reverse connection accepted", wrapper_ptr, false); + + // Even though trigger write failed, connection should be queued for accept. + EXPECT_EQ(getEstablishedConnectionsSize(), 1); +} + +// Internal address with zero hosts should early fail and update CannotConnect state. +TEST_F(ReverseConnectionIOHandleTest, InitiateOneReverseConnectionInternalAddressNoHosts) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Provide non-null info and an empty host set to yield host_count == 0. + auto mock_cluster_info = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, info()).WillRepeatedly(Return(mock_cluster_info)); + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + std::vector host_sets; // empty + EXPECT_CALL(*mock_priority_set, hostSetsPerPriority()).WillRepeatedly(ReturnRef(host_sets)); + + auto mock_host = createMockPipeHost("/tmp/rev.sock"); + bool ok = initiateOneReverseConnection("test-cluster", "envoy://internal", mock_host); + EXPECT_FALSE(ok); +} + +// Pipe address host exercises the log branch that prints address without a port. +TEST_F(ReverseConnectionIOHandleTest, InitiateOneReverseConnectionLogsWithoutPort) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + auto host_map = std::make_shared(); + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + auto mock_host = createMockPipeHost("/tmp/rev.sock"); + auto mock_connection = setupMockConnection(); + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + mock_connection.release(); + + bool created = initiateOneReverseConnection("test-cluster", "10.0.0.9", mock_host); + EXPECT_TRUE(created); +} + +// Test 3: Connection failure and recovery scenario. +TEST_F(ReverseConnectionIOHandleTest, OnConnectionDoneFailureAndRecovery) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Step 1: Create initial connection. + auto mock_connection1 = setupMockConnection(); + + Upstream::MockHost::MockCreateConnectionData success_conn_data1; + success_conn_data1.connection_ = mock_connection1.get(); + success_conn_data1.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data1)); + + mock_connection1.release(); + + // Call initiateOneReverseConnection to create the wrapper. + bool result1 = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result1); + + // Get the wrapper. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Verify host and cluster stats after connection initiation. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connecting"], 1); + + // Step 2: Simulate connection failure by calling onConnectionDone with error. + io_handle_->onConnectionDone("connection timeout", wrapper_ptr, true); + + // Verify wrapper was removed from tracking maps after failure. + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + EXPECT_EQ(getConnectionWrappers().size(), 0); + + // Verify failure stats - onConnectionDone should have called trackConnectionFailure. + stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.failed"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.backoff"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], + 0); // Should be decremented + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.failed"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.backoff"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connecting"], + 0); // Should be decremented + + // Verify host is now in backoff. + EXPECT_FALSE(shouldAttemptConnectionToHost("192.168.1.1", "test-cluster")); + + // Step 3: Create a new connection for recovery. + auto mock_connection2 = setupMockConnection(); + + Upstream::MockHost::MockCreateConnectionData success_conn_data2; + success_conn_data2.connection_ = mock_connection2.get(); + success_conn_data2.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data2)); + + mock_connection2.release(); + + // Call initiateOneReverseConnection again for recovery. + bool result2 = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result2); + + // Verify new wrapper was created and mapped. + const auto& connection_wrappers2 = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers2.size(), 1); + + const auto& wrapper_to_host_map2 = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map2.size(), 1); + + RCConnectionWrapper* wrapper_ptr2 = connection_wrappers2[0].get(); + EXPECT_EQ(wrapper_to_host_map2.at(wrapper_ptr2), "192.168.1.1"); + + // Verify stats after recovery connection initiation. + stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], + 1); // New connection + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connecting"], + 1); // New connection + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.backoff"], + 0); // Reset by initiateOneReverseConnection + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.backoff"], + 0); // Reset by initiateOneReverseConnection + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.recovered"], + 1); // Recovery recorded + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.recovered"], + 1); // Recovery recorded + + // Step 4: Simulate connection success (recovery) by calling onConnectionDone with success. + io_handle_->onConnectionDone("reverse connection accepted", wrapper_ptr2, false); + + // Verify wrapper was removed from tracking maps after success. + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + EXPECT_EQ(getConnectionWrappers().size(), 0); + + // Verify recovery stats - onConnectionDone should have called resetHostBackoff. + stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connected"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.recovered"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.backoff"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], + 0); // Should be decremented + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.failed"], + 0); // Reset by initiateOneReverseConnection + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connected"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.recovered"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.backoff"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connecting"], + 0); // Should be decremented + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.failed"], + 0); // Reset by initiateOneReverseConnection + + // Verify host is no longer in backoff. + EXPECT_TRUE(shouldAttemptConnectionToHost("192.168.1.1", "test-cluster")); + + // Verify final state - all maps should be clean. + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + EXPECT_EQ(getConnectionWrappers().size(), 0); + + // Verify host info is still present (should not be removed) + const auto& host_to_conn_info_map = getHostToConnInfoMap(); + EXPECT_EQ(host_to_conn_info_map.size(), 1); + EXPECT_NE(host_to_conn_info_map.find("192.168.1.1"), host_to_conn_info_map.end()); +} + +// Test downstream connection closure and re-initiation. +TEST_F(ReverseConnectionIOHandleTest, OnDownstreamConnectionClosedTriggersReInitiation) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create trigger pipe BEFORE initiating connection to ensure it's ready. + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Set up mock thread local cluster. + auto mock_thread_local_cluster = std::make_shared>(); + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test-cluster")) + .WillRepeatedly(Return(mock_thread_local_cluster.get())); + + // Set up priority set with hosts. + auto mock_priority_set = std::make_shared>(); + EXPECT_CALL(*mock_thread_local_cluster, prioritySet()) + .WillRepeatedly(ReturnRef(*mock_priority_set)); + + // Create host map with a host. + auto host_map = std::make_shared(); + auto mock_host = createMockHost("192.168.1.1"); + (*host_map)["192.168.1.1"] = std::const_pointer_cast(mock_host); + + EXPECT_CALL(*mock_priority_set, crossPriorityHostMap()).WillRepeatedly(Return(host_map)); + + // Create HostConnectionInfo entry. + addHostConnectionInfo("192.168.1.1", "test-cluster", 1); + + // Step 1: Create initial connection. + auto mock_connection = setupMockConnection(); + + Upstream::MockHost::MockCreateConnectionData success_conn_data; + success_conn_data.connection_ = mock_connection.get(); + success_conn_data.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data)); + + mock_connection.release(); + + // Call initiateOneReverseConnection to create the wrapper. + bool result = initiateOneReverseConnection("test-cluster", "192.168.1.1", mock_host); + EXPECT_TRUE(result); + + // Verify wrapper was created and mapped. + const auto& connection_wrappers = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers.size(), 1); + + const auto& wrapper_to_host_map = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map.size(), 1); + + RCConnectionWrapper* wrapper_ptr = connection_wrappers[0].get(); + EXPECT_EQ(wrapper_to_host_map.at(wrapper_ptr), "192.168.1.1"); + + // Verify initial state - no established connections yet. + EXPECT_EQ(getEstablishedConnectionsSize(), 0); + + // Step 2: Simulate successful connection completion. + io_handle_->onConnectionDone("reverse connection accepted", wrapper_ptr, false); + + // Verify wrapper was removed from tracking (cleanup should happen) + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + EXPECT_EQ(getConnectionWrappers().size(), 0); + + // Verify that connection was pushed to established_connections_ + EXPECT_EQ(getEstablishedConnectionsSize(), 1); + + // Verify that trigger mechanism was executed. + char trigger_byte; + int pipe_read_fd = getTriggerPipeReadFd(); + EXPECT_GE(pipe_read_fd, 0); + + ssize_t bytes_read = ::read(pipe_read_fd, &trigger_byte, 1); + EXPECT_EQ(bytes_read, 1) << "Expected to read 1 byte from trigger pipe, got " << bytes_read; + EXPECT_EQ(trigger_byte, 1) << "Expected trigger byte to be 1, got " + << static_cast(trigger_byte); + + // Step 3: Get the actual connection key that was used for tracking. + // The connection key should be the local address of the connection. + auto host_it = getHostToConnInfoMap().find("192.168.1.1"); + EXPECT_NE(host_it, getHostToConnInfoMap().end()); + + // The connection key should have been added during onConnectionDone. + // Let's find what connection key was actually used. + std::string connection_key; + if (!host_it->second.connection_keys.empty()) { + connection_key = *host_it->second.connection_keys.begin(); + ENVOY_LOG_MISC(debug, "Found connection key: {}", connection_key); + } else { + // If no connection key was added, use a mock one for testing. + connection_key = "192.168.1.1:12345"; + ENVOY_LOG_MISC(debug, "No connection key found, using mock: {}", connection_key); + } + + // Step 4: Simulate downstream connection closure. + io_handle_->onDownstreamConnectionClosed(connection_key); + + // Verify connection key is removed from host tracking. + host_it = getHostToConnInfoMap().find("192.168.1.1"); + EXPECT_NE(host_it, getHostToConnInfoMap().end()); + EXPECT_EQ(host_it->second.connection_keys.count(connection_key), 0); + + // Step 5: Set up expectation for new connection attempts. + auto mock_connection2 = setupMockConnection(); + + Upstream::MockHost::MockCreateConnectionData success_conn_data2; + success_conn_data2.connection_ = mock_connection2.get(); + success_conn_data2.host_description_ = mock_host; + + EXPECT_CALL(*mock_thread_local_cluster, tcpConn_(_)).WillOnce(Return(success_conn_data2)); + + mock_connection2.release(); + + // Step 6: Trigger maintenance cycle to verify re-initiation. + RemoteClusterConnectionConfig cluster_config("test-cluster", 1); + + maintainClusterConnections("test-cluster", cluster_config); + + // Since the connection key was removed, the host should need a new connection. + // and initiateOneReverseConnection should be called again + + // Verify that a new wrapper was created. + const auto& connection_wrappers2 = getConnectionWrappers(); + EXPECT_EQ(connection_wrappers2.size(), 1); + + const auto& wrapper_to_host_map2 = getConnWrapperToHostMap(); + EXPECT_EQ(wrapper_to_host_map2.size(), 1); + + RCConnectionWrapper* wrapper_ptr2 = connection_wrappers2[0].get(); + EXPECT_EQ(wrapper_to_host_map2.at(wrapper_ptr2), "192.168.1.1"); + + // Verify stats show new connection attempt. + auto stat_map = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.192.168.1.1.connecting"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.connecting"], 1); +} + +// Test ReverseConnectionIOHandle::close() method without trigger pipe. +TEST_F(ReverseConnectionIOHandleTest, CloseMethodWithoutTriggerPipe) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Verify initial state - trigger pipe not ready. + EXPECT_FALSE(isTriggerPipeReady()); + + // Get initial file descriptor (this is the original socket FD) + int initial_fd = io_handle_->fdDoNotUse(); + EXPECT_GE(initial_fd, 0); + + // Call close() - should close only the original socket FD and delegate to base class. + auto result = io_handle_->close(); + + // After close(), the FD should be -1. + EXPECT_EQ(io_handle_->fdDoNotUse(), -1); +} + +// Test ReverseConnectionIOHandle::close() method with trigger pipe. +TEST_F(ReverseConnectionIOHandleTest, CloseMethodWithTriggerPipe) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Get the original socket FD before creating trigger pipe. + int original_socket_fd = io_handle_->fdDoNotUse(); + EXPECT_GE(original_socket_fd, 0); + + // Create trigger pipe and initialize file event to set up the scenario where fd_ points to. + // trigger pipe Mock file event callback + Event::FileReadyCb mock_callback = [](uint32_t) -> absl::Status { return absl::OkStatus(); }; + + // Initialize file event to ensure the monitored FD is set to the trigger pipe. + io_handle_->initializeFileEvent(dispatcher_, mock_callback, Event::FileTriggerType::Level, + Event::FileReadyType::Read); + EXPECT_TRUE(isTriggerPipeReady()); + + // Get the pipe monitor FD (this becomes the monitored fd_ after initializeFileEvent) + int pipe_monitor_fd = getTriggerPipeReadFd(); + EXPECT_GE(pipe_monitor_fd, 0); + EXPECT_NE(original_socket_fd, pipe_monitor_fd); // Should be different FDs + + // Verify that the active FD is now the pipe monitor FD. + EXPECT_EQ(io_handle_->fdDoNotUse(), pipe_monitor_fd); + + // Call close() - should: + // 1. Close the original socket FD (original_socket_fd_) + // 2. Let base class close() handle fd_ + + auto result = io_handle_->close(); + EXPECT_EQ(result.return_value_, 0); + EXPECT_EQ(io_handle_->fdDoNotUse(), -1); +} + +// Test ReverseConnectionIOHandle::cleanup() method. +TEST_F(ReverseConnectionIOHandleTest, CleanupMethod) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Set up initial state with trigger pipe. + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + EXPECT_GE(getTriggerPipeReadFd(), 0); + EXPECT_GE(getTriggerPipeWriteFd(), 0); + + // Add some host connection info. + addHostConnectionInfo("192.168.1.1", "test-cluster", 2); + addHostConnectionInfo("192.168.1.2", "test-cluster", 1); + + // Verify initial state. + EXPECT_EQ(getHostToConnInfoMap().size(), 2); + EXPECT_TRUE(isTriggerPipeReady()); + + // Call cleanup() - should reset all resources. + cleanup(); + + // Verify that trigger pipe FDs are reset to -1. + EXPECT_FALSE(isTriggerPipeReady()); + EXPECT_EQ(getTriggerPipeReadFd(), -1); + EXPECT_EQ(getTriggerPipeWriteFd(), -1); + + // Verify that host connection info is cleared. + EXPECT_EQ(getHostToConnInfoMap().size(), 0); + + // Verify that connection wrappers are cleared. + EXPECT_EQ(getConnectionWrappers().size(), 0); + EXPECT_EQ(getConnWrapperToHostMap().size(), 0); + + // Verify that the base class fd_ is still valid (cleanup doesn't close the main socket) + EXPECT_GE(io_handle_->fdDoNotUse(), 0); +} + +// Test cleanup() closes any established connections in the queue. +TEST_F(ReverseConnectionIOHandleTest, CleanupClosesEstablishedConnections) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Create two mock connections and add them to the established queue. + // 1) An open connection should be closed with FlushWrite. + { + auto open_conn = std::make_unique>(); + EXPECT_CALL(*open_conn, state()).WillOnce(Return(Network::Connection::State::Open)); + EXPECT_CALL(*open_conn, close(Network::ConnectionCloseType::FlushWrite)); + addConnectionToEstablishedQueue(std::move(open_conn)); + } + // 2) A closed connection should not be closed again. + { + auto closed_conn = std::make_unique>(); + EXPECT_CALL(*closed_conn, state()).WillOnce(Return(Network::Connection::State::Closed)); + // No close() expected for closed connection. + addConnectionToEstablishedQueue(std::move(closed_conn)); + } + + // Call cleanup and ensure queue is drained without crashes. + EXPECT_GT(getEstablishedConnectionsSize(), 0); + cleanup(); + EXPECT_EQ(getEstablishedConnectionsSize(), 0); +} + +// Test initializeFileEvent early-return path when already started. +TEST_F(ReverseConnectionIOHandleTest, InitializeFileEventSkipWhenAlreadyStarted) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + Event::FileReadyCb cb = [](uint32_t) -> absl::Status { return absl::OkStatus(); }; + io_handle_->initializeFileEvent(dispatcher_, cb, Event::FileTriggerType::Level, 0); + + // Call again; should skip without changing fd or creating a new pipe. + const int fd_before = io_handle_->fdDoNotUse(); + io_handle_->initializeFileEvent(dispatcher_, cb, Event::FileTriggerType::Level, 0); + EXPECT_EQ(fd_before, io_handle_->fdDoNotUse()); +} + +// Test maintainReverseConnections early return when src_node_id is empty. +TEST_F(ReverseConnectionIOHandleTest, MaintainReverseConnectionsMissingSrcNodeId) { + ReverseConnectionSocketConfig cfg; + cfg.src_cluster_id = "test-cluster"; + cfg.src_node_id = ""; // Intentionally empty + cfg.remote_clusters.push_back(RemoteClusterConnectionConfig("remote", 1)); + + io_handle_ = createTestIOHandle(cfg); + EXPECT_NE(io_handle_, nullptr); + maintainReverseConnections(); +} + +// Test maintainClusterConnections early return when cluster is not found. +TEST_F(ReverseConnectionIOHandleTest, MaintainClusterConnectionsNoCluster) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + RemoteClusterConnectionConfig cluster_cfg{"missing-cluster", 1}; + // Default mock ClusterManager returns nullptr for unknown cluster. + maintainClusterConnections("missing-cluster", cluster_cfg); +} + +// Test shouldAttemptConnectionToHost creates host entry on-demand and returns true. +TEST_F(ReverseConnectionIOHandleTest, ShouldAttemptConnectionCreatesHostEntry) { + // Set up TLS registry to provide a time source for getTimeSource(). + setupThreadLocalSlot(); + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + EXPECT_TRUE(shouldAttemptConnectionToHost("10.0.0.5", "cluster-x")); + const auto& map = getHostToConnInfoMap(); + EXPECT_NE(map.find("10.0.0.5"), map.end()); +} + +// Test maybeUpdateHostsMappingsAndConnections removes stale hosts. +TEST_F(ReverseConnectionIOHandleTest, MaybeUpdateHostsRemovesStaleHosts) { + // Ensure a valid time source via TLS registry. + setupThreadLocalSlot(); + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Initial set of hosts: a, b + maybeUpdateHostsMappingsAndConnections("c1", std::vector{"a", "b"}); + EXPECT_EQ(getHostToConnInfoMap().size(), 2); + + // Updated set: only a → b should be removed. + maybeUpdateHostsMappingsAndConnections("c1", std::vector{"a"}); + const auto& map = getHostToConnInfoMap(); + EXPECT_NE(map.find("a"), map.end()); + EXPECT_EQ(map.find("b"), map.end()); +} + +// Lightly exercise read/write/connect wrappers for coverage. +TEST_F(ReverseConnectionIOHandleTest, ReadWriteConnectCoverage) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + Buffer::OwnedImpl buf("hello"); + (void)io_handle_->write(buf); + Buffer::OwnedImpl rbuf; + (void)io_handle_->read(rbuf, absl::optional(64)); + + auto addr = std::make_shared("127.0.0.1", 0); + (void)io_handle_->connect(addr); +} + +// Test ReverseConnectionIOHandle::onAboveWriteBufferHighWatermark method (no-op) +TEST_F(ReverseConnectionIOHandleTest, OnAboveWriteBufferHighWatermark) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Call onAboveWriteBufferHighWatermark - should be a no-op. + io_handle_->onAboveWriteBufferHighWatermark(); + // The test passes if no exceptions are thrown. +} + +// Test ReverseConnectionIOHandle::onBelowWriteBufferLowWatermark method (no-op) +TEST_F(ReverseConnectionIOHandleTest, OnBelowWriteBufferLowWatermark) { + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Call onBelowWriteBufferLowWatermark - should be a no-op. + io_handle_->onBelowWriteBufferLowWatermark(); + // The test passes if no exceptions are thrown. +} + +// Test updateStateGauge() method with null extension. +TEST_F(ReverseConnectionIOHandleTest, UpdateStateGaugeWithNullExtension) { + // Create a test IO handle with null extension BEFORE setting up thread local slot. + auto config = createDefaultTestConfig(); + int test_fd = ::socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(test_fd, 0); + + auto io_handle_null_extension = std::make_unique( + test_fd, config, cluster_manager_, nullptr, *stats_scope_); + + // Call updateConnectionState which internally calls updateStateGauge. + // This should exit early when extension is null. + io_handle_null_extension->updateConnectionState("test-host2", "test-cluster", "test-key2", + ReverseConnectionState::Connected); + + // Now set up thread local slot and create a test IO handle with extension. + setupThreadLocalSlot(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + io_handle_->updateConnectionState("test-host", "test-cluster", "test-key", + ReverseConnectionState::Connected); + + // Verify that stats were updated with extension. + auto stat_map_with_extension = extension_->getCrossWorkerStatMap(); + EXPECT_EQ(stat_map_with_extension["test_scope.reverse_connections.host.test-host.connected"], 1); + EXPECT_EQ( + stat_map_with_extension["test_scope.reverse_connections.cluster.test-cluster.connected"], 1); + + // Check that no stats exist for the null extension call + EXPECT_EQ(stat_map_with_extension["test_scope.reverse_connections.host.test-host2.connected"], 0); +} + +// Test updateStateGauge() method with unknown state. +TEST_F(ReverseConnectionIOHandleTest, UpdateStateGaugeWithUnknownState) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Create a test IO handle with extension. + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // First ensure host entry exists so the updateConnectionState call doesn't fail. + addHostConnectionInfo("test-host", "test-cluster", 1); + + // Call updateConnectionState with an unknown state value. + // We'll use a value that's not in the enum to trigger the default case. + io_handle_->updateConnectionState("test-host", "test-cluster", "test-key", + static_cast(999)); + + // Verify that the unknown state was handled correctly by checking if a gauge was created + // with "unknown" suffix. + auto stat_map = extension_->getCrossWorkerStatMap(); + + // The unknown state should have been handled and a gauge with "unknown" suffix should exist. + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.test-host.unknown"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.test-cluster.unknown"], 1); +} + +// Test ReverseConnectionIOHandle::accept() method - trigger pipe edge cases. +TEST_F(ReverseConnectionIOHandleTest, AcceptMethodTriggerPipeEdgeCases) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + // Test Case 1: Trigger pipe not ready - should return nullptr. + auto result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); + + // Create trigger pipe. + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Test Case 2: Trigger pipe ready but no data to read (EAGAIN/EWOULDBLOCK) - should return + // nullptr. + result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); + + // Test Case 3: Trigger pipe closed (read returns 0) - should return nullptr. + ::close(getTriggerPipeWriteFd()); + result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); + createTriggerPipe(); + + // Test Case 4: Trigger pipe read error (not EAGAIN/EWOULDBLOCK) - should return nullptr. + ::close(getTriggerPipeReadFd()); + result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); + createTriggerPipe(); + + // Test Case 5: Trigger pipe ready, data read, but no established connections - should return + // nullptr. + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); +} + +// Test ReverseConnectionIOHandle::accept() method - successful accept with address parameters. +TEST_F(ReverseConnectionIOHandleTest, AcceptMethodSuccessfulWithAddress) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Create a mock connection with proper socket setup. + auto mock_connection = setupMockConnection(); + + // Set up connection info provider with remote address. + auto mock_remote_address = + std::make_shared("192.168.1.100", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12345); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke( + [mock_remote_address, mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = std::make_unique( + mock_local_address, mock_remote_address); + return *mock_provider; + })); + + // Set up socket expectations. + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::NoFlush)); + + // Add connection to the established queue. + addConnectionToEstablishedQueue(std::move(mock_connection)); + + // Write trigger byte. + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + // Test accept with address parameters. + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + auto result = io_handle_->accept(reinterpret_cast(&addr), &addrlen); + + EXPECT_NE(result, nullptr); + EXPECT_EQ(addrlen, sizeof(addr)); + EXPECT_EQ(addr.sin_family, AF_INET); +} + +// Test ReverseConnectionIOHandle::accept() method - address handling edge cases. +TEST_F(ReverseConnectionIOHandleTest, AcceptMethodAddressHandlingEdgeCases) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Test Case 1: Address buffer too small for remote address. + { + auto mock_connection = setupMockConnection(); + + auto mock_remote_address = + std::make_shared("192.168.1.101", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12346); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke( + [mock_remote_address, mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = std::make_unique( + mock_local_address, mock_remote_address); + return *mock_provider; + })); + + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::NoFlush)); + + addConnectionToEstablishedQueue(std::move(mock_connection)); + + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + struct sockaddr_in addr; + socklen_t addrlen = 1; // Too small + auto result = io_handle_->accept(reinterpret_cast(&addr), &addrlen); + + EXPECT_NE(result, nullptr); + EXPECT_GT(addrlen, 1); + } + + // Test Case 2: No remote address, fallback to synthetic address. + { + auto mock_connection = setupMockConnection(); + + auto mock_local_address = std::make_shared("127.0.0.1", 12347); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke([mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = + std::make_unique(mock_local_address, nullptr); + return *mock_provider; + })); + + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::NoFlush)); + + addConnectionToEstablishedQueue(std::move(mock_connection)); + + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + auto result = io_handle_->accept(reinterpret_cast(&addr), &addrlen); + + EXPECT_NE(result, nullptr); + EXPECT_EQ(addrlen, sizeof(addr)); + EXPECT_EQ(addr.sin_family, AF_INET); + EXPECT_EQ(addr.sin_addr.s_addr, htonl(INADDR_LOOPBACK)); + } + + // Test Case 3: Synthetic address buffer too small. + { + auto mock_connection = setupMockConnection(); + + auto mock_local_address = std::make_shared("127.0.0.1", 12348); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke([mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = + std::make_unique(mock_local_address, nullptr); + return *mock_provider; + })); + + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::NoFlush)); + + addConnectionToEstablishedQueue(std::move(mock_connection)); + + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + struct sockaddr_in addr; + socklen_t addrlen = 1; // Too small + auto result = io_handle_->accept(reinterpret_cast(&addr), &addrlen); + + EXPECT_NE(result, nullptr); + EXPECT_GT(addrlen, 1); + } +} + +// Test ReverseConnectionIOHandle::accept() method - successful accept scenarios. +TEST_F(ReverseConnectionIOHandleTest, AcceptMethodSuccessfulScenarios) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Test Case 1: Accept without address parameters. + { + auto mock_connection = setupMockConnection(); + + auto mock_remote_address = + std::make_shared("192.168.1.102", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12349); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke( + [mock_remote_address, mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = std::make_unique( + mock_local_address, mock_remote_address); + return *mock_provider; + })); + + EXPECT_CALL(*mock_connection, close(Network::ConnectionCloseType::NoFlush)); + + addConnectionToEstablishedQueue(std::move(mock_connection)); + + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + auto result = io_handle_->accept(nullptr, nullptr); + EXPECT_NE(result, nullptr); + } +} + +// Test ReverseConnectionIOHandle::accept() method - socket and file descriptor failures. +TEST_F(ReverseConnectionIOHandleTest, AcceptMethodSocketAndFdFailures) { + setupThreadLocalSlot(); + + auto config = createDefaultTestConfig(); + io_handle_ = createTestIOHandle(config); + EXPECT_NE(io_handle_, nullptr); + + createTriggerPipe(); + EXPECT_TRUE(isTriggerPipeReady()); + + // Test Case 1: Original socket not available or not open. + { + auto mock_connection = std::make_unique>(); + + // Create a mock socket that returns isOpen() = false. + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle = std::make_unique>(); + + // Set up IO handle expectations. + EXPECT_CALL(*mock_io_handle, resetFileEvents()).WillRepeatedly(Return()); + EXPECT_CALL(*mock_io_handle, isOpen()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_io_handle, duplicate()).WillRepeatedly(Invoke([]() { + auto duplicated_handle = std::make_unique>(); + EXPECT_CALL(*duplicated_handle, isOpen()).WillRepeatedly(Return(true)); + return duplicated_handle; + })); + + // Set up socket expectations - but isOpen returns false to simulate failure. + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(false)); + + // Store the mock_io_handle in the socket before casting. + mock_socket_ptr->io_handle_ = std::move(mock_io_handle); + + // Create the socket and set up connection expectations. + auto mock_socket = std::unique_ptr(mock_socket_ptr.release()); + EXPECT_CALL(*mock_connection, getSocket()).WillRepeatedly(ReturnRef(mock_socket)); + + auto mock_remote_address = + std::make_shared("192.168.1.103", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12350); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke( + [mock_remote_address, mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = std::make_unique( + mock_local_address, mock_remote_address); + return *mock_provider; + })); + + addConnectionToEstablishedQueue(std::move(mock_connection)); + + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + auto result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); + } + + // Test Case 2: Failed to duplicate file descriptor. + { + auto mock_connection = std::make_unique>(); + + // Create a mock socket with IO handle that fails to duplicate. + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle = std::make_unique>(); + + // Set up IO handle expectations - but duplicate returns nullptr to simulate failure. + EXPECT_CALL(*mock_io_handle, resetFileEvents()).WillRepeatedly(Return()); + EXPECT_CALL(*mock_io_handle, isOpen()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_io_handle, duplicate()).WillRepeatedly(Invoke([]() { + return std::unique_ptr(nullptr); + })); + + // Set up socket expectations. + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(true)); + + // Store the mock_io_handle in the socket before casting. + mock_socket_ptr->io_handle_ = std::move(mock_io_handle); + + // Create the socket and set up connection expectations. + auto mock_socket = std::unique_ptr(mock_socket_ptr.release()); + EXPECT_CALL(*mock_connection, getSocket()).WillRepeatedly(ReturnRef(mock_socket)); + + auto mock_remote_address = + std::make_shared("192.168.1.104", 8080); + auto mock_local_address = std::make_shared("127.0.0.1", 12351); + + EXPECT_CALL(*mock_connection, connectionInfoProvider()) + .WillRepeatedly(Invoke( + [mock_remote_address, mock_local_address]() -> const Network::ConnectionInfoProvider& { + static auto mock_provider = std::make_unique( + mock_local_address, mock_remote_address); + return *mock_provider; + })); + + addConnectionToEstablishedQueue(std::move(mock_connection)); + + char trigger_byte = 1; + ssize_t bytes_written = ::write(getTriggerPipeWriteFd(), &trigger_byte, 1); + EXPECT_EQ(bytes_written, 1); + + auto result = io_handle_->accept(nullptr, nullptr); + EXPECT_EQ(result, nullptr); + } +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver_test.cc new file mode 100644 index 0000000000000..ab7ebfc075e32 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver_test.cc @@ -0,0 +1,199 @@ +#include "envoy/config/core/v3/address.pb.h" + +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_resolver.h" + +#include "test/test_common/logging.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ReverseConnectionResolverTest : public testing::Test { +protected: + void SetUp() override {} + + // Helper function to create a valid socket address. + envoy::config::core::v3::SocketAddress createSocketAddress(const std::string& address, + uint32_t port = 0) { + envoy::config::core::v3::SocketAddress socket_address; + socket_address.set_address(address); + socket_address.set_port_value(port); + return socket_address; + } + + // Helper function to create a valid reverse connection address string. + std::string createReverseConnectionAddress(const std::string& src_node_id, + const std::string& src_cluster_id, + const std::string& src_tenant_id, + const std::string& cluster_name, uint32_t count) { + return fmt::format("rc://{}:{}:{}@{}:{}", src_node_id, src_cluster_id, src_tenant_id, + cluster_name, count); + } + + // Helper function to access the private extractReverseConnectionConfig method. + absl::StatusOr + extractReverseConnectionConfig(const envoy::config::core::v3::SocketAddress& socket_address) { + return resolver_.extractReverseConnectionConfig(socket_address); + } + + ReverseConnectionResolver resolver_; + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); +}; + +// Test the name() method. +TEST_F(ReverseConnectionResolverTest, Name) { + EXPECT_EQ(resolver_.name(), "envoy.resolvers.reverse_connection"); +} + +// Test successful resolution of a valid reverse connection address. +TEST_F(ReverseConnectionResolverTest, ResolveValidAddress) { + std::string address_str = createReverseConnectionAddress("test-node", "test-cluster", + "test-tenant", "remote-cluster", 5); + auto socket_address = createSocketAddress(address_str); + + auto result = resolver_.resolve(socket_address); + EXPECT_TRUE(result.ok()); + + auto resolved_address = result.value(); + EXPECT_NE(resolved_address, nullptr); + + // Verify it's a ReverseConnectionAddress. + auto reverse_address = + std::dynamic_pointer_cast(resolved_address); + EXPECT_NE(reverse_address, nullptr); + + // Verify the configuration. + const auto& config = reverse_address->reverseConnectionConfig(); + EXPECT_EQ(config.src_node_id, "test-node"); + EXPECT_EQ(config.src_cluster_id, "test-cluster"); + EXPECT_EQ(config.src_tenant_id, "test-tenant"); + EXPECT_EQ(config.remote_cluster, "remote-cluster"); + EXPECT_EQ(config.connection_count, 5); +} + +// Test resolution failure for non-reverse connection address. +TEST_F(ReverseConnectionResolverTest, ResolveNonReverseConnectionAddress) { + auto socket_address = createSocketAddress("127.0.0.1"); + + auto result = resolver_.resolve(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Address must start with 'rc://'")); +} + +// Test resolution failure for non-zero port. +TEST_F(ReverseConnectionResolverTest, ResolveNonZeroPort) { + std::string address_str = createReverseConnectionAddress("test-node", "test-cluster", + "test-tenant", "remote-cluster", 5); + auto socket_address = createSocketAddress(address_str, 8080); // Non-zero port + + auto result = resolver_.resolve(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Only port 0 is supported")); +} + +// Test successful extraction of reverse connection config. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigValid) { + std::string address_str = createReverseConnectionAddress("node-123", "cluster-456", "tenant-789", + "remote-cluster-abc", 10); + auto socket_address = createSocketAddress(address_str); + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_TRUE(result.ok()); + + const auto& config = result.value(); + EXPECT_EQ(config.src_node_id, "node-123"); + EXPECT_EQ(config.src_cluster_id, "cluster-456"); + EXPECT_EQ(config.src_tenant_id, "tenant-789"); + EXPECT_EQ(config.remote_cluster, "remote-cluster-abc"); + EXPECT_EQ(config.connection_count, 10); +} + +// Test resolution failure for invalid format, +TEST_F(ReverseConnectionResolverTest, ResolveInvalidFormat) { + auto socket_address = createSocketAddress("rc://node:cluster:tenant:cluster:5"); // Missing @ + + auto result = resolver_.resolve(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), + testing::HasSubstr("Invalid reverse connection address format")); +} + +// Test extraction failure for invalid source info format. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigInvalidSourceInfo) { + auto socket_address = createSocketAddress("rc://node:cluster@remote:5"); // Missing tenant_id + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Invalid source info format")); +} + +// Test extraction failure for empty node ID. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigEmptyNodeId) { + auto socket_address = createSocketAddress("rc://:cluster:tenant@remote:5"); + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Source node ID cannot be empty")); +} + +// Test extraction failure for empty cluster ID. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigEmptyClusterId) { + auto socket_address = createSocketAddress("rc://node::tenant@remote:5"); + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Source cluster ID cannot be empty")); +} + +// Test extraction failure for invalid cluster config format. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigInvalidClusterConfig) { + auto socket_address = createSocketAddress("rc://node:cluster:tenant@remote"); // Missing count + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Invalid cluster config format")); +} + +// Test extraction failure for invalid connection count. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigInvalidCount) { + auto socket_address = createSocketAddress("rc://node:cluster:tenant@remote:invalid"); + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Invalid connection count")); +} + +// Test extraction with zero connection count. +TEST_F(ReverseConnectionResolverTest, ExtractReverseConnectionConfigZeroCount) { + std::string address_str = + createReverseConnectionAddress("node-123", "cluster-456", "tenant-789", "remote-cluster", 0); + auto socket_address = createSocketAddress(address_str); + + auto result = extractReverseConnectionConfig(socket_address); + EXPECT_TRUE(result.ok()); + + const auto& config = result.value(); + EXPECT_EQ(config.connection_count, 0); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension_test.cc new file mode 100644 index 0000000000000..a65d65e09413a --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension_test.cc @@ -0,0 +1,583 @@ +#include + +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/server/factory_context.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ReverseTunnelInitiatorExtensionTest : public testing::Test { +protected: + ReverseTunnelInitiatorExtensionTest() { + // Set up the stats scope. + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context. + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + + // Create the socket interface. + socket_interface_ = std::make_unique(context_); + + // Create the extension. + extension_ = std::make_unique(context_, config_); + } + + // Helper function to set up thread local slot for tests. + void setupThreadLocalSlot() { + // Create a thread local registry. + thread_local_registry_ = + std::make_shared(dispatcher_, *stats_scope_); + + // Create the actual TypedSlot. + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the slot to return our registry. + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Set the slot in the extension using the test-only method. + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + } + + void setupAnotherThreadLocalSlot() { + // Create a thread local registry for the other dispatcher. + another_thread_local_registry_ = + std::make_shared(dispatcher_, *stats_scope_); + + // Create the actual TypedSlot. + another_tls_slot_ = + ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the slot to return our registry. + another_tls_slot_->set( + [registry = another_thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Set the slot in the extension using the test-only method. + extension_->setTestOnlyTLSRegistry(std::move(another_tls_slot_)); + } + + void TearDown() override { + tls_slot_.reset(); + thread_local_registry_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + NiceMock context_; + NiceMock thread_local_; + NiceMock cluster_manager_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + + std::unique_ptr> tls_slot_; + std::shared_ptr thread_local_registry_; + std::unique_ptr> another_tls_slot_; + std::shared_ptr another_thread_local_registry_; +}; + +// Basic functionality tests. +TEST_F(ReverseTunnelInitiatorExtensionTest, InitializeWithDefaultConfig) { + // Test with empty config (should initialize successfully). + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface empty_config; + + auto extension_with_default = + std::make_unique(context_, empty_config); + + EXPECT_NE(extension_with_default, nullptr); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, OnServerInitialized) { + // This should be a no-op. + extension_->onServerInitialized(); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, OnWorkerThreadInitialized) { + // Test that onWorkerThreadInitialized creates thread local slot. + extension_->onWorkerThreadInitialized(); + + // Verify that the thread local slot was created by checking getLocalRegistry. + EXPECT_NE(extension_->getLocalRegistry(), nullptr); +} + +// Thread local registry access tests. +TEST_F(ReverseTunnelInitiatorExtensionTest, GetLocalRegistryBeforeInitialization) { + // Before tls_slot_ is set, getLocalRegistry should return nullptr. + EXPECT_EQ(extension_->getLocalRegistry(), nullptr); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, GetLocalRegistryAfterInitialization) { + + // First test with uninitialized TLS. + EXPECT_EQ(extension_->getLocalRegistry(), nullptr); + + // Initialize the thread local slot. + setupThreadLocalSlot(); + + // Now getLocalRegistry should return the actual registry. + auto* registry = extension_->getLocalRegistry(); + EXPECT_NE(registry, nullptr); + EXPECT_EQ(registry, thread_local_registry_.get()); + + // Test multiple calls return same registry. + auto* registry2 = extension_->getLocalRegistry(); + EXPECT_EQ(registry, registry2); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, GetStatsScope) { + // Test that getStatsScope returns the correct scope. + EXPECT_EQ(&extension_->getStatsScope(), stats_scope_.get()); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, DownstreamSocketThreadLocalScope) { + // Set up thread local slot first. + setupThreadLocalSlot(); + + // Get the thread local registry. + auto* registry = extension_->getLocalRegistry(); + EXPECT_NE(registry, nullptr); + + // Test that the scope() method returns the correct scope. + EXPECT_EQ(®istry->scope(), stats_scope_.get()); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, UpdateConnectionStatsIncrement) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Test updateConnectionStats with increment=true. + std::string node_id = "test-node-123"; + std::string cluster_id = "test-cluster-456"; + std::string state_suffix = "connecting"; + + // Call updateConnectionStats to increment. + extension_->updateConnectionStats(node_id, cluster_id, state_suffix, true); + + // Verify that the correct stats were created and incremented using cross-worker stat map. + auto stat_map = extension_->getCrossWorkerStatMap(); + + std::string expected_node_stat = + fmt::format("test_scope.reverse_connections.host.{}.{}", node_id, state_suffix); + std::string expected_cluster_stat = + fmt::format("test_scope.reverse_connections.cluster.{}.{}", cluster_id, state_suffix); + + EXPECT_EQ(stat_map[expected_node_stat], 1); + EXPECT_EQ(stat_map[expected_cluster_stat], 1); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, UpdateConnectionStatsDecrement) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Test updateConnectionStats with increment=false. + std::string node_id = "test-node-789"; + std::string cluster_id = "test-cluster-012"; + std::string state_suffix = "connected"; + + // First increment to have something to decrement. + extension_->updateConnectionStats(node_id, cluster_id, state_suffix, true); + extension_->updateConnectionStats(node_id, cluster_id, state_suffix, true); + + // Verify incremented values using cross-worker stat map. + auto stat_map = extension_->getCrossWorkerStatMap(); + std::string expected_node_stat = + fmt::format("test_scope.reverse_connections.host.{}.{}", node_id, state_suffix); + std::string expected_cluster_stat = + fmt::format("test_scope.reverse_connections.cluster.{}.{}", cluster_id, state_suffix); + + EXPECT_EQ(stat_map[expected_node_stat], 2); + EXPECT_EQ(stat_map[expected_cluster_stat], 2); + + // Now decrement. + extension_->updateConnectionStats(node_id, cluster_id, state_suffix, false); + + // Get updated stats after decrement. + stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map[expected_node_stat], 1); + EXPECT_EQ(stat_map[expected_cluster_stat], 1); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, UpdateConnectionStatsMultipleStates) { + // Set up thread local slot first so stats can be properly tracked. + setupThreadLocalSlot(); + + // Test updateConnectionStats with multiple different states. + std::string node_id = "test-node-multi"; + std::string cluster_id = "test-cluster-multi"; + + // Create stats for different states. + extension_->updateConnectionStats(node_id, cluster_id, "connecting", true); + extension_->updateConnectionStats(node_id, cluster_id, "connected", true); + extension_->updateConnectionStats(node_id, cluster_id, "failed", true); + + // Verify all states have separate gauges using cross-worker stat map. + auto stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map[fmt::format("test_scope.reverse_connections.host.{}.connecting", node_id)], 1); + EXPECT_EQ(stat_map[fmt::format("test_scope.reverse_connections.host.{}.connected", node_id)], 1); + EXPECT_EQ(stat_map[fmt::format("test_scope.reverse_connections.host.{}.failed", node_id)], 1); +} + +TEST_F(ReverseTunnelInitiatorExtensionTest, UpdateConnectionStatsEmptyValues) { + // Test updateConnectionStats with empty values - should not update stats. + auto& stats_store = extension_->getStatsScope(); + + // Empty host_id - should not create/update stats. + extension_->updateConnectionStats("", "test-cluster", "connecting", true); + auto& empty_host_gauge = stats_store.gaugeFromString("reverse_connections.host..connecting", + Stats::Gauge::ImportMode::Accumulate); + EXPECT_EQ(empty_host_gauge.value(), 0); + + // Empty cluster_id - should not create/update stats. + extension_->updateConnectionStats("test-host", "", "connecting", true); + auto& empty_cluster_gauge = stats_store.gaugeFromString("reverse_connections.cluster..connecting", + Stats::Gauge::ImportMode::Accumulate); + EXPECT_EQ(empty_cluster_gauge.value(), 0); + + // Empty state_suffix - should not create/update stats. + extension_->updateConnectionStats("test-host", "test-cluster", "", true); + auto& empty_state_gauge = stats_store.gaugeFromString("reverse_connections.host.test-host.", + Stats::Gauge::ImportMode::Accumulate); + EXPECT_EQ(empty_state_gauge.value(), 0); +} + +// Test per-worker stats aggregation for one thread only (test thread) +TEST_F(ReverseTunnelInitiatorExtensionTest, GetPerWorkerStatMapSingleThread) { + // Set up thread local slot first. + setupThreadLocalSlot(); + + // Update per-worker stats for the current (test) thread. + extension_->updatePerWorkerConnectionStats("host1", "cluster1", "connecting", true); + extension_->updatePerWorkerConnectionStats("host2", "cluster2", "connected", true); + extension_->updatePerWorkerConnectionStats("host2", "cluster2", "connected", true); + + // Get the per-worker stat map. + auto stat_map = extension_->getPerWorkerStatMap(); + + // Verify the stats are collected correctly for worker_0. + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.host.host1.connecting"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.host.host2.connected"], 2); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster1.connecting"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster2.connected"], 2); + + // Verify that only worker_0 stats are included. + for (const auto& [stat_name, value] : stat_map) { + EXPECT_TRUE(stat_name.find("worker_0") != std::string::npos); + } +} + +// Test cross-thread stat map functions using multiple dispatchers. +TEST_F(ReverseTunnelInitiatorExtensionTest, GetCrossWorkerStatMapMultiThread) { + // Set up thread local slot for the test thread (dispatcher name: "worker_0") + setupThreadLocalSlot(); + + // Set up another thread local slot for a different dispatcher (dispatcher name: "worker_1") + setupAnotherThreadLocalSlot(); + + // Simulate stats updates from worker_0. + extension_->updateConnectionStats("host1", "cluster1", "connecting", true); + extension_->updateConnectionStats("host1", "cluster1", "connecting", true); // Increment twice + extension_->updateConnectionStats("host2", "cluster2", "connected", true); + + // Temporarily switch the thread local registry to simulate updates from worker_1. + auto original_registry = thread_local_registry_; + thread_local_registry_ = another_thread_local_registry_; + + // Update stats from worker_1. + extension_->updateConnectionStats("host1", "cluster1", "connecting", + true); // Increment from worker_1 + extension_->updateConnectionStats("host3", "cluster3", "failed", true); // New host from worker_1 + + // Restore the original registry. + thread_local_registry_ = original_registry; + + // Get the cross-worker stat map. + auto stat_map = extension_->getCrossWorkerStatMap(); + + // Verify that cross-worker stats are collected correctly across multiple dispatchers. + // host1: incremented 3 times total (2 from worker_0 + 1 from worker_1) + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.host1.connecting"], 3); + // host2: incremented 1 time from worker_0 + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.host2.connected"], 1); + // host3: incremented 1 time from worker_1 + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.host3.failed"], 1); + + // cluster1: incremented 3 times total (2 from worker_0 + 1 from worker_1) + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.cluster1.connecting"], 3); + // cluster2: incremented 1 time from worker_0 + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.cluster2.connected"], 1); + // cluster3: incremented 1 time from worker_1 + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.cluster3.failed"], 1); + + // Test StatNameManagedStorage behavior: verify that calling updateConnectionStats again. + // with the same names increments the existing gauges (not creates new ones) + extension_->updateConnectionStats("host1", "cluster1", "connecting", true); // Increment again + extension_->updateConnectionStats("host2", "cluster2", "connected", false); // Decrement to 0 + + // Get stats again to verify the same gauges were updated. + stat_map = extension_->getCrossWorkerStatMap(); + + // Verify the gauge values were updated correctly (StatNameManagedStorage ensures same gauge) + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.host1.connecting"], 4); // 3 + 1 + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.cluster1.connecting"], 4); // 3 + 1 + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.host2.connected"], 0); // 1 - 1 + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.cluster2.connected"], 0); // 1 - 1 + EXPECT_EQ(stat_map["test_scope.reverse_connections.host.host3.failed"], 1); // unchanged + EXPECT_EQ(stat_map["test_scope.reverse_connections.cluster.cluster3.failed"], 1); // unchanged + + // Test per-worker decrement operations to cover the per-worker decrement code paths. + // First, test decrements from worker_0 context. + extension_->updatePerWorkerConnectionStats("host1", "cluster1", "connecting", + false); // Decrement from worker_0 + + // Get per-worker stats to verify decrements worked correctly for worker_0. + auto per_worker_stat_map = extension_->getPerWorkerStatMap(); + + // Verify worker_0 stats were decremented correctly. + EXPECT_EQ(per_worker_stat_map["test_scope.reverse_connections.worker_0.host.host1.connecting"], + 3); // 4 - 1 + EXPECT_EQ( + per_worker_stat_map["test_scope.reverse_connections.worker_0.cluster.cluster1.connecting"], + 3); // 4 - 1 + + // Now test decrements from worker_1 context. + thread_local_registry_ = another_thread_local_registry_; + + // Decrement some stats from worker_1. + extension_->updatePerWorkerConnectionStats("host1", "cluster1", "connecting", + false); // Decrement from worker_1 + extension_->updatePerWorkerConnectionStats("host3", "cluster3", "failed", + false); // Decrement host3 to 0 + + // Get per-worker stats from worker_1 context. + auto worker1_stat_map = extension_->getPerWorkerStatMap(); + + // Verify worker_1 stats were decremented correctly. + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.host.host1.connecting"], + 0); // 1 - 1 + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.cluster.cluster1.connecting"], + 0); // 1 - 1 + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.host.host3.failed"], + 0); // 1 - 1 + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.cluster.cluster3.failed"], + 0); // 1 - 1 + + // Restore original registry. + thread_local_registry_ = original_registry; +} + +// Test getConnectionStatsSync using multiple dispatchers. +TEST_F(ReverseTunnelInitiatorExtensionTest, GetConnectionStatsSyncMultiThread) { + // Set up thread local slot for the test thread (dispatcher name: "worker_0") + setupThreadLocalSlot(); + + // Set up another thread local slot for a different dispatcher (dispatcher name: "worker_1") + setupAnotherThreadLocalSlot(); + + // Simulate stats updates from worker_0. + extension_->updateConnectionStats("host1", "cluster1", "connected", true); + extension_->updateConnectionStats("host1", "cluster1", "connected", true); // Increment twice + extension_->updateConnectionStats("host2", "cluster2", "connected", true); + + // Simulate stats updates from worker_1. + // Temporarily switch the thread local registry to simulate the other dispatcher. + auto original_registry = thread_local_registry_; + thread_local_registry_ = another_thread_local_registry_; + + // Update stats from worker_1. + extension_->updateConnectionStats("host1", "cluster1", "connected", + true); // Increment from worker_1 + extension_->updateConnectionStats("host3", "cluster3", "connected", + true); // New host from worker_1 + + // Restore the original registry. + thread_local_registry_ = original_registry; + + // Get connection stats synchronously. + auto result = extension_->getConnectionStatsSync(std::chrono::milliseconds(100)); + auto& [connected_nodes, accepted_connections] = result; + + // Verify the result contains the expected data. + EXPECT_FALSE(connected_nodes.empty() || accepted_connections.empty()); + + // Verify that we have the expected host and cluster data. + // host1: should be present (incremented 3 times total) + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host1") != + connected_nodes.end()); + // host2: should be present (incremented 1 time) + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host2") != + connected_nodes.end()); + // host3: should be present (incremented 1 time) + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host3") != + connected_nodes.end()); + + // cluster1: should be present (incremented 3 times total) + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster1") != + accepted_connections.end()); + // cluster2: should be present (incremented 1 time) + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster2") != + accepted_connections.end()); + // cluster3: should be present (incremented 1 time) + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster3") != + accepted_connections.end()); + + // Test StatNameManagedStorage behavior: verify that calling updateConnectionStats again. + // with the same names updates the existing gauges and the sync result reflects this + extension_->updateConnectionStats("host1", "cluster1", "connected", true); // Increment again + extension_->updateConnectionStats("host2", "cluster2", "connected", false); // Decrement to 0 + + // Get connection stats again to verify the updated values. + result = extension_->getConnectionStatsSync(std::chrono::milliseconds(100)); + auto& [updated_connected_nodes, updated_accepted_connections] = result; + + // Verify that host2 is no longer present (gauge value is 0) + EXPECT_TRUE(std::find(updated_connected_nodes.begin(), updated_connected_nodes.end(), "host2") == + updated_connected_nodes.end()); + EXPECT_TRUE(std::find(updated_accepted_connections.begin(), updated_accepted_connections.end(), + "cluster2") == updated_accepted_connections.end()); + + // Verify that host1 and host3 are still present. + EXPECT_TRUE(std::find(updated_connected_nodes.begin(), updated_connected_nodes.end(), "host1") != + updated_connected_nodes.end()); + EXPECT_TRUE(std::find(updated_connected_nodes.begin(), updated_connected_nodes.end(), "host3") != + updated_connected_nodes.end()); + EXPECT_TRUE(std::find(updated_accepted_connections.begin(), updated_accepted_connections.end(), + "cluster1") != updated_accepted_connections.end()); + EXPECT_TRUE(std::find(updated_accepted_connections.begin(), updated_accepted_connections.end(), + "cluster3") != updated_accepted_connections.end()); +} + +// Test getConnectionStatsSync with timeouts. +TEST_F(ReverseTunnelInitiatorExtensionTest, GetConnectionStatsSyncTimeout) { + // Test with a very short timeout to verify timeout behavior. + auto result = extension_->getConnectionStatsSync(std::chrono::milliseconds(1)); + + // With no connections and short timeout, should return empty results. + auto& [connected_nodes, accepted_connections] = result; + EXPECT_TRUE(connected_nodes.empty()); + EXPECT_TRUE(accepted_connections.empty()); +} + +// Test getConnectionStatsSync filters only "connected" state. +TEST_F(ReverseTunnelInitiatorExtensionTest, GetConnectionStatsSyncFiltersConnectedState) { + // Set up thread local slot. + setupThreadLocalSlot(); + + // Add connections with different states. + extension_->updateConnectionStats("host1", "cluster1", "connecting", true); + extension_->updateConnectionStats("host2", "cluster2", "connected", true); + extension_->updateConnectionStats("host3", "cluster3", "failed", true); + extension_->updateConnectionStats("host4", "cluster4", "connected", true); + + // Get connection stats synchronously. + auto result = extension_->getConnectionStatsSync(std::chrono::milliseconds(100)); + auto& [connected_nodes, accepted_connections] = result; + + // Should only include hosts/clusters with "connected" state. + EXPECT_EQ(connected_nodes.size(), 2); + EXPECT_EQ(accepted_connections.size(), 2); + + // Verify only connected hosts are included. + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host2") != + connected_nodes.end()); + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host4") != + connected_nodes.end()); + + // Verify connecting and failed hosts are NOT included. + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host1") == + connected_nodes.end()); + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "host3") == + connected_nodes.end()); + + // Verify only connected clusters are included. + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster2") != + accepted_connections.end()); + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster4") != + accepted_connections.end()); + + // Verify connecting and failed clusters are NOT included. + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster1") == + accepted_connections.end()); + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster3") == + accepted_connections.end()); +} + +// Configuration validation tests. +class ConfigValidationTest : public testing::Test { +protected: + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + NiceMock context_; + NiceMock thread_local_; + NiceMock cluster_manager_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + + ConfigValidationTest() { + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + } +}; + +TEST_F(ConfigValidationTest, ValidConfiguration) { + // Test that valid configuration gets accepted. + ReverseTunnelInitiator initiator(context_); + + // Should not throw when creating bootstrap extension. + EXPECT_NO_THROW(initiator.createBootstrapExtension(config_, context_)); +} + +TEST_F(ConfigValidationTest, EmptyConfiguration) { + // Test that empty configuration still works. + ReverseTunnelInitiator initiator(context_); + + // Should not throw with empty config. + EXPECT_NO_THROW(initiator.createBootstrapExtension(config_, context_)); +} + +TEST_F(ConfigValidationTest, EmptyStatPrefix) { + // Test that empty stat_prefix still works with default. + ReverseTunnelInitiator initiator(context_); + + // Should not throw and should use default prefix. + EXPECT_NO_THROW(initiator.createBootstrapExtension(config_, context_)); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_test.cc b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_test.cc new file mode 100644 index 0000000000000..0c488e1cce4df --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_test.cc @@ -0,0 +1,377 @@ +#include + +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/server/factory_context.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/network/address_impl.h" +#include "source/common/network/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_address.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_io_handle.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/logging.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// ReverseTunnelInitiator Test Class. + +class ReverseTunnelInitiatorTest : public testing::Test { +protected: + ReverseTunnelInitiatorTest() { + // Set up the stats scope. + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context. + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + + // Create the socket interface. + socket_interface_ = std::make_unique(context_); + + // Create the extension. + extension_ = std::make_unique(context_, config_); + } + + // Thread Local Setup Helpers. + + // Helper function to set up thread local slot for tests. + void setupThreadLocalSlot() { + // First, call onServerInitialized to set up the extension reference properly. + extension_->onServerInitialized(); + + // Create a thread local registry with the properly initialized extension. + thread_local_registry_ = + std::make_shared(dispatcher_, *stats_scope_); + + // Create the actual TypedSlot. + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the slot to return our registry. + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Override the TLS slot with our test version. + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + + // Set the extension reference in the socket interface. + socket_interface_->extension_ = extension_.get(); + } + + // Helper to create a test address. + Network::Address::InstanceConstSharedPtr createTestAddress(const std::string& ip = "127.0.0.1", + uint32_t port = 8080) { + return Network::Utility::parseInternetAddressNoThrow(ip, port); + } + + void TearDown() override { + tls_slot_.reset(); + thread_local_registry_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + NiceMock context_; + NiceMock thread_local_; + NiceMock cluster_manager_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + + // Real thread local slot and registry. + std::unique_ptr> tls_slot_; + std::shared_ptr thread_local_registry_; + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); +}; + +TEST_F(ReverseTunnelInitiatorTest, CreateBootstrapExtension) { + // Test createBootstrapExtension function. + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface config; + + auto extension = socket_interface_->createBootstrapExtension(config, context_); + EXPECT_NE(extension, nullptr); + + // Verify extension is stored in socket interface. + EXPECT_NE(socket_interface_->getExtension(), nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, CreateEmptyConfigProto) { + // Test createEmptyConfigProto function. + auto config = socket_interface_->createEmptyConfigProto(); + EXPECT_NE(config, nullptr); + + // Should be able to cast to the correct type. + auto* typed_config = + dynamic_cast(config.get()); + EXPECT_NE(typed_config, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, IpFamilySupported) { + // Test IP family support. + EXPECT_TRUE(socket_interface_->ipFamilySupported(AF_INET)); + EXPECT_TRUE(socket_interface_->ipFamilySupported(AF_INET6)); + EXPECT_FALSE(socket_interface_->ipFamilySupported(AF_UNIX)); +} + +TEST_F(ReverseTunnelInitiatorTest, GetLocalRegistryNoExtension) { + // Test getLocalRegistry when extension is not set. + auto* registry = socket_interface_->getLocalRegistry(); + EXPECT_EQ(registry, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, GetLocalRegistryWithExtension) { + // Test getLocalRegistry when extension is set. + setupThreadLocalSlot(); + + auto* registry = socket_interface_->getLocalRegistry(); + EXPECT_NE(registry, nullptr); + EXPECT_EQ(registry, thread_local_registry_.get()); +} + +TEST_F(ReverseTunnelInitiatorTest, FactoryName) { + EXPECT_EQ(socket_interface_->name(), + "envoy.bootstrap.reverse_tunnel.downstream_socket_interface"); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodBasicIPv4) { + // Test basic socket creation for IPv4. + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, Network::Address::Type::Ip, + Network::Address::IpVersion::v4, false, + Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); + + // Verify it's NOT a ReverseConnectionIOHandle (should be a regular socket) + auto* reverse_handle = dynamic_cast(socket.get()); + EXPECT_EQ(reverse_handle, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodBasicIPv6) { + // Test basic socket creation for IPv6. + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, Network::Address::Type::Ip, + Network::Address::IpVersion::v6, false, + Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodDatagram) { + // Test datagram socket creation. + auto socket = socket_interface_->socket( + Network::Socket::Type::Datagram, Network::Address::Type::Ip, Network::Address::IpVersion::v4, + false, Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodUnixDomain) { + // Test Unix domain socket creation. + auto socket = socket_interface_->socket( + Network::Socket::Type::Stream, Network::Address::Type::Pipe, Network::Address::IpVersion::v4, + false, Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodWithAddressIPv4) { + // Test socket creation with IPv4 address. + auto address = std::make_shared("127.0.0.1", 8080); + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, address, + Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodWithAddressIPv6) { + // Test socket creation with IPv6 address. + auto address = std::make_shared("::1", 8080); + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, address, + Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodWithReverseConnectionAddress) { + // Test socket creation with ReverseConnectionAddress. + ReverseConnectionAddress::ReverseConnectionConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.src_tenant_id = "test-tenant"; + config.remote_cluster = "remote-cluster"; + config.connection_count = 2; + + auto reverse_address = std::make_shared(config); + + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, reverse_address, + Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); + + // Verify it's a ReverseConnectionIOHandle (not a regular socket) + auto* reverse_handle = dynamic_cast(socket.get()); + EXPECT_NE(reverse_handle, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, CreateReverseConnectionSocketStreamIPv4) { + // Test createReverseConnectionSocket for stream IPv4 with TLS registry setup. + setupThreadLocalSlot(); + + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.src_tenant_id = "test-tenant"; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + + auto socket = socket_interface_->createReverseConnectionSocket( + Network::Socket::Type::Stream, Network::Address::Type::Ip, Network::Address::IpVersion::v4, + config); + + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); + + // Verify it's a ReverseConnectionIOHandle. + auto* reverse_handle = dynamic_cast(socket.get()); + EXPECT_NE(reverse_handle, nullptr); + + // Verify that the TLS registry scope is being used. + // The socket should be created with the scope from TLS registry, not context scope. + EXPECT_EQ(&reverse_handle->getDownstreamExtension()->getStatsScope(), stats_scope_.get()); +} + +TEST_F(ReverseTunnelInitiatorTest, CreateReverseConnectionSocketStreamIPv6) { + // Test createReverseConnectionSocket for stream IPv6. + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.src_tenant_id = "test-tenant"; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + + auto socket = socket_interface_->createReverseConnectionSocket( + Network::Socket::Type::Stream, Network::Address::Type::Ip, Network::Address::IpVersion::v6, + config); + + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); + + // Verify it's a ReverseConnectionIOHandle. + auto* reverse_handle = dynamic_cast(socket.get()); + EXPECT_NE(reverse_handle, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, CreateReverseConnectionSocketDatagram) { + // Test createReverseConnectionSocket for datagram (should fallback to regular socket) + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.src_tenant_id = "test-tenant"; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + + auto socket = socket_interface_->createReverseConnectionSocket( + Network::Socket::Type::Datagram, Network::Address::Type::Ip, Network::Address::IpVersion::v4, + config); + + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); + + // Verify it's NOT a ReverseConnectionIOHandle. + auto* reverse_handle = dynamic_cast(socket.get()); + EXPECT_EQ(reverse_handle, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, CreateReverseConnectionSocketNonIP) { + // Test createReverseConnectionSocket for non-IP address (should fallback to regular socket) + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.src_tenant_id = "test-tenant"; + config.remote_clusters.push_back(RemoteClusterConnectionConfig("remote-cluster", 2)); + + auto socket = socket_interface_->createReverseConnectionSocket( + Network::Socket::Type::Stream, Network::Address::Type::Pipe, Network::Address::IpVersion::v4, + config); + + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); + + // Verify it's NOT a ReverseConnectionIOHandle (should be a regular socket) + auto* reverse_handle = dynamic_cast(socket.get()); + EXPECT_EQ(reverse_handle, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, CreateReverseConnectionSocketEmptyRemoteClusters) { + // Test createReverseConnectionSocket with empty remote_clusters (should return early) + ReverseConnectionSocketConfig config; + config.src_cluster_id = "test-cluster"; + config.src_node_id = "test-node"; + config.src_tenant_id = "test-tenant"; + // No remote_clusters added - should return early. + + auto socket = socket_interface_->createReverseConnectionSocket( + Network::Socket::Type::Stream, Network::Address::Type::Ip, Network::Address::IpVersion::v4, + config); + + // Should return nullptr due to empty remote_clusters. + EXPECT_EQ(socket, nullptr); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodWithEmptyReverseConnectionAddress) { + // Test socket creation with empty ReverseConnectionAddress. + ReverseConnectionAddress::ReverseConnectionConfig config; + config.src_cluster_id = ""; + config.src_node_id = ""; + config.src_tenant_id = ""; + config.remote_cluster = ""; + config.connection_count = 0; + + auto reverse_address = std::make_shared(config); + + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, reverse_address, + Network::SocketCreationOptions{}); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +TEST_F(ReverseTunnelInitiatorTest, SocketMethodWithSocketCreationOptions) { + // Test socket creation with socket creation options. + Network::SocketCreationOptions options; + options.mptcp_enabled_ = true; + options.max_addresses_cache_size_ = 100; + + auto address = std::make_shared("127.0.0.1", 0); + auto socket = socket_interface_->socket(Network::Socket::Type::Stream, address, options); + EXPECT_NE(socket, nullptr); + EXPECT_TRUE(socket->isOpen()); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/reverse_tunnel_test.cc b/test/extensions/bootstrap/reverse_tunnel/reverse_tunnel_test.cc new file mode 100644 index 0000000000000..0f283efd37e27 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/reverse_tunnel_test.cc @@ -0,0 +1,89 @@ +#include "source/extensions/bootstrap/reverse_tunnel/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/reverse_tunnel_initiator.h" + +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { +namespace { + +/** + * Test ReverseTunnelAcceptor factory creation and basic functionality. + */ +TEST(ReverseTunnelTest, AcceptorFactoryCreation) { + ReverseTunnelAcceptorFactory factory; + EXPECT_EQ(factory.name(), + "envoy.bootstrap.reverse_connection.upstream_reverse_connection_socket_interface"); + + // Test factory creation through public interface + auto empty_config = factory.createEmptyConfigProto(); + EXPECT_NE(empty_config, nullptr); +} + +/** + * Test ReverseTunnelInitiator factory creation and basic functionality. + */ +TEST(ReverseTunnelTest, InitiatorFactoryCreation) { + ReverseTunnelInitiatorFactory factory; + EXPECT_EQ(factory.name(), + "envoy.bootstrap.reverse_connection.downstream_reverse_connection_socket_interface"); + + // Test factory creation through public interface + auto empty_config = factory.createEmptyConfigProto(); + EXPECT_NE(empty_config, nullptr); +} + +/** + * Test basic configuration validation. + */ +TEST(ReverseTunnelTest, ConfigurationValidation) { + // Test acceptor configuration + envoy::extensions::bootstrap::reverse_connection_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface acceptor_config; + const std::string acceptor_yaml = R"EOF( +stat_prefix: "reverse_connection_test" +)EOF"; + TestUtility::loadFromYaml(acceptor_yaml, acceptor_config); + EXPECT_EQ(acceptor_config.stat_prefix(), "reverse_connection_test"); + + // Test initiator configuration + envoy::extensions::bootstrap::reverse_connection_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface initiator_config; + const std::string initiator_yaml = R"EOF( +stat_prefix: "reverse_connection_test" +)EOF"; + TestUtility::loadFromYaml(initiator_yaml, initiator_config); + EXPECT_EQ(initiator_config.stat_prefix(), "reverse_connection_test"); +} + +/** + * Test factory pattern implementation. + */ +TEST(ReverseTunnelTest, FactoryPatternImplementation) { + // Test acceptor factory + ReverseTunnelAcceptorFactory acceptor_factory; + EXPECT_EQ(acceptor_factory.name(), + "envoy.bootstrap.reverse_connection.upstream_reverse_connection_socket_interface"); + + // Test initiator factory + ReverseTunnelInitiatorFactory initiator_factory; + EXPECT_EQ(initiator_factory.name(), + "envoy.bootstrap.reverse_connection.downstream_reverse_connection_socket_interface"); + + // Test empty config creation + auto acceptor_config = acceptor_factory.createEmptyConfigProto(); + auto initiator_config = initiator_factory.createEmptyConfigProto(); + + EXPECT_NE(acceptor_config, nullptr); + EXPECT_NE(initiator_config, nullptr); +} + +} // namespace +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/BUILD b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/BUILD new file mode 100644 index 0000000000000..c2db86a5312b1 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/BUILD @@ -0,0 +1,80 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "reverse_tunnel_acceptor_test", + size = "medium", + srcs = ["reverse_tunnel_acceptor_test.cc"], + extension_names = ["envoy.bootstrap.reverse_tunnel.upstream_socket_interface"], + deps = [ + "//source/common/network:utility_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "reverse_tunnel_acceptor_extension_test", + size = "medium", + srcs = ["reverse_tunnel_acceptor_extension_test.cc"], + deps = [ + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/test_common:logging_lib", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "upstream_socket_manager_test", + size = "large", + srcs = ["upstream_socket_manager_test.cc"], + deps = [ + "//source/common/network:utility_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "upstream_reverse_connection_io_handle_test", + size = "medium", + srcs = ["upstream_reverse_connection_io_handle_test.cc"], + deps = [ + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//test/mocks/network:network_mocks", + ], +) + +envoy_cc_test( + name = "config_validation_test", + size = "small", + srcs = ["config_validation_test.cc"], + deps = [ + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//test/mocks/server:factory_context_mocks", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/config_validation_test.cc b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/config_validation_test.cc new file mode 100644 index 0000000000000..7c9d808dd3cd4 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/config_validation_test.cc @@ -0,0 +1,42 @@ +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" + +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" + +#include "test/mocks/server/factory_context.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ConfigValidationTest : public testing::Test { +protected: + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface config_; + NiceMock context_; +}; + +TEST_F(ConfigValidationTest, ValidConfiguration) { + config_.set_stat_prefix("reverse_tunnel"); + + ReverseTunnelAcceptor acceptor(context_); + + EXPECT_NO_THROW(acceptor.createBootstrapExtension(config_, context_)); +} + +TEST_F(ConfigValidationTest, EmptyStatPrefix) { + ReverseTunnelAcceptor acceptor(context_); + + EXPECT_NO_THROW(acceptor.createBootstrapExtension(config_, context_)); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension_test.cc b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension_test.cc new file mode 100644 index 0000000000000..41bba716429b5 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension_test.cc @@ -0,0 +1,440 @@ +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" + +#include "source/common/network/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/logging.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class ReverseTunnelAcceptorExtensionTest : public testing::Test { +protected: + ReverseTunnelAcceptorExtensionTest() { + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + config_.set_stat_prefix("test_prefix"); + socket_interface_ = std::make_unique(context_); + extension_ = + std::make_unique(*socket_interface_, context_, config_); + } + + void setupThreadLocalSlot() { + thread_local_registry_ = + std::make_shared(dispatcher_, extension_.get()); + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + extension_->tls_slot_ = std::move(tls_slot_); + extension_->socket_interface_->extension_ = extension_.get(); + } + + void setupAnotherThreadLocalSlot() { + another_thread_local_registry_ = + std::make_shared(another_dispatcher_, extension_.get()); + } + + void TearDown() override { + tls_slot_.reset(); + thread_local_registry_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + NiceMock context_; + NiceMock thread_local_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + + std::unique_ptr> tls_slot_; + std::shared_ptr thread_local_registry_; + + NiceMock another_dispatcher_{"worker_1"}; + std::shared_ptr another_thread_local_registry_; + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); +}; + +TEST_F(ReverseTunnelAcceptorExtensionTest, InitializeWithDefaultStatPrefix) { + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface empty_config; + + auto extension_with_default = + std::make_unique(*socket_interface_, context_, empty_config); + + EXPECT_EQ(extension_with_default->statPrefix(), "upstream_reverse_connection"); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, InitializeWithCustomStatPrefix) { + EXPECT_EQ(extension_->statPrefix(), "test_prefix"); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetStatsScope) { + EXPECT_EQ(&extension_->getStatsScope(), stats_scope_.get()); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, OnWorkerThreadInitialized) { + extension_->onWorkerThreadInitialized(); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, OnServerInitializedSetsExtensionReference) { + extension_->onServerInitialized(); + EXPECT_EQ(socket_interface_->getExtension(), extension_.get()); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetLocalRegistryBeforeInitialization) { + EXPECT_EQ(extension_->getLocalRegistry(), nullptr); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetLocalRegistryAfterInitialization) { + setupThreadLocalSlot(); + + auto* registry = extension_->getLocalRegistry(); + EXPECT_NE(registry, nullptr); + + auto* socket_manager = registry->socketManager(); + EXPECT_NE(socket_manager, nullptr); + EXPECT_EQ(socket_manager->getUpstreamExtension(), extension_.get()); + + const auto* const_registry = extension_->getLocalRegistry(); + EXPECT_NE(const_registry, nullptr); + + const auto* const_socket_manager = const_registry->socketManager(); + EXPECT_NE(const_socket_manager, nullptr); + EXPECT_EQ(const_socket_manager->getUpstreamExtension(), extension_.get()); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetPerWorkerStatMapSingleThread) { + setupThreadLocalSlot(); + + extension_->updatePerWorkerConnectionStats("node1", "cluster1", true); + extension_->updatePerWorkerConnectionStats("node2", "cluster2", true); + extension_->updatePerWorkerConnectionStats("node2", "cluster2", true); + + auto stat_map = extension_->getPerWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node1"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node2"], 2); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster1"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster2"], 2); + + for (const auto& [stat_name, value] : stat_map) { + EXPECT_TRUE(stat_name.find("worker_0") != std::string::npos); + } + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node1", "cluster1", true); + + stat_map = extension_->getPerWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node1"], 3); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster1"], 3); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node2"], 2); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster2"], 2); + + extension_->updatePerWorkerConnectionStats("node1", "cluster1", false); + extension_->updatePerWorkerConnectionStats("node2", "cluster2", false); + extension_->updatePerWorkerConnectionStats("node2", "cluster2", false); + + stat_map = extension_->getPerWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node1"], 2); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster1"], 2); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node2"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster2"], 0); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetCrossWorkerStatMapMultiThread) { + setupThreadLocalSlot(); + setupAnotherThreadLocalSlot(); + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node2", "cluster2", true); + + auto original_registry = thread_local_registry_; + thread_local_registry_ = another_thread_local_registry_; + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node3", "cluster3", true); + + thread_local_registry_ = original_registry; + + auto stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.nodes.node1"], 3); + EXPECT_EQ(stat_map["test_scope.reverse_connections.nodes.node2"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.nodes.node3"], 1); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.clusters.cluster1"], 3); + EXPECT_EQ(stat_map["test_scope.reverse_connections.clusters.cluster2"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.clusters.cluster3"], 1); + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node2", "cluster2", false); + + stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.nodes.node1"], 4); + EXPECT_EQ(stat_map["test_scope.reverse_connections.clusters.cluster1"], 4); + EXPECT_EQ(stat_map["test_scope.reverse_connections.nodes.node2"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.clusters.cluster2"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.nodes.node3"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.clusters.cluster3"], 1); + + extension_->updatePerWorkerConnectionStats("node1", "cluster1", false); + + auto per_worker_stat_map = extension_->getPerWorkerStatMap(); + + EXPECT_EQ(per_worker_stat_map["test_scope.reverse_connections.worker_0.node.node1"], 3); + EXPECT_EQ(per_worker_stat_map["test_scope.reverse_connections.worker_0.cluster.cluster1"], 3); + + extension_->updateConnectionStats("node2", "cluster2", false); + + auto cross_worker_stat_map = extension_->getCrossWorkerStatMap(); + + EXPECT_EQ(cross_worker_stat_map["test_scope.reverse_connections.clusters.cluster2"], 0); + + per_worker_stat_map = extension_->getPerWorkerStatMap(); + + EXPECT_EQ(per_worker_stat_map["test_scope.reverse_connections.worker_0.node.node2"], 0); + EXPECT_EQ(per_worker_stat_map["test_scope.reverse_connections.worker_0.cluster.cluster2"], 0); + + thread_local_registry_ = another_thread_local_registry_; + + extension_->updatePerWorkerConnectionStats("node1", "cluster1", false); + extension_->updatePerWorkerConnectionStats("node3", "cluster3", false); + + auto worker1_stat_map = extension_->getPerWorkerStatMap(); + + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.node.node1"], 0); + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.cluster.cluster1"], 0); + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.node.node3"], 0); + EXPECT_EQ(worker1_stat_map["test_scope.reverse_connections.worker_1.cluster.cluster3"], 0); + + thread_local_registry_ = original_registry; +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetConnectionStatsSyncMultiThread) { + setupThreadLocalSlot(); + setupAnotherThreadLocalSlot(); + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node2", "cluster2", true); + + auto original_registry = thread_local_registry_; + thread_local_registry_ = another_thread_local_registry_; + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node3", "cluster3", true); + + thread_local_registry_ = original_registry; + + auto result = extension_->getConnectionStatsSync(); + auto& [connected_nodes, accepted_connections] = result; + + EXPECT_FALSE(connected_nodes.empty() || accepted_connections.empty()); + + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "node1") != + connected_nodes.end()); + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "node2") != + connected_nodes.end()); + EXPECT_TRUE(std::find(connected_nodes.begin(), connected_nodes.end(), "node3") != + connected_nodes.end()); + + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster1") != + accepted_connections.end()); + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster2") != + accepted_connections.end()); + EXPECT_TRUE(std::find(accepted_connections.begin(), accepted_connections.end(), "cluster3") != + accepted_connections.end()); + + extension_->updateConnectionStats("node1", "cluster1", true); + extension_->updateConnectionStats("node2", "cluster2", false); + + result = extension_->getConnectionStatsSync(); + auto& [updated_connected_nodes, updated_accepted_connections] = result; + + EXPECT_TRUE(std::find(updated_connected_nodes.begin(), updated_connected_nodes.end(), "node2") == + updated_connected_nodes.end()); + EXPECT_TRUE(std::find(updated_accepted_connections.begin(), updated_accepted_connections.end(), + "cluster2") == updated_accepted_connections.end()); + + EXPECT_TRUE(std::find(updated_connected_nodes.begin(), updated_connected_nodes.end(), "node1") != + updated_connected_nodes.end()); + EXPECT_TRUE(std::find(updated_connected_nodes.begin(), updated_connected_nodes.end(), "node3") != + updated_connected_nodes.end()); + EXPECT_TRUE(std::find(updated_accepted_connections.begin(), updated_accepted_connections.end(), + "cluster1") != updated_accepted_connections.end()); + EXPECT_TRUE(std::find(updated_accepted_connections.begin(), updated_accepted_connections.end(), + "cluster3") != updated_accepted_connections.end()); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, GetConnectionStatsSyncTimeout) { + auto result = extension_->getConnectionStatsSync(std::chrono::milliseconds(1)); + + auto& [connected_nodes, accepted_connections] = result; + EXPECT_TRUE(connected_nodes.empty()); + EXPECT_TRUE(accepted_connections.empty()); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, IpFamilySupportIPv4) { + EXPECT_TRUE(socket_interface_->ipFamilySupported(AF_INET)); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, IpFamilySupportIPv6) { + EXPECT_TRUE(socket_interface_->ipFamilySupported(AF_INET6)); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, IpFamilySupportUnknown) { + EXPECT_FALSE(socket_interface_->ipFamilySupported(AF_UNIX)); + EXPECT_FALSE(socket_interface_->ipFamilySupported(-1)); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, ExtensionNotInitialized) { + ReverseTunnelAcceptor acceptor(context_); + auto registry = acceptor.getLocalRegistry(); + EXPECT_EQ(registry, nullptr); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, CreateEmptyConfigProto) { + auto proto = socket_interface_->createEmptyConfigProto(); + EXPECT_NE(proto, nullptr); + + auto* typed_proto = + dynamic_cast(proto.get()); + EXPECT_NE(typed_proto, nullptr); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, MissThresholdOneMarksDeadOnFirstInvalidPing) { + // Recreate extension_ with threshold = 1. + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface cfg; + cfg.set_stat_prefix("test_prefix"); + cfg.mutable_ping_failure_threshold()->set_value(1); + extension_.reset(new ReverseTunnelAcceptorExtension(*socket_interface_, context_, cfg)); + + // Provide dispatcher to thread local and set expectations for timers/file events. + thread_local_.setDispatcher(&dispatcher_); + EXPECT_CALL(dispatcher_, createTimer_(_)) + .WillRepeatedly(testing::ReturnNew>()); + EXPECT_CALL(dispatcher_, createFileEvent_(_, _, _, _)) + .WillRepeatedly(testing::ReturnNew>()); + + // Use helper to install TLS registry for the (recreated) extension_. + setupThreadLocalSlot(); + + // Get the registry and socket manager back through the API and apply threshold. + auto* registry = extension_->getLocalRegistry(); + ASSERT_NE(registry, nullptr); + auto* socket_manager = registry->socketManager(); + ASSERT_NE(socket_manager, nullptr); + socket_manager->setMissThreshold(extension_->pingFailureThreshold()); + + // Create a mock socket with FD and addresses. + auto socket = std::make_unique>(); + auto io_handle = std::make_unique>(); + EXPECT_CALL(*io_handle, fdDoNotUse()).WillRepeatedly(testing::Return(123)); + EXPECT_CALL(*socket, ioHandle()).WillRepeatedly(testing::ReturnRef(*io_handle)); + socket->io_handle_ = std::move(io_handle); + + auto local_address = Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 10000); + auto remote_address = Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 10001); + socket->connection_info_provider_->setLocalAddress(local_address); + socket->connection_info_provider_->setRemoteAddress(remote_address); + + const std::string node_id = "n1"; + const std::string cluster_id = "c1"; + socket_manager->addConnectionSocket(node_id, cluster_id, std::move(socket), + std::chrono::seconds(30), false); + + // Simulate an invalid ping response (not RPING). With threshold=1, one miss should kill it. + NiceMock mock_read_handle; + EXPECT_CALL(mock_read_handle, fdDoNotUse()).WillRepeatedly(testing::Return(123)); + EXPECT_CALL(mock_read_handle, read(testing::_, testing::_)) + .WillOnce(testing::Invoke([](Buffer::Instance& buffer, absl::optional) { + buffer.add("XXXXX"); // 5 bytes, not RPING + return Api::IoCallUint64Result{5, Api::IoError::none()}; + })); + + socket_manager->onPingResponse(mock_read_handle); + + // With threshold=1, the socket should be marked dead immediately. + auto retrieved = socket_manager->getConnectionSocket(node_id); + EXPECT_EQ(retrieved, nullptr); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, PingFailureThresholdConfiguration) { + // Test default threshold value + EXPECT_EQ(extension_->pingFailureThreshold(), 3); // Default threshold should be 3. + + // Create extension with custom threshold = 5 + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface custom_config; + custom_config.set_stat_prefix("test_custom"); + custom_config.mutable_ping_failure_threshold()->set_value(5); + + auto custom_extension = + std::make_unique(*socket_interface_, context_, custom_config); + + EXPECT_EQ(custom_extension->pingFailureThreshold(), 5); + + // Test threshold = 1 (minimum value) + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface min_config; + min_config.set_stat_prefix("test_min"); + min_config.mutable_ping_failure_threshold()->set_value(1); + + auto min_extension = + std::make_unique(*socket_interface_, context_, min_config); + + EXPECT_EQ(min_extension->pingFailureThreshold(), 1); + + // Test very high threshold + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface max_config; + max_config.set_stat_prefix("test_max"); + max_config.mutable_ping_failure_threshold()->set_value(100); + + auto max_extension = + std::make_unique(*socket_interface_, context_, max_config); + + EXPECT_EQ(max_extension->pingFailureThreshold(), 100); +} + +TEST_F(ReverseTunnelAcceptorExtensionTest, FactoryName) { + EXPECT_EQ(socket_interface_->name(), "envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_test.cc b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_test.cc new file mode 100644 index 0000000000000..73b2a395f9837 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_test.cc @@ -0,0 +1,241 @@ +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" + +#include "source/common/network/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class TestReverseTunnelAcceptor : public testing::Test { +protected: + TestReverseTunnelAcceptor() { + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + config_.set_stat_prefix("test_prefix"); + socket_interface_ = std::make_unique(context_); + extension_ = + std::make_unique(*socket_interface_, context_, config_); + + EXPECT_CALL(dispatcher_, createTimer_(_)) + .WillRepeatedly(testing::ReturnNew>()); + EXPECT_CALL(dispatcher_, createFileEvent_(_, _, _, _)) + .WillRepeatedly(testing::ReturnNew>()); + + socket_manager_ = std::make_unique(dispatcher_, extension_.get()); + } + + void TearDown() override { + socket_manager_.reset(); + tls_slot_.reset(); + thread_local_registry_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + void setupThreadLocalSlot() { + extension_->onServerInitialized(); + thread_local_registry_ = + std::make_shared(dispatcher_, extension_.get()); + tls_slot_ = ThreadLocal::TypedSlot::makeUnique(thread_local_); + thread_local_.setDispatcher(&dispatcher_); + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + } + + Network::ConnectionSocketPtr createMockSocket(int fd = 123, + const std::string& local_addr = "127.0.0.1:8080", + const std::string& remote_addr = "127.0.0.1:9090") { + auto socket = std::make_unique>(); + + auto local_colon_pos = local_addr.find(':'); + std::string local_ip = local_addr.substr(0, local_colon_pos); + uint32_t local_port = std::stoi(local_addr.substr(local_colon_pos + 1)); + auto local_address = Network::Utility::parseInternetAddressNoThrow(local_ip, local_port); + + auto remote_colon_pos = remote_addr.find(':'); + std::string remote_ip = remote_addr.substr(0, remote_colon_pos); + uint32_t remote_port = std::stoi(remote_addr.substr(remote_colon_pos + 1)); + auto remote_address = Network::Utility::parseInternetAddressNoThrow(remote_ip, remote_port); + + auto mock_io_handle = std::make_unique>(); + auto* mock_io_handle_ptr = mock_io_handle.get(); + EXPECT_CALL(*mock_io_handle_ptr, fdDoNotUse()).WillRepeatedly(Return(fd)); + EXPECT_CALL(*socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_ptr)); + + socket->io_handle_ = std::move(mock_io_handle); + socket->connection_info_provider_->setLocalAddress(local_address); + socket->connection_info_provider_->setRemoteAddress(remote_address); + + return socket; + } + + Network::Address::InstanceConstSharedPtr + createAddressWithLogicalName(const std::string& logical_name) { + class TestAddress : public Network::Address::Instance { + public: + TestAddress(const std::string& logical_name) : logical_name_(logical_name) { + address_string_ = "127.0.0.1:8080"; + } + + bool operator==(const Instance& rhs) const override { + return logical_name_ == rhs.logicalName(); + } + Network::Address::Type type() const override { return Network::Address::Type::Ip; } + const std::string& asString() const override { return address_string_; } + absl::string_view asStringView() const override { return address_string_; } + const std::string& logicalName() const override { return logical_name_; } + const Network::Address::Ip* ip() const override { return nullptr; } + const Network::Address::Pipe* pipe() const override { return nullptr; } + const Network::Address::EnvoyInternalAddress* envoyInternalAddress() const override { + return nullptr; + } + const sockaddr* sockAddr() const override { return nullptr; } + socklen_t sockAddrLen() const override { return 0; } + absl::string_view addressType() const override { return "test"; } + absl::optional networkNamespace() const override { return absl::nullopt; } + const Network::SocketInterface& socketInterface() const override { + return Network::SocketInterfaceSingleton::get(); + } + + private: + std::string logical_name_; + std::string address_string_; + }; + + return std::make_shared(logical_name); + } + + NiceMock context_; + NiceMock thread_local_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_; + + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + std::unique_ptr socket_manager_; + + std::unique_ptr> tls_slot_; + std::shared_ptr thread_local_registry_; +}; + +TEST_F(TestReverseTunnelAcceptor, GetLocalRegistryNoExtension) { + auto* registry = socket_interface_->getLocalRegistry(); + EXPECT_EQ(registry, nullptr); +} + +TEST_F(TestReverseTunnelAcceptor, GetLocalRegistryWithExtension) { + setupThreadLocalSlot(); + + auto* registry = socket_interface_->getLocalRegistry(); + EXPECT_NE(registry, nullptr); + EXPECT_EQ(registry, thread_local_registry_.get()); +} + +TEST_F(TestReverseTunnelAcceptor, CreateBootstrapExtension) { + auto extension = socket_interface_->createBootstrapExtension(config_, context_); + EXPECT_NE(extension, nullptr); +} + +TEST_F(TestReverseTunnelAcceptor, CreateEmptyConfigProto) { + auto config = socket_interface_->createEmptyConfigProto(); + EXPECT_NE(config, nullptr); +} + +TEST_F(TestReverseTunnelAcceptor, SocketWithoutAddress) { + Network::SocketCreationOptions options; + auto io_handle = + socket_interface_->socket(Network::Socket::Type::Stream, Network::Address::Type::Ip, + Network::Address::IpVersion::v4, false, options); + EXPECT_EQ(io_handle, nullptr); +} + +TEST_F(TestReverseTunnelAcceptor, SocketWithAddressNoThreadLocal) { + const std::string node_id = "test-node"; + auto address = createAddressWithLogicalName(node_id); + Network::SocketCreationOptions options; + auto io_handle = socket_interface_->socket(Network::Socket::Type::Stream, address, options); + EXPECT_NE(io_handle, nullptr); + EXPECT_EQ(dynamic_cast(io_handle.get()), nullptr); + + // Verify fallback counter increments for diagnostics. + // Counter name is "..fallback_no_reverse_socket". + auto& scope = extension_->getStatsScope(); + std::string counter_name = absl::StrCat(extension_->statPrefix(), ".fallback_no_reverse_socket"); + Stats::StatNameManagedStorage counter_name_storage(counter_name, scope.symbolTable()); + auto& counter = scope.counterFromStatName(counter_name_storage.statName()); + EXPECT_EQ(counter.value(), 1); +} + +TEST_F(TestReverseTunnelAcceptor, SocketWithAddressAndThreadLocalNoCachedSockets) { + setupThreadLocalSlot(); + + const std::string node_id = "test-node"; + auto address = createAddressWithLogicalName(node_id); + + Network::SocketCreationOptions options; + auto io_handle = socket_interface_->socket(Network::Socket::Type::Stream, address, options); + EXPECT_NE(io_handle, nullptr); + EXPECT_EQ(dynamic_cast(io_handle.get()), nullptr); +} + +TEST_F(TestReverseTunnelAcceptor, SocketWithAddressAndThreadLocalWithCachedSockets) { + setupThreadLocalSlot(); + + auto* tls_socket_manager = socket_interface_->getLocalRegistry()->socketManager(); + EXPECT_NE(tls_socket_manager, nullptr); + + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + tls_socket_manager->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + auto address = createAddressWithLogicalName(node_id); + + Network::SocketCreationOptions options; + auto io_handle = socket_interface_->socket(Network::Socket::Type::Stream, address, options); + EXPECT_NE(io_handle, nullptr); + + auto* upstream_io_handle = dynamic_cast(io_handle.get()); + EXPECT_NE(upstream_io_handle, nullptr); + + auto another_io_handle = + socket_interface_->socket(Network::Socket::Type::Stream, address, options); + EXPECT_NE(another_io_handle, nullptr); + EXPECT_EQ(dynamic_cast(another_io_handle.get()), nullptr); +} + +TEST_F(TestReverseTunnelAcceptor, IpFamilySupported) { + EXPECT_TRUE(socket_interface_->ipFamilySupported(AF_INET)); + EXPECT_TRUE(socket_interface_->ipFamilySupported(AF_INET6)); + EXPECT_FALSE(socket_interface_->ipFamilySupported(AF_UNIX)); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_reverse_connection_io_handle_test.cc b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_reverse_connection_io_handle_test.cc new file mode 100644 index 0000000000000..bd803318597d5 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_reverse_connection_io_handle_test.cc @@ -0,0 +1,72 @@ +#include + +#include "source/common/network/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_connection_io_handle.h" + +#include "test/mocks/network/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class UpstreamReverseConnectionIOHandleTest : public testing::Test { +protected: + UpstreamReverseConnectionIOHandleTest() { + mock_socket_ = std::make_unique>(); + + auto mock_io_handle = std::make_unique>(); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(Return(123)); + EXPECT_CALL(*mock_socket_, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + + mock_socket_->io_handle_ = std::move(mock_io_handle); + + io_handle_ = std::make_unique(std::move(mock_socket_), + "test-cluster"); + } + + void TearDown() override { io_handle_.reset(); } + + std::unique_ptr> mock_socket_; + std::unique_ptr io_handle_; +}; + +TEST_F(UpstreamReverseConnectionIOHandleTest, ConnectReturnsSuccess) { + auto address = Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 8080); + + auto result = io_handle_->connect(address); + + EXPECT_EQ(result.return_value_, 0); + EXPECT_EQ(result.errno_, 0); +} + +TEST_F(UpstreamReverseConnectionIOHandleTest, CloseCleansUpSocket) { + auto result = io_handle_->close(); + + EXPECT_EQ(result.err_, nullptr); +} + +TEST_F(UpstreamReverseConnectionIOHandleTest, GetSocketReturnsConstReference) { + const auto& socket = io_handle_->getSocket(); + + EXPECT_NE(&socket, nullptr); +} + +TEST_F(UpstreamReverseConnectionIOHandleTest, ShutdownIgnoredWhenOwned) { + auto result = io_handle_->shutdown(SHUT_RDWR); + EXPECT_EQ(result.return_value_, 0); + EXPECT_EQ(result.errno_, 0); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager_test.cc b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager_test.cc new file mode 100644 index 0000000000000..8f37d69e50e04 --- /dev/null +++ b/test/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager_test.cc @@ -0,0 +1,760 @@ +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" + +#include "source/common/network/utility.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class TestUpstreamSocketManager : public testing::Test { +protected: + TestUpstreamSocketManager() { + // Set up the stats scope + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + + // Create the config + config_.set_stat_prefix("test_prefix"); + + // Create the socket interface + socket_interface_ = std::make_unique(context_); + + // Create the extension + extension_ = + std::make_unique(*socket_interface_, context_, config_); + + // Set up mock dispatcher with default expectations + EXPECT_CALL(dispatcher_, createTimer_(_)) + .WillRepeatedly(testing::ReturnNew>()); + EXPECT_CALL(dispatcher_, createFileEvent_(_, _, _, _)) + .WillRepeatedly(testing::ReturnNew>()); + + // Create the socket manager with real extension + socket_manager_ = std::make_unique(dispatcher_, extension_.get()); + } + + void TearDown() override { + socket_manager_.reset(); + extension_.reset(); + socket_interface_.reset(); + } + + // Helper methods to access private members (friend class works for these methods) + void verifyInitialState() { + EXPECT_EQ(socket_manager_->accepted_reverse_connections_.size(), 0); + EXPECT_EQ(socket_manager_->fd_to_node_map_.size(), 0); + EXPECT_EQ(socket_manager_->node_to_cluster_map_.size(), 0); + EXPECT_EQ(socket_manager_->cluster_to_node_map_.size(), 0); + } + + bool verifyFDToNodeMap(int fd) { + return socket_manager_->fd_to_node_map_.find(fd) != socket_manager_->fd_to_node_map_.end(); + } + + bool verifyFDToEventMap(int fd) { + return socket_manager_->fd_to_event_map_.find(fd) != socket_manager_->fd_to_event_map_.end(); + } + + bool verifyFDToTimerMap(int fd) { + return socket_manager_->fd_to_timer_map_.find(fd) != socket_manager_->fd_to_timer_map_.end(); + } + + size_t getFDToEventMapSize() { return socket_manager_->fd_to_event_map_.size(); } + size_t getFDToTimerMapSize() { return socket_manager_->fd_to_timer_map_.size(); } + + size_t verifyAcceptedReverseConnectionsMap(const std::string& node_id) { + auto it = socket_manager_->accepted_reverse_connections_.find(node_id); + if (it == socket_manager_->accepted_reverse_connections_.end()) { + return 0; + } + return it->second.size(); + } + + std::string getNodeToClusterMapping(const std::string& node_id) { + auto it = socket_manager_->node_to_cluster_map_.find(node_id); + if (it == socket_manager_->node_to_cluster_map_.end()) { + return ""; + } + return it->second; + } + + std::vector getClusterToNodeMapping(const std::string& cluster_id) { + auto it = socket_manager_->cluster_to_node_map_.find(cluster_id); + if (it == socket_manager_->cluster_to_node_map_.end()) { + return {}; + } + return it->second; + } + + size_t getNodeToClusterMapSize() { return socket_manager_->node_to_cluster_map_.size(); } + size_t getClusterToNodeMapSize() { return socket_manager_->cluster_to_node_map_.size(); } + size_t getAcceptedReverseConnectionsSize() { + return socket_manager_->accepted_reverse_connections_.size(); + } + + // Helper methods for the new test cases + void addNodeToClusterMapping(const std::string& node_id, const std::string& cluster_id) { + socket_manager_->node_to_cluster_map_[node_id] = cluster_id; + socket_manager_->cluster_to_node_map_[cluster_id].push_back(node_id); + } + + void addFDToNodeMapping(int fd, const std::string& node_id) { + socket_manager_->fd_to_node_map_[fd] = node_id; + } + + // Helper to create a mock socket with proper address setup + Network::ConnectionSocketPtr createMockSocket(int fd = 123, + const std::string& local_addr = "127.0.0.1:8080", + const std::string& remote_addr = "127.0.0.1:9090") { + auto socket = std::make_unique>(); + + // Parse local address (IP:port format) + auto local_colon_pos = local_addr.find(':'); + std::string local_ip = local_addr.substr(0, local_colon_pos); + uint32_t local_port = std::stoi(local_addr.substr(local_colon_pos + 1)); + auto local_address = Network::Utility::parseInternetAddressNoThrow(local_ip, local_port); + + // Parse remote address (IP:port format) + auto remote_colon_pos = remote_addr.find(':'); + std::string remote_ip = remote_addr.substr(0, remote_colon_pos); + uint32_t remote_port = std::stoi(remote_addr.substr(remote_colon_pos + 1)); + auto remote_address = Network::Utility::parseInternetAddressNoThrow(remote_ip, remote_port); + + // Create a mock IO handle and set it up + auto mock_io_handle = std::make_unique>(); + auto* mock_io_handle_ptr = mock_io_handle.get(); + EXPECT_CALL(*mock_io_handle_ptr, fdDoNotUse()).WillRepeatedly(Return(fd)); + EXPECT_CALL(*socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_ptr)); + + // Store the mock_io_handle in the socket + socket->io_handle_ = std::move(mock_io_handle); + + // Set up connection info provider with the desired addresses + socket->connection_info_provider_->setLocalAddress(local_address); + socket->connection_info_provider_->setRemoteAddress(remote_address); + + return socket; + } + + // Helper to get sockets for a node + std::list& getSocketsForNode(const std::string& node_id) { + return socket_manager_->accepted_reverse_connections_[node_id]; + } + + NiceMock context_; + NiceMock thread_local_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_; + + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface config_; + + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + std::unique_ptr socket_manager_; +}; + +TEST_F(TestUpstreamSocketManager, CreateUpstreamSocketManager) { + EXPECT_NE(socket_manager_, nullptr); + auto socket_manager_no_extension = std::make_unique(dispatcher_, nullptr); + EXPECT_NE(socket_manager_no_extension, nullptr); +} + +TEST_F(TestUpstreamSocketManager, GetUpstreamExtension) { + EXPECT_EQ(socket_manager_->getUpstreamExtension(), extension_.get()); + auto socket_manager_no_extension = std::make_unique(dispatcher_, nullptr); + EXPECT_EQ(socket_manager_no_extension->getUpstreamExtension(), nullptr); +} + +TEST_F(TestUpstreamSocketManager, AddConnectionSocketEmptyClusterId) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = ""; + const std::chrono::seconds ping_interval(30); + + verifyInitialState(); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + verifyInitialState(); + EXPECT_EQ(getFDToEventMapSize(), 0); + EXPECT_EQ(getFDToTimerMapSize(), 0); + + auto retrieved_socket = socket_manager_->getConnectionSocket(node_id); + EXPECT_EQ(retrieved_socket, nullptr); +} + +TEST_F(TestUpstreamSocketManager, AddConnectionSocketEmptyNodeId) { + auto socket = createMockSocket(456); + const std::string node_id = ""; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + verifyInitialState(); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + verifyInitialState(); + EXPECT_EQ(getFDToEventMapSize(), 0); + EXPECT_EQ(getFDToTimerMapSize(), 0); + + auto retrieved_socket = socket_manager_->getConnectionSocket(node_id); + EXPECT_EQ(retrieved_socket, nullptr); +} + +TEST_F(TestUpstreamSocketManager, AddAndGetMultipleSocketsSameNode) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + auto socket3 = createMockSocket(789); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + verifyInitialState(); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_TRUE(verifyFDToNodeMap(123)); + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + EXPECT_TRUE(verifyFDToNodeMap(456)); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket3), ping_interval, + false); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 3); + EXPECT_TRUE(verifyFDToNodeMap(789)); + + EXPECT_EQ(getFDToEventMapSize(), 3); + EXPECT_EQ(getFDToTimerMapSize(), 3); + + auto retrieved_socket1 = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket1, nullptr); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + + auto retrieved_socket2 = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket2, nullptr); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + + auto retrieved_socket3 = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket3, nullptr); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 0); + + auto retrieved_socket4 = socket_manager_->getConnectionSocket(node_id); + EXPECT_EQ(retrieved_socket4, nullptr); +} + +TEST_F(TestUpstreamSocketManager, AddAndGetSocketsMultipleNodes) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node1 = "node1"; + const std::string node2 = "node2"; + const std::string cluster1 = "cluster1"; + const std::string cluster2 = "cluster2"; + const std::chrono::seconds ping_interval(30); + + verifyInitialState(); + + socket_manager_->addConnectionSocket(node1, cluster1, std::move(socket1), ping_interval, false); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node1), 1); + EXPECT_EQ(getNodeToClusterMapping(node1), cluster1); + + socket_manager_->addConnectionSocket(node2, cluster2, std::move(socket2), ping_interval, false); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node1), 1); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node2), 1); + EXPECT_EQ(getNodeToClusterMapping(node1), cluster1); + EXPECT_EQ(getNodeToClusterMapping(node2), cluster2); + + EXPECT_EQ(getFDToEventMapSize(), 2); + EXPECT_EQ(getFDToTimerMapSize(), 2); + + auto retrieved_socket1 = socket_manager_->getConnectionSocket(node1); + EXPECT_NE(retrieved_socket1, nullptr); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node1), 0); + + auto retrieved_socket2 = socket_manager_->getConnectionSocket(node2); + EXPECT_NE(retrieved_socket2, nullptr); + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node2), 0); +} + +TEST_F(TestUpstreamSocketManager, TestGetNodeID) { + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + auto socket1 = createMockSocket(123); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + + std::string result_for_cluster = socket_manager_->getNodeID(cluster_id); + EXPECT_EQ(result_for_cluster, node_id); + + std::string result_for_node = socket_manager_->getNodeID(node_id); + EXPECT_EQ(result_for_node, node_id); + + const std::string non_existent_cluster = "non-existent-cluster"; + std::string result_for_non_existent = socket_manager_->getNodeID(non_existent_cluster); + EXPECT_EQ(result_for_non_existent, non_existent_cluster); +} + +TEST_F(TestUpstreamSocketManager, GetConnectionSocketEmpty) { + auto socket = socket_manager_->getConnectionSocket("non-existent-node"); + EXPECT_EQ(socket, nullptr); +} + +TEST_F(TestUpstreamSocketManager, CleanStaleNodeEntryWithActiveSockets) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + EXPECT_EQ(getClusterToNodeMapping(cluster_id).size(), 1); + + socket_manager_->cleanStaleNodeEntry(node_id); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + EXPECT_EQ(getClusterToNodeMapping(cluster_id).size(), 1); +} + +TEST_F(TestUpstreamSocketManager, CleanStaleNodeEntryClusterCleanup) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node1 = "node1"; + const std::string node2 = "node2"; + const std::string cluster_id = "shared-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node1, cluster_id, std::move(socket1), ping_interval, false); + socket_manager_->addConnectionSocket(node2, cluster_id, std::move(socket2), ping_interval, false); + + EXPECT_EQ(getNodeToClusterMapping(node1), cluster_id); + EXPECT_EQ(getNodeToClusterMapping(node2), cluster_id); + auto cluster_nodes = getClusterToNodeMapping(cluster_id); + EXPECT_EQ(cluster_nodes.size(), 2); + EXPECT_EQ(getClusterToNodeMapSize(), 1); + + auto retrieved_socket1 = socket_manager_->getConnectionSocket(node1); + EXPECT_NE(retrieved_socket1, nullptr); + + EXPECT_EQ(getNodeToClusterMapping(node1), ""); + EXPECT_EQ(getNodeToClusterMapping(node2), cluster_id); + cluster_nodes = getClusterToNodeMapping(cluster_id); + EXPECT_EQ(cluster_nodes.size(), 1); + EXPECT_EQ(cluster_nodes[0], node2); + EXPECT_EQ(getClusterToNodeMapSize(), 1); + + auto retrieved_socket2 = socket_manager_->getConnectionSocket(node2); + EXPECT_NE(retrieved_socket2, nullptr); + + EXPECT_EQ(getNodeToClusterMapping(node1), ""); + EXPECT_EQ(getNodeToClusterMapping(node2), ""); + cluster_nodes = getClusterToNodeMapping(cluster_id); + EXPECT_EQ(cluster_nodes.size(), 0); + EXPECT_EQ(getClusterToNodeMapSize(), 0); +} + +TEST_F(TestUpstreamSocketManager, FileEventAndTimerCleanup) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + + EXPECT_EQ(getFDToEventMapSize(), 2); + EXPECT_EQ(getFDToTimerMapSize(), 2); + + auto retrieved_socket1 = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket1, nullptr); + + EXPECT_FALSE(verifyFDToEventMap(123)); + EXPECT_FALSE(verifyFDToTimerMap(123)); + + auto retrieved_socket2 = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket2, nullptr); + + EXPECT_EQ(getFDToEventMapSize(), 0); + EXPECT_EQ(getFDToTimerMapSize(), 0); +} + +TEST_F(TestUpstreamSocketManager, MarkSocketNotPresentDead) { + socket_manager_->markSocketDead(999); + socket_manager_->markSocketDead(-1); + socket_manager_->markSocketDead(0); +} + +TEST_F(TestUpstreamSocketManager, MarkIdleSocketDead) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + EXPECT_TRUE(verifyFDToNodeMap(123)); + + socket_manager_->markSocketDead(123); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_FALSE(verifyFDToNodeMap(123)); + EXPECT_FALSE(verifyFDToEventMap(123)); + EXPECT_FALSE(verifyFDToTimerMap(123)); + + auto retrieved_socket = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket, nullptr); +} + +TEST_F(TestUpstreamSocketManager, MarkUsedSocketDead) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_TRUE(verifyFDToNodeMap(123)); + + auto retrieved_socket = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket, nullptr); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 0); + + socket_manager_->markSocketDead(123); + + EXPECT_FALSE(verifyFDToNodeMap(123)); + EXPECT_EQ(getNodeToClusterMapping(node_id), ""); + auto cluster_nodes = getClusterToNodeMapping(cluster_id); + EXPECT_EQ(cluster_nodes.size(), 0); +} + +TEST_F(TestUpstreamSocketManager, MarkSocketDeadTriggerCleanup) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + auto cluster_nodes = getClusterToNodeMapping(cluster_id); + EXPECT_EQ(cluster_nodes.size(), 1); + + socket_manager_->markSocketDead(123); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 0); + EXPECT_EQ(getNodeToClusterMapping(node_id), ""); + cluster_nodes = getClusterToNodeMapping(cluster_id); + EXPECT_EQ(cluster_nodes.size(), 0); + EXPECT_EQ(getAcceptedReverseConnectionsSize(), 0); +} + +TEST_F(TestUpstreamSocketManager, MarkSocketDeadMultipleSockets) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + auto socket3 = createMockSocket(789); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket3), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 3); + EXPECT_EQ(getFDToEventMapSize(), 3); + EXPECT_EQ(getFDToTimerMapSize(), 3); + + socket_manager_->markSocketDead(123); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + EXPECT_EQ(getFDToEventMapSize(), 2); + EXPECT_EQ(getFDToTimerMapSize(), 2); + EXPECT_FALSE(verifyFDToNodeMap(123)); + EXPECT_FALSE(verifyFDToEventMap(123)); + EXPECT_FALSE(verifyFDToTimerMap(123)); + EXPECT_TRUE(verifyFDToNodeMap(456)); + EXPECT_TRUE(verifyFDToNodeMap(789)); + + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + + socket_manager_->markSocketDead(456); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_FALSE(verifyFDToNodeMap(456)); + EXPECT_FALSE(verifyFDToEventMap(456)); + EXPECT_FALSE(verifyFDToTimerMap(456)); + EXPECT_TRUE(verifyFDToNodeMap(789)); + + socket_manager_->markSocketDead(789); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 0); + EXPECT_EQ(getNodeToClusterMapping(node_id), ""); + EXPECT_EQ(getAcceptedReverseConnectionsSize(), 0); + EXPECT_EQ(getFDToEventMapSize(), 0); + EXPECT_EQ(getFDToTimerMapSize(), 0); +} + +TEST_F(TestUpstreamSocketManager, PingConnectionsWriteSuccess) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + + auto& sockets = getSocketsForNode(node_id); + auto* mock_io_handle1 = + dynamic_cast*>(&sockets.front()->ioHandle()); + auto* mock_io_handle2 = + dynamic_cast*>(&sockets.back()->ioHandle()); + + EXPECT_CALL(*mock_io_handle1, write(_)) + .WillRepeatedly(Invoke([](Buffer::Instance& buffer) -> Api::IoCallUint64Result { + buffer.drain(buffer.length()); + return Api::IoCallUint64Result{0, Network::IoSocketError::getIoSocketEagainError()}; + })); + EXPECT_CALL(*mock_io_handle2, write(_)) + .WillRepeatedly(Invoke([](Buffer::Instance& buffer) -> Api::IoCallUint64Result { + buffer.drain(buffer.length()); + return Api::IoCallUint64Result{0, Network::IoSocketError::getIoSocketEagainError()}; + })); + + socket_manager_->pingConnections(); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); +} + +TEST_F(TestUpstreamSocketManager, PingConnectionsWriteFailure) { + auto socket1 = createMockSocket(123); + auto socket2 = createMockSocket(456); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket1), ping_interval, + false); + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket2), ping_interval, + false); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 2); + + auto& sockets = getSocketsForNode(node_id); + auto* mock_io_handle1 = + dynamic_cast*>(&sockets.front()->ioHandle()); + auto* mock_io_handle2 = + dynamic_cast*>(&sockets.back()->ioHandle()); + + EXPECT_CALL(*mock_io_handle1, write(_)) + .Times(1) + .WillRepeatedly(Invoke([](Buffer::Instance& buffer) -> Api::IoCallUint64Result { + buffer.drain(buffer.length()); + return Api::IoCallUint64Result{0, Network::IoSocketError::create(ECONNRESET)}; + })); + + socket_manager_->pingConnections(node_id); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 1); + EXPECT_FALSE(verifyFDToNodeMap(123)); + EXPECT_TRUE(verifyFDToNodeMap(456)); + EXPECT_EQ(getNodeToClusterMapping(node_id), cluster_id); + EXPECT_EQ(getAcceptedReverseConnectionsSize(), 1); + + EXPECT_CALL(*mock_io_handle2, write(_)) + .Times(1) + .WillRepeatedly(Invoke([](Buffer::Instance& buffer) -> Api::IoCallUint64Result { + buffer.drain(buffer.length()); + return Api::IoCallUint64Result{0, Network::IoSocketError::create(EPIPE)}; + })); + + socket_manager_->pingConnections(node_id); + + EXPECT_EQ(verifyAcceptedReverseConnectionsMap(node_id), 0); + EXPECT_FALSE(verifyFDToNodeMap(123)); + EXPECT_FALSE(verifyFDToNodeMap(456)); + EXPECT_EQ(getNodeToClusterMapping(node_id), ""); + EXPECT_EQ(getAcceptedReverseConnectionsSize(), 0); +} + +TEST_F(TestUpstreamSocketManager, OnPingResponseValidResponse) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + auto mock_io_handle = std::make_unique>(); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(Return(123)); + + const std::string ping_response = "RPING"; + EXPECT_CALL(*mock_io_handle, read(_, _)) + .WillOnce([&](Buffer::Instance& buffer, absl::optional) -> Api::IoCallUint64Result { + buffer.add(ping_response); + return Api::IoCallUint64Result{ping_response.size(), Api::IoError::none()}; + }); + + socket_manager_->onPingResponse(*mock_io_handle); + + EXPECT_TRUE(verifyFDToNodeMap(123)); +} + +TEST_F(TestUpstreamSocketManager, OnPingResponseReadError) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + auto mock_io_handle = std::make_unique>(); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(Return(123)); + + EXPECT_CALL(*mock_io_handle, read(_, _)) + .WillOnce( + Return(Api::IoCallUint64Result{0, Network::IoSocketError::getIoSocketEagainError()})); + + socket_manager_->onPingResponse(*mock_io_handle); + + EXPECT_FALSE(verifyFDToNodeMap(123)); +} + +TEST_F(TestUpstreamSocketManager, OnPingResponseConnectionClosed) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + auto mock_io_handle = std::make_unique>(); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(Return(123)); + + EXPECT_CALL(*mock_io_handle, read(_, _)) + .WillOnce(Return(Api::IoCallUint64Result{0, Api::IoError::none()})); + + socket_manager_->onPingResponse(*mock_io_handle); + + EXPECT_FALSE(verifyFDToNodeMap(123)); +} + +TEST_F(TestUpstreamSocketManager, OnPingResponseInvalidData) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + auto mock_io_handle = std::make_unique>(); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(Return(123)); + + const std::string invalid_response = "INVALID_DATA"; + EXPECT_CALL(*mock_io_handle, read(_, _)) + .WillOnce([&](Buffer::Instance& buffer, absl::optional) -> Api::IoCallUint64Result { + buffer.add(invalid_response); + return Api::IoCallUint64Result{invalid_response.size(), Api::IoError::none()}; + }); + + // First invalid response should increment miss count but not immediately remove the fd. + socket_manager_->onPingResponse(*mock_io_handle); + EXPECT_TRUE(verifyFDToNodeMap(123)); + + // Simulate two more timeouts to cross the default threshold (3). + socket_manager_->onPingTimeout(123); + socket_manager_->onPingTimeout(123); + EXPECT_FALSE(verifyFDToNodeMap(123)); +} + +TEST_F(TestUpstreamSocketManager, GetConnectionSocketNoSocketsButValidMapping) { + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + + addNodeToClusterMapping(node_id, cluster_id); + + auto socket = socket_manager_->getConnectionSocket(node_id); + EXPECT_EQ(socket, nullptr); +} + +TEST_F(TestUpstreamSocketManager, MarkSocketDeadInvalidSocketNotInPool) { + auto socket = createMockSocket(123); + const std::string node_id = "test-node"; + const std::string cluster_id = "test-cluster"; + const std::chrono::seconds ping_interval(30); + + socket_manager_->addConnectionSocket(node_id, cluster_id, std::move(socket), ping_interval, + false); + + auto retrieved_socket = socket_manager_->getConnectionSocket(node_id); + EXPECT_NE(retrieved_socket, nullptr); + + addFDToNodeMapping(123, node_id); + + socket_manager_->markSocketDead(123); + + EXPECT_FALSE(verifyFDToNodeMap(123)); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/bootstrap/wasm/config_test.cc b/test/extensions/bootstrap/wasm/config_test.cc index b2c66245a9c95..77b6d52528f81 100644 --- a/test/extensions/bootstrap/wasm/config_test.cc +++ b/test/extensions/bootstrap/wasm/config_test.cc @@ -119,7 +119,7 @@ TEST_P(WasmFactoryTest, UnknownRuntime) { } TEST_P(WasmFactoryTest, StartFailed) { - ProtobufWkt::StringValue plugin_configuration; + Protobuf::StringValue plugin_configuration; plugin_configuration.set_value("bad"); config_.mutable_config()->mutable_vm_config()->mutable_configuration()->PackFrom( plugin_configuration); @@ -129,7 +129,7 @@ TEST_P(WasmFactoryTest, StartFailed) { } TEST_P(WasmFactoryTest, ConfigureFailed) { - ProtobufWkt::StringValue plugin_configuration; + Protobuf::StringValue plugin_configuration; plugin_configuration.set_value("bad"); config_.mutable_config()->mutable_configuration()->PackFrom(plugin_configuration); diff --git a/test/extensions/clusters/aggregate/cluster_integration_test.cc b/test/extensions/clusters/aggregate/cluster_integration_test.cc index 5d8af67437583..fdb1cc51e5635 100644 --- a/test/extensions/clusters/aggregate/cluster_integration_test.cc +++ b/test/extensions/clusters/aggregate/cluster_integration_test.cc @@ -230,8 +230,8 @@ class AggregateIntegrationTest acceptXdsConnection(); // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for cluster_1. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "55"); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); @@ -265,9 +265,9 @@ TEST_P(AggregateIntegrationTest, ClusterUpDownUp) { testRouterHeaderOnlyRequestAndResponse(nullptr, FirstUpstreamIndex, "/aggregatecluster"); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {FirstClusterName}, "42"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {FirstClusterName}, "42"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. test_server_->waitForCounterGe("cluster_manager.cluster_removed", 1); @@ -283,8 +283,8 @@ TEST_P(AggregateIntegrationTest, ClusterUpDownUp) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is back. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "42", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "42", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "413"); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); @@ -302,9 +302,9 @@ TEST_P(AggregateIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_2 is here. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); // The '4' includes the fake CDS server and aggregate cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); @@ -314,9 +314,9 @@ TEST_P(AggregateIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "42", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "42", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster2_}, {}, {FirstClusterName}, "43"); + Config::TestTypeUrl::get().Cluster, {cluster2_}, {}, {FirstClusterName}, "43"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. test_server_->waitForCounterGe("cluster_manager.cluster_removed", 1); @@ -326,9 +326,9 @@ TEST_P(AggregateIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is back. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "43", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "43", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_}, {}, "413"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_}, {}, "413"); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); testRouterHeaderOnlyRequestAndResponse(nullptr, FirstUpstreamIndex, "/aggregatecluster"); @@ -344,7 +344,7 @@ TEST_P(AggregateIntegrationTest, PreviousPrioritiesRetryPredicate) { // Tell Envoy that cluster_2 is here. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); // The '4' includes the fake CDS server and aggregate cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); @@ -398,8 +398,8 @@ TEST_P(AggregateIntegrationTest, CircuitBreakerTestMaxConnections) { setCircuitBreakerLimits(cluster1_, CircuitBreakerLimits{}.withMaxConnections(1)); setMaxConcurrentStreams(cluster1_, 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "56"); test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); @@ -518,8 +518,8 @@ TEST_P(AggregateIntegrationTest, CircuitBreakerTestMaxRequests) { setCircuitBreakerLimits(cluster1_, CircuitBreakerLimits{}.withMaxRequests(1)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "56"); test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); @@ -642,8 +642,8 @@ TEST_P(AggregateIntegrationTest, CircuitBreakerTestMaxPendingRequests) { CircuitBreakerLimits{}.withMaxConnections(1).withMaxPendingRequests(1)); setMaxConcurrentStreams(cluster1_, 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "56"); test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); @@ -767,6 +767,8 @@ TEST_P(AggregateIntegrationTest, CircuitBreakerTestMaxPendingRequests) { TEST_P(AggregateIntegrationTest, CircuitBreakerMaxRetriesTest) { setDownstreamProtocol(Http::CodecType::HTTP2); + config_helper_.setDownstreamHttp2MaxConcurrentStreams(2048); + config_helper_.setUpstreamHttp2MaxConcurrentStreams(2048); config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* static_resources = bootstrap.mutable_static_resources(); auto* listener = static_resources->mutable_listeners(0); @@ -794,8 +796,8 @@ TEST_P(AggregateIntegrationTest, CircuitBreakerMaxRetriesTest) { setCircuitBreakerLimits(cluster1_, CircuitBreakerLimits{}.withMaxRetries(1)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "56"); test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); diff --git a/test/extensions/clusters/aggregate/cluster_test.cc b/test/extensions/clusters/aggregate/cluster_test.cc index 0ccccccbbaed7..0c1287ca5095c 100644 --- a/test/extensions/clusters/aggregate/cluster_test.cc +++ b/test/extensions/clusters/aggregate/cluster_test.cc @@ -73,7 +73,7 @@ class AggregateClusterTest : public Event::TestUsingSimulatedTime, public testin priority, Upstream::HostSetImpl::partitionHosts(std::make_shared(hosts), Upstream::HostsPerLocalityImpl::empty()), - nullptr, hosts, {}, 123, absl::nullopt, 100); + nullptr, hosts, {}, absl::nullopt, 100); } void setupSecondary(int priority, int healthy_hosts, int degraded_hosts, int unhealthy_hosts) { @@ -83,7 +83,7 @@ class AggregateClusterTest : public Event::TestUsingSimulatedTime, public testin priority, Upstream::HostSetImpl::partitionHosts(std::make_shared(hosts), Upstream::HostsPerLocalityImpl::empty()), - nullptr, hosts, {}, 123, absl::nullopt, 100); + nullptr, hosts, {}, absl::nullopt, 100); } void setupPrioritySet() { diff --git a/test/extensions/clusters/aggregate/cluster_update_test.cc b/test/extensions/clusters/aggregate/cluster_update_test.cc index 4be27c4897ffc..85c44f33f8d91 100644 --- a/test/extensions/clusters/aggregate/cluster_update_test.cc +++ b/test/extensions/clusters/aggregate/cluster_update_test.cc @@ -35,10 +35,7 @@ envoy::config::bootstrap::v3::Bootstrap parseBootstrapFromV2Yaml(const std::stri class AggregateClusterUpdateTest : public Event::TestUsingSimulatedTime, public testing::TestWithParam { public: - AggregateClusterUpdateTest() - : ads_mux_(std::make_shared>()), - http_context_(stats_store_.symbolTable()), grpc_context_(stats_store_.symbolTable()), - router_context_(stats_store_.symbolTable()) {} + AggregateClusterUpdateTest() : ads_mux_(std::make_shared>()) {} void initialize(const std::string& yaml_config) { auto bootstrap = parseBootstrapFromV2Yaml(yaml_config); @@ -46,11 +43,9 @@ class AggregateClusterUpdateTest : public Event::TestUsingSimulatedTime, bootstrap.mutable_cluster_manager()->set_enable_deferred_cluster_creation(use_deferred_cluster); // Replace the adsMux to have mocked GrpcMux object that will allow invoking // methods when creating the cluster-manager. - ON_CALL(xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); + ON_CALL(factory_.server_context_.xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); cluster_manager_ = Upstream::TestClusterManagerImpl::createTestClusterManager( - bootstrap, factory_, factory_.server_context_, factory_.stats_, factory_.tls_, - factory_.runtime_, factory_.local_info_, log_manager_, factory_.dispatcher_, admin_, - *factory_.api_, http_context_, grpc_context_, router_context_, server_, xds_manager_); + bootstrap, factory_, factory_.server_context_); ON_CALL(factory_.server_context_, clusterManager()).WillByDefault(ReturnRef(*cluster_manager_)); THROW_IF_NOT_OK(cluster_manager_->initialize(bootstrap)); @@ -59,18 +54,10 @@ class AggregateClusterUpdateTest : public Event::TestUsingSimulatedTime, cluster_ = cluster_manager_->getThreadLocalCluster("aggregate_cluster"); } - Stats::IsolatedStoreImpl stats_store_; - NiceMock admin_; NiceMock factory_; Upstream::ThreadLocalCluster* cluster_; std::shared_ptr> ads_mux_; - NiceMock xds_manager_; std::unique_ptr cluster_manager_; - AccessLog::MockAccessLogManager log_manager_; - Http::ContextImpl http_context_; - Grpc::ContextImpl grpc_context_; - Router::ContextImpl router_context_; - NiceMock server_; const std::string default_yaml_config_ = R"EOF( static_resources: @@ -168,7 +155,7 @@ TEST_P(AggregateClusterUpdateTest, LoadBalancingTest) { Upstream::HostSetImpl::partitionHosts( std::make_shared(Upstream::HostVector{host1, host2, host3}), Upstream::HostsPerLocalityImpl::empty()), - nullptr, {host1, host2, host3}, {}, 0, absl::nullopt, 100); + nullptr, {host1, host2, host3}, {}, absl::nullopt, 100); // Set up the HostSet with 1 healthy, 1 degraded and 1 unhealthy. Upstream::HostSharedPtr host4 = Upstream::makeTestHost(secondary->info(), "tcp://127.0.0.4:80"); @@ -182,7 +169,7 @@ TEST_P(AggregateClusterUpdateTest, LoadBalancingTest) { Upstream::HostSetImpl::partitionHosts( std::make_shared(Upstream::HostVector{host4, host5, host6}), Upstream::HostsPerLocalityImpl::empty()), - nullptr, {host4, host5, host6}, {}, 0, absl::nullopt, 100); + nullptr, {host4, host5, host6}, {}, absl::nullopt, 100); Upstream::HostConstSharedPtr host; for (int i = 0; i < 33; ++i) { @@ -219,7 +206,7 @@ TEST_P(AggregateClusterUpdateTest, LoadBalancingTest) { Upstream::HostSetImpl::partitionHosts( std::make_shared(Upstream::HostVector{host7, host8, host9}), Upstream::HostsPerLocalityImpl::empty()), - nullptr, {host7, host8, host9}, {}, 0, absl::nullopt, 100); + nullptr, {host7, host8, host9}, {}, absl::nullopt, 100); // Priority set // Priority 0: 1/3 healthy, 1/3 degraded @@ -279,11 +266,9 @@ TEST_P(AggregateClusterUpdateTest, InitializeAggregateClusterAfterOtherClusters) auto bootstrap = parseBootstrapFromV2Yaml(config); // Replace the adsMux to have mocked GrpcMux object that will allow invoking // methods when creating the cluster-manager. - ON_CALL(xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); + ON_CALL(factory_.server_context_.xds_manager_, adsMux()).WillByDefault(Return(ads_mux_)); cluster_manager_ = Upstream::TestClusterManagerImpl::createTestClusterManager( - bootstrap, factory_, factory_.server_context_, factory_.stats_, factory_.tls_, - factory_.runtime_, factory_.local_info_, log_manager_, factory_.dispatcher_, admin_, - *factory_.api_, http_context_, grpc_context_, router_context_, server_, xds_manager_); + bootstrap, factory_, factory_.server_context_); ON_CALL(factory_.server_context_, clusterManager()).WillByDefault(ReturnRef(*cluster_manager_)); THROW_IF_NOT_OK(cluster_manager_->initialize(bootstrap)); @@ -309,7 +294,7 @@ TEST_P(AggregateClusterUpdateTest, InitializeAggregateClusterAfterOtherClusters) Upstream::HostSetImpl::partitionHosts( std::make_shared(Upstream::HostVector{host1, host2, host3}), Upstream::HostsPerLocalityImpl::empty()), - nullptr, {host1, host2, host3}, {}, 0, absl::nullopt, 100); + nullptr, {host1, host2, host3}, {}, absl::nullopt, 100); for (int i = 0; i < 50; ++i) { EXPECT_CALL(factory_.random_, random()).WillRepeatedly(Return(i)); diff --git a/test/extensions/clusters/common/BUILD b/test/extensions/clusters/common/BUILD index 82b2239190f6b..13f1deed35e8e 100644 --- a/test/extensions/clusters/common/BUILD +++ b/test/extensions/clusters/common/BUILD @@ -40,7 +40,6 @@ envoy_cc_test( "//source/common/config:utility_lib", "//source/extensions/clusters/common:logical_host_lib", "//source/extensions/clusters/dns:dns_cluster_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", "//test/integration:http_integration_lib", "//test/mocks/network:network_mocks", "//test/test_common:registry_lib", diff --git a/test/extensions/clusters/logical_dns/BUILD b/test/extensions/clusters/logical_dns/BUILD index 5c1608f9b9e8f..8836173b6a5e9 100644 --- a/test/extensions/clusters/logical_dns/BUILD +++ b/test/extensions/clusters/logical_dns/BUILD @@ -18,7 +18,6 @@ envoy_cc_test( "//source/common/network:utility_lib", "//source/common/upstream:upstream_lib", "//source/extensions/clusters/dns:dns_cluster_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", "//source/extensions/load_balancing_policies/round_robin:config", "//source/extensions/transport_sockets/raw_buffer:config", "//source/server:transport_socket_config_lib", @@ -33,6 +32,7 @@ envoy_cc_test( "//test/mocks/ssl:ssl_mocks", "//test/mocks/thread_local:thread_local_mocks", "//test/mocks/upstream:cluster_manager_mocks", + "//test/test_common:test_runtime_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/test/extensions/clusters/logical_dns/logical_dns_cluster_test.cc b/test/extensions/clusters/logical_dns/logical_dns_cluster_test.cc index 27249cbc543a6..75531cee4898e 100644 --- a/test/extensions/clusters/logical_dns/logical_dns_cluster_test.cc +++ b/test/extensions/clusters/logical_dns/logical_dns_cluster_test.cc @@ -13,6 +13,7 @@ #include "source/common/network/utility.h" #include "source/common/singleton/manager_impl.h" +#include "source/common/upstream/upstream_impl.h" #include "source/extensions/clusters/common/dns_cluster_backcompat.h" #include "source/extensions/clusters/dns/dns_cluster.h" #include "source/extensions/clusters/logical_dns/logical_dns_cluster.h" @@ -29,6 +30,7 @@ #include "test/mocks/ssl/mocks.h" #include "test/mocks/thread_local/mocks.h" #include "test/mocks/upstream/cluster_manager.h" +#include "test/test_common/test_runtime.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" @@ -59,7 +61,7 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi envoy::config::cluster::v3::Cluster cluster_config = parseClusterFromV3Yaml(yaml); Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr, false); - absl::StatusOr> status_or_cluster; + absl::StatusOr> status_or_cluster; envoy::extensions::clusters::dns::v3::DnsCluster dns_cluster{}; if (cluster_config.has_cluster_type()) { @@ -76,8 +78,15 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi createDnsClusterFromLegacyFields(cluster_config, dns_cluster); } - status_or_cluster = - LogicalDnsCluster::create(cluster_config, dns_cluster, factory_context, dns_resolver_); + // Here we tell the DnsClusterImpl it's going to behave like a logic DNS cluster: + dns_cluster.set_all_addresses_in_single_endpoint(true); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + status_or_cluster = + DnsClusterImpl::create(cluster_config, dns_cluster, factory_context, dns_resolver_); + } else { + status_or_cluster = + LogicalDnsCluster::create(cluster_config, dns_cluster, factory_context, dns_resolver_); + } THROW_IF_NOT_OK_REF(status_or_cluster.status()); cluster_ = std::move(*status_or_cluster); priority_update_cb_ = cluster_->prioritySet().addPriorityUpdateCb( @@ -100,7 +109,12 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi auto status_or_cluster = ClusterFactoryImplBase::create(cluster_config, server_context_, resolver_fn, nullptr, false); if (status_or_cluster.ok()) { - cluster_ = std::dynamic_pointer_cast(status_or_cluster->first); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.enable_new_dns_implementation")) { + cluster_ = std::dynamic_pointer_cast(status_or_cluster->first); + } else { + cluster_ = std::dynamic_pointer_cast(status_or_cluster->first); + } priority_update_cb_ = cluster_->prioritySet().addPriorityUpdateCb( [&](uint32_t, const HostVector&, const HostVector&) { membership_updated_.ready(); @@ -141,6 +155,16 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"})); + const auto previous_update_no_rebuild = + cluster_->info()->configUpdateStats().update_no_rebuild_.value(); + + EXPECT_CALL(*resolve_timer_, enableTimer(std::chrono::milliseconds(4000), _)); + dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", + TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"})); + + EXPECT_EQ(previous_update_no_rebuild + 1, + cluster_->info()->configUpdateStats().update_no_rebuild_.value()); + EXPECT_EQ(1UL, cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size()); EXPECT_EQ(1UL, cluster_->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); EXPECT_EQ(1UL, @@ -167,11 +191,14 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi expectResolve(Network::DnsLookupFamily::V4Only, expected_address); resolve_timer_->invokeCallback(); - // Should not cause any changes. + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + EXPECT_CALL(membership_updated_, ready()); + } EXPECT_CALL(*resolve_timer_, enableTimer(_, _)); dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2", "127.0.0.3"})); + logical_host = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts()[0]; EXPECT_EQ("127.0.0.1:" + std::to_string(expected_hc_port), logical_host->healthCheckAddress()->asString()); EXPECT_EQ("127.0.0.1:" + std::to_string(expected_port), logical_host->address()->asString()); @@ -208,9 +235,13 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi // Should cause a change. EXPECT_CALL(*resolve_timer_, enableTimer(_, _)); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + EXPECT_CALL(membership_updated_, ready()); + } dns_callback_(Network::DnsResolver::ResolutionStatus::Completed, "", TestUtility::makeDnsResponse({"127.0.0.3", "127.0.0.1", "127.0.0.2"})); + logical_host = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts()[0]; EXPECT_EQ("127.0.0.3:" + std::to_string(expected_hc_port), logical_host->healthCheckAddress()->asString()); EXPECT_EQ("127.0.0.3:" + std::to_string(expected_port), logical_host->address()->asString()); @@ -267,7 +298,8 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi Event::MockTimer* resolve_timer_; ReadyWatcher membership_updated_; ReadyWatcher initialized_; - std::shared_ptr cluster_; + std::shared_ptr cluster_; + TestScopedRuntime scoped_runtime_; NiceMock validation_visitor_; Common::CallbackHandlePtr priority_update_cb_; NiceMock access_log_manager_; @@ -275,35 +307,39 @@ class LogicalDnsClusterTest : public Event::TestUsingSimulatedTime, public testi namespace { using LogicalDnsConfigTuple = - std::tuple>; + std::tuple, std::string>; std::vector generateLogicalDnsParams() { std::vector dns_config; { std::string family_yaml(""); Network::DnsLookupFamily family(Network::DnsLookupFamily::Auto); std::list dns_response{"127.0.0.1", "127.0.0.2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); } { std::string family_yaml(R"EOF(dns_lookup_family: v4_only )EOF"); Network::DnsLookupFamily family(Network::DnsLookupFamily::V4Only); std::list dns_response{"127.0.0.1", "127.0.0.2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); } { std::string family_yaml(R"EOF(dns_lookup_family: v6_only )EOF"); Network::DnsLookupFamily family(Network::DnsLookupFamily::V6Only); std::list dns_response{"::1", "::2"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); } { std::string family_yaml(R"EOF(dns_lookup_family: auto )EOF"); Network::DnsLookupFamily family(Network::DnsLookupFamily::Auto); std::list dns_response{"::1"}; - dns_config.push_back(std::make_tuple(family_yaml, family, dns_response)); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "false")); + dns_config.push_back(std::make_tuple(family_yaml, family, dns_response, "true")); } return dns_config; } @@ -318,6 +354,9 @@ INSTANTIATE_TEST_SUITE_P(DnsParam, LogicalDnsParamTest, // constructor, we have the expected host state and initialization callback // invocation. TEST_P(LogicalDnsParamTest, ImmediateResolve) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", std::get<3>(GetParam())}}); + const std::string yaml = R"EOF( name: name connect_timeout: 0.25s @@ -354,7 +393,16 @@ TEST_P(LogicalDnsParamTest, ImmediateResolve) { HealthCheckHostMonitor::UnhealthyType::ImmediateHealthCheckFail); } -TEST_F(LogicalDnsParamTest, FailureRefreshRateBackoffResetsWhenSuccessHappens) { +class LogicalDnsImplementationsTest : public LogicalDnsClusterTest, + public testing::WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P(DnsImplementations, LogicalDnsImplementationsTest, + testing::ValuesIn({"true", "false"})); + +TEST_P(LogicalDnsImplementationsTest, FailureRefreshRateBackoffResetsWhenSuccessHappens) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name type: LOGICAL_DNS @@ -402,7 +450,10 @@ TEST_F(LogicalDnsParamTest, FailureRefreshRateBackoffResetsWhenSuccessHappens) { dns_callback_(Network::DnsResolver::ResolutionStatus::Failure, "", {}); } -TEST_F(LogicalDnsParamTest, TtlAsDnsRefreshRate) { +TEST_P(LogicalDnsImplementationsTest, TtlAsDnsRefreshRate) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string yaml = R"EOF( name: name type: LOGICAL_DNS @@ -444,7 +495,9 @@ TEST_F(LogicalDnsParamTest, TtlAsDnsRefreshRate) { TestUtility::makeDnsResponse({}, std::chrono::seconds(5))); } -TEST_F(LogicalDnsClusterTest, BadConfig) { +TEST_P(LogicalDnsImplementationsTest, BadConfig) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string multiple_hosts_yaml = R"EOF( name: name type: LOGICAL_DNS @@ -692,10 +745,26 @@ TEST_F(LogicalDnsClusterTest, BadConfig) { EXPECT_EQ(factorySetupFromV3Yaml(custom_resolver_cluster_type_yaml).message(), "LOGICAL_DNS clusters must NOT have a custom resolver name set"); + + const std::string no_load_assignment_yaml = R"EOF( + name: name + cluster_type: + name: abc + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dns.v3.DnsCluster + all_addresses_in_single_endpoint: true + connect_timeout: 0.25s + lb_policy: ROUND_ROBIN + )EOF"; + + EXPECT_EQ(factorySetupFromV3Yaml(no_load_assignment_yaml).message(), + "LOGICAL_DNS clusters must have a single host"); } // Test using both types of names in the cluster type. -TEST_F(LogicalDnsClusterTest, UseDnsExtension) { +TEST_P(LogicalDnsImplementationsTest, UseDnsExtension) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string config = R"EOF( name: name cluster_type: @@ -732,7 +801,9 @@ TEST_F(LogicalDnsClusterTest, UseDnsExtension) { TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"}, std::chrono::seconds(3000))); } -TEST_F(LogicalDnsClusterTest, TypedConfigBackcompat) { +TEST_P(LogicalDnsImplementationsTest, TypedConfigBackcompat) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string config = R"EOF( name: name cluster_type: @@ -766,7 +837,9 @@ TEST_F(LogicalDnsClusterTest, TypedConfigBackcompat) { TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"}, std::chrono::seconds(3000))); } -TEST_F(LogicalDnsClusterTest, Basic) { +TEST_P(LogicalDnsImplementationsTest, Basic) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string basic_yaml_hosts = R"EOF( name: name type: LOGICAL_DNS @@ -819,7 +892,9 @@ TEST_F(LogicalDnsClusterTest, Basic) { testBasicSetup(basic_yaml_load_assignment, "foo.bar.com", 443, 8000); } -TEST_F(LogicalDnsClusterTest, DontWaitForDNSOnInit) { +TEST_P(LogicalDnsImplementationsTest, DontWaitForDNSOnInit) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string config = R"EOF( name: name type: LOGICAL_DNS @@ -853,7 +928,9 @@ TEST_F(LogicalDnsClusterTest, DontWaitForDNSOnInit) { TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"})); } -TEST_F(LogicalDnsClusterTest, DNSRefreshHasJitter) { +TEST_P(LogicalDnsImplementationsTest, DNSRefreshHasJitter) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string config = R"EOF( name: name type: LOGICAL_DNS @@ -893,7 +970,9 @@ TEST_F(LogicalDnsClusterTest, DNSRefreshHasJitter) { TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"}, std::chrono::seconds(3000))); } -TEST_F(LogicalDnsClusterTest, NegativeDnsJitter) { +TEST_P(LogicalDnsImplementationsTest, NegativeDnsJitter) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); const std::string yaml = R"EOF( name: name type: LOGICAL_DNS @@ -913,7 +992,9 @@ TEST_F(LogicalDnsClusterTest, NegativeDnsJitter) { "(?s)Invalid duration: Expected positive duration:.*seconds: -1\n"); } -TEST_F(LogicalDnsClusterTest, ExtremeJitter) { +TEST_P(LogicalDnsImplementationsTest, ExtremeJitter) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); // When random returns large values, they were being reinterpreted as very negative values causing // negative refresh rates. const std::string jitter_yaml = R"EOF( @@ -950,6 +1031,87 @@ TEST_F(LogicalDnsClusterTest, ExtremeJitter) { TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"}, std::chrono::seconds(3000))); } +// This test makes sure that the logical DNS cluster updates not only the +// primary address of a host, but also the following addresses returned by +// the DNS response. This is important for Happy Eyeballs. +TEST_P(LogicalDnsImplementationsTest, LogicalDnsUpdatesEntireAddressList) { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.enable_new_dns_implementation", GetParam()}}); + const std::string config = R"EOF( + name: name + cluster_type: + name: cluster1 + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dns.v3.DnsCluster + dns_refresh_rate: 4s + dns_lookup_family: V4_ONLY + all_addresses_in_single_endpoint: true # logical DNS + lb_policy: ROUND_ROBIN + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: foo.bar.com + port_value: 443 + )EOF"; + + EXPECT_CALL(initialized_, ready()); + expectResolve(Network::DnsLookupFamily::V4Only, "foo.bar.com"); + ASSERT_TRUE(factorySetupFromV3Yaml(config).ok()); + + EXPECT_CALL(membership_updated_, ready()); + EXPECT_CALL(*resolve_timer_, enableTimer(testing::Ge(std::chrono::milliseconds(3000)), _)) + .Times(AnyNumber()); + + dns_callback_( + Network::DnsResolver::ResolutionStatus::Completed, "", + TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.2"}, std::chrono::seconds(3000))); + + auto logical_host = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts()[0]; + EXPECT_CALL(server_context_.dispatcher_, + createClientConnection_( + PointeesEq(*Network::Utility::resolveUrl("tcp://127.0.0.1:443")), _, _, _)) + .WillOnce(Return(new NiceMock())); + EXPECT_CALL(server_context_.dispatcher_, createTimer_(_)).Times(AnyNumber()); + + auto data = logical_host->createConnection(server_context_.dispatcher_, nullptr, nullptr); + ASSERT_NE(data.host_description_->addressListOrNull(), nullptr); + std::vector expected_addresses = {"127.0.0.1:443", "127.0.0.2:443"}; + std::vector actual_addresses; + for (const auto& addr : *data.host_description_->addressListOrNull()) { + actual_addresses.push_back(addr->asString()); + } + EXPECT_THAT(actual_addresses, ::testing::UnorderedElementsAreArray(expected_addresses)); + + expectResolve(Network::DnsLookupFamily::V4Only, "foo.bar.com"); + resolve_timer_->invokeCallback(); + + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_new_dns_implementation")) { + EXPECT_CALL(membership_updated_, ready()); + } + + dns_callback_( + Network::DnsResolver::ResolutionStatus::Completed, "", + TestUtility::makeDnsResponse({"127.0.0.1", "127.0.0.3"}, std::chrono::seconds(3000))); + + logical_host = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts()[0]; + EXPECT_CALL(server_context_.dispatcher_, + createClientConnection_( + PointeesEq(*Network::Utility::resolveUrl("tcp://127.0.0.1:443")), _, _, _)) + .WillOnce(Return(new NiceMock())); + EXPECT_CALL(server_context_.dispatcher_, createTimer_(_)).Times(AnyNumber()); + data = logical_host->createConnection(server_context_.dispatcher_, nullptr, nullptr); + ASSERT_NE(data.host_description_->addressListOrNull(), nullptr); + expected_addresses = {"127.0.0.1:443", "127.0.0.3:443"}; + actual_addresses.clear(); + for (const auto& addr : *data.host_description_->addressListOrNull()) { + actual_addresses.push_back(addr->asString()); + } + EXPECT_THAT(actual_addresses, ::testing::UnorderedElementsAreArray(expected_addresses)); +} + } // namespace } // namespace Upstream } // namespace Envoy diff --git a/test/extensions/clusters/redis/BUILD b/test/extensions/clusters/redis/BUILD index 6f0a607172659..d0b8031f86312 100644 --- a/test/extensions/clusters/redis/BUILD +++ b/test/extensions/clusters/redis/BUILD @@ -31,6 +31,7 @@ envoy_extension_cc_test( "//source/server:transport_socket_config_lib", "//test/common/upstream:utility_lib", "//test/extensions/clusters/redis:redis_cluster_mocks", + "//test/extensions/common/redis:mocks_lib", "//test/extensions/filters/network/common/redis:redis_mocks", "//test/extensions/filters/network/common/redis:test_utils_lib", "//test/extensions/filters/network/redis_proxy:redis_mocks", @@ -105,6 +106,7 @@ envoy_extension_cc_test( "//source/extensions/filters/network/redis_proxy:config", "//source/extensions/load_balancing_policies/cluster_provided:config", "//source/extensions/load_balancing_policies/round_robin:config", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:ads_integration_lib", "//test/integration:integration_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", diff --git a/test/extensions/clusters/redis/redis_cluster_integration_test.cc b/test/extensions/clusters/redis/redis_cluster_integration_test.cc index a2ea2415615e6..092d0f1e02e92 100644 --- a/test/extensions/clusters/redis/redis_cluster_integration_test.cc +++ b/test/extensions/clusters/redis/redis_cluster_integration_test.cc @@ -6,6 +6,7 @@ #include "source/common/common/macros.h" #include "source/extensions/filters/network/redis_proxy/command_splitter_impl.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/integration/ads_integration.h" #include "test/integration/integration.h" @@ -156,6 +157,13 @@ class RedisClusterIntegrationTest : public testing::TestWithParamset_name("envoy.network.dns_resolver.getaddrinfo"); + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + config; + typed_dns_resolver_config->mutable_typed_config()->PackFrom(config); + uint32_t upstream_idx = 0; auto* cluster_0 = bootstrap.mutable_static_resources()->mutable_clusters(0); if (version_ == Network::Address::IpVersion::v4) { @@ -677,34 +685,35 @@ TEST_P(RedisAdsIntegrationTest, RedisClusterRemoval) { initialize(); // Send initial configuration with a redis cluster and a redis proxy listener. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildRedisCluster("redis_cluster")}, + Config::TestTypeUrl::get().Cluster, {buildRedisCluster("redis_cluster")}, {buildRedisCluster("redis_cluster")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"redis_cluster"}, {"redis_cluster"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("redis_cluster")}, - {buildClusterLoadAssignment("redis_cluster")}, {}, "1"); + Config::TestTypeUrl::get().ClusterLoadAssignment, + {buildClusterLoadAssignment("redis_cluster")}, {buildClusterLoadAssignment("redis_cluster")}, + {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildRedisListener("listener_0", "redis_cluster")}, + Config::TestTypeUrl::get().Listener, {buildRedisListener("listener_0", "redis_cluster")}, {buildRedisListener("listener_0", "redis_cluster")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"redis_cluster"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); // Validate that redis listener is successfully created. test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); // Now send a CDS update, removing redis cluster added above. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("cluster_2")}, {buildCluster("cluster_2")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_2")}, {buildCluster("cluster_2")}, {"redis_cluster"}, "2"); // Validate that the cluster is removed successfully. diff --git a/test/extensions/clusters/redis/redis_cluster_test.cc b/test/extensions/clusters/redis/redis_cluster_test.cc index ae331cf2fa171..baa81cac8fe46 100644 --- a/test/extensions/clusters/redis/redis_cluster_test.cc +++ b/test/extensions/clusters/redis/redis_cluster_test.cc @@ -18,6 +18,7 @@ #include "test/common/upstream/utility.h" #include "test/extensions/clusters/redis/mocks.h" +#include "test/extensions/common/redis/mocks.h" #include "test/extensions/filters/network/common/redis/mocks.h" #include "test/mocks/common.h" #include "test/mocks/protobuf/mocks.h" @@ -1487,6 +1488,44 @@ TEST_F(RedisClusterTest, HostRemovalAfterHcFail) { */ } +// Test that verifies cluster destruction does not cause segfault when refresh manager +// triggers callback after cluster is destroyed. This reproduces the issue from #38585. +TEST_F(RedisClusterTest, NoSegfaultOnClusterDestructionWithPendingCallback) { + // This test verifies that destroying the cluster properly cleans up resources + // and doesn't cause a segfault. The key protection is in the destructor that + // sets is_destroying_ flag and cleans up the redis_discovery_session_. + + // Create the cluster with basic configuration + setupFromV3Yaml(BasicConfig); + const std::list resolved_addresses{"127.0.0.1"}; + expectResolveDiscovery(Network::DnsLookupFamily::V4Only, "foo.bar.com", resolved_addresses); + expectRedisResolve(true); + + cluster_->initialize([&]() { + initialized_.ready(); + return absl::OkStatus(); + }); + + EXPECT_CALL(membership_updated_, ready()); + EXPECT_CALL(initialized_, ready()); + EXPECT_CALL(*cluster_callback_, onClusterSlotUpdate(_, _)); + std::bitset single_slot_primary(0xfff); + std::bitset no_replica(0); + expectClusterSlotResponse(createResponse(single_slot_primary, no_replica)); + expectHealthyHosts(std::list({"127.0.0.1:22120"})); + + // Now destroy the cluster. With the fix in place (destructor setting is_destroying_ + // and resetting redis_discovery_session_), this should not crash. + // Without the fix, accessing resolve_timer_ after destruction would segfault. + cluster_.reset(); + + // If we reach here without crashing, the test passes. + // The fix ensures that: + // 1. The destructor sets is_destroying_ = true + // 2. The destructor resets redis_discovery_session_ + // 3. Timer callbacks check is_destroying_ before accessing cluster members +} + } // namespace Redis } // namespace Clusters } // namespace Extensions diff --git a/test/extensions/clusters/reverse_connection/BUILD b/test/extensions/clusters/reverse_connection/BUILD new file mode 100644 index 0000000000000..24c6ee62720ae --- /dev/null +++ b/test/extensions/clusters/reverse_connection/BUILD @@ -0,0 +1,31 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "reverse_connection_cluster_test", + srcs = ["reverse_connection_cluster_test.cc"], + deps = [ + "//envoy/registry", + "//source/extensions/clusters/reverse_connection:reverse_connection_lib", + "//source/extensions/load_balancing_policies/cluster_provided:config", + "//test/common/upstream:utility_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/protobuf:protobuf_mocks", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/server:admin_mocks", + "//test/mocks/server:instance_mocks", + "//test/mocks/ssl:ssl_mocks", + "//test/mocks/upstream:cluster_manager_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/clusters/reverse_connection/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/clusters/reverse_connection/reverse_connection_cluster_test.cc b/test/extensions/clusters/reverse_connection/reverse_connection_cluster_test.cc new file mode 100644 index 0000000000000..7fa63aee50b29 --- /dev/null +++ b/test/extensions/clusters/reverse_connection/reverse_connection_cluster_test.cc @@ -0,0 +1,1571 @@ +#include +#include +#include +#include + +#include "envoy/common/callback.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/extensions/clusters/reverse_connection/v3/reverse_connection.pb.h" +#include "envoy/server/bootstrap_extension_config.h" +#include "envoy/stats/scope.h" + +#include "source/common/config/utility.h" +#include "source/common/http/headers.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/common/singleton/manager_impl.h" +#include "source/common/singleton/threadsafe_singleton.h" +#include "source/common/upstream/upstream_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/clusters/reverse_connection/reverse_connection.h" +#include "source/extensions/transport_sockets/raw_buffer/config.h" +#include "source/server/transport_socket_config_impl.h" + +#include "test/common/upstream/utility.h" +#include "test/mocks/common.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/protobuf/mocks.h" +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/server/admin.h" +#include "test/mocks/server/instance.h" +#include "test/mocks/ssl/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" +#include "test/test_common/registry.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +// Add namespace alias for convenience +namespace BootstrapReverseConnection = Envoy::Extensions::Bootstrap::ReverseConnection; + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace ReverseConnection { + +class TestLoadBalancerContext : public Upstream::LoadBalancerContextBase { +public: + TestLoadBalancerContext(const Network::Connection* connection) + : TestLoadBalancerContext(connection, nullptr) {} + TestLoadBalancerContext(const Network::Connection* connection, + StreamInfo::StreamInfo* request_stream_info) + : connection_(connection), request_stream_info_(request_stream_info) {} + TestLoadBalancerContext(const Network::Connection* connection, const std::string& key, + const std::string& value) + : TestLoadBalancerContext(connection) { + downstream_headers_ = + Http::RequestHeaderMapPtr{new Http::TestRequestHeaderMapImpl{{key, value}}}; + } + + // Upstream::LoadBalancerContext. + absl::optional computeHashKey() override { return 0; } + const Network::Connection* downstreamConnection() const override { return connection_; } + StreamInfo::StreamInfo* requestStreamInfo() const override { return request_stream_info_; } + const Http::RequestHeaderMap* downstreamHeaders() const override { + return downstream_headers_.get(); + } + + absl::optional hash_key_; + const Network::Connection* connection_; + StreamInfo::StreamInfo* request_stream_info_; + Http::RequestHeaderMapPtr downstream_headers_; +}; + +class ReverseConnectionClusterTest : public Event::TestUsingSimulatedTime, public testing::Test { +public: + ReverseConnectionClusterTest() { + // Set up the stats scope. + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context. + EXPECT_CALL(server_context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(server_context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + + // Create the config. + config_.set_stat_prefix("test_prefix"); + } + + ~ReverseConnectionClusterTest() override = default; + + // Set up the upstream extension components (socket interface and extension). + void setupUpstreamExtension() { + // Create the socket interface. + socket_interface_ = + std::make_unique(server_context_); + + // Create the extension. + extension_ = std::make_unique( + *socket_interface_, server_context_, config_); + + // Get the registered socket interface from the global registry and set up its extension. + auto* registered_socket_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (registered_socket_interface) { + auto* registered_acceptor = dynamic_cast( + const_cast(registered_socket_interface)); + if (registered_acceptor) { + // Set up the extension for the registered socket interface. + registered_acceptor->extension_ = extension_.get(); + } + } + } + + void setupFromYaml(const std::string& yaml, bool expect_success = true) { + if (expect_success) { + cleanup_timer_ = new Event::MockTimer(&server_context_.dispatcher_); + EXPECT_CALL(*cleanup_timer_, enableTimer(_, _)); + } + setup(Upstream::parseClusterFromV3Yaml(yaml)); + } + + void setup(const envoy::config::cluster::v3::Cluster& cluster_config) { + + Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr, + false); + + RevConClusterFactory factory; + + // Parse the RevConClusterConfig from the cluster's typed_config. + envoy::extensions::clusters::reverse_connection::v3::RevConClusterConfig rev_con_config; + THROW_IF_NOT_OK(Config::Utility::translateOpaqueConfig( + cluster_config.cluster_type().typed_config(), validation_visitor_, rev_con_config)); + + auto status_or_pair = + factory.createClusterWithConfig(cluster_config, rev_con_config, factory_context); + THROW_IF_NOT_OK_REF(status_or_pair.status()); + + cluster_ = std::dynamic_pointer_cast(status_or_pair.value().first); + priority_update_cb_ = cluster_->prioritySet().addPriorityUpdateCb( + [&](uint32_t, const Upstream::HostVector&, const Upstream::HostVector&) { + membership_updated_.ready(); + return absl::OkStatus(); + }); + ON_CALL(initialized_, ready()).WillByDefault(testing::Invoke([this] { + init_complete_ = true; + })); + cluster_->initialize([&]() { + initialized_.ready(); + return absl::OkStatus(); + }); + } + + void TearDown() override { + if (init_complete_) { + EXPECT_CALL(*cleanup_timer_, disableTimer()); + } + + // Clean up thread local resources if they were set up. + if (tls_slot_) { + tls_slot_.reset(); + } + if (thread_local_registry_) { + thread_local_registry_.reset(); + } + if (extension_) { + extension_.reset(); + } + if (socket_interface_) { + socket_interface_.reset(); + } + } + + // Helper function to set up thread local slot for tests. + void setupThreadLocalSlot() { + // Check if extension is set up. + if (!extension_) { + return; + } + + // Set up mock expectations for timer creation that will be needed by UpstreamSocketManager. + auto mock_timer = new NiceMock(); + EXPECT_CALL(server_context_.dispatcher_, createTimer_(_)).WillOnce(Return(mock_timer)); + + // First, call onServerInitialized to set up the extension reference properly. + extension_->onServerInitialized(); + + // Create a thread local registry with the properly initialized extension. + thread_local_registry_ = + std::make_shared( + server_context_.dispatcher_, extension_.get()); + + // Create the actual TypedSlot. + tls_slot_ = + ThreadLocal::TypedSlot::makeUnique( + thread_local_); + thread_local_.setDispatcher(&server_context_.dispatcher_); + + // Set up the slot to return our registry. + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Override the TLS slot with our test version. + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + } + + // Helper to add a socket to the manager for testing. + void addTestSocket(const std::string& node_id, const std::string& cluster_id) { + if (!thread_local_registry_ || !thread_local_registry_->socketManager() || !socket_interface_) { + return; + } + + // Set up mock expectations for timer and file event creation. + auto mock_timer = new NiceMock(); + auto mock_file_event = new NiceMock(); + EXPECT_CALL(server_context_.dispatcher_, createTimer_(_)).WillOnce(Return(mock_timer)); + EXPECT_CALL(server_context_.dispatcher_, createFileEvent_(_, _, _, _)) + .WillOnce(Return(mock_file_event)); + + // Create a mock socket. + auto socket = std::make_unique>(); + auto mock_io_handle = std::make_unique>(); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(Return(123)); + EXPECT_CALL(*socket, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle)); + socket->io_handle_ = std::move(mock_io_handle); + + // Get the socket manager from the thread local registry. + auto* tls_socket_manager = socket_interface_->getLocalRegistry()->socketManager(); + EXPECT_NE(tls_socket_manager, nullptr); + + // Add the socket to the manager. + tls_socket_manager->addConnectionSocket(node_id, cluster_id, std::move(socket), + std::chrono::seconds(30), false); + } + + // Helper method to call cleanup since this class is a friend of RevConCluster. + void callCleanup() { cluster_->cleanup(); } + + // Helper method to create LoadBalancerFactory instance for testing. + std::unique_ptr createLoadBalancerFactory() { + return std::make_unique(cluster_); + } + + // Helper method to create ThreadAwareLoadBalancer instance for testing. + std::unique_ptr createThreadAwareLoadBalancer() { + return std::make_unique(cluster_); + } + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); + + NiceMock server_context_; + NiceMock validation_visitor_; + + std::shared_ptr cluster_; + ReadyWatcher membership_updated_; + ReadyWatcher initialized_; + Event::MockTimer* cleanup_timer_; + Common::CallbackHandlePtr priority_update_cb_; + bool init_complete_{false}; + + // Real thread local slot and registry for reverse connection testing. + std::unique_ptr> + tls_slot_; + std::shared_ptr thread_local_registry_; + + // Real socket interface and extension. + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + + // Mock thread local instance. + NiceMock thread_local_; + + // Mock dispatcher. + NiceMock dispatcher_{"worker_0"}; + + // Stats and config. + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface config_; +}; + +// Test cluster creation with valid config. +TEST(ReverseConnectionClusterConfigTest, ValidConfig) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + envoy::config::cluster::v3::Cluster cluster_config = Upstream::parseClusterFromV3Yaml(yaml); + EXPECT_TRUE(cluster_config.has_cluster_type()); + EXPECT_EQ(cluster_config.cluster_type().name(), "envoy.clusters.reverse_connection"); +} + +// Test cluster creation failure due to invalid load assignment. +TEST_F(ReverseConnectionClusterTest, BadConfigWithLoadAssignment) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + load_assignment: + cluster_name: name + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8000 + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + )EOF"; + + EXPECT_THROW_WITH_MESSAGE(setupFromYaml(yaml, false), EnvoyException, + "Reverse Conn clusters must have no load assignment configured"); +} + +// Test cluster creation failure due to wrong load balancing policy. +TEST_F(ReverseConnectionClusterTest, BadConfigWithWrongLbPolicy) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: ROUND_ROBIN + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + )EOF"; + + EXPECT_THROW_WITH_MESSAGE(setupFromYaml(yaml, false), EnvoyException, + "cluster: LB policy ROUND_ROBIN is not valid for Cluster type " + "envoy.clusters.reverse_connection. Only 'CLUSTER_PROVIDED' is allowed " + "with cluster type 'REVERSE_CONNECTION'"); +} + +TEST_F(ReverseConnectionClusterTest, BasicSetup) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + EXPECT_CALL(membership_updated_, ready()).Times(0); + setupFromYaml(yaml); + + EXPECT_EQ(0UL, cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size()); + EXPECT_EQ(0UL, cluster_->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); +} + +// Test host creation failure due to no context. +TEST_F(ReverseConnectionClusterTest, NoContext) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + EXPECT_CALL(membership_updated_, ready()).Times(0); + setupFromYaml(yaml); + + EXPECT_EQ(0UL, cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size()); + EXPECT_EQ(0UL, cluster_->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); + EXPECT_EQ(0UL, cluster_->prioritySet().hostSetsPerPriority()[0]->hostsPerLocality().get().size()); + EXPECT_EQ( + 0UL, + cluster_->prioritySet().hostSetsPerPriority()[0]->healthyHostsPerLocality().get().size()); + + // No downstream connection => no host. + { + TestLoadBalancerContext lb_context(nullptr); + RevConCluster::LoadBalancer lb(cluster_); + EXPECT_CALL(server_context_.dispatcher_, post(_)).Times(0); + Upstream::HostConstSharedPtr host = lb.chooseHost(&lb_context).host; + EXPECT_EQ(host, nullptr); + } + + // Test null context - should return nullptr. + { + RevConCluster::LoadBalancer lb(cluster_); + Upstream::HostConstSharedPtr host = lb.chooseHost(nullptr).host; + EXPECT_EQ(host, nullptr); + } +} + +// Test host creation failure due to no headers. +TEST_F(ReverseConnectionClusterTest, NoHeaders) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Downstream connection but no headers => no host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + RevConCluster::LoadBalancer lb(cluster_); + EXPECT_CALL(server_context_.dispatcher_, post(_)).Times(0); + Upstream::HostConstSharedPtr host = lb.chooseHost(&lb_context).host; + EXPECT_EQ(host, nullptr); + } +} + +// Test host creation failure due to missing required headers. +TEST_F(ReverseConnectionClusterTest, MissingRequiredHeaders) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Request with unsupported headers but missing all required headers (EnvoyDstNodeUUID,. + // EnvoyDstClusterUUID, proper Host header). + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection, "x-random-header", "random-value"); + RevConCluster::LoadBalancer lb(cluster_); + EXPECT_CALL(server_context_.dispatcher_, post(_)).Times(0); + Upstream::HostConstSharedPtr host = lb.chooseHost(&lb_context).host; + EXPECT_EQ(host, nullptr); + } + + // Test with empty header value - this should be skipped and continue to next header. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection, "x-remote-node-id", ""); + RevConCluster::LoadBalancer lb(cluster_); + Upstream::HostConstSharedPtr host = lb.chooseHost(&lb_context).host; + EXPECT_EQ(host, nullptr); + } +} + +// Test host creation failure due to thread local slot not being set. +TEST_F(ReverseConnectionClusterTest, HostCreationWithoutSocketManager) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + RevConCluster::LoadBalancer lb(cluster_); + // Do not set up thread local slot - no socket manager initialized. + + // Test host creation when matcher would otherwise match but socket manager is not available. + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result = lb.chooseHost(&lb_context); + // Should return nullptr when socket manager is not found. + EXPECT_EQ(result.host, nullptr); +} + +// Test when the socket interface is not registered in the registry. +TEST_F(ReverseConnectionClusterTest, SocketInterfaceNotRegistered) { + // Temporarily remove the upstream reverse connection socket interface from the registry + // This will make Network::socketInterface() return nullptr for the specific name. + auto saved_factories = + Registry::FactoryRegistry::factories(); + + // Find and remove the specific socket interface factory. + auto& factories = + Registry::FactoryRegistry::factories(); + auto it = factories.find("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (it != factories.end()) { + factories.erase(it); + } + + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + RevConCluster::LoadBalancer lb(cluster_); + + // Test host creation when socket interface is not registered. + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result = lb.chooseHost(&lb_context); + // Should return nullptr when socket interface is not found. + EXPECT_EQ(result.host, nullptr); + + // Restore the registry + Registry::FactoryRegistry::factories() = + saved_factories; +} + +// Test host creation with socket manager. +TEST_F(ReverseConnectionClusterTest, HostCreationWithSocketManager) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-456 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-456" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test. + setupUpstreamExtension(); + // Set up the thread local slot, initializing the socket manager. + setupThreadLocalSlot(); + + // Add test sockets to the socket manager. + addTestSocket("test-uuid-123", "cluster-123"); + addTestSocket("test-uuid-456", "cluster-456"); + + RevConCluster::LoadBalancer lb(cluster_); + + // Test host creation with Host header. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result = lb.chooseHost(&lb_context); + EXPECT_NE(result.host, nullptr); + EXPECT_EQ(result.host->address()->logicalName(), "test-uuid-123"); + } + + // Test host creation with header mapping to a different node id (test-uuid-456). + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-456"}}}; + + auto result = lb.chooseHost(&lb_context); + EXPECT_NE(result.host, nullptr); + EXPECT_EQ(result.host->address()->logicalName(), "test-uuid-456"); + } + + // Test host creation with HTTP headers. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection, "x-remote-node-id", "test-uuid-123"); + + auto result = lb.chooseHost(&lb_context); + EXPECT_NE(result.host, nullptr); + EXPECT_EQ(result.host->address()->logicalName(), "test-uuid-123"); + } +} + +// Test host reuse for requests with same UUID. +TEST_F(ReverseConnectionClusterTest, HostReuse) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test. + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Add test socket to the socket manager. + addTestSocket("test-uuid-123", "cluster-123"); + + RevConCluster::LoadBalancer lb(cluster_); + + // Create first host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result1 = lb.chooseHost(&lb_context); + EXPECT_NE(result1.host, nullptr); + + // Create second host with same UUID - should reuse the same host. + auto result2 = lb.chooseHost(&lb_context); + EXPECT_NE(result2.host, nullptr); + EXPECT_EQ(result1.host, result2.host); + } +} + +// Test different hosts for different UUIDs. +TEST_F(ReverseConnectionClusterTest, DifferentHostsForDifferentUUIDs) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-456 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-456" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test. + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Add test sockets to the socket manager. + addTestSocket("test-uuid-123", "cluster-123"); + addTestSocket("test-uuid-456", "cluster-456"); + + RevConCluster::LoadBalancer lb(cluster_); + + // Create first host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result1 = lb.chooseHost(&lb_context); + EXPECT_NE(result1.host, nullptr); + + // Create second host with different UUID - should be different host. + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-456"}}}; + auto result2 = lb.chooseHost(&lb_context); + EXPECT_NE(result2.host, nullptr); + EXPECT_NE(result1.host, result2.host); + } +} + +// Test cleanup of hosts. +TEST_F(ReverseConnectionClusterTest, TestCleanup) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-456 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-456" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test. + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Add test sockets to the socket manager. + addTestSocket("test-uuid-123", "cluster-123"); + addTestSocket("test-uuid-456", "cluster-456"); + + RevConCluster::LoadBalancer lb(cluster_); + + // Create two hosts. + Upstream::HostSharedPtr host1, host2; + + // Create first host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result1 = lb.chooseHost(&lb_context); + EXPECT_NE(result1.host, nullptr); + host1 = std::const_pointer_cast(result1.host); + } + + // Create second host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-456"}}}; + + auto result2 = lb.chooseHost(&lb_context); + EXPECT_NE(result2.host, nullptr); + host2 = std::const_pointer_cast(result2.host); + } + + // Verify hosts are different. + EXPECT_NE(host1, host2); + + // Expect the cleanup timer to be enabled after cleanup. + EXPECT_CALL(*cleanup_timer_, enableTimer(std::chrono::milliseconds(10000), nullptr)); + + // Call cleanup via the helper method. + callCleanup(); + + // Verify that hosts can still be accessed after cleanup. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result = lb.chooseHost(&lb_context); + EXPECT_NE(result.host, nullptr); + } +} + +// Test cleanup of hosts with used hosts. +TEST_F(ReverseConnectionClusterTest, TestCleanupWithUsedHosts) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-456 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-456" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test. + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Add test sockets to the socket manager. + addTestSocket("test-uuid-123", "cluster-123"); + addTestSocket("test-uuid-456", "cluster-456"); + + RevConCluster::LoadBalancer lb(cluster_); + + // Create two hosts. + Upstream::HostSharedPtr host1, host2; + + // Create first host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result1 = lb.chooseHost(&lb_context); + EXPECT_NE(result1.host, nullptr); + host1 = std::const_pointer_cast(result1.host); + } + + // Create second host. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-456"}}}; + + auto result2 = lb.chooseHost(&lb_context); + EXPECT_NE(result2.host, nullptr); + host2 = std::const_pointer_cast(result2.host); + } + + // Mark one host as used by acquiring a handle. + auto handle1 = host1->acquireHandle(); + EXPECT_TRUE(host1->used()); + + // Expect the cleanup timer to be enabled after cleanup. + EXPECT_CALL(*cleanup_timer_, enableTimer(std::chrono::milliseconds(10000), nullptr)); + + // Call cleanup via the helper method. + callCleanup(); + + // Verify that the used host is still accessible after cleanup. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + lb_context.downstream_headers_ = Http::RequestHeaderMapPtr{ + new Http::TestRequestHeaderMapImpl{{"x-remote-node-id", "test-uuid-123"}}}; + + auto result = lb.chooseHost(&lb_context); + EXPECT_NE(result.host, nullptr); + } + + // Release the handle. + handle1.reset(); +} + +// LoadBalancerFactory tests. +TEST_F(ReverseConnectionClusterTest, LoadBalancerFactory) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Test LoadBalancerFactory using helper method. + auto factory = createLoadBalancerFactory(); + EXPECT_NE(factory, nullptr); + + // Test that the factory creates load balancers. + Upstream::LoadBalancerParams params{cluster_->prioritySet()}; + auto lb = factory->create(params); + EXPECT_NE(lb, nullptr); + + // Test that multiple load balancers are different instances. + auto lb2 = factory->create(params); + EXPECT_NE(lb2, nullptr); + EXPECT_NE(lb.get(), lb2.get()); + + // Test create() without parameters. + auto lb3 = factory->create(); + EXPECT_NE(lb3, nullptr); +} + +// ThreadAwareLoadBalancer tests +TEST_F(ReverseConnectionClusterTest, ThreadAwareLoadBalancer) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + // Set up the upstream extension for this test + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Test ThreadAwareLoadBalancer using helper method. + auto thread_aware_lb = createThreadAwareLoadBalancer(); + EXPECT_NE(thread_aware_lb, nullptr); + + // Test initialize() method. + auto init_status = thread_aware_lb->initialize(); + EXPECT_TRUE(init_status.ok()); + + // Test factory() method. + auto factory = thread_aware_lb->factory(); + EXPECT_NE(factory, nullptr); + + // Test that factory creates load balancers. + Upstream::LoadBalancerParams params{cluster_->prioritySet()}; + auto lb = factory->create(params); + EXPECT_NE(lb, nullptr); +} + +// Test no-op methods for load balancer. +TEST_F(ReverseConnectionClusterTest, LoadBalancerNoopMethods) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + lb_policy: CLUSTER_PROVIDED + cleanup_interval: 1s + cluster_type: + name: envoy.clusters.reverse_connection + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.RevConClusterConfig + cleanup_interval: 10s + host_id_matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + typed_config: + '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-remote-node-id + value_match: + exact: test-uuid-123 + on_match: + action: + typed_config: + '@type': type.googleapis.com/envoy.extensions.clusters.reverse_connection.v3.HostIdAction + host_id: "test-uuid-123" + )EOF"; + + EXPECT_CALL(initialized_, ready()); + setupFromYaml(yaml); + + RevConCluster::LoadBalancer lb(cluster_); + + // Test peekAnotherHost - should return nullptr. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + Upstream::HostConstSharedPtr peeked_host = lb.peekAnotherHost(&lb_context); + EXPECT_EQ(peeked_host, nullptr); + } + + // Test selectExistingConnection - should return nullopt. + { + NiceMock connection; + TestLoadBalancerContext lb_context(&connection); + std::vector hash_key; + + // Create a mock host for testing. + auto mock_host = std::make_shared>(); + auto selected_connection = lb.selectExistingConnection(&lb_context, *mock_host, hash_key); + EXPECT_FALSE(selected_connection.has_value()); + } + + // Test lifetimeCallbacks - should return empty OptRef. + { + auto lifetime_callbacks = lb.lifetimeCallbacks(); + EXPECT_FALSE(lifetime_callbacks.has_value()); + } +} + +// UpstreamReverseConnectionAddress tests +class UpstreamReverseConnectionAddressTest : public testing::Test { +public: + UpstreamReverseConnectionAddressTest() { + // Set up the stats scope. + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context. + EXPECT_CALL(server_context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(server_context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + } + + void SetUp() override {} + + void TearDown() override { + // Clean up thread local resources if they were set up. + if (tls_slot_) { + tls_slot_.reset(); + } + if (thread_local_registry_) { + thread_local_registry_.reset(); + } + if (extension_) { + extension_.reset(); + } + if (socket_interface_) { + socket_interface_.reset(); + } + } + + // Set up the upstream extension components (socket interface and extension). + void setupUpstreamExtension() { + // Create the socket interface. + socket_interface_ = + std::make_unique(server_context_); + + // Create the extension. + extension_ = std::make_unique( + *socket_interface_, server_context_, config_); + } + + // Set up the thread local slot with the extension. + void setupThreadLocalSlot() { + // Check if extension is set up + if (!extension_) { + return; + } + + // Set up mock expectations for timer creation that will be needed by UpstreamSocketManager. + auto mock_timer = new NiceMock(); + EXPECT_CALL(server_context_.dispatcher_, createTimer_(_)).WillOnce(Return(mock_timer)); + + // First, call onServerInitialized to set up the extension reference properly. + extension_->onServerInitialized(); + + // Create a thread local registry with the properly initialized extension. + thread_local_registry_ = + std::make_shared( + server_context_.dispatcher_, extension_.get()); + + // Create the actual TypedSlot. + tls_slot_ = + ThreadLocal::TypedSlot::makeUnique( + thread_local_); + thread_local_.setDispatcher(&server_context_.dispatcher_); + + // Set up the slot to return our registry. + tls_slot_->set([registry = thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Override the TLS slot with our test version. + extension_->setTestOnlyTLSRegistry(std::move(tls_slot_)); + + // Get the registered socket interface from the global registry and set up its extension. + auto* registered_socket_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (registered_socket_interface) { + auto* registered_acceptor = dynamic_cast( + const_cast(registered_socket_interface)); + if (registered_acceptor) { + // Set up the extension for the registered socket interface. + registered_acceptor->extension_ = extension_.get(); + } + } + } + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); + + NiceMock server_context_; + NiceMock validation_visitor_; + + // Real thread local slot and registry for reverse connection testing. + std::unique_ptr> + tls_slot_; + std::shared_ptr thread_local_registry_; + + // Real socket interface and extension. + std::unique_ptr socket_interface_; + std::unique_ptr extension_; + + // Configuration for the extension. + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface config_; + + // Stats store and scope. + Stats::TestUtil::TestStore stats_store_; + Stats::ScopeSharedPtr stats_scope_; + + // Thread local mock. + NiceMock thread_local_; +}; + +TEST_F(UpstreamReverseConnectionAddressTest, BasicSetup) { + const std::string node_id = "test-node-123"; + UpstreamReverseConnectionAddress address(node_id); + + // Test basic properties. + EXPECT_EQ(address.asString(), "127.0.0.1:0"); + EXPECT_EQ(address.asStringView(), "127.0.0.1:0"); + EXPECT_EQ(address.logicalName(), node_id); + EXPECT_EQ(address.type(), Network::Address::Type::Ip); + EXPECT_EQ(address.addressType(), "default"); + EXPECT_FALSE(address.networkNamespace().has_value()); +} + +TEST_F(UpstreamReverseConnectionAddressTest, EqualityOperator) { + UpstreamReverseConnectionAddress address1("node-1"); + UpstreamReverseConnectionAddress address2("node-1"); + UpstreamReverseConnectionAddress address3("node-2"); + + // Same node ID should be equal. + EXPECT_TRUE(address1 == address2); + EXPECT_TRUE(address2 == address1); + + // Different node IDs should not be equal. + EXPECT_FALSE(address1 == address3); + EXPECT_FALSE(address3 == address1); + + // Test with different address types. + Network::Address::Ipv4Instance ipv4_address("127.0.0.1", 8080); + EXPECT_FALSE(address1 == ipv4_address); +} + +TEST_F(UpstreamReverseConnectionAddressTest, SocketAddressMethods) { + UpstreamReverseConnectionAddress address("test-node"); + + // Test sockAddr and sockAddrLen. + const sockaddr* sock_addr = address.sockAddr(); + EXPECT_NE(sock_addr, nullptr); + + socklen_t addr_len = address.sockAddrLen(); + EXPECT_EQ(addr_len, sizeof(struct sockaddr_in)); + + // Verify the socket address structure. + const struct sockaddr_in* addr_in = reinterpret_cast(sock_addr); + EXPECT_EQ(addr_in->sin_family, AF_INET); + EXPECT_EQ(ntohs(addr_in->sin_port), 0); + EXPECT_EQ(ntohl(addr_in->sin_addr.s_addr), 0x7f000001); // 127.0.0.1 +} + +// Test IP-related methods for UpstreamReverseConnectionAddress. +TEST_F(UpstreamReverseConnectionAddressTest, IPMethods) { + UpstreamReverseConnectionAddress address("test-node"); + + // Test IP-related methods. + const Network::Address::Ip* ip = address.ip(); + EXPECT_NE(ip, nullptr); + + // Test IP address properties. + EXPECT_EQ(ip->addressAsString(), "0.0.0.0:0"); + EXPECT_TRUE(ip->isAnyAddress()); + EXPECT_FALSE(ip->isUnicastAddress()); + EXPECT_EQ(ip->port(), 0); + EXPECT_EQ(ip->version(), Network::Address::IpVersion::v4); + + // Test additional IP methods. + EXPECT_FALSE(ip->isLinkLocalAddress()); + EXPECT_FALSE(ip->isUniqueLocalAddress()); + EXPECT_FALSE(ip->isSiteLocalAddress()); + EXPECT_FALSE(ip->isTeredoAddress()); + + // Test IPv4/IPv6 methods. + EXPECT_EQ(ip->ipv4(), nullptr); + EXPECT_EQ(ip->ipv6(), nullptr); +} + +TEST_F(UpstreamReverseConnectionAddressTest, PipeAndInternalAddressMethods) { + UpstreamReverseConnectionAddress address("test-node"); + + // Test pipe and internal address methods. + EXPECT_EQ(address.pipe(), nullptr); + EXPECT_EQ(address.envoyInternalAddress(), nullptr); +} + +// Test socketInterface() functionality for UpstreamReverseConnectionAddress. +TEST_F(UpstreamReverseConnectionAddressTest, SocketInterfaceWithAvailableInterface) { + // Set up the upstream extension and thread local slot. + setupUpstreamExtension(); + setupThreadLocalSlot(); + + // Create an address instance. + UpstreamReverseConnectionAddress address("test-node"); + const Network::SocketInterface& socket_interface = address.socketInterface(); + + // Should return the upstream reverse connection socket interface. + EXPECT_NE(&socket_interface, nullptr); + + // Verify that the returned interface is of type ReverseTunnelAcceptor. + const auto* reverse_tunnel_acceptor = + dynamic_cast(&socket_interface); + EXPECT_NE(reverse_tunnel_acceptor, nullptr); +} + +// Test socketInterface() functionality when the upstream socket interface is not found. +TEST_F(UpstreamReverseConnectionAddressTest, SocketInterfaceWithUnavailableInterface) { + // Temporarily remove the upstream reverse connection socket interface from the registry + // This will make Network::socketInterface() return nullptr for the specific name. + auto saved_factories = + Registry::FactoryRegistry::factories(); + + // Find and remove the specific socket interface factory. + auto& factories = + Registry::FactoryRegistry::factories(); + auto it = factories.find("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (it != factories.end()) { + factories.erase(it); + } + + // Create an address instance. + UpstreamReverseConnectionAddress address("test-node"); + + // The socketInterface() method should fall back to the default socket interface + // when the upstream reverse connection socket interface is not found. + const Network::SocketInterface& socket_interface = address.socketInterface(); + + // Should return the default socket interface. + EXPECT_NE(&socket_interface, nullptr); + + // Verify that it's not the reverse tunnel acceptor type. + const auto* reverse_tunnel_acceptor = + dynamic_cast(&socket_interface); + EXPECT_EQ(reverse_tunnel_acceptor, nullptr); + + // Explicitly verify that the returned interface is the one registered with + // "envoy.extensions.network.socket_interface.default_socket_interface". + const Network::SocketInterface* default_interface = Network::socketInterface( + "envoy.extensions.network.socket_interface.default_socket_interface"); + EXPECT_NE(default_interface, nullptr); + EXPECT_EQ(&socket_interface, default_interface); + Registry::FactoryRegistry::factories() = + saved_factories; +} + +// Test logical name for multiple instances of UpstreamReverseConnectionAddress. +TEST_F(UpstreamReverseConnectionAddressTest, MultipleInstances) { + UpstreamReverseConnectionAddress address1("node-1"); + UpstreamReverseConnectionAddress address2("node-2"); + + // Test that different instances have different logical names. + EXPECT_EQ(address1.logicalName(), "node-1"); + EXPECT_EQ(address2.logicalName(), "node-2"); + + // Test that they are not equal. + EXPECT_FALSE(address1 == address2); +} + +TEST_F(UpstreamReverseConnectionAddressTest, EmptyNodeId) { + UpstreamReverseConnectionAddress address(""); + + // Test with empty node ID. + EXPECT_EQ(address.logicalName(), ""); + EXPECT_EQ(address.asString(), "127.0.0.1:0"); + EXPECT_EQ(address.type(), Network::Address::Type::Ip); +} + +TEST_F(UpstreamReverseConnectionAddressTest, LongNodeId) { + const std::string long_node_id = + "very-long-node-id-that-might-be-used-in-production-environments"; + UpstreamReverseConnectionAddress address(long_node_id); + + // Test with long node ID. + EXPECT_EQ(address.logicalName(), long_node_id); + EXPECT_EQ(address.asString(), "127.0.0.1:0"); + EXPECT_EQ(address.type(), Network::Address::Type::Ip); +} + +} // namespace ReverseConnection +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/aws/credential_provider_chains_test.cc b/test/extensions/common/aws/credential_provider_chains_test.cc index 241ac9713fd72..372eb3bf1c0cd 100644 --- a/test/extensions/common/aws/credential_provider_chains_test.cc +++ b/test/extensions/common/aws/credential_provider_chains_test.cc @@ -427,6 +427,33 @@ TEST_F(CustomCredentialsProviderChainTest, AssumeRoleWithEnvironment) { EXPECT_EQ(2, chain.value()->getNumProviders()); } +TEST_F(CustomCredentialsProviderChainTest, AssumeRoleWithoutSessionName) { + envoy::extensions::common::aws::v3::AwsCredentialProvider credential_provider_config = {}; + credential_provider_config.set_custom_credential_provider_chain(true); + credential_provider_config.mutable_assume_role_credential_provider()->set_role_arn( + "test-role-arn"); + // Intentionally not setting role_session_name to test auto-generation. + + std::string role_session_name; + time_system_.setSystemTime(std::chrono::milliseconds(1234567890)); + + EXPECT_CALL(factories_, createAssumeRoleCredentialsProvider(Ref(context_), _, _, _)) + .WillOnce(Invoke(WithArg<3>( + [&role_session_name]( + const envoy::extensions::common::aws::v3::AssumeRoleCredentialProvider& provider) + -> CredentialsProviderSharedPtr { + role_session_name = provider.role_session_name(); + return std::make_shared(); + }))); + + CommonCredentialsProviderChain chain(context_, "us-east-1", credential_provider_config, + factories_); + + // Verify that a session name was auto-generated based on the timestamp. + EXPECT_FALSE(role_session_name.empty()); + EXPECT_EQ(role_session_name, "1234567890000000"); +} + } // namespace Aws } // namespace Common } // namespace Extensions diff --git a/test/extensions/common/aws/credential_providers/assume_role_credentials_provider_test.cc b/test/extensions/common/aws/credential_providers/assume_role_credentials_provider_test.cc index 5ca452718026c..874a31634c12a 100644 --- a/test/extensions/common/aws/credential_providers/assume_role_credentials_provider_test.cc +++ b/test/extensions/common/aws/credential_providers/assume_role_credentials_provider_test.cc @@ -632,7 +632,27 @@ TEST_F(AssumeRoleCredentialsProviderTest, Coverage) { TEST_F(AssumeRoleCredentialsProviderTest, WithSessionDuration) { timer_ = new NiceMock(&context_.dispatcher_); - expectDocument(200, std::move(R"EOF( + // Custom matcher for request with session duration parameter. + Http::TestRequestHeaderMapImpl headers_with_duration{ + {":path", "/?Version=2011-06-15&Action=AssumeRole&RoleArn=aws:iam::123456789012:role/" + "arn&RoleSessionName=role-session-name&DurationSeconds=3600"}, + {":authority", "sts.region.amazonaws.com"}, + {":scheme", "https"}, + {":method", "GET"}, + {"Accept", "application/json"}, + {"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"x-amz-security-token", "token"}, + {"x-amz-date", "20180102T030405Z"}, + {"authorization", + "AWS4-HMAC-SHA256 Credential=akid/20180102/region/sts/aws4_request, " + "SignedHeaders=accept;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, " + "Signature=88533b93b82077848fa88b5d1fe69540a0916960fdd48a1df66b764dc73a6d9a"}}; + + // Use a custom expectation for this test to verify the DurationSeconds parameter. + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers_with_duration), _, _)) + .WillRepeatedly(Invoke( + [](Http::RequestMessage&, Tracing::Span&, MetadataFetcher::MetadataReceiver& receiver) { + receiver.onMetadataSuccess(std::move(R"EOF( { "AssumeRoleResponse": { "AssumeRoleResult": { @@ -645,6 +665,7 @@ TEST_F(AssumeRoleCredentialsProviderTest, WithSessionDuration) { } } )EOF")); + })); // Setup provider with session duration ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); @@ -862,6 +883,90 @@ TEST_F(AssumeRoleCredentialsProviderTest, CredentialsPendingReturn) { delete (raw_metadata_fetcher_); } +TEST_F(AssumeRoleCredentialsProviderTest, WithExternalId) { + timer_ = new NiceMock(&context_.dispatcher_); + + // Custom matcher for request with external ID parameter. + Http::TestRequestHeaderMapImpl headers_with_external_id{ + {":path", "/?Version=2011-06-15&Action=AssumeRole&RoleArn=aws:iam::123456789012:role/" + "arn&RoleSessionName=role-session-name&ExternalId=test-external-id"}, + {":authority", "sts.region.amazonaws.com"}, + {":scheme", "https"}, + {":method", "GET"}, + {"Accept", "application/json"}, + {"x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {"x-amz-security-token", "token"}, + {"x-amz-date", "20180102T030405Z"}, + {"authorization", + "AWS4-HMAC-SHA256 Credential=akid/20180102/region/sts/aws4_request, " + "SignedHeaders=accept;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, " + "Signature=cc05851f97c3e6c1d8c28c205a1dcbb6248a944375f3e7b349800c2b3744fc48"}}; + + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers_with_external_id), _, _)) + .WillRepeatedly(Invoke( + [](Http::RequestMessage&, Tracing::Span&, MetadataFetcher::MetadataReceiver& receiver) { + receiver.onMetadataSuccess(std::move(R"EOF( +{ + "AssumeRoleResponse": { + "AssumeRoleResult": { + "Credentials": { + "AccessKeyId": "test-access-key", + "SecretAccessKey": "test-secret-key", + "SessionToken": "test-session-token" + } + } + } +} +)EOF")); + })); + + // Setup provider with external ID. + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + envoy::extensions::common::aws::v3::AssumeRoleCredentialProvider cred_provider = {}; + cred_provider.set_role_arn("aws:iam::123456789012:role/arn"); + cred_provider.set_role_session_name("role-session-name"); + cred_provider.set_external_id("test-external-id"); + + mock_manager_ = std::make_shared(); + EXPECT_CALL(*mock_manager_, getUriFromClusterName(_)) + .WillRepeatedly(Return("sts.region.amazonaws.com:443")); + + auto cluster_name = "credentials_provider_cluster"; + envoy::extensions::common::aws::v3::AwsCredentialProvider defaults; + envoy::extensions::common::aws::v3::EnvironmentCredentialProvider env_provider; + TestEnvironment::setEnvVar("AWS_ACCESS_KEY_ID", "akid", 1); + TestEnvironment::setEnvVar("AWS_SECRET_ACCESS_KEY", "secret", 1); + TestEnvironment::setEnvVar("AWS_SESSION_TOKEN", "token", 1); + + defaults.mutable_environment_credential_provider()->CopyFrom(env_provider); + auto credentials_provider_chain = + std::make_shared(context_, "region", + defaults); + auto signer = std::make_unique( + STS_SERVICE_NAME, "region", credentials_provider_chain, context_, + Extensions::Common::Aws::AwsSigningHeaderExclusionVector{}); + + provider_ = std::make_shared( + context_, mock_manager_, cluster_name, + [this](Upstream::ClusterManager&, absl::string_view) { + metadata_fetcher_.reset(raw_metadata_fetcher_); + return std::move(metadata_fetcher_); + }, + "region", MetadataFetcher::MetadataReceiver::RefreshState::Ready, std::chrono::seconds(2), + std::move(signer), cred_provider); + + timer_->enableTimer(std::chrono::milliseconds(1), nullptr); + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + + auto provider_friend = MetadataCredentialsProviderBaseFriend(provider_); + provider_friend.onClusterAddOrUpdate(); + timer_->invokeCallback(); + + const auto credentials = provider_->getCredentials(); + EXPECT_TRUE(credentials.accessKeyId().has_value()); + EXPECT_EQ("test-access-key", credentials.accessKeyId().value()); +} + } // namespace Aws } // namespace Common } // namespace Extensions diff --git a/test/extensions/common/aws/credentials_provider_test.cc b/test/extensions/common/aws/credentials_provider_test.cc index 5c7238419ec48..d09c17c0ff1d7 100644 --- a/test/extensions/common/aws/credentials_provider_test.cc +++ b/test/extensions/common/aws/credentials_provider_test.cc @@ -3,6 +3,7 @@ #include "source/extensions/common/aws/signers/sigv4_signer_impl.h" #include "test/extensions/common/aws/mocks.h" +#include "test/mocks/event/mocks.h" #include "test/mocks/server/server_factory_context.h" #include "gtest/gtest.h" @@ -81,7 +82,7 @@ class AsyncCredentialHandlingTest : public testing::Test { MetadataFetcherPtr metadata_fetcher_; NiceMock context_; WebIdentityCredentialsProviderPtr provider_; - Event::MockTimer* timer_{}; + Event::MockTimer* timer_; NiceMock cm_; std::shared_ptr mock_manager_; Http::RequestMessagePtr message_; @@ -180,7 +181,7 @@ TEST_F(AsyncCredentialHandlingTest, ChainCallbackCalledWhenCredentialsReturned) } )EOF"; - auto handle = provider_->subscribeToCredentialUpdates(*chain); + auto handle = provider_->subscribeToCredentialUpdates(chain); auto signer = std::make_unique( "vpc-lattice-svcs", "ap-southeast-2", chain, context_, @@ -249,8 +250,8 @@ TEST_F(AsyncCredentialHandlingTest, SubscriptionsCleanedUp) { } )EOF"; - auto handle = provider_->subscribeToCredentialUpdates(*chain); - auto handle2 = provider_->subscribeToCredentialUpdates(*chain); + auto handle = provider_->subscribeToCredentialUpdates(chain); + auto handle2 = provider_->subscribeToCredentialUpdates(chain); auto signer = std::make_unique( "vpc-lattice-svcs", "ap-southeast-2", chain, context_, @@ -275,6 +276,135 @@ TEST_F(AsyncCredentialHandlingTest, SubscriptionsCleanedUp) { ASSERT_TRUE(result.ok()); } +// Mock WebIdentityCredentialsProvider to track refresh calls +class MockWebIdentityProvider : public WebIdentityCredentialsProvider { +public: + MockWebIdentityProvider( + Server::Configuration::ServerFactoryContext& context, + AwsClusterManagerPtr aws_cluster_manager, absl::string_view cluster_name, + CreateMetadataFetcherCb create_metadata_fetcher_cb, + MetadataFetcher::MetadataReceiver::RefreshState refresh_state, + std::chrono::seconds initialization_timer, + const envoy::extensions::common::aws::v3::AssumeRoleWithWebIdentityCredentialProvider& config) + : WebIdentityCredentialsProvider(context, aws_cluster_manager, cluster_name, + create_metadata_fetcher_cb, refresh_state, + initialization_timer, config) {} + MOCK_METHOD(void, refresh, (), (override)); +}; + +TEST_F(AsyncCredentialHandlingTest, WeakPtrProtectionInTimerCallback) { + + MetadataFetcher::MetadataReceiver::RefreshState refresh_state = + MetadataFetcher::MetadataReceiver::RefreshState::Ready; + std::chrono::seconds initialization_timer = std::chrono::seconds(2); + + envoy::extensions::common::aws::v3::AssumeRoleWithWebIdentityCredentialProvider cred_provider = + {}; + cred_provider.mutable_web_identity_token_data_source()->set_inline_string("token"); + cred_provider.set_role_arn("aws:iam::123456789012:role/arn"); + cred_provider.set_role_session_name("session"); + + mock_manager_ = std::make_shared(); + EXPECT_CALL(*mock_manager_, getUriFromClusterName(_)).WillRepeatedly(Return("uri")); + + auto mock_provider = std::make_shared( + context_, mock_manager_, "cluster", + [this](Upstream::ClusterManager&, absl::string_view) { + metadata_fetcher_.reset(raw_metadata_fetcher_); + return std::move(metadata_fetcher_); + }, + refresh_state, initialization_timer, cred_provider); + + timer_ = new NiceMock(&context_.dispatcher_); + Event::MockTimer* timer_ptr = timer_; // Keep raw pointer to test after provider destruction + auto provider_friend = MetadataCredentialsProviderBaseFriend(mock_provider); + + // When provider is alive, refresh should be called + EXPECT_CALL(*mock_provider, refresh()); + provider_friend.onClusterAddOrUpdate(); + timer_ptr->enabled_ = true; + timer_ptr->invokeCallback(); + delete (raw_metadata_fetcher_); +} + +TEST_F(AsyncCredentialHandlingTest, WeakPtrProtectionForStatsInTimerCallback) { + MetadataFetcher::MetadataReceiver::RefreshState refresh_state = + MetadataFetcher::MetadataReceiver::RefreshState::Ready; + std::chrono::seconds initialization_timer = std::chrono::seconds(2); + + envoy::extensions::common::aws::v3::AssumeRoleWithWebIdentityCredentialProvider cred_provider = + {}; + cred_provider.mutable_web_identity_token_data_source()->set_inline_string("token"); + cred_provider.set_role_arn("aws:iam::123456789012:role/arn"); + cred_provider.set_role_session_name("session"); + + mock_manager_ = std::make_shared(); + EXPECT_CALL(*mock_manager_, getUriFromClusterName(_)).WillRepeatedly(Return("uri")); + + auto mock_provider = std::make_shared( + context_, mock_manager_, "cluster", + [this](Upstream::ClusterManager&, absl::string_view) { + metadata_fetcher_.reset(raw_metadata_fetcher_); + return std::move(metadata_fetcher_); + }, + refresh_state, initialization_timer, cred_provider); + + timer_ = new NiceMock(&context_.dispatcher_); + Event::MockTimer* timer_ptr = timer_; + auto provider_friend = MetadataCredentialsProviderBaseFriend(mock_provider); + provider_friend.onClusterAddOrUpdate(); + + // Invalidate stats pointer + provider_friend.invalidateStats(); + + // Timer callback will skip the stats call due to weak_ptr lock failing + EXPECT_CALL(*mock_provider, refresh()); + timer_ptr->enabled_ = true; + timer_ptr->invokeCallback(); + delete (raw_metadata_fetcher_); +} + +TEST_F(AsyncCredentialHandlingTest, WeakPtrProtectionInSubscriberCallback) { + MetadataFetcher::MetadataReceiver::RefreshState refresh_state = + MetadataFetcher::MetadataReceiver::RefreshState::Ready; + std::chrono::seconds initialization_timer = std::chrono::seconds(2); + + envoy::extensions::common::aws::v3::AssumeRoleWithWebIdentityCredentialProvider cred_provider = + {}; + cred_provider.mutable_web_identity_token_data_source()->set_inline_string("token"); + cred_provider.set_role_arn("aws:iam::123456789012:role/arn"); + cred_provider.set_role_session_name("session"); + + mock_manager_ = std::make_shared(); + EXPECT_CALL(*mock_manager_, getUriFromClusterName(_)).WillRepeatedly(Return("uri")); + + provider_ = std::make_shared( + context_, mock_manager_, "cluster", + [this](Upstream::ClusterManager&, absl::string_view) { + metadata_fetcher_.reset(raw_metadata_fetcher_); + return std::move(metadata_fetcher_); + }, + refresh_state, initialization_timer, cred_provider); + + auto provider_friend = MetadataCredentialsProviderBaseFriend(provider_); + + // Test 1: When subscriber is alive, onCredentialUpdate should be called + auto chain = std::make_shared(); + EXPECT_CALL(*chain, onCredentialUpdate()); + auto handle = provider_->subscribeToCredentialUpdates(chain); + + // Trigger credential update + provider_friend.setCredentialsToAllThreads(std::make_unique("key", "secret")); + + // Test 2: When subscriber is destroyed, onCredentialUpdate should not be called + EXPECT_CALL(*chain, onCredentialUpdate()).Times(0); + chain.reset(); // Destroy the subscriber + + // Trigger credential update - should not crash due to weak_ptr protection + provider_friend.setCredentialsToAllThreads(std::make_unique("key2", "secret2")); + delete (raw_metadata_fetcher_); +} + class ControlledCredentialsProvider : public CredentialsProvider { public: ControlledCredentialsProvider(CredentialSubscriberCallbacks* cb) : cb_(cb) {} diff --git a/test/extensions/common/aws/mocks.h b/test/extensions/common/aws/mocks.h index ec11ec1288fc9..5ffe21c265980 100644 --- a/test/extensions/common/aws/mocks.h +++ b/test/extensions/common/aws/mocks.h @@ -49,10 +49,12 @@ class MockSigner : public Signer { MockSigner(); ~MockSigner() override; - MOCK_METHOD(absl::Status, sign, (Http::RequestMessage&, bool, absl::string_view)); - MOCK_METHOD(absl::Status, sign, (Http::RequestHeaderMap&, const std::string&, absl::string_view)); - MOCK_METHOD(absl::Status, signEmptyPayload, (Http::RequestHeaderMap&, absl::string_view)); - MOCK_METHOD(absl::Status, signUnsignedPayload, (Http::RequestHeaderMap&, absl::string_view)); + MOCK_METHOD(absl::Status, sign, (Http::RequestMessage&, bool, const absl::string_view)); + MOCK_METHOD(absl::Status, sign, + (Http::RequestHeaderMap&, const std::string&, const absl::string_view)); + MOCK_METHOD(absl::Status, signEmptyPayload, (Http::RequestHeaderMap&, const absl::string_view)); + MOCK_METHOD(absl::Status, signUnsignedPayload, + (Http::RequestHeaderMap&, const absl::string_view)); MOCK_METHOD(bool, addCallbackIfCredentialsPending, (CredentialsPendingCallback&&)); }; @@ -155,6 +157,11 @@ class MetadataCredentialsProviderBaseFriend { provider_->metadata_fetcher_ = std::move(fetcher); } void setCacheDurationTimer(Event::Timer* timer) { provider_->cache_duration_timer_.reset(timer); } + void setCredentialsToAllThreads(CredentialsConstUniquePtr&& creds) { + provider_->setCredentialsToAllThreads(std::move(creds)); + } + void invalidateStats() { provider_->stats_.reset(); } + std::shared_ptr provider_; }; @@ -194,6 +201,8 @@ class MockIAMRolesAnywhereSigV4Signer : public IAMRolesAnywhereSigV4Signer { MOCK_METHOD(absl::Status, sign, (Http::RequestMessage & message, bool sign_body, const absl::string_view override_region)); + MOCK_METHOD(absl::Status, sign, + (Http::RequestHeaderMap&, const std::string&, const absl::string_view)); private: MOCK_METHOD(std::string, createCredentialScope, diff --git a/test/extensions/common/aws/utility_test.cc b/test/extensions/common/aws/utility_test.cc index 88df73339465c..09d8372ee1944 100644 --- a/test/extensions/common/aws/utility_test.cc +++ b/test/extensions/common/aws/utility_test.cc @@ -323,6 +323,12 @@ TEST(UtilityTest, CanonicalizeQueryStringWithPlus) { EXPECT_EQ("a=1%202", canonical_query); } +TEST(UtilityTest, CanonicalizeQueryStringWithPlusEncoded) { + const absl::string_view query = "a=1%2B2"; + const auto canonical_query = Utility::canonicalizeQueryString(query); + EXPECT_EQ("a=1%2B2", canonical_query); +} + TEST(UtilityTest, CanonicalizeQueryStringWithTilde) { const absl::string_view query = "a=1%7E~2"; const auto canonical_query = Utility::canonicalizeQueryString(query); @@ -331,13 +337,13 @@ TEST(UtilityTest, CanonicalizeQueryStringWithTilde) { TEST(UtilityTest, EncodeQuerySegment) { const absl::string_view query = "^!@/-_~."; - const auto encoded_query = Utility::encodeQueryComponent(query); + const auto encoded_query = Utility::encodeQueryComponentPreservingPlus(query); EXPECT_EQ("%5E%21%40%2F-_~.", encoded_query); } TEST(UtilityTest, EncodeQuerySegmentReserved) { const absl::string_view query = "?=&"; - const auto encoded_query = Utility::encodeQueryComponent(query); + const auto encoded_query = Utility::encodeQueryComponentPreservingPlus(query); EXPECT_EQ("%3F%3D%26", encoded_query); } @@ -353,7 +359,7 @@ TEST(UtilityTest, CanonicalizationFuzzTest) { fuzz.push_back(k); Utility::uriEncodePath(fuzz); Utility::normalizePath(fuzz); - Utility::encodeQueryComponent(fuzz); + Utility::encodeQueryComponentPreservingPlus(fuzz); Utility::canonicalizeQueryString(fuzz); fuzz.pop_back(); } diff --git a/test/extensions/common/matcher/BUILD b/test/extensions/common/matcher/BUILD index 3e12b6b801197..345fe7fc6fa52 100644 --- a/test/extensions/common/matcher/BUILD +++ b/test/extensions/common/matcher/BUILD @@ -21,15 +21,15 @@ envoy_cc_test( ) envoy_cc_test( - name = "trie_matcher_test", - srcs = ["trie_matcher_test.cc"], + name = "ip_range_matcher_test", + srcs = ["ip_range_matcher_test.cc"], rbe_pool = "6gig", deps = [ "//source/common/matcher:matcher_lib", "//source/common/network:address_lib", "//source/common/network/matching:data_impl_lib", "//source/common/network/matching:inputs_lib", - "//source/extensions/common/matcher:trie_matcher_lib", + "//source/extensions/common/matcher:ip_range_matcher_lib", "//source/extensions/matching/network/application_protocol:config", "//test/common/matcher:test_utility_lib", "//test/mocks/http:http_mocks", diff --git a/test/extensions/common/matcher/domain_matcher_test.cc b/test/extensions/common/matcher/domain_matcher_test.cc index 1ea833d294e1c..20e57359201d7 100644 --- a/test/extensions/common/matcher/domain_matcher_test.cc +++ b/test/extensions/common/matcher/domain_matcher_test.cc @@ -38,7 +38,6 @@ using ::Envoy::Matcher::MatchResult; using ::Envoy::Matcher::MatchTreeFactory; using ::Envoy::Matcher::MockMatchTreeValidationVisitor; using ::Envoy::Matcher::SkippedMatchCb; -using ::Envoy::Matcher::StringAction; using ::Envoy::Matcher::StringActionFactory; using ::Envoy::Matcher::TestData; using ::Envoy::Matcher::TestDataInputBoolFactory; @@ -625,8 +624,8 @@ TEST_F(DomainMatcherTest, KeepMatchingSupport) { loadConfig(yaml); validation_visitor_.setSupportKeepMatching(true); - std::vector skipped_results; - skipped_match_cb_ = [&skipped_results](Envoy::Matcher::ActionFactoryCb cb) { + std::vector skipped_results; + skipped_match_cb_ = [&skipped_results](const Envoy::Matcher::ActionConstSharedPtr& cb) { skipped_results.push_back(cb); }; diff --git a/test/extensions/common/matcher/trie_matcher_test.cc b/test/extensions/common/matcher/ip_range_matcher_test.cc similarity index 95% rename from test/extensions/common/matcher/trie_matcher_test.cc rename to test/extensions/common/matcher/ip_range_matcher_test.cc index 02f40777e9db2..be6fef2eccf4a 100644 --- a/test/extensions/common/matcher/trie_matcher_test.cc +++ b/test/extensions/common/matcher/ip_range_matcher_test.cc @@ -8,7 +8,7 @@ #include "source/common/network/address_impl.h" #include "source/common/network/matching/data_impl.h" #include "source/common/protobuf/utility.h" -#include "source/extensions/common/matcher/trie_matcher.h" +#include "source/extensions/common/matcher/ip_range_matcher.h" #include "test/common/matcher/test_utility.h" #include "test/mocks/matcher/mocks.h" @@ -27,8 +27,8 @@ namespace Common { namespace Matcher { namespace { +using ::Envoy::Matcher::ActionConstSharedPtr; using ::Envoy::Matcher::ActionFactory; -using ::Envoy::Matcher::ActionFactoryCb; using ::Envoy::Matcher::CustomMatcherFactory; using ::Envoy::Matcher::DataInputGetResult; using ::Envoy::Matcher::HasInsufficientData; @@ -36,22 +36,21 @@ using ::Envoy::Matcher::HasNoMatch; using ::Envoy::Matcher::HasStringAction; using ::Envoy::Matcher::IsStringAction; using ::Envoy::Matcher::MatchResult; -using ::Envoy::Matcher::MatchTree; using ::Envoy::Matcher::MatchTreeFactory; using ::Envoy::Matcher::MatchTreePtr; using ::Envoy::Matcher::MatchTreeSharedPtr; using ::Envoy::Matcher::MockMatchTreeValidationVisitor; -using ::Envoy::Matcher::StringAction; +using ::Envoy::Matcher::SkippedMatchCb; using ::Envoy::Matcher::StringActionFactory; using ::Envoy::Matcher::TestData; using ::Envoy::Matcher::TestDataInputBoolFactory; using ::Envoy::Matcher::TestDataInputStringFactory; using ::testing::ElementsAre; -class TrieMatcherTest : public ::testing::Test { +class IpRangeMatcherTest : public ::testing::Test { public: - TrieMatcherTest() - : inject_action_(action_factory_), inject_matcher_(trie_matcher_factory_), + IpRangeMatcherTest() + : inject_action_(action_factory_), inject_matcher_(ip_range_matcher_factory_), factory_(context_, factory_context_, validation_visitor_) { EXPECT_CALL(validation_visitor_, performDataInputValidation(_, _)).Times(testing::AnyNumber()); } @@ -68,7 +67,7 @@ class TrieMatcherTest : public ::testing::Test { StringActionFactory action_factory_; Registry::InjectFactory> inject_action_; - TrieMatcherFactoryBase trie_matcher_factory_; + IpRangeMatcherFactoryBase ip_range_matcher_factory_; Registry::InjectFactory> inject_matcher_; MockMatchTreeValidationVisitor validation_visitor_; @@ -80,7 +79,7 @@ class TrieMatcherTest : public ::testing::Test { SkippedMatchCb skipped_match_cb_ = nullptr; }; -TEST_F(TrieMatcherTest, TestMatcher) { +TEST_F(IpRangeMatcherTest, TestMatcher) { const std::string yaml = R"EOF( matcher_tree: input: @@ -131,7 +130,7 @@ TEST_F(TrieMatcherTest, TestMatcher) { } } -TEST_F(TrieMatcherTest, TestInvalidMatcher) { +TEST_F(IpRangeMatcherTest, TestInvalidMatcher) { const std::string yaml = R"EOF( matcher_tree: input: @@ -166,11 +165,11 @@ TEST_F(TrieMatcherTest, TestInvalidMatcher) { auto input_factory = ::Envoy::Matcher::TestDataInputFloatFactory(3.14); auto match_tree = factory_.create(matcher_); std::string error_message = absl::StrCat("Unsupported data input type: float, currently only " - "string type is supported in trie matcher"); + "string type is supported in IP range matcher"); EXPECT_THROW_WITH_MESSAGE(match_tree(), EnvoyException, error_message); } -TEST_F(TrieMatcherTest, TestMatcherOnNoMatch) { +TEST_F(IpRangeMatcherTest, TestMatcherOnNoMatch) { const std::string yaml = R"EOF( matcher_tree: input: @@ -222,7 +221,7 @@ TEST_F(TrieMatcherTest, TestMatcherOnNoMatch) { } } -TEST_F(TrieMatcherTest, OverlappingMatcher) { +TEST_F(IpRangeMatcherTest, OverlappingMatcher) { const std::string yaml = R"EOF( matcher_tree: input: @@ -277,7 +276,7 @@ TEST_F(TrieMatcherTest, OverlappingMatcher) { } } -TEST_F(TrieMatcherTest, NestedInclusiveMatcher) { +TEST_F(IpRangeMatcherTest, NestedInclusiveMatcher) { const std::string yaml = R"EOF( matcher_tree: input: @@ -335,7 +334,7 @@ TEST_F(TrieMatcherTest, NestedInclusiveMatcher) { } } -TEST_F(TrieMatcherTest, NestedExclusiveMatcher) { +TEST_F(IpRangeMatcherTest, NestedExclusiveMatcher) { const std::string yaml = R"EOF( matcher_tree: input: @@ -395,7 +394,7 @@ TEST_F(TrieMatcherTest, NestedExclusiveMatcher) { } } -TEST_F(TrieMatcherTest, RecursiveMatcherTree) { +TEST_F(IpRangeMatcherTest, RecursiveMatcherTree) { const std::string yaml = R"EOF( matcher_tree: input: @@ -468,7 +467,7 @@ TEST_F(TrieMatcherTest, RecursiveMatcherTree) { } } -TEST_F(TrieMatcherTest, NoData) { +TEST_F(IpRangeMatcherTest, NoData) { const std::string yaml = R"EOF( matcher_tree: input: @@ -532,7 +531,7 @@ TEST_F(TrieMatcherTest, NoData) { } } -TEST_F(TrieMatcherTest, ExerciseKeepMatching) { +TEST_F(IpRangeMatcherTest, ExerciseKeepMatching) { const std::string yaml = R"EOF( matcher_tree: input: @@ -604,8 +603,10 @@ TEST_F(TrieMatcherTest, ExerciseKeepMatching) { // Skip baz because the nested matcher is set with keep_matching. // Skip bag because the nested matcher returns on_no_match, but the top-level matcher is set to // keep_matching. - std::vector skipped_results{}; - skipped_match_cb_ = [&skipped_results](ActionFactoryCb cb) { skipped_results.push_back(cb); }; + std::vector skipped_results{}; + skipped_match_cb_ = [&skipped_results](const ActionConstSharedPtr& cb) { + skipped_results.push_back(cb); + }; auto input = TestDataInputStringFactory("192.101.0.1"); auto nested = TestDataInputBoolFactory("baz"); @@ -615,7 +616,7 @@ TEST_F(TrieMatcherTest, ExerciseKeepMatching) { ElementsAre(IsStringAction("foo"), IsStringAction("baz"), IsStringAction("bag"))); } -TEST(TrieMatcherIntegrationTest, NetworkMatchingData) { +TEST(IpRangeMatcherIntegrationTest, NetworkMatchingData) { const std::string yaml = R"EOF( matcher_tree: input: @@ -661,7 +662,7 @@ TEST(TrieMatcherIntegrationTest, NetworkMatchingData) { EXPECT_THAT(result, HasStringAction("foo")); } -TEST(TrieMatcherIntegrationTest, UdpNetworkMatchingData) { +TEST(IpRangeMatcherIntegrationTest, UdpNetworkMatchingData) { const std::string yaml = R"EOF( matcher_tree: input: @@ -704,7 +705,7 @@ TEST(TrieMatcherIntegrationTest, UdpNetworkMatchingData) { EXPECT_THAT(result, HasStringAction("foo")); } -TEST(TrieMatcherIntegrationTest, HttpMatchingData) { +TEST(IpRangeMatcherIntegrationTest, HttpMatchingData) { const std::string yaml = R"EOF( matcher_tree: input: diff --git a/test/extensions/common/tap/admin_test.cc b/test/extensions/common/tap/admin_test.cc index 9a0e9e6d54398..ec3c7e2cabb92 100644 --- a/test/extensions/common/tap/admin_test.cc +++ b/test/extensions/common/tap/admin_test.cc @@ -190,9 +190,8 @@ TEST(TypedExtensionConfigTest, AddTestConfig) { MockTapSinkFactory factory_impl; EXPECT_CALL(factory_impl, name).Times(AtLeast(1)); EXPECT_CALL(factory_impl, createEmptyConfigProto) - .WillRepeatedly(Invoke([]() -> ProtobufTypes::MessagePtr { - return std::make_unique(); - })); + .WillRepeatedly(Invoke( + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); EXPECT_CALL(factory_impl, createSinkPtr(_, _)); Registry::InjectFactory factory(factory_impl); @@ -222,9 +221,8 @@ TEST(TypedExtensionConfigTest, AddTestConfigForMinStreamedSentBytes) { MockTapSinkFactory factory_impl; EXPECT_CALL(factory_impl, name).Times(AtLeast(1)); EXPECT_CALL(factory_impl, createEmptyConfigProto) - .WillRepeatedly(Invoke([]() -> ProtobufTypes::MessagePtr { - return std::make_unique(); - })); + .WillRepeatedly(Invoke( + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); EXPECT_CALL(factory_impl, createSinkPtr(_, _)); Registry::InjectFactory factory(factory_impl); @@ -253,9 +251,8 @@ TEST(TypedExtensionConfigTest, BufferedAdminNoAdminStreamerRejected) { MockTapSinkFactory factory_impl; EXPECT_CALL(factory_impl, name).Times(AtLeast(1)); EXPECT_CALL(factory_impl, createEmptyConfigProto) - .WillRepeatedly(Invoke([]() -> ProtobufTypes::MessagePtr { - return std::make_unique(); - })); + .WillRepeatedly(Invoke( + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); Registry::InjectFactory factory(factory_impl); NiceMock factory_context; @@ -281,9 +278,8 @@ TEST(TypedExtensionConfigTest, StreamingAdminNoAdminStreamerRejected) { MockTapSinkFactory factory_impl; EXPECT_CALL(factory_impl, name).Times(AtLeast(1)); EXPECT_CALL(factory_impl, createEmptyConfigProto) - .WillRepeatedly(Invoke([]() -> ProtobufTypes::MessagePtr { - return std::make_unique(); - })); + .WillRepeatedly(Invoke( + []() -> ProtobufTypes::MessagePtr { return std::make_unique(); })); Registry::InjectFactory factory(factory_impl); NiceMock factory_context; diff --git a/test/extensions/common/wasm/wasm_test.cc b/test/extensions/common/wasm/wasm_test.cc index aee687e10ccea..783a35184b0a6 100644 --- a/test/extensions/common/wasm/wasm_test.cc +++ b/test/extensions/common/wasm/wasm_test.cc @@ -148,7 +148,7 @@ TEST_P(WasmCommonTest, WasmFailState) { Filters::Common::Expr::CelValue::Type::kNullType); wasm_state->setValue("foo"); auto any = wasm_state->serializeAsProto(); - EXPECT_TRUE(static_cast(any.get())->Is()); + EXPECT_TRUE(static_cast(any.get())->Is()); } TEST_P(WasmCommonTest, Logging) { @@ -580,7 +580,7 @@ TEST_P(WasmCommonTest, VmCache) { auto vm_config = plugin_config.mutable_vm_config(); vm_config->set_runtime(absl::StrCat("envoy.wasm.runtime.", std::get<0>(GetParam()))); - ProtobufWkt::StringValue vm_configuration_string; + Protobuf::StringValue vm_configuration_string; vm_configuration_string.set_value(vm_configuration); vm_config->mutable_configuration()->PackFrom(vm_configuration_string); std::string code; @@ -669,7 +669,7 @@ TEST_P(WasmCommonTest, RemoteCode) { auto vm_config = plugin_config.mutable_vm_config(); vm_config->set_runtime(absl::StrCat("envoy.wasm.runtime.", std::get<0>(GetParam()))); - ProtobufWkt::BytesValue vm_configuration_bytes; + Protobuf::BytesValue vm_configuration_bytes; vm_configuration_bytes.set_value(vm_configuration); vm_config->mutable_configuration()->PackFrom(vm_configuration_bytes); std::string sha256 = Extensions::Common::Wasm::sha256(code); @@ -772,7 +772,7 @@ TEST_P(WasmCommonTest, RemoteCodeMultipleRetry) { auto vm_config = plugin_config.mutable_vm_config(); vm_config->set_runtime(absl::StrCat("envoy.wasm.runtime.", std::get<0>(GetParam()))); - ProtobufWkt::StringValue vm_configuration_string; + Protobuf::StringValue vm_configuration_string; vm_configuration_string.set_value(vm_configuration); vm_config->mutable_configuration()->PackFrom(vm_configuration_string); std::string sha256 = Extensions::Common::Wasm::sha256(code); diff --git a/test/extensions/config/validators/minimum_clusters/config_test.cc b/test/extensions/config/validators/minimum_clusters/config_test.cc index adc2c2ef7f967..eda3911252f72 100644 --- a/test/extensions/config/validators/minimum_clusters/config_test.cc +++ b/test/extensions/config/validators/minimum_clusters/config_test.cc @@ -20,7 +20,7 @@ TEST(MinimumClustersValidatorFactoryTest, CreateValidator) { envoy::extensions::config::validators::minimum_clusters::v3::MinimumClustersValidator config; config.set_min_clusters_num(5); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(config); auto validator = factory->createConfigValidator(typed_config, ProtobufMessage::getStrictValidationVisitor()); @@ -38,7 +38,7 @@ TEST(MinimumClustersValidatorFactoryTest, CreateEmptyValidator) { empty_proto.get()); EXPECT_EQ(0, config.min_clusters_num()); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(config); auto validator = factory->createConfigValidator(typed_config, ProtobufMessage::getStrictValidationVisitor()); diff --git a/test/extensions/config/validators/minimum_clusters/minimum_clusters_validator_integration_test.cc b/test/extensions/config/validators/minimum_clusters/minimum_clusters_validator_integration_test.cc index 087b5833d39fa..42ac0309d646a 100644 --- a/test/extensions/config/validators/minimum_clusters/minimum_clusters_validator_integration_test.cc +++ b/test/extensions/config/validators/minimum_clusters/minimum_clusters_validator_integration_test.cc @@ -91,8 +91,8 @@ class MinimumClustersValidatorIntegrationTest : public Grpc::DeltaSotwIntegratio acceptXdsConnection(); // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for the clusters. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, clusters_, clusters_, {}, "7"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of @@ -164,12 +164,12 @@ TEST_P(MinimumClustersValidatorIntegrationTest, RemoveAllClustersThreshold0) { return cluster.name(); }); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "7", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {removed_clusters_names}, "8"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "7", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {removed_clusters_names}, "8"); // Receive ACK. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "8", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "8", {}, {}, {})); EXPECT_EQ(1, test_server_->gauge("cluster_manager.active_clusters")->value()); } @@ -187,13 +187,13 @@ TEST_P(MinimumClustersValidatorIntegrationTest, RemoveAllClustersThreshold1) { return cluster.name(); }); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "7", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {removed_clusters_names}, "8"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "7", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {removed_clusters_names}, "8"); // Receive NACK. EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "7", {}, {}, {}, false, + compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "7", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Internal, "CDS update attempts to reduce clusters below configured minimum.")); EXPECT_EQ(3, test_server_->gauge("cluster_manager.active_clusters")->value()); diff --git a/test/extensions/config_subscription/common/subscription_factory_impl_test.cc b/test/extensions/config_subscription/common/subscription_factory_impl_test.cc index 0c1138c604640..76d04b57c3008 100644 --- a/test/extensions/config_subscription/common/subscription_factory_impl_test.cc +++ b/test/extensions/config_subscription/common/subscription_factory_impl_test.cc @@ -31,15 +31,16 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +namespace Envoy { +namespace Config { +namespace { + using ::testing::_; +using ::testing::Eq; using ::testing::Invoke; using ::testing::Return; using ::testing::ReturnRef; -namespace Envoy { -namespace Config { -namespace { - enum class LegacyOrUnified { Legacy, Unified }; class SubscriptionFactoryTest : public testing::Test { @@ -57,7 +58,7 @@ class SubscriptionFactoryTest : public testing::Test { SubscriptionPtr subscriptionFromConfigSource(const envoy::config::core::v3::ConfigSource& config) { return THROW_OR_RETURN_VALUE(subscription_factory_.subscriptionFromConfigSource( - config, Config::TypeUrl::get().ClusterLoadAssignment, + config, Config::TestTypeUrl::get().ClusterLoadAssignment, *stats_store_.rootScope(), callbacks_, resource_decoder_, {}), SubscriptionPtr); } @@ -65,7 +66,8 @@ class SubscriptionFactoryTest : public testing::Test { SubscriptionPtr subscriptionOverAdsGrpcMux(GrpcMuxSharedPtr grpc_mux, const envoy::config::core::v3::ConfigSource& config) { return THROW_OR_RETURN_VALUE(subscription_factory_.subscriptionOverAdsGrpcMux( - grpc_mux, config, Config::TypeUrl::get().ClusterLoadAssignment, + grpc_mux, config, + Config::TestTypeUrl::get().ClusterLoadAssignment, *stats_store_.rootScope(), callbacks_, resource_decoder_, {}), SubscriptionPtr); } @@ -369,7 +371,7 @@ TEST_F(SubscriptionFactoryTest, FilesystemSubscription) { config.set_path(test_path); auto* watcher = new Filesystem::MockWatcher(); EXPECT_CALL(dispatcher_, createFilesystemWatcher_()).WillOnce(Return(watcher)); - EXPECT_CALL(*watcher, addWatch(test_path, _, _)); + EXPECT_CALL(*watcher, addWatch(Eq(test_path), _, _)); EXPECT_CALL(callbacks_, onConfigUpdateFailed(_, _)); subscriptionFromConfigSource(config)->start({"foo"}); } @@ -386,7 +388,7 @@ TEST_F(SubscriptionFactoryTest, FilesystemCollectionSubscription) { std::string test_path = TestEnvironment::temporaryDirectory(); auto* watcher = new Filesystem::MockWatcher(); EXPECT_CALL(dispatcher_, createFilesystemWatcher_()).WillOnce(Return(watcher)); - EXPECT_CALL(*watcher, addWatch(test_path, _, _)); + EXPECT_CALL(*watcher, addWatch(Eq(test_path), _, _)); EXPECT_CALL(callbacks_, onConfigUpdateFailed(_, _)); // Unix paths start with /, Windows with c:/. const std::string file_path = test_path[0] == '/' ? test_path.substr(1) : test_path; @@ -659,7 +661,7 @@ TEST_F(SubscriptionFactoryTest, LogWarningOnDeprecatedV2Transport) { EXPECT_THAT(subscription_factory_ .subscriptionFromConfigSource( - config, Config::TypeUrl::get().ClusterLoadAssignment, + config, Config::TestTypeUrl::get().ClusterLoadAssignment, *stats_store_.rootScope(), callbacks_, resource_decoder_, {}) .status() .message(), diff --git a/test/extensions/config_subscription/grpc/delta_subscription_impl_test.cc b/test/extensions/config_subscription/grpc/delta_subscription_impl_test.cc index 05c6fd03eb557..f378c4bec1727 100644 --- a/test/extensions/config_subscription/grpc/delta_subscription_impl_test.cc +++ b/test/extensions/config_subscription/grpc/delta_subscription_impl_test.cc @@ -82,7 +82,7 @@ TEST_P(DeltaSubscriptionImplTest, PauseQueuesAcks) { resource->set_version("version1A"); const std::string nonce = std::to_string(HashUtil::xxHash64("version1A")); message->set_nonce(nonce); - message->set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + message->set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); nonce_acks_required_.push(nonce); onDiscoveryResponse(std::move(message)); } @@ -95,7 +95,7 @@ TEST_P(DeltaSubscriptionImplTest, PauseQueuesAcks) { resource->set_version("version2A"); const std::string nonce = std::to_string(HashUtil::xxHash64("version2A")); message->set_nonce(nonce); - message->set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + message->set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); nonce_acks_required_.push(nonce); onDiscoveryResponse(std::move(message)); } @@ -108,7 +108,7 @@ TEST_P(DeltaSubscriptionImplTest, PauseQueuesAcks) { resource->set_version("version1B"); const std::string nonce = std::to_string(HashUtil::xxHash64("version1B")); message->set_nonce(nonce); - message->set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + message->set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); nonce_acks_required_.push(nonce); onDiscoveryResponse(std::move(message)); } @@ -175,8 +175,9 @@ TEST_P(DeltaSubscriptionNoGrpcStreamTest, NoGrpcStream) { } GrpcSubscriptionImplPtr subscription = std::make_unique( - xds_context, callbacks, resource_decoder, stats, Config::TypeUrl::get().ClusterLoadAssignment, - dispatcher, std::chrono::milliseconds(12345), false, SubscriptionOptions()); + xds_context, callbacks, resource_decoder, stats, + Config::TestTypeUrl::get().ClusterLoadAssignment, dispatcher, + std::chrono::milliseconds(12345), false, SubscriptionOptions()); EXPECT_CALL(*async_client, startRaw(_, _, _, _)).WillOnce(Return(nullptr)); diff --git a/test/extensions/config_subscription/grpc/delta_subscription_state_test.cc b/test/extensions/config_subscription/grpc/delta_subscription_state_test.cc index bea8da1e122cc..8beec372fa14f 100644 --- a/test/extensions/config_subscription/grpc/delta_subscription_state_test.cc +++ b/test/extensions/config_subscription/grpc/delta_subscription_state_test.cc @@ -474,7 +474,7 @@ TEST_P(DeltaSubscriptionStateTestBlank, AmbiguousResourceTTL) { } if (ttl_s) { - ProtobufWkt::Duration ttl; + Protobuf::Duration ttl; ttl.set_seconds(ttl_s->count()); resource->mutable_ttl()->CopyFrom(ttl); } @@ -1197,7 +1197,7 @@ TEST_P(DeltaSubscriptionStateTest, ResourceTTL) { } if (ttl_s) { - ProtobufWkt::Duration ttl; + Protobuf::Duration ttl; ttl.set_seconds(ttl_s->count()); resource->mutable_ttl()->CopyFrom(ttl); } @@ -1264,95 +1264,7 @@ TEST_P(DeltaSubscriptionStateTest, HeartbeatResourcesNotProcessed) { } if (ttl_s) { - ProtobufWkt::Duration ttl; - ttl.set_seconds(ttl_s->count()); - resource.mutable_ttl()->CopyFrom(ttl); - } - - return resource; - }; - - // Add 3 resources. - { - Protobuf::RepeatedPtrField added_resources; - const auto r1 = create_resource_with_ttl("name1", "version1", std::chrono::seconds(1), true); - const auto r2 = create_resource_with_ttl("name2", "version1", std::chrono::seconds(1), true); - const auto r3 = create_resource_with_ttl("name3", "version1", std::chrono::seconds(1), true); - added_resources.Add()->CopyFrom(r1); - added_resources.Add()->CopyFrom(r2); - added_resources.Add()->CopyFrom(r3); - EXPECT_CALL(*ttl_timer_, enabled()); - EXPECT_CALL(*ttl_timer_, enableTimer(std::chrono::milliseconds(1000), _)); - deliverDiscoveryResponse(added_resources, {}, "sys_version1", "nonce1", {r1, r2, r3}); - } - - // Update 3 resources, the second is a heartbeat. Validate that only the other - // two are passed to onConfigUpdate. - { - Protobuf::RepeatedPtrField added_resources; - const auto r1 = create_resource_with_ttl("name1", "version2", std::chrono::seconds(1), true); - const auto r2 = create_resource_with_ttl("name2", "version1", std::chrono::seconds(1), false); - const auto r3 = create_resource_with_ttl("name3", "version2", std::chrono::seconds(1), true); - added_resources.Add()->CopyFrom(r1); - added_resources.Add()->CopyFrom(r2); - added_resources.Add()->CopyFrom(r3); - EXPECT_CALL(*ttl_timer_, enabled()); - deliverDiscoveryResponse(added_resources, {}, "sys_version2", "nonce2", {r1, r3}); - } - - // Update 3 resources, the first is a heartbeat. Validate that only the other - // two are passed to onConfigUpdate. - { - Protobuf::RepeatedPtrField added_resources; - const auto r1 = create_resource_with_ttl("name1", "version2", std::chrono::seconds(1), false); - const auto r2 = create_resource_with_ttl("name2", "version3", std::chrono::seconds(1), true); - const auto r3 = create_resource_with_ttl("name3", "version3", std::chrono::seconds(1), true); - added_resources.Add()->CopyFrom(r1); - added_resources.Add()->CopyFrom(r2); - added_resources.Add()->CopyFrom(r3); - EXPECT_CALL(*ttl_timer_, enabled()); - deliverDiscoveryResponse(added_resources, {}, "sys_version3", "nonce3", {r2, r3}); - } - - // Update 3 resources, the last is a heartbeat. Validate that only the other - // two are passed to onConfigUpdate. - { - Protobuf::RepeatedPtrField added_resources; - const auto r1 = create_resource_with_ttl("name1", "version4", std::chrono::seconds(1), true); - const auto r2 = create_resource_with_ttl("name2", "version4", std::chrono::seconds(1), true); - const auto r3 = create_resource_with_ttl("name3", "version3", std::chrono::seconds(1), false); - added_resources.Add()->CopyFrom(r1); - added_resources.Add()->CopyFrom(r2); - added_resources.Add()->CopyFrom(r3); - EXPECT_CALL(*ttl_timer_, enabled()); - deliverDiscoveryResponse(added_resources, {}, "sys_version4", "nonce4", {r1, r2}); - } -} - -// Validates that when both heartbeat and non-heartbeat resources are sent, only -// the non-heartbeat resources are processed. -// Once "envoy.reloadable_features.xds_prevent_resource_copy" is removed, this -// test should be removed (the HeartbeatResourcesNotProcessed will validate the -// required aspects). -TEST_P(DeltaSubscriptionStateTest, HeartbeatResourcesNotProcessedWithResourceCopy) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.xds_prevent_resource_copy", "false"}}); - Event::SimulatedTimeSystem time_system; - time_system.setSystemTime(std::chrono::milliseconds(0)); - - auto create_resource_with_ttl = [](absl::string_view name, absl::string_view version, - absl::optional ttl_s, - bool include_resource) { - envoy::service::discovery::v3::Resource resource; - resource.set_name(name); - resource.set_version(version); - - if (include_resource) { - resource.mutable_resource(); - } - - if (ttl_s) { - ProtobufWkt::Duration ttl; + Protobuf::Duration ttl; ttl.set_seconds(ttl_s->count()); resource.mutable_ttl()->CopyFrom(ttl); } @@ -1500,7 +1412,7 @@ TEST_P(VhdsDeltaSubscriptionStateTest, ResourceTTL) { resource->mutable_resource(); } - ProtobufWkt::Duration ttl; + Protobuf::Duration ttl; ttl.set_seconds(1); resource->mutable_ttl()->CopyFrom(ttl); diff --git a/test/extensions/config_subscription/grpc/delta_subscription_test_harness.h b/test/extensions/config_subscription/grpc/delta_subscription_test_harness.h index 57196a1ff8c1d..6e91199b7e05f 100644 --- a/test/extensions/config_subscription/grpc/delta_subscription_test_harness.h +++ b/test/extensions/config_subscription/grpc/delta_subscription_test_harness.h @@ -71,7 +71,7 @@ class DeltaSubscriptionTestHarness : public SubscriptionTestHarness { } subscription_ = std::make_unique( xds_context_, callbacks_, resource_decoder_, stats_, - Config::TypeUrl::get().ClusterLoadAssignment, dispatcher_, init_fetch_timeout, false, + Config::TestTypeUrl::get().ClusterLoadAssignment, dispatcher_, init_fetch_timeout, false, SubscriptionOptions()); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); } @@ -128,7 +128,7 @@ class DeltaSubscriptionTestHarness : public SubscriptionTestHarness { nonce_acks_required_.push(last_response_nonce_); last_response_nonce_ = ""; } - expected_request.set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + expected_request.set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); for (auto const& resource : initial_resource_versions) { (*expected_request.mutable_initial_resource_versions())[resource.first] = resource.second; @@ -169,7 +169,7 @@ class DeltaSubscriptionTestHarness : public SubscriptionTestHarness { last_response_nonce_ = std::to_string(HashUtil::xxHash64(version)); response->set_nonce(last_response_nonce_); response->set_system_version_info(version); - response->set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + response->set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); Protobuf::RepeatedPtrField typed_resources; for (const auto& cluster : cluster_names) { diff --git a/test/extensions/config_subscription/grpc/grpc_mux_impl_test.cc b/test/extensions/config_subscription/grpc/grpc_mux_impl_test.cc index a87b7b9844799..40628e31ddaba 100644 --- a/test/extensions/config_subscription/grpc/grpc_mux_impl_test.cc +++ b/test/extensions/config_subscription/grpc/grpc_mux_impl_test.cc @@ -308,7 +308,7 @@ TEST_P(GrpcMuxImplTest, ReconnectionResetsNonceAndAcks) { .WillRepeatedly(Return(ttl_mgr_timer)); setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = grpc_mux_->addWatch(type_url, {"x", "y"}, callbacks_, resource_decoder, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // Send on connection. @@ -481,7 +481,7 @@ TEST_P(GrpcMuxImplTest, ResourceTTL) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; auto* ttl_timer = new Event::MockTimer(&dispatcher_); auto eds_sub = grpc_mux_->addWatch(type_url, {"x"}, callbacks_, resource_decoder, {}); @@ -642,7 +642,7 @@ TEST_P(GrpcMuxImplTest, WildcardWatch) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); @@ -680,7 +680,7 @@ TEST_P(GrpcMuxImplTest, WatchDemux) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; NiceMock foo_callbacks; auto foo_sub = grpc_mux_->addWatch(type_url, {"x", "y"}, foo_callbacks, resource_decoder, {}); NiceMock bar_callbacks; @@ -765,7 +765,7 @@ TEST_P(GrpcMuxImplTest, WatchDemux) { TEST_P(GrpcMuxImplTest, MultipleWatcherWithEmptyUpdates) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; NiceMock foo_callbacks; auto foo_sub = grpc_mux_->addWatch(type_url, {"x", "y"}, foo_callbacks, resource_decoder_, {}); @@ -787,7 +787,7 @@ TEST_P(GrpcMuxImplTest, MultipleWatcherWithEmptyUpdates) { // Validate behavior when we have Single Watcher that sends Empty updates. TEST_P(GrpcMuxImplTest, SingleWatcherWithEmptyUpdates) { setup(); - const std::string& type_url = Config::TypeUrl::get().Cluster; + const std::string& type_url = Config::TestTypeUrl::get().Cluster; NiceMock foo_callbacks; auto foo_sub = grpc_mux_->addWatch(type_url, {}, foo_callbacks, resource_decoder_, {}); @@ -972,7 +972,7 @@ TEST_P(GrpcMuxImplTest, UnwatchedTypeAcceptsEmptyResources) { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; grpc_mux_->start(); { @@ -1008,7 +1008,7 @@ TEST_P(GrpcMuxImplTest, UnwatchedTypeRejectsResources) { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; grpc_mux_->start(); // subscribe and unsubscribe (by not keeping the return watch) so that the type is known to envoy @@ -1107,7 +1107,7 @@ TEST_P(GrpcMuxImplTest, CacheEdsResource) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; auto eds_sub = grpc_mux_->addWatch(type_url, {"x"}, callbacks_, resource_decoder, {}); @@ -1149,7 +1149,7 @@ TEST_P(GrpcMuxImplTest, UpdateCacheEdsResource) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; auto eds_sub = grpc_mux_->addWatch(type_url, {"x"}, callbacks_, resource_decoder, {}); @@ -1197,7 +1197,7 @@ TEST_P(GrpcMuxImplTest, AddRemoveSubscriptions) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; { @@ -1275,7 +1275,7 @@ TEST_P(GrpcMuxImplTest, RemoveCachedResourceOnLastSubscription) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; NiceMock eds_sub1_callbacks; @@ -1397,7 +1397,7 @@ TEST_P(GrpcMuxImplTest, MuxDynamicReplacementFetchingResources) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); diff --git a/test/extensions/config_subscription/grpc/grpc_stream_test.cc b/test/extensions/config_subscription/grpc/grpc_stream_test.cc index 2ef55b4b367bf..ebe23e6e82c39 100644 --- a/test/extensions/config_subscription/grpc/grpc_stream_test.cc +++ b/test/extensions/config_subscription/grpc/grpc_stream_test.cc @@ -39,7 +39,7 @@ class GrpcStreamTest : public testing::Test { dispatcher_, *stats_.rootScope(), std::move(backoff_strategy_), rate_limit_settings_, GrpcStream::ConnectedStateValue:: - FIRST_ENTRY)) {} + FirstEntry)) {} void setUpCustomBackoffRetryTimer(uint32_t retry_initial_delay_ms, absl::optional retry_max_delay_ms, @@ -59,7 +59,7 @@ class GrpcStreamTest : public testing::Test { dispatcher_, *stats_.rootScope(), std::move(backoff_strategy_), rate_limit_settings_, GrpcStream< envoy::service::discovery::v3::DiscoveryRequest, - envoy::service::discovery::v3::DiscoveryResponse>::ConnectedStateValue::FIRST_ENTRY); + envoy::service::discovery::v3::DiscoveryResponse>::ConnectedStateValue::FirstEntry); } NiceMock dispatcher_; diff --git a/test/extensions/config_subscription/grpc/new_grpc_mux_impl_test.cc b/test/extensions/config_subscription/grpc/new_grpc_mux_impl_test.cc index 49a9b6e62dedd..d25687bbe469c 100644 --- a/test/extensions/config_subscription/grpc/new_grpc_mux_impl_test.cc +++ b/test/extensions/config_subscription/grpc/new_grpc_mux_impl_test.cc @@ -243,7 +243,7 @@ TEST_P(NewGrpcMuxImplTest, ReconnectionResetsNonceAndAcks) { .WillRepeatedly(Return(ttl_mgr_timer)); setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = grpc_mux_->addWatch(type_url, {"x", "y"}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // Send on connection. @@ -303,7 +303,7 @@ TEST_P(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { .WillRepeatedly(Return(ttl_mgr_timer)); setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = grpc_mux_->addWatch(type_url, {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // Send a wildcard request on new connection. @@ -388,7 +388,7 @@ TEST_P(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { TEST_P(NewGrpcMuxImplTest, DiscoveryResponseNonexistentSub) { setup(); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto watch = grpc_mux_->addWatch(type_url, {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); @@ -428,7 +428,7 @@ TEST_P(NewGrpcMuxImplTest, DiscoveryResponseNonexistentSub) { TEST_P(NewGrpcMuxImplTest, ConfigUpdateWithAliases) { setup(); - const std::string& type_url = Config::TypeUrl::get().VirtualHost; + const std::string& type_url = Config::TestTypeUrl::get().VirtualHost; SubscriptionOptions options; options.use_namespace_matching_ = true; auto watch = grpc_mux_->addWatch(type_url, {"prefix"}, callbacks_, resource_decoder_, options); @@ -465,7 +465,7 @@ TEST_P(NewGrpcMuxImplTest, ConfigUpdateWithAliases) { TEST_P(NewGrpcMuxImplTest, ConfigUpdateWithNotFoundResponse) { setup(); - const std::string& type_url = Config::TypeUrl::get().VirtualHost; + const std::string& type_url = Config::TestTypeUrl::get().VirtualHost; SubscriptionOptions options; options.use_namespace_matching_ = true; auto watch = grpc_mux_->addWatch(type_url, {"prefix"}, callbacks_, resource_decoder_, options); @@ -486,7 +486,7 @@ TEST_P(NewGrpcMuxImplTest, ConfigUpdateWithNotFoundResponse) { TEST_P(NewGrpcMuxImplTest, XdsTpGlobCollection) { setup(); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; xds::core::v3::ContextParams context_params; (*context_params.mutable_params())["foo"] = "bar"; EXPECT_CALL(local_info_.context_provider_, nodeContext()).WillOnce(ReturnRef(context_params)); @@ -526,7 +526,7 @@ TEST_P(NewGrpcMuxImplTest, XdsTpGlobCollection) { TEST_P(NewGrpcMuxImplTest, XdsTpSingleton) { setup(); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; EXPECT_CALL(local_info_.context_provider_, nodeContext()).Times(0); // We verify that the gRPC mux normalizes the context parameter order below. Node context // parameters are skipped. @@ -627,7 +627,7 @@ TEST_P(NewGrpcMuxImplTest, CacheEdsResource) { eds_resources_cache_ = new NiceMock(); setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto watch = grpc_mux_->addWatch(type_url, {"x"}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); @@ -672,7 +672,7 @@ TEST_P(NewGrpcMuxImplTest, UpdateCacheEdsResource) { eds_resources_cache_ = new NiceMock(); setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto watch = grpc_mux_->addWatch(type_url, {"x"}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); @@ -722,7 +722,7 @@ TEST_P(NewGrpcMuxImplTest, AddRemoveSubscriptions) { eds_resources_cache_ = new NiceMock(); setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; { auto watch1 = grpc_mux_->addWatch(type_url, {"x"}, callbacks_, resource_decoder_, {}); @@ -846,7 +846,7 @@ TEST_P(NewGrpcMuxImplTest, MuxDynamicReplacementFetchingResources) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = grpc_mux_->addWatch(type_url, {"x", "y"}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage(type_url, {"x", "y"}, {}); @@ -934,7 +934,7 @@ TEST_P(NewGrpcMuxImplTest, RejectMuxDynamicReplacementRateLimitSettingsError) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = grpc_mux_->addWatch(type_url, {"x", "y"}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage(type_url, {"x", "y"}, {}); diff --git a/test/extensions/config_subscription/grpc/watch_map_test.cc b/test/extensions/config_subscription/grpc/watch_map_test.cc index 014a1221b0c4b..222bab7743a2b 100644 --- a/test/extensions/config_subscription/grpc/watch_map_test.cc +++ b/test/extensions/config_subscription/grpc/watch_map_test.cc @@ -88,8 +88,7 @@ void expectEmptySotwNoDeltaUpdate(MockSubscriptionCallbacks& callbacks, } Protobuf::RepeatedPtrField -wrapInResource(const Protobuf::RepeatedPtrField& anys, - const std::string& version) { +wrapInResource(const Protobuf::RepeatedPtrField& anys, const std::string& version) { Protobuf::RepeatedPtrField ret; for (const auto& a : anys) { envoy::config::endpoint::v3::ClusterLoadAssignment cur_endpoint; @@ -103,7 +102,7 @@ wrapInResource(const Protobuf::RepeatedPtrField& anys, } void doDeltaUpdate(WatchMap& watch_map, - const Protobuf::RepeatedPtrField& sotw_resources, + const Protobuf::RepeatedPtrField& sotw_resources, const std::vector& removed_names, const std::string& version) { Protobuf::RepeatedPtrField delta_resources = @@ -118,7 +117,7 @@ void doDeltaUpdate(WatchMap& watch_map, // Similar to expectDeltaAndSotwUpdate(), but making the onConfigUpdate() happen, rather than // EXPECT-ing it. void doDeltaAndSotwUpdate(WatchMap& watch_map, - const Protobuf::RepeatedPtrField& sotw_resources, + const Protobuf::RepeatedPtrField& sotw_resources, const std::vector& removed_names, const std::string& version) { watch_map.onConfigUpdate(sotw_resources, version); @@ -150,7 +149,7 @@ TEST(WatchMapTest, Basic) { EXPECT_TRUE(added_removed.removed_.empty()); // ...the update is going to contain Bob and Carol... - Protobuf::RepeatedPtrField updated_resources; + Protobuf::RepeatedPtrField updated_resources; envoy::config::endpoint::v3::ClusterLoadAssignment bob; bob.set_cluster_name("bob"); updated_resources.Add()->PackFrom(bob); @@ -173,7 +172,7 @@ TEST(WatchMapTest, Basic) { EXPECT_EQ(absl::flat_hash_set({"alice"}), added_removed.removed_); // ...the update is going to contain Alice, Carol, Dave... - Protobuf::RepeatedPtrField updated_resources; + Protobuf::RepeatedPtrField updated_resources; envoy::config::endpoint::v3::ClusterLoadAssignment alice; alice.set_cluster_name("alice"); updated_resources.Add()->PackFrom(alice); @@ -211,7 +210,7 @@ TEST(WatchMapTest, Overlap) { Watch* watch1 = watch_map.addWatch(callbacks1, resource_decoder); Watch* watch2 = watch_map.addWatch(callbacks2, resource_decoder); - Protobuf::RepeatedPtrField updated_resources; + Protobuf::RepeatedPtrField updated_resources; envoy::config::endpoint::v3::ClusterLoadAssignment alice; alice.set_cluster_name("alice"); updated_resources.Add()->PackFrom(alice); @@ -283,7 +282,7 @@ TEST(WatchMapTest, CacheResourceAddResource) { Watch* watch1 = watch_map.addWatch(callbacks1, resource_decoder); Watch* watch2 = watch_map.addWatch(callbacks2, resource_decoder); - Protobuf::RepeatedPtrField updated_resources; + Protobuf::RepeatedPtrField updated_resources; envoy::config::endpoint::v3::ClusterLoadAssignment alice; alice.set_cluster_name("alice"); updated_resources.Add()->PackFrom(alice); @@ -383,7 +382,7 @@ class SameWatchRemoval : public testing::Test { WatchMap watch_map_; NiceMock callbacks1_; MockSubscriptionCallbacks callbacks2_; - Protobuf::RepeatedPtrField updated_resources_; + Protobuf::RepeatedPtrField updated_resources_; Watch* watch1_; Watch* watch2_; bool watch_cb_invoked_{}; @@ -441,7 +440,7 @@ TEST(WatchMapTest, AddRemoveAdd) { Watch* watch1 = watch_map.addWatch(callbacks1, resource_decoder); Watch* watch2 = watch_map.addWatch(callbacks2, resource_decoder); - Protobuf::RepeatedPtrField updated_resources; + Protobuf::RepeatedPtrField updated_resources; envoy::config::endpoint::v3::ClusterLoadAssignment alice; alice.set_cluster_name("alice"); updated_resources.Add()->PackFrom(alice); @@ -498,12 +497,12 @@ TEST(WatchMapTest, UninterestingUpdate) { Watch* watch = watch_map.addWatch(callbacks, resource_decoder); watch_map.updateWatchInterest(watch, {"alice"}); - Protobuf::RepeatedPtrField alice_update; + Protobuf::RepeatedPtrField alice_update; envoy::config::endpoint::v3::ClusterLoadAssignment alice; alice.set_cluster_name("alice"); alice_update.Add()->PackFrom(alice); - Protobuf::RepeatedPtrField bob_update; + Protobuf::RepeatedPtrField bob_update; envoy::config::endpoint::v3::ClusterLoadAssignment bob; bob.set_cluster_name("bob"); bob_update.Add()->PackFrom(bob); @@ -545,7 +544,7 @@ TEST(WatchMapTest, WatchingEverything) { // watch1 never specifies any names, and so is treated as interested in everything. watch_map.updateWatchInterest(watch2, {"alice"}); - Protobuf::RepeatedPtrField updated_resources; + Protobuf::RepeatedPtrField updated_resources; envoy::config::endpoint::v3::ClusterLoadAssignment alice; alice.set_cluster_name("alice"); updated_resources.Add()->PackFrom(alice); @@ -588,7 +587,7 @@ TEST(WatchMapTest, DeltaOnConfigUpdate) { // onConfigUpdate. But, if SotW holds no resources, then an update with nothing it cares about // will just not trigger any onConfigUpdate at all. { - Protobuf::RepeatedPtrField prepare_removed; + Protobuf::RepeatedPtrField prepare_removed; envoy::config::endpoint::v3::ClusterLoadAssignment will_be_removed_later; will_be_removed_later.set_cluster_name("removed"); prepare_removed.Add()->PackFrom(will_be_removed_later); @@ -597,7 +596,7 @@ TEST(WatchMapTest, DeltaOnConfigUpdate) { doDeltaAndSotwUpdate(watch_map, prepare_removed, {}, "version0"); } - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; envoy::config::endpoint::v3::ClusterLoadAssignment updated; updated.set_cluster_name("updated"); update.Add()->PackFrom(updated); @@ -640,7 +639,7 @@ TEST(WatchMapTest, OnConfigUpdateXdsTpGlobCollections) { { // Verify that we pay attention to all matching resources, no matter the order of context // params. - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; envoy::config::endpoint::v3::ClusterLoadAssignment resource1; resource1.set_cluster_name("xdstp://foo/bar/baz/a?some=thing&thing=some"); update.Add()->PackFrom(resource1); @@ -662,7 +661,7 @@ TEST(WatchMapTest, OnConfigUpdateXdsTpGlobCollections) { } // verify removal { - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; expectDeltaUpdate(callbacks, {}, {"xdstp://foo/bar/baz/a?thing=some&some=thing"}, "version1"); doDeltaUpdate( watch_map, update, @@ -685,7 +684,7 @@ TEST(WatchMapTest, OnConfigUpdateXdsTpSingletons) { { // Verify that we pay attention to all matching resources, no matter the order of context // params. - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; envoy::config::endpoint::v3::ClusterLoadAssignment resource1; resource1.set_cluster_name("xdstp://foo/bar/baz?thing=some&some=thing"); update.Add()->PackFrom(resource1); @@ -704,7 +703,7 @@ TEST(WatchMapTest, OnConfigUpdateXdsTpSingletons) { } // verify removal { - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; expectDeltaUpdate(callbacks, {}, {"xdstp://foo/bar/baz?thing=some&some=thing"}, "version1"); doDeltaUpdate(watch_map, update, {"xdstp://foo/bar/baz?thing=some&some=thing", "whatevs"}, "version1"); @@ -728,7 +727,7 @@ TEST(WatchMapTest, OnConfigUpdateUsingNamespaces) { // verify update { - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; envoy::config::endpoint::v3::ClusterLoadAssignment resource; resource.set_cluster_name("ns1/resource1"); update.Add()->PackFrom(resource); @@ -738,7 +737,7 @@ TEST(WatchMapTest, OnConfigUpdateUsingNamespaces) { } // verify removal { - Protobuf::RepeatedPtrField update; + Protobuf::RepeatedPtrField update; expectDeltaUpdate(callbacks2, {}, {"ns2/removed"}, "version1"); doDeltaUpdate(watch_map, update, {"ns2/removed"}, "version1"); } diff --git a/test/extensions/config_subscription/grpc/xds_failover_integration_test.cc b/test/extensions/config_subscription/grpc/xds_failover_integration_test.cc index eb02ed99510a6..d5ff3093dfc0b 100644 --- a/test/extensions/config_subscription/grpc/xds_failover_integration_test.cc +++ b/test/extensions/config_subscription/grpc/xds_failover_integration_test.cc @@ -179,6 +179,15 @@ class XdsFailoverAdsIntegrationTest : public AdsDeltaSotwIntegrationSubStatePara void primaryConnectionFailure() { AssertionResult result = xds_upstream_->waitForHttpConnection(*dispatcher_, xds_connection_); RELEASE_ASSERT(result, result.message()); + // When GoogleGrpc is used, there may be cases where the connection will be + // disconnected before the gRPC library observes the TLS handshake, which will + // end up in a fast retry without notifying Envoy that the connection was + // disconnected. We wait for a stream to ensure that the gRPC library + // observed a successful connection. + if (clientType() == Grpc::ClientType::GoogleGrpc) { + result = xds_connection_->waitForNewStream(*dispatcher_, xds_stream_); + RELEASE_ASSERT(result, result.message()); + } result = xds_connection_->close(); RELEASE_ASSERT(result, result.message()); } @@ -263,17 +272,17 @@ class XdsFailoverAdsIntegrationTest : public AdsDeltaSotwIntegrationSubStatePara EXPECT_TRUE(compareDiscoveryRequest(CdsTypeUrl, "1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", xds_stream)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", xds_stream)); sendDiscoveryResponse( LdsTypeUrl, {buildSimpleListener("listener_0", "cluster_0")}, {buildSimpleListener("listener_0", "cluster_0")}, {}, "1", {}, xds_stream); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", xds_stream)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {}, false, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", xds_stream)); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -629,7 +638,7 @@ TEST_P(XdsFailoverAdsIntegrationTest, PrimaryUseAfterFailoverResponseAndDisconne EXPECT_TRUE(compareDiscoveryRequest(CdsTypeUrl, "failover1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", failover_xds_stream_.get())); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", failover_xds_stream_.get())); @@ -748,7 +757,7 @@ TEST_P(XdsFailoverAdsIntegrationTest, FailoverUseAfterFailoverResponseAndDisconn EXPECT_TRUE(compareDiscoveryRequest(CdsTypeUrl, "failover1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", failover_xds_stream_.get())); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", failover_xds_stream_.get())); @@ -872,7 +881,7 @@ TEST_P(XdsFailoverAdsIntegrationTest, EXPECT_TRUE(compareDiscoveryRequest(CdsTypeUrl, "failover1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", failover_xds_stream_.get())); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", failover_xds_stream_.get())); diff --git a/test/extensions/config_subscription/grpc/xds_grpc_mux_impl_test.cc b/test/extensions/config_subscription/grpc/xds_grpc_mux_impl_test.cc index 7f555470011e3..b63f3003c5f5c 100644 --- a/test/extensions/config_subscription/grpc/xds_grpc_mux_impl_test.cc +++ b/test/extensions/config_subscription/grpc/xds_grpc_mux_impl_test.cc @@ -373,7 +373,7 @@ TEST_P(GrpcMuxImplTest, ResourceTTL) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; auto* ttl_timer = new Event::MockTimer(&dispatcher_); auto eds_sub = makeWatch(type_url, {"x"}, callbacks_, resource_decoder); @@ -538,7 +538,7 @@ TEST_P(GrpcMuxImplTest, LogsControlPlaneIndentifier) { TEST_P(GrpcMuxImplTest, WildcardWatch) { setup(); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = makeWatch(type_url, {}, callbacks_, resource_decoder_); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage(type_url, {}, "", true); @@ -571,7 +571,7 @@ TEST_P(GrpcMuxImplTest, WatchDemux) { setup(); // We will not require InSequence here: an update that causes multiple onConfigUpdates // causes them in an indeterminate order, based on the whims of the hash map. - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; NiceMock foo_callbacks; auto foo_sub = makeWatch(type_url, {"x", "y"}, foo_callbacks, resource_decoder_); @@ -660,7 +660,7 @@ TEST_P(GrpcMuxImplTest, WatchDemux) { TEST_P(GrpcMuxImplTest, MultipleWatcherWithEmptyUpdates) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; NiceMock foo_callbacks; auto foo_sub = makeWatch(type_url, {"x", "y"}, foo_callbacks, resource_decoder_); @@ -682,7 +682,7 @@ TEST_P(GrpcMuxImplTest, MultipleWatcherWithEmptyUpdates) { // Validate behavior when we have Single Watcher that sends Empty updates. TEST_P(GrpcMuxImplTest, SingleWatcherWithEmptyUpdates) { setup(); - const std::string& type_url = Config::TypeUrl::get().Cluster; + const std::string& type_url = Config::TestTypeUrl::get().Cluster; NiceMock foo_callbacks; auto foo_sub = makeWatch(type_url, {}, foo_callbacks, resource_decoder_); @@ -867,7 +867,7 @@ TEST_P(GrpcMuxImplTest, UnwatchedTypeAcceptsEmptyResources) { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; grpc_mux_->start(); { @@ -905,7 +905,7 @@ TEST_P(GrpcMuxImplTest, UnwatchedTypeAcceptsEmptyResources) { TEST_P(GrpcMuxImplTest, UnwatchedTypeAcceptsResources) { setup(); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; grpc_mux_->start(); // subscribe and unsubscribe so that the type is known to envoy @@ -980,7 +980,7 @@ TEST_P(GrpcMuxImplTest, BadLocalInfoEmptyNodeName) { // Validate that a valid resource decoder is used after removing a subscription. TEST_P(GrpcMuxImplTest, ValidResourceDecoderAfterRemoval) { setup(); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; { // Subscribe to resource "x" with some callbacks and resource decoder. @@ -1127,7 +1127,7 @@ TEST_P(GrpcMuxImplTest, CacheEdsResource) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; auto eds_sub = makeWatch(type_url, {"x"}); @@ -1169,7 +1169,7 @@ TEST_P(GrpcMuxImplTest, UpdateCacheEdsResource) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; auto eds_sub = makeWatch(type_url, {"x"}); @@ -1216,7 +1216,7 @@ TEST_P(GrpcMuxImplTest, AddRemoveSubscriptions) { OpaqueResourceDecoderSharedPtr resource_decoder( std::make_shared>("cluster_name")); - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; InSequence s; { @@ -1329,7 +1329,7 @@ TEST_P(GrpcMuxImplTest, MuxDynamicReplacementFetchingResources) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = makeWatch(type_url, {"x", "y"}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage(type_url, {"x", "y"}, "", true); @@ -1410,7 +1410,7 @@ TEST_P(GrpcMuxImplTest, RejectMuxDynamicReplacementRateLimitSettingsError) { setup(); InSequence s; - const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + const std::string& type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; auto foo_sub = makeWatch(type_url, {"x", "y"}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage(type_url, {"x", "y"}, "", true); diff --git a/test/extensions/config_subscription/rest/http_subscription_test_harness.h b/test/extensions/config_subscription/rest/http_subscription_test_harness.h index ce53d19a5c8a7..36059d460535d 100644 --- a/test/extensions/config_subscription/rest/http_subscription_test_harness.h +++ b/test/extensions/config_subscription/rest/http_subscription_test_harness.h @@ -57,7 +57,7 @@ class HttpSubscriptionTestHarness : public SubscriptionTestHarness { subscription_ = std::make_unique( local_info_, cm_, "eds_cluster", dispatcher_, random_gen_, std::chrono::milliseconds(1), std::chrono::milliseconds(1000), *method_descriptor_, - Config::TypeUrl::get().ClusterLoadAssignment, callbacks_, resource_decoder_, stats_, + Config::TestTypeUrl::get().ClusterLoadAssignment, callbacks_, resource_decoder_, stats_, init_fetch_timeout, validation_visitor_); } diff --git a/test/extensions/dynamic_modules/http/BUILD b/test/extensions/dynamic_modules/http/BUILD index 9ede15afb784b..06f288dc26bf8 100644 --- a/test/extensions/dynamic_modules/http/BUILD +++ b/test/extensions/dynamic_modules/http/BUILD @@ -55,6 +55,7 @@ envoy_cc_test( "//test/extensions/dynamic_modules:util", "//test/mocks/http:http_mocks", "//test/mocks/server:server_factory_context_mocks", + "//test/mocks/stats:stats_mocks", "//test/mocks/upstream:cluster_manager_mocks", "//test/mocks/upstream:thread_local_cluster_mocks", ], @@ -67,8 +68,10 @@ envoy_cc_test( rbe_pool = "6gig", deps = [ "//source/extensions/filters/http/dynamic_modules:abi_impl", + "//test/common/stats:stat_test_utility_lib", "//test/mocks/http:http_mocks", "//test/mocks/network:network_mocks", + "//test/mocks/server:server_factory_context_mocks", "//test/mocks/ssl:ssl_mocks", ], ) diff --git a/test/extensions/dynamic_modules/http/abi_impl_test.cc b/test/extensions/dynamic_modules/http/abi_impl_test.cc index 4cc23934afc2e..1a79eab75c491 100644 --- a/test/extensions/dynamic_modules/http/abi_impl_test.cc +++ b/test/extensions/dynamic_modules/http/abi_impl_test.cc @@ -1,7 +1,12 @@ +#include +#include + #include "source/extensions/filters/http/dynamic_modules/filter.h" +#include "test/common/stats/stat_test_utility.h" #include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" +#include "test/mocks/server/server_factory_context.h" #include "test/mocks/ssl/mocks.h" #include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" @@ -14,11 +19,12 @@ namespace HttpFilters { class DynamicModuleHttpFilterTest : public testing::Test { public: void SetUp() override { - filter_ = std::make_unique(nullptr); + filter_ = std::make_unique(nullptr, symbol_table_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); filter_->setEncoderFilterCallbacks(encoder_callbacks_); } + Stats::SymbolTableImpl symbol_table_; NiceMock decoder_callbacks_; NiceMock encoder_callbacks_; Http::TestRequestHeaderMapImpl request_headers_; @@ -421,7 +427,8 @@ TEST_F(DynamicModuleHttpFilterTest, SendResponseWithBody) { } TEST(ABIImpl, metadata) { - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; const std::string namespace_str = "foo"; const std::string key_str = "key"; envoy_dynamic_module_type_buffer_module_ptr namespace_ptr = @@ -525,8 +532,8 @@ TEST(ABIImpl, metadata) { auto upstream_host = std::make_shared(); EXPECT_CALL(*upstream_info, upstreamHost).WillRepeatedly(testing::Return(upstream_host)); auto locality_metadata = std::make_shared(); - locality_metadata->mutable_filter_metadata()->insert({namespace_str, ProtobufWkt::Struct()}); - ProtobufWkt::Value lbendpoint_value_proto; + locality_metadata->mutable_filter_metadata()->insert({namespace_str, Protobuf::Struct()}); + Protobuf::Value lbendpoint_value_proto; lbendpoint_value_proto.set_string_value(lbendpoint_value); locality_metadata->mutable_filter_metadata() ->at(namespace_str) @@ -544,7 +551,8 @@ TEST(ABIImpl, metadata) { } TEST(ABIImpl, filter_state) { - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; const std::string key_str = "key"; envoy_dynamic_module_type_buffer_module_ptr key_ptr = const_cast(key_str.data()); size_t key_length = key_str.size(); @@ -593,7 +601,8 @@ bufferVectorToString(const std::vector& } TEST(ABIImpl, RequestBody) { - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; Http::MockStreamDecoderFilterCallbacks callbacks; StreamInfo::MockStreamInfo stream_info; EXPECT_CALL(callbacks, streamInfo()).WillRepeatedly(testing::ReturnRef(stream_info)); @@ -666,7 +675,8 @@ TEST(ABIImpl, RequestBody) { } TEST(ABIImpl, ResponseBody) { - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; Http::MockStreamEncoderFilterCallbacks callbacks; StreamInfo::MockStreamInfo stream_info; EXPECT_CALL(callbacks, streamInfo()).WillRepeatedly(testing::ReturnRef(stream_info)); @@ -749,7 +759,8 @@ TEST(ABIImpl, ResponseBody) { } TEST(ABIImpl, ClearRouteCache) { - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; Http::MockStreamDecoderFilterCallbacks callbacks; StreamInfo::MockStreamInfo stream_info; EXPECT_CALL(callbacks, streamInfo()).WillRepeatedly(testing::ReturnRef(stream_info)); @@ -762,8 +773,9 @@ TEST(ABIImpl, ClearRouteCache) { } TEST(ABIImpl, GetAttributes) { - DynamicModuleHttpFilter filter_without_callbacks{nullptr}; - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter_without_callbacks{nullptr, symbol_table}; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; Http::MockStreamDecoderFilterCallbacks callbacks; StreamInfo::MockStreamInfo stream_info; EXPECT_CALL(callbacks, streamInfo()).WillRepeatedly(testing::ReturnRef(stream_info)); @@ -1052,7 +1064,8 @@ TEST(ABIImpl, GetAttributes) { } TEST(ABIImpl, HttpCallout) { - DynamicModuleHttpFilter filter{nullptr}; + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter{nullptr, symbol_table}; const std::string cluster{"some_cluster"}; EXPECT_EQ(envoy_dynamic_module_callback_http_filter_http_callout( &filter, 1, const_cast(cluster.data()), cluster.size(), nullptr, 0, nullptr, @@ -1060,6 +1073,270 @@ TEST(ABIImpl, HttpCallout) { envoy_dynamic_module_type_http_callout_init_result_MissingRequiredHeaders); } +TEST(ABIImpl, Log) { + Envoy::Logger::Registry::setLogLevel(spdlog::level::err); + EXPECT_FALSE( + envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level_Trace)); + EXPECT_FALSE( + envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level_Debug)); + EXPECT_FALSE(envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level_Info)); + EXPECT_FALSE(envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level_Warn)); + EXPECT_TRUE(envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level_Error)); + EXPECT_TRUE( + envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level_Critical)); + + // Use all log levels, mostly for coverage. + const std::string msg = "test log message"; + envoy_dynamic_module_type_buffer_module_ptr ptr = const_cast(msg.data()); + size_t len = msg.size(); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Trace, ptr, len); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Debug, ptr, len); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Info, ptr, len); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Warn, ptr, len); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Error, ptr, len); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Critical, ptr, len); + envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level_Off, ptr, len); +} + +TEST(ABIImpl, Stats) { + Stats::TestUtil::TestStore stats_store; + Stats::TestUtil::TestScope stats_scope{"", stats_store}; + NiceMock context; + auto filter_config = std::make_shared( + "some_name", "some_config", nullptr, stats_scope, context); + DynamicModuleHttpFilter filter{filter_config, stats_scope.symbolTable()}; + + const std::string counter_name{"some_counter"}; + size_t counter_id; + auto result = envoy_dynamic_module_callback_http_filter_config_define_counter( + filter_config.get(), const_cast(counter_name.data()), counter_name.size(), + &counter_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + Stats::CounterOptConstRef counter = + stats_store.findCounterByString("dynamicmodulescustom.some_counter"); + EXPECT_TRUE(counter.has_value()); + EXPECT_EQ(counter->get().value(), 0); + result = envoy_dynamic_module_callback_http_filter_increment_counter(&filter, counter_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(counter->get().value(), 10); + result = envoy_dynamic_module_callback_http_filter_increment_counter(&filter, counter_id, 42); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(counter->get().value(), 52); + + const std::string counter_vec_name{"some_counter_vec"}; + const std::string counter_vec_label_name{"some_label"}; + std::vector counter_vec_labels = { + {const_cast(counter_vec_label_name.data()), counter_vec_label_name.size()}, + }; + size_t counter_vec_id; + result = envoy_dynamic_module_callback_http_filter_config_define_counter_vec( + filter_config.get(), const_cast(counter_vec_name.data()), counter_vec_name.size(), + counter_vec_labels.data(), counter_vec_labels.size(), &counter_vec_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + + const std::string counter_vec_label_value{"some_value"}; + std::vector counter_vec_labels_values = { + {const_cast(counter_vec_label_value.data()), counter_vec_label_value.size()}, + }; + result = envoy_dynamic_module_callback_http_filter_increment_counter_vec( + &filter, counter_vec_id, counter_vec_labels_values.data(), counter_vec_labels_values.size(), + 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + Stats::CounterOptConstRef counter_vec = stats_store.findCounterByString( + "dynamicmodulescustom.some_counter_vec.some_label.some_value"); + EXPECT_TRUE(counter_vec.has_value()); + EXPECT_EQ(counter_vec->get().value(), 10); + result = envoy_dynamic_module_callback_http_filter_increment_counter_vec( + &filter, counter_vec_id, counter_vec_labels_values.data(), counter_vec_labels_values.size(), + 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(counter_vec->get().value(), 20); + result = envoy_dynamic_module_callback_http_filter_increment_counter_vec( + &filter, counter_vec_id, counter_vec_labels_values.data(), counter_vec_labels_values.size(), + 42); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(counter_vec->get().value(), 62); + + const std::string gauge_name{"some_gauge"}; + size_t gauge_id; + result = envoy_dynamic_module_callback_http_filter_config_define_gauge( + filter_config.get(), const_cast(gauge_name.data()), gauge_name.size(), &gauge_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + Stats::GaugeOptConstRef gauge = stats_store.findGaugeByString("dynamicmodulescustom.some_gauge"); + EXPECT_TRUE(gauge.has_value()); + EXPECT_EQ(gauge->get().value(), 0); + result = envoy_dynamic_module_callback_http_filter_increase_gauge(&filter, gauge_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge->get().value(), 10); + result = envoy_dynamic_module_callback_http_filter_increase_gauge(&filter, gauge_id, 42); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge->get().value(), 52); + result = envoy_dynamic_module_callback_http_filter_decrease_gauge(&filter, gauge_id, 50); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge->get().value(), 2); + result = envoy_dynamic_module_callback_http_filter_set_gauge(&filter, gauge_id, 9001); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge->get().value(), 9001); + + const std::string gauge_vec_name{"some_gauge_vec"}; + const std::string gauge_vec_label_name{"some_label"}; + std::vector gauge_vec_labels = { + {const_cast(gauge_vec_label_name.data()), gauge_vec_label_name.size()}, + }; + size_t gauge_vec_id; + result = envoy_dynamic_module_callback_http_filter_config_define_gauge_vec( + filter_config.get(), const_cast(gauge_vec_name.data()), gauge_vec_name.size(), + gauge_vec_labels.data(), gauge_vec_labels.size(), &gauge_vec_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + + const std::string gauge_vec_label_value{"some_value"}; + std::vector gauge_vec_labels_values = { + {const_cast(gauge_vec_label_value.data()), gauge_vec_label_value.size()}, + }; + result = envoy_dynamic_module_callback_http_filter_increase_gauge_vec( + &filter, gauge_vec_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + Stats::GaugeOptConstRef gauge_vec = + stats_store.findGaugeByString("dynamicmodulescustom.some_gauge_vec.some_label.some_value"); + EXPECT_TRUE(gauge_vec.has_value()); + EXPECT_EQ(gauge_vec->get().value(), 10); + result = envoy_dynamic_module_callback_http_filter_increase_gauge_vec( + &filter, gauge_vec_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge_vec->get().value(), 20); + result = envoy_dynamic_module_callback_http_filter_decrease_gauge_vec( + &filter, gauge_vec_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 12); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge_vec->get().value(), 8); + result = envoy_dynamic_module_callback_http_filter_set_gauge_vec( + &filter, gauge_vec_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 9001); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(gauge_vec->get().value(), 9001); + + const std::string histogram_name{"some_histogram"}; + size_t histogram_id; + result = envoy_dynamic_module_callback_http_filter_config_define_histogram( + filter_config.get(), const_cast(histogram_name.data()), histogram_name.size(), + &histogram_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + Stats::HistogramOptConstRef histogram = + stats_store.findHistogramByString("dynamicmodulescustom.some_histogram"); + EXPECT_TRUE(histogram.has_value()); + EXPECT_FALSE(stats_store.histogramRecordedValues("dynamicmodulescustom.some_histogram")); + result = + envoy_dynamic_module_callback_http_filter_record_histogram_value(&filter, histogram_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(stats_store.histogramValues("dynamicmodulescustom.some_histogram", false), + (std::vector{10})); + result = + envoy_dynamic_module_callback_http_filter_record_histogram_value(&filter, histogram_id, 42); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(stats_store.histogramValues("dynamicmodulescustom.some_histogram", false), + (std::vector{10, 42})); + + const std::string histogram_vec_name{"some_histogram_vec"}; + const std::string histogram_vec_label_name{"some_label"}; + std::vector histogram_vec_labels = { + {const_cast(histogram_vec_label_name.data()), histogram_vec_label_name.size()}, + }; + size_t histogram_vec_id; + result = envoy_dynamic_module_callback_http_filter_config_define_histogram_vec( + filter_config.get(), const_cast(histogram_vec_name.data()), histogram_vec_name.size(), + histogram_vec_labels.data(), histogram_vec_labels.size(), &histogram_vec_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + + const std::string histogram_vec_label_value{"some_value"}; + std::vector histogram_vec_labels_values = { + {const_cast(histogram_vec_label_value.data()), histogram_vec_label_value.size()}, + }; + result = envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + &filter, histogram_vec_id, histogram_vec_labels_values.data(), + histogram_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + Stats::HistogramOptConstRef histogram_vec = stats_store.findHistogramByString( + "dynamicmodulescustom.some_histogram_vec.some_label.some_value"); + EXPECT_TRUE(histogram_vec.has_value()); + EXPECT_EQ(stats_store.histogramValues( + "dynamicmodulescustom.some_histogram_vec.some_label.some_value", false), + (std::vector{10})); + result = envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + &filter, histogram_vec_id, histogram_vec_labels_values.data(), + histogram_vec_labels_values.size(), 42); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Success); + EXPECT_EQ(stats_store.histogramValues( + "dynamicmodulescustom.some_histogram_vec.some_label.some_value", false), + (std::vector{10, 42})); + + // test using invalid stat id + size_t invalid_stat_id = 9999; + result = + envoy_dynamic_module_callback_http_filter_increment_counter(&filter, invalid_stat_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_increment_counter_vec( + &filter, invalid_stat_id, counter_vec_labels_values.data(), counter_vec_labels_values.size(), + 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_increase_gauge(&filter, invalid_stat_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_decrease_gauge(&filter, invalid_stat_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_set_gauge(&filter, invalid_stat_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_increase_gauge_vec( + &filter, invalid_stat_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_decrease_gauge_vec( + &filter, invalid_stat_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_set_gauge_vec( + &filter, invalid_stat_id, gauge_vec_labels_values.data(), gauge_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_record_histogram_value(&filter, + invalid_stat_id, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + result = envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + &filter, invalid_stat_id, histogram_vec_labels_values.data(), + histogram_vec_labels_values.size(), 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_MetricNotFound); + + // test using invalid labels + result = envoy_dynamic_module_callback_http_filter_increment_counter_vec(&filter, counter_vec_id, + {}, 0, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_InvalidLabels); + result = envoy_dynamic_module_callback_http_filter_increase_gauge_vec(&filter, gauge_vec_id, {}, + 0, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_InvalidLabels); + result = envoy_dynamic_module_callback_http_filter_record_histogram_value_vec( + &filter, histogram_vec_id, {}, 0, 10); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_InvalidLabels); + + // test stat creation after freezing + filter_config->stat_creation_frozen_ = true; + result = envoy_dynamic_module_callback_http_filter_config_define_counter( + filter_config.get(), const_cast(counter_name.data()), counter_name.size(), + &counter_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Frozen); + result = envoy_dynamic_module_callback_http_filter_config_define_counter_vec( + filter_config.get(), const_cast(counter_vec_name.data()), counter_vec_name.size(), + counter_vec_labels.data(), counter_vec_labels.size(), &counter_vec_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Frozen); + result = envoy_dynamic_module_callback_http_filter_config_define_gauge( + filter_config.get(), const_cast(gauge_name.data()), gauge_name.size(), &gauge_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Frozen); + result = envoy_dynamic_module_callback_http_filter_config_define_gauge_vec( + filter_config.get(), const_cast(gauge_vec_name.data()), gauge_vec_name.size(), + gauge_vec_labels.data(), gauge_vec_labels.size(), &gauge_vec_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Frozen); + result = envoy_dynamic_module_callback_http_filter_config_define_histogram( + filter_config.get(), const_cast(histogram_name.data()), histogram_name.size(), + &histogram_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Frozen); + result = envoy_dynamic_module_callback_http_filter_config_define_histogram_vec( + filter_config.get(), const_cast(histogram_vec_name.data()), histogram_vec_name.size(), + histogram_vec_labels.data(), histogram_vec_labels.size(), &histogram_vec_id); + EXPECT_EQ(result, envoy_dynamic_module_type_metrics_result_Frozen); +} + } // namespace HttpFilters } // namespace DynamicModules } // namespace Extensions diff --git a/test/extensions/dynamic_modules/http/factory_test.cc b/test/extensions/dynamic_modules/http/factory_test.cc index 9cee6638ea49f..46bd4be287e63 100644 --- a/test/extensions/dynamic_modules/http/factory_test.cc +++ b/test/extensions/dynamic_modules/http/factory_test.cc @@ -37,6 +37,8 @@ filter_name: foo TestUtility::loadFromYamlAndValidate(yaml, proto_config); NiceMock context; + Api::ApiPtr api = Api::createApiForTest(); + EXPECT_CALL(context.server_factory_context_, api()).WillRepeatedly(testing::ReturnRef(*api)); Envoy::Server::Configuration::DynamicModuleConfigFactory factory; auto result = factory.createFilterFactoryFromProto(proto_config, "", context); @@ -66,6 +68,8 @@ filter_name: foo TestUtility::loadFromYamlAndValidate(yaml, proto_config); NiceMock context; + Api::ApiPtr api = Api::createApiForTest(); + EXPECT_CALL(context.server_factory_context_, api()).WillRepeatedly(testing::ReturnRef(*api)); Envoy::Server::Configuration::DynamicModuleConfigFactory factory; auto result = factory.createFilterFactoryFromProto(proto_config, "", context); @@ -98,6 +102,8 @@ filter_name: foo TestUtility::loadFromYamlAndValidate(yaml, proto_config); NiceMock context; + Api::ApiPtr api = Api::createApiForTest(); + EXPECT_CALL(context.server_factory_context_, api()).WillRepeatedly(testing::ReturnRef(*api)); Envoy::Server::Configuration::DynamicModuleConfigFactory factory; auto result = factory.createFilterFactoryFromProto(proto_config, "", context); diff --git a/test/extensions/dynamic_modules/http/filter_test.cc b/test/extensions/dynamic_modules/http/filter_test.cc index 63b86a3892b4f..687e5d43ae465 100644 --- a/test/extensions/dynamic_modules/http/filter_test.cc +++ b/test/extensions/dynamic_modules/http/filter_test.cc @@ -5,6 +5,7 @@ #include "test/extensions/dynamic_modules/util.h" #include "test/mocks/http/mocks.h" #include "test/mocks/server/server_factory_context.h" +#include "test/mocks/stats/mocks.h" #include "test/mocks/upstream/cluster_manager.h" #include "test/mocks/upstream/thread_local_cluster.h" #include "test/test_common/utility.h" @@ -26,12 +27,15 @@ TEST_P(DynamicModuleTestLanguages, Nop) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), + *stats_store.createScope(""), context); EXPECT_TRUE(filter_config_or_status.ok()); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_store.symbolTable()); filter->initializeInModuleFilter(); // The followings are mostly for coverage at the moment. @@ -64,13 +68,135 @@ TEST(DynamicModulesTest, ConfigInitializationFailure) { auto dynamic_module = newDynamicModule(testSharedObjectPath("http", "rust"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); NiceMock context; - auto filter_config_or_status = newDynamicModuleHttpFilterConfig( - "config_init_failure", "", std::move(dynamic_module.value()), context); + Stats::IsolatedStoreImpl stats_store; + auto filter_config_or_status = + newDynamicModuleHttpFilterConfig("config_init_failure", "", std::move(dynamic_module.value()), + *stats_store.createScope(""), context); EXPECT_FALSE(filter_config_or_status.ok()); EXPECT_THAT(filter_config_or_status.status().message(), testing::HasSubstr("Failed to initialize dynamic module")); } +TEST(DynamicModulesTest, StatsCallbacks) { + const std::string filter_name = "stats_callbacks"; + const std::string filter_config = ""; + // TODO: Add non-Rust test program once we have non-Rust SDK. + auto dynamic_module = newDynamicModule(testSharedObjectPath("http", "rust"), false); + if (!dynamic_module.ok()) { + ENVOY_LOG_MISC(debug, "Failed to load dynamic module: {}", dynamic_module.status().message()); + } + EXPECT_TRUE(dynamic_module.ok()); + + NiceMock context; + Stats::TestUtil::TestStore stats_store; + Stats::TestUtil::TestScope stats_scope{"", stats_store}; + auto filter_config_or_status = + Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( + filter_name, filter_config, std::move(dynamic_module.value()), stats_scope, context); + EXPECT_TRUE(filter_config_or_status.ok()); + + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope.symbolTable()); + filter->initializeInModuleFilter(); + + Stats::CounterOptConstRef counter = + stats_store.findCounterByString("dynamicmodulescustom.streams_total"); + EXPECT_TRUE(counter.has_value()); + EXPECT_EQ(counter->get().value(), 1); + Stats::GaugeOptConstRef gauge = + stats_store.findGaugeByString("dynamicmodulescustom.concurrent_streams"); + EXPECT_TRUE(gauge.has_value()); + EXPECT_EQ(gauge->get().value(), 1); + Stats::GaugeOptConstRef magicNumberGauge = + stats_store.findGaugeByString("dynamicmodulescustom.magic_number"); + EXPECT_TRUE(gauge.has_value()); + EXPECT_EQ(magicNumberGauge->get().value(), 42); + Stats::HistogramOptConstRef histogram = + stats_store.findHistogramByString("dynamicmodulescustom.ones"); + EXPECT_TRUE(histogram.has_value()); + EXPECT_FALSE(stats_store.histogramRecordedValues("dynamicmodulescustom.ones")); + + Stats::CounterOptConstRef counter_vec_increment = + stats_store.findCounterByString("dynamicmodulescustom.test_counter_vec.test_label.increment"); + EXPECT_TRUE(counter_vec_increment.has_value()); + EXPECT_EQ(counter_vec_increment->get().value(), 1); + Stats::GaugeOptConstRef gauge_vec_increase = + stats_store.findGaugeByString("dynamicmodulescustom.test_gauge_vec.test_label.increase"); + EXPECT_TRUE(gauge_vec_increase.has_value()); + EXPECT_EQ(gauge_vec_increase->get().value(), 1); + Stats::GaugeOptConstRef gauge_vec_decrease = + stats_store.findGaugeByString("dynamicmodulescustom.test_gauge_vec.test_label.decrease"); + EXPECT_TRUE(gauge_vec_decrease.has_value()); + EXPECT_EQ(gauge_vec_decrease->get().value(), 2); + Stats::GaugeOptConstRef gauge_vec_set = + stats_store.findGaugeByString("dynamicmodulescustom.test_gauge_vec.test_label.set"); + EXPECT_TRUE(gauge_vec_set.has_value()); + EXPECT_EQ(gauge_vec_set->get().value(), 9001); + Stats::HistogramOptConstRef histogram_vec_record = stats_store.findHistogramByString( + "dynamicmodulescustom.test_histogram_vec.test_label.record"); + EXPECT_TRUE(histogram_vec_record.has_value()); + EXPECT_EQ(stats_store.histogramValues("dynamicmodulescustom.test_histogram_vec.test_label.record", + false), + (std::vector{1})); + + Http::MockStreamDecoderFilterCallbacks decoder_callbacks; + StreamInfo::MockStreamInfo stream_info; + EXPECT_CALL(decoder_callbacks, streamInfo()).WillRepeatedly(testing::ReturnRef(stream_info)); + Http::MockDownstreamStreamFilterCallbacks downstream_callbacks; + filter->setDecoderFilterCallbacks(decoder_callbacks); + Http::MockStreamEncoderFilterCallbacks encoder_callbacks; + filter->setEncoderFilterCallbacks(encoder_callbacks); + + std::initializer_list> headers = {{"header", "header_value"}}; + Http::TestRequestHeaderMapImpl request_headers{headers}; + Http::TestRequestTrailerMapImpl request_trailers{headers}; + Http::TestResponseHeaderMapImpl response_headers{headers}; + Http::TestResponseTrailerMapImpl response_trailers{headers}; + EXPECT_CALL(decoder_callbacks, requestHeaders()) + .WillRepeatedly(testing::Return(makeOptRef(request_headers))); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter->decodeHeaders(request_headers, false)); + Stats::CounterOptConstRef counter_vec_header = stats_store.findCounterByString( + "dynamicmodulescustom.test_counter_vec.test_label.header_value"); + EXPECT_EQ(counter_vec_header->get().value(), 1); + Stats::GaugeOptConstRef gauge_vec_header = + stats_store.findGaugeByString("dynamicmodulescustom.test_gauge_vec.test_label.header_value"); + EXPECT_EQ(gauge_vec_header->get().value(), 1); + Stats::HistogramOptConstRef histogram_vec_header = stats_store.findHistogramByString( + "dynamicmodulescustom.test_histogram_vec.test_label.header_value"); + EXPECT_TRUE(histogram_vec_header.has_value()); + EXPECT_EQ(stats_store.histogramValues( + "dynamicmodulescustom.test_histogram_vec.test_label.header_value", false), + (std::vector{1})); + + EXPECT_EQ(FilterTrailersStatus::Continue, filter->decodeTrailers(request_trailers)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter->encodeHeaders(response_headers, false)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter->encodeTrailers(response_trailers)); + EXPECT_EQ(counter->get().value(), 1); + EXPECT_EQ(gauge->get().value(), 1); + EXPECT_EQ(stats_store.histogramValues("dynamicmodulescustom.ones", false), + (std::vector{1})); + + filter->onStreamComplete(); + EXPECT_EQ(counter->get().value(), 1); + EXPECT_EQ(gauge->get().value(), 0); + EXPECT_EQ(stats_store.histogramValues("dynamicmodulescustom.ones", false), + (std::vector{1})); + Stats::CounterOptConstRef counter_vec_local_var = + stats_store.findCounterByString("dynamicmodulescustom.test_counter_vec.test_label.local_var"); + EXPECT_EQ(counter_vec_local_var->get().value(), 1); + Stats::GaugeOptConstRef gauge_vec_local_var = + stats_store.findGaugeByString("dynamicmodulescustom.test_gauge_vec.test_label.local_var"); + EXPECT_EQ(gauge_vec_local_var->get().value(), 1); + Stats::HistogramOptConstRef histogram_vec_local_var = stats_store.findHistogramByString( + "dynamicmodulescustom.test_histogram_vec.test_label.local_var"); + EXPECT_TRUE(histogram_vec_local_var.has_value()); + EXPECT_EQ(stats_store.histogramValues( + "dynamicmodulescustom.test_histogram_vec.test_label.local_var", false), + (std::vector{1})); + filter->onDestroy(); +} + TEST(DynamicModulesTest, HeaderCallbacks) { const std::string filter_name = "header_callbacks"; const std::string filter_config = ""; @@ -82,12 +208,15 @@ TEST(DynamicModulesTest, HeaderCallbacks) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), + *stats_store.createScope(""), context); EXPECT_TRUE(filter_config_or_status.ok()); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_store.symbolTable()); filter->initializeInModuleFilter(); Http::MockStreamDecoderFilterCallbacks decoder_callbacks; @@ -141,12 +270,15 @@ TEST(DynamicModulesTest, DynamicMetadataCallbacks) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); auto route = std::make_shared>(); @@ -221,12 +353,15 @@ TEST(DynamicModulesTest, FilterStateCallbacks) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); Http::MockStreamDecoderFilterCallbacks callbacks; @@ -295,12 +430,15 @@ TEST(DynamicModulesTest, BodyCallbacks) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); Http::MockStreamDecoderFilterCallbacks decoder_callbacks; @@ -360,6 +498,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_non_existing_cluster) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); Upstream::MockClusterManager cluster_manager; NiceMock thread_local_cluster; EXPECT_CALL(cluster_manager, getThreadLocalCluster(_)) @@ -371,11 +511,12 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_non_existing_cluster) { .WillOnce(testing::Return(nullptr)); auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); Http::MockStreamDecoderFilterCallbacks callbacks; - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); filter->setDecoderFilterCallbacks(callbacks); EXPECT_CALL(callbacks, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); @@ -396,6 +537,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_immediate_failing_cluster) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); Upstream::MockClusterManager cluster_manager; NiceMock thread_local_cluster; EXPECT_CALL(cluster_manager, getThreadLocalCluster(_)) @@ -405,7 +548,7 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_immediate_failing_cluster) { const std::string filter_config = "immediate_failing_cluster"; auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); std::shared_ptr cluster = @@ -425,7 +568,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_immediate_failing_cluster) { })); Http::MockStreamDecoderFilterCallbacks callbacks; - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); filter->setDecoderFilterCallbacks(callbacks); EXPECT_CALL(callbacks, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); @@ -446,6 +590,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_success) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); Upstream::MockClusterManager cluster_manager; NiceMock thread_local_cluster; EXPECT_CALL(cluster_manager, getThreadLocalCluster(_)) @@ -455,7 +601,7 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_success) { const std::string filter_config = "success_cluster"; auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); std::shared_ptr cluster = @@ -479,7 +625,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_success) { })); Http::MockStreamDecoderFilterCallbacks callbacks; - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); filter->setDecoderFilterCallbacks(callbacks); EXPECT_CALL(callbacks, sendLocalReply(Http::Code::OK, _, _, _, _)); @@ -512,6 +659,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_resetting) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); Upstream::MockClusterManager cluster_manager; NiceMock thread_local_cluster; EXPECT_CALL(cluster_manager, getThreadLocalCluster(_)) @@ -521,7 +670,7 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_resetting) { const std::string filter_config = "resetting_cluster"; auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); std::shared_ptr cluster = @@ -539,7 +688,8 @@ TEST(DynamicModulesTest, HttpFilterHttpCallout_resetting) { return &request; })); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); filter->initializeInModuleFilter(); TestRequestHeaderMapImpl headers{{}}; @@ -562,6 +712,8 @@ TEST(DynamicModulesTest, HttpFilterPerFilterConfigLifetimes) { EXPECT_TRUE(dynamic_module.ok()); NiceMock context; + Stats::IsolatedStoreImpl stats_store; + auto stats_scope = stats_store.createScope(""); Upstream::MockClusterManager cluster_manager; NiceMock thread_local_cluster; EXPECT_CALL(cluster_manager, getThreadLocalCluster(_)) @@ -571,7 +723,7 @@ TEST(DynamicModulesTest, HttpFilterPerFilterConfigLifetimes) { const std::string filter_config = "listener config"; auto filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig( - filter_name, filter_config, std::move(dynamic_module.value()), context); + filter_name, filter_config, std::move(dynamic_module.value()), *stats_scope, context); EXPECT_TRUE(filter_config_or_status.ok()); auto dynamic_module_for_route = @@ -581,7 +733,8 @@ TEST(DynamicModulesTest, HttpFilterPerFilterConfigLifetimes) { } EXPECT_TRUE(dynamic_module_for_route.ok()); - auto filter = std::make_shared(filter_config_or_status.value()); + auto filter = std::make_shared(filter_config_or_status.value(), + stats_scope->symbolTable()); NiceMock decoder_callbacks; NiceMock encoder_callbacks; @@ -593,8 +746,8 @@ TEST(DynamicModulesTest, HttpFilterPerFilterConfigLifetimes) { // Now simulate a per-route config that is very short lived, and verify that the filter doesn't // segfaults if it uses it after after it discarded. { - // do all per-route config in an inner scope to make sure the is destroyed before the filter - // response headers is called. + // do all per-route config in an inner scope to make sure the per-route config is destroyed + // before the filter response headers is called. const std::string route_filter_config_str = "router config"; auto route_filter_config_or_status = Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpPerRouteConfig( @@ -625,7 +778,8 @@ TEST(DynamicModulesTest, HttpFilterPerFilterConfigLifetimes) { } TEST(HttpFilter, HeaderMapGetter) { - DynamicModuleHttpFilter filter(nullptr); + Stats::SymbolTableImpl symbol_table; + DynamicModuleHttpFilter filter(nullptr, symbol_table); EXPECT_EQ(absl::nullopt, filter.requestHeaders()); EXPECT_EQ(absl::nullopt, filter.requestTrailers()); diff --git a/test/extensions/dynamic_modules/http/integration_test.cc b/test/extensions/dynamic_modules/http/integration_test.cc index f1c8380b7f4d5..b8836e7835ae2 100644 --- a/test/extensions/dynamic_modules/http/integration_test.cc +++ b/test/extensions/dynamic_modules/http/integration_test.cc @@ -406,4 +406,225 @@ TEST_P(DynamicModulesIntegrationTest, FakeExternalCache) { } } +TEST_P(DynamicModulesIntegrationTest, StatsCallbacks) { + initializeFilter("stats_callbacks", "header_to_count,header_to_set"); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // End-to-end request + { + Http::TestRequestHeaderMapImpl request_headers = default_request_headers_; + request_headers.addCopy(Http::LowerCaseString("header_to_count"), "3"); + request_headers.addCopy(Http::LowerCaseString("header_to_set"), "100"); + auto encoder_decoder = codec_client_->startRequest(request_headers, true); + auto response = std::move(encoder_decoder.second); + waitForNextUpstreamRequest(); + test_server_->waitUntilHistogramHasSamples("dynamicmodulescustom.requests_header_values"); + + EXPECT_EQ(test_server_->counter("dynamicmodulescustom.requests_total")->value(), 1); + EXPECT_EQ(test_server_->gauge("dynamicmodulescustom.requests_pending")->value(), 1); + EXPECT_EQ(test_server_->gauge("dynamicmodulescustom.requests_set_value")->value(), 100); + auto requests_header_values = + test_server_->histogram("dynamicmodulescustom.requests_header_values"); + EXPECT_EQ( + TestUtility::readSampleCount(test_server_->server().dispatcher(), *requests_header_values), + 1); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *requests_header_values)), + 3); + + EXPECT_EQ( + test_server_ + ->counter( + "dynamicmodulescustom.entrypoint_total.entrypoint.on_request_headers.method.GET") + ->value(), + 1); + EXPECT_EQ( + test_server_ + ->gauge( + "dynamicmodulescustom.entrypoint_pending.entrypoint.on_request_headers.method.GET") + ->value(), + 1); + EXPECT_EQ(test_server_ + ->gauge("dynamicmodulescustom.entrypoint_set_value.entrypoint.on_request_headers." + "method.GET") + ->value(), + 100); + auto request_entrypoint_header_values = test_server_->histogram( + "dynamicmodulescustom.entrypoint_header_values.entrypoint.on_request_headers.method.GET"); + EXPECT_EQ(TestUtility::readSampleCount(test_server_->server().dispatcher(), + *request_entrypoint_header_values), + 1); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *request_entrypoint_header_values)), + 3); + + Http::TestResponseHeaderMapImpl response_headers = default_response_headers_; + response_headers.addCopy(Http::LowerCaseString("header_to_count"), "3"); + response_headers.addCopy(Http::LowerCaseString("header_to_set"), "999"); + upstream_request_->encodeHeaders(response_headers, false); + response->waitForHeaders(); + test_server_->waitUntilHistogramHasSamples( + "dynamicmodulescustom.entrypoint_header_values.entrypoint.on_response_headers.method.GET"); + + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + + EXPECT_EQ( + test_server_ + ->counter( + "dynamicmodulescustom.entrypoint_total.entrypoint.on_response_headers.method.GET") + ->value(), + 1); + EXPECT_EQ( + test_server_ + ->gauge( + "dynamicmodulescustom.entrypoint_pending.entrypoint.on_response_headers.method.GET") + ->value(), + 1); + EXPECT_EQ(test_server_ + ->gauge("dynamicmodulescustom.entrypoint_set_value.entrypoint.on_response_" + "headers.method.GET") + ->value(), + 999); + auto response_entrypoint_header_values = test_server_->histogram( + "dynamicmodulescustom.entrypoint_header_values.entrypoint.on_response_headers.method.GET"); + EXPECT_EQ(TestUtility::readSampleCount(test_server_->server().dispatcher(), + *response_entrypoint_header_values), + 1); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *response_entrypoint_header_values)), + 3); + + Buffer::OwnedImpl response_data("goodbye"); + upstream_request_->encodeData(response_data, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + + EXPECT_EQ( + test_server_ + ->gauge( + "dynamicmodulescustom.entrypoint_pending.entrypoint.on_response_headers.method.GET") + ->value(), + 0); + + // Check if stats preserved within filter + EXPECT_EQ( + test_server_ + ->counter( + "dynamicmodulescustom.entrypoint_total.entrypoint.on_request_headers.method.GET") + ->value(), + 1); + EXPECT_EQ( + test_server_ + ->gauge( + "dynamicmodulescustom.entrypoint_pending.entrypoint.on_request_headers.method.GET") + ->value(), + 0); + EXPECT_EQ(test_server_ + ->gauge("dynamicmodulescustom.entrypoint_set_value.entrypoint.on_request_headers." + "method.GET") + ->value(), + 100); + EXPECT_EQ(TestUtility::readSampleCount(test_server_->server().dispatcher(), + *request_entrypoint_header_values), + 1); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *request_entrypoint_header_values)), + 3); + EXPECT_EQ( + test_server_ + ->counter( + "dynamicmodulescustom.entrypoint_total.entrypoint.on_response_headers.method.GET") + ->value(), + 1); + EXPECT_EQ(test_server_ + ->gauge("dynamicmodulescustom.entrypoint_set_value.entrypoint.on_response_" + "headers.method.GET") + ->value(), + 999); + EXPECT_EQ(TestUtility::readSampleCount(test_server_->server().dispatcher(), + *response_entrypoint_header_values), + 1); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *response_entrypoint_header_values)), + 3); + } + + // Test stat values persisted after filter is destroyed + { + Http::TestRequestHeaderMapImpl request_headers = default_request_headers_; + request_headers.addCopy(Http::LowerCaseString("header_to_count"), "13"); + auto encoder_decoder = codec_client_->startRequest(request_headers, true); + auto response = std::move(encoder_decoder.second); + waitForNextUpstreamRequest(); + test_server_->waitForNumHistogramSamplesGe("dynamicmodulescustom.requests_header_values", 2); + + EXPECT_EQ(test_server_->counter("dynamicmodulescustom.requests_total")->value(), 2); + EXPECT_EQ(test_server_->gauge("dynamicmodulescustom.requests_pending")->value(), 1); + EXPECT_EQ(test_server_->gauge("dynamicmodulescustom.requests_set_value")->value(), + 100); // set above in first request + auto requests_header_values = + test_server_->histogram("dynamicmodulescustom.requests_header_values"); + EXPECT_EQ( + TestUtility::readSampleCount(test_server_->server().dispatcher(), *requests_header_values), + 2); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *requests_header_values)), + 3 + 13); + + EXPECT_EQ( + test_server_ + ->counter( + "dynamicmodulescustom.entrypoint_total.entrypoint.on_request_headers.method.GET") + ->value(), + 2); + EXPECT_EQ( + test_server_ + ->gauge( + "dynamicmodulescustom.entrypoint_pending.entrypoint.on_request_headers.method.GET") + ->value(), + 1); + EXPECT_EQ(test_server_ + ->gauge("dynamicmodulescustom.entrypoint_set_value.entrypoint.on_request_headers." + "method.GET") + ->value(), + 100); // set above in first request + auto request_entrypoint_header_values = test_server_->histogram( + "dynamicmodulescustom.entrypoint_header_values.entrypoint.on_request_headers.method.GET"); + EXPECT_EQ(TestUtility::readSampleCount(test_server_->server().dispatcher(), + *request_entrypoint_header_values), + 2); + EXPECT_EQ(static_cast(TestUtility::readSampleSum(test_server_->server().dispatcher(), + *request_entrypoint_header_values)), + 3 + 13); + + Http::TestResponseHeaderMapImpl response_headers = default_response_headers_; + response_headers.addCopy(Http::LowerCaseString("header_to_count"), "5"); + response_headers.addCopy(Http::LowerCaseString("header_to_set"), "1000"); + upstream_request_->encodeHeaders(response_headers, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + } +} + +TEST_P(DynamicModulesIntegrationTest, InjectBody) { + initializeFilter("inject_body"); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + + auto response = + codec_client_->makeRequestWithBody(default_request_headers_, "ignored_request_body"); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData("ignored_response_body", true); + + ASSERT_TRUE(response->waitForEndStream()); + + // Verify the injected request was received upstream, as expected. + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ("injected_request_body", upstream_request_->body().toString()); + // Verify the injected response was received downstream, as expected. + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_EQ("injected_response_body", response->body()); +} + } // namespace Envoy diff --git a/test/extensions/dynamic_modules/test_data/c/no_http_filter_new.c b/test/extensions/dynamic_modules/test_data/c/no_http_filter_new.c index 7a0cea56a2a03..c1d1490565b34 100644 --- a/test/extensions/dynamic_modules/test_data/c/no_http_filter_new.c +++ b/test/extensions/dynamic_modules/test_data/c/no_http_filter_new.c @@ -16,3 +16,4 @@ envoy_dynamic_module_on_http_filter_config_new( void envoy_dynamic_module_on_http_filter_config_destroy( envoy_dynamic_module_type_http_filter_config_module_ptr filter_config_ptr) {} + diff --git a/test/extensions/dynamic_modules/test_data/rust/http.rs b/test/extensions/dynamic_modules/test_data/rust/http.rs index b2317fcb766b1..6a1c87e2a8976 100644 --- a/test/extensions/dynamic_modules/test_data/rust/http.rs +++ b/test/extensions/dynamic_modules/test_data/rust/http.rs @@ -14,11 +14,34 @@ fn init() -> bool { /// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::NewHttpFilterConfigFunction`] /// signature. fn new_http_filter_config_fn( - _envoy_filter_config: &mut EC, + envoy_filter_config: &mut EC, name: &str, _config: &[u8], -) -> Option>> { +) -> Option>> { match name { + "stats_callbacks" => Some(Box::new(StatsCallbacksFilterConfig { + streams_total: envoy_filter_config + .define_counter("streams_total") + .expect("failed to define counter"), + concurrent_streams: envoy_filter_config + .define_gauge("concurrent_streams") + .expect("failed to define gauge"), + ones: envoy_filter_config + .define_histogram("ones") + .expect("failed to define histogram"), + magic_number: envoy_filter_config + .define_gauge("magic_number") + .expect("failed to define gauge"), + test_counter_vec: envoy_filter_config + .define_counter_vec("test_counter_vec", &["test_label"]) + .expect("failed to define counter vec"), + test_gauge_vec: envoy_filter_config + .define_gauge_vec("test_gauge_vec", &["test_label"]) + .expect("failed to define gauge vec"), + test_histogram_vec: envoy_filter_config + .define_histogram_vec("test_histogram_vec", &["test_label"]) + .expect("failed to define histogram vec"), + })), "header_callbacks" => Some(Box::new(HeaderCallbacksFilterConfig {})), "send_response" => Some(Box::new(SendResponseFilterConfig {})), "dynamic_metadata_callbacks" => Some(Box::new(DynamicMetadataCallbacksFilterConfig {})), @@ -29,15 +52,120 @@ fn new_http_filter_config_fn( } } +/// An HTTP filter configuration that implements +/// [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilterConfig`] to test the stats +/// related callbacks. +struct StatsCallbacksFilterConfig { + streams_total: EnvoyCounterId, + concurrent_streams: EnvoyGaugeId, + magic_number: EnvoyGaugeId, + // It's full of 1s. + ones: EnvoyHistogramId, + test_counter_vec: EnvoyCounterVecId, + test_gauge_vec: EnvoyGaugeVecId, + test_histogram_vec: EnvoyHistogramVecId, +} + +impl HttpFilterConfig for StatsCallbacksFilterConfig { + fn new_http_filter(&mut self, envoy_filter: &mut EHF) -> Box> { + envoy_filter + .increment_counter(self.streams_total, 1) + .expect("failed to increment counter"); + envoy_filter + .increase_gauge(self.concurrent_streams, 1) + .expect("failed to increase gauge"); + envoy_filter + .set_gauge(self.magic_number, 42) + .expect("failed to set gauge"); + envoy_filter + .increment_counter_vec(self.test_counter_vec, &["increment"], 1) + .expect("failed to increment counter vec"); + envoy_filter + .increase_gauge_vec(self.test_gauge_vec, &["increase"], 1) + .expect("failed to increase gauge vec"); + envoy_filter + .increase_gauge_vec(self.test_gauge_vec, &["decrease"], 10) + .expect("failed to increase gauge vec"); + envoy_filter + .decrease_gauge_vec(self.test_gauge_vec, &["decrease"], 8) + .expect("failed to decrease gauge vec"); + envoy_filter + .set_gauge_vec(self.test_gauge_vec, &["set"], 9001) + .expect("failed to set gauge vec"); + envoy_filter + .record_histogram_value_vec(self.test_histogram_vec, &["record"], 1) + .expect("failed to record histogram value vec"); + // Copy the stats handles onto the filter so that we can observe stats while + // handling requests. + Box::new(StatsCallbacksFilter { + concurrent_streams: self.concurrent_streams, + ones: self.ones, + test_counter_vec: self.test_counter_vec, + test_gauge_vec: self.test_gauge_vec, + test_histogram_vec: self.test_histogram_vec, + }) + } +} + +/// An HTTP filter that implements [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilter`]. +struct StatsCallbacksFilter { + concurrent_streams: EnvoyGaugeId, + ones: EnvoyHistogramId, + test_counter_vec: EnvoyCounterVecId, + test_gauge_vec: EnvoyGaugeVecId, + test_histogram_vec: EnvoyHistogramVecId, +} + +impl HttpFilter for StatsCallbacksFilter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status { + envoy_filter + .record_histogram_value(self.ones, 1) + .expect("failed to record histogram value"); + + let header = envoy_filter.get_request_header_value("header").unwrap(); + let header = std::str::from_utf8(header.as_slice()).unwrap(); + envoy_filter + .increment_counter_vec(self.test_counter_vec, &[header], 1) + .expect("failed to increment counter vec"); + envoy_filter + .increase_gauge_vec(self.test_gauge_vec, &[header], 1) + .expect("failed to increase gauge vec"); + envoy_filter + .record_histogram_value_vec(self.test_histogram_vec, &[header], 1) + .expect("failed to record histogram value vec"); + + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } + + fn on_stream_complete(&mut self, envoy_filter: &mut EHF) { + envoy_filter + .decrease_gauge(self.concurrent_streams, 1) + .expect("failed to decrease gauge"); + + let local_var = "local_var".to_owned(); + envoy_filter + .increment_counter_vec(self.test_counter_vec, &[&local_var], 1) + .expect("failed to increment counter vec"); + envoy_filter + .increase_gauge_vec(self.test_gauge_vec, &[&local_var], 1) + .expect("failed to increase gauge vec"); + envoy_filter + .record_histogram_value_vec(self.test_histogram_vec, &[&local_var], 1) + .expect("failed to record histogram value vec"); + } +} + /// A HTTP filter configuration that implements /// [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilterConfig`] to test the header/trailer /// related callbacks. struct HeaderCallbacksFilterConfig {} -impl HttpFilterConfig - for HeaderCallbacksFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for HeaderCallbacksFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(HeaderCallbacksFilter {}) } } @@ -89,9 +217,9 @@ impl HttpFilter for HeaderCallbacksFilter { assert_eq!(all_headers[3].0.as_slice(), b"new"); assert_eq!(all_headers[3].1.as_slice(), b"value"); - let downstrean_port = + let downstream_port = envoy_filter.get_attribute_int(abi::envoy_dynamic_module_type_attribute_id::SourcePort); - assert_eq!(downstrean_port, Some(1234)); + assert_eq!(downstream_port, Some(1234)); let downstream_addr = envoy_filter.get_attribute_string(abi::envoy_dynamic_module_type_attribute_id::SourceAddress); assert!(downstream_addr.is_some()); @@ -255,10 +383,8 @@ impl HttpFilter for HeaderCallbacksFilter { /// callback struct SendResponseFilterConfig {} -impl HttpFilterConfig - for SendResponseFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for SendResponseFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(SendResponseFilter {}) } } @@ -290,10 +416,8 @@ impl HttpFilter for SendResponseFilter { /// callbacks. struct DynamicMetadataCallbacksFilterConfig {} -impl HttpFilterConfig - for DynamicMetadataCallbacksFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for DynamicMetadataCallbacksFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(DynamicMetadataCallbacksFilter {}) } } @@ -450,10 +574,8 @@ impl HttpFilter for DynamicMetadataCallbacksFilter { /// callbacks. struct FilterStateCallbacksFilterConfig {} -impl HttpFilterConfig - for FilterStateCallbacksFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for FilterStateCallbacksFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(FilterStateCallbacksFilter {}) } } @@ -559,10 +681,8 @@ impl HttpFilter for FilterStateCallbacksFilter { /// to test the body related callbacks. struct BodyCallbacksFilterConfig {} -impl HttpFilterConfig - for BodyCallbacksFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for BodyCallbacksFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(BodyCallbacksFilter::default()) } } diff --git a/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs index 284873b5c4bba..061aa0b87e864 100644 --- a/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs @@ -13,10 +13,10 @@ fn init() -> bool { } fn new_http_filter_config_fn( - _envoy_filter_config: &mut EC, + envoy_filter_config: &mut EC, name: &str, config: &[u8], -) -> Option>> { +) -> Option>> { match name { "passthrough" => Some(Box::new(PassthroughHttpFilterConfig {})), "header_callbacks" => Some(Box::new(HeadersHttpFilterConfig { @@ -34,6 +34,39 @@ fn new_http_filter_config_fn( "send_response" => Some(Box::new(SendResponseHttpFilterConfig::new(config))), "http_filter_scheduler" => Some(Box::new(HttpFilterSchedulerConfig {})), "fake_external_cache" => Some(Box::new(FakeExternalCachingFilterConfig {})), + "stats_callbacks" => { + let config = String::from_utf8(config.to_owned()).unwrap(); + let mut config_iter = config.split(','); + Some(Box::new(StatsCallbacksFilterConfig { + requests_total: envoy_filter_config + .define_counter("requests_total") + .unwrap(), + requests_pending: envoy_filter_config + .define_gauge("requests_pending") + .unwrap(), + requests_set_value: envoy_filter_config + .define_gauge("requests_set_value") + .unwrap(), + requests_header_values: envoy_filter_config + .define_histogram("requests_header_values") + .unwrap(), + entrypoint_total: envoy_filter_config + .define_counter_vec("entrypoint_total", &["entrypoint", "method"]) + .unwrap(), + entrypoint_set_value: envoy_filter_config + .define_gauge_vec("entrypoint_set_value", &["entrypoint", "method"]) + .unwrap(), + entrypoint_pending: envoy_filter_config + .define_gauge_vec("entrypoint_pending", &["entrypoint", "method"]) + .unwrap(), + entrypoint_header_values: envoy_filter_config + .define_histogram_vec("entrypoint_header_values", &["entrypoint", "method"]) + .unwrap(), + header_to_count: config_iter.next().unwrap().to_owned(), + header_to_set: config_iter.next().unwrap().to_owned(), + })) + }, + "inject_body" => Some(Box::new(InjectBodyHttpFilterConfig {})), _ => panic!("Unknown filter name: {}", name), } } @@ -49,26 +82,44 @@ fn new_http_filter_per_route_config_fn(name: &str, config: &[u8]) -> Option HttpFilterConfig - for PassthroughHttpFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for PassthroughHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { + // Just to test that loggers can be accessible in a filter config callback. + envoy_log_trace!("new_http_filter called"); + envoy_log_debug!("new_http_filter called"); + envoy_log_info!("new_http_filter called"); + envoy_log_warn!("new_http_filter called"); + envoy_log_error!("new_http_filter called"); + envoy_log_critical!("new_http_filter called"); Box::new(PassthroughHttpFilter {}) } } struct PassthroughHttpFilter {} -impl HttpFilter for PassthroughHttpFilter {} +impl HttpFilter for PassthroughHttpFilter { + fn on_request_headers( + &mut self, + _envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> envoy_dynamic_module_type_on_http_filter_request_headers_status { + // Just to test that loggers can be accessible in a filter callback. + envoy_log_trace!("on_request_headers called"); + envoy_log_debug!("on_request_headers called"); + envoy_log_info!("on_request_headers called"); + envoy_log_warn!("on_request_headers called"); + envoy_log_error!("on_request_headers called"); + envoy_log_critical!("on_request_headers called"); + envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } +} struct HeadersHttpFilterConfig { headers_to_add: String, } -impl HttpFilterConfig - for HeadersHttpFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for HeadersHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { let headers_to_add: Vec<(String, String)> = self .headers_to_add .split(',') @@ -182,10 +233,8 @@ struct PerRouteFilterConfig { value: String, } -impl HttpFilterConfig - for PerRouteFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for PerRouteFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(PerRouteFilter { value: self.value.clone(), per_route_config: None, @@ -242,10 +291,8 @@ struct BodyCallbacksFilterConfig { immediate_end_of_stream: bool, } -impl HttpFilterConfig - for BodyCallbacksFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for BodyCallbacksFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(BodyCallbacksFilter { immediate_end_of_stream: self.immediate_end_of_stream, seen_request_body: false, @@ -347,10 +394,8 @@ impl SendResponseHttpFilterConfig { } } -impl HttpFilterConfig - for SendResponseHttpFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for SendResponseHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(self.f.clone()) } } @@ -418,10 +463,8 @@ struct HttpCalloutsFilterConfig { cluster_name: String, } -impl HttpFilterConfig - for HttpCalloutsFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for HttpCalloutsFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(HttpCalloutsFilter { cluster_name: self.cluster_name.clone(), }) @@ -517,10 +560,8 @@ impl HttpFilter for HttpCalloutsFilter { struct HttpFilterSchedulerConfig {} -impl HttpFilterConfig - for HttpFilterSchedulerConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for HttpFilterSchedulerConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(HttpFilterScheduler { event_ids: vec![], thread_handles: vec![], @@ -584,14 +625,11 @@ impl Drop for HttpFilterScheduler { } } - /// This implements a fake external caching filter that simulates an asynchronous cache lookup. struct FakeExternalCachingFilterConfig {} -impl HttpFilterConfig - for FakeExternalCachingFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for FakeExternalCachingFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(FakeExternalCachingFilter { rx: None }) } } @@ -679,3 +717,261 @@ impl HttpFilter for FakeExternalCachingFilter { envoy_dynamic_module_type_on_http_filter_response_headers_status::StopIteration } } + +struct StatsCallbacksFilterConfig { + requests_total: EnvoyCounterId, + requests_pending: EnvoyGaugeId, + requests_header_values: EnvoyHistogramId, + requests_set_value: EnvoyGaugeId, + entrypoint_total: EnvoyCounterVecId, + entrypoint_pending: EnvoyGaugeVecId, + entrypoint_header_values: EnvoyHistogramVecId, + entrypoint_set_value: EnvoyGaugeVecId, + header_to_count: String, + header_to_set: String, +} + +impl HttpFilterConfig for StatsCallbacksFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { + Box::new(StatsCallbacksFilter { + requests_total: self.requests_total, + requests_pending: self.requests_pending, + requests_header_values: self.requests_header_values, + requests_set_value: self.requests_set_value, + entrypoint_total: self.entrypoint_total, + entrypoint_pending: self.entrypoint_pending, + entrypoint_header_values: self.entrypoint_header_values, + entrypoint_set_value: self.entrypoint_set_value, + header_to_count: self.header_to_count.clone(), + header_to_set: self.header_to_set.clone(), + method: None, + }) + } +} + +struct StatsCallbacksFilter { + requests_total: EnvoyCounterId, + requests_pending: EnvoyGaugeId, + requests_set_value: EnvoyGaugeId, + requests_header_values: EnvoyHistogramId, + + entrypoint_total: EnvoyCounterVecId, + entrypoint_pending: EnvoyGaugeVecId, + entrypoint_set_value: EnvoyGaugeVecId, + entrypoint_header_values: EnvoyHistogramVecId, + header_to_count: String, + header_to_set: String, + method: Option, +} + +impl HttpFilter for StatsCallbacksFilter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status { + envoy_filter + .increment_counter(self.requests_total, 1) + .unwrap(); + envoy_filter + .increase_gauge(self.requests_pending, 1) + .unwrap(); + let method = envoy_filter.get_request_header_value(":method").unwrap(); + let method = std::str::from_utf8(method.as_slice()).unwrap(); + envoy_filter + .increment_counter_vec(self.entrypoint_total, &["on_request_headers", method], 1) + .unwrap(); + envoy_filter + .increase_gauge_vec(self.entrypoint_pending, &["on_request_headers", method], 1) + .unwrap(); + self.method = Some(method.to_owned()); + + // Record histogram value to provided value in header + if let Some(header_val) = envoy_filter.get_request_header_value(self.header_to_count.as_str()) { + let header_val = std::str::from_utf8(header_val.as_slice()) + .unwrap() + .parse() + .unwrap(); + envoy_filter + .record_histogram_value(self.requests_header_values, header_val) + .unwrap(); + envoy_filter + .record_histogram_value_vec( + self.entrypoint_header_values, + &["on_request_headers", method], + header_val, + ) + .unwrap(); + } + + + // Set gauges to provided value in header + if let Some(header_val) = envoy_filter.get_request_header_value(self.header_to_set.as_str()) { + let header_val = std::str::from_utf8(header_val.as_slice()) + .unwrap() + .parse() + .unwrap(); + envoy_filter + .set_gauge(self.requests_set_value, header_val) + .unwrap(); + envoy_filter + .set_gauge_vec( + self.entrypoint_set_value, + &["on_request_headers", method], + header_val, + ) + .unwrap(); + } + + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } + + fn on_response_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status { + envoy_filter + .increment_counter_vec( + self.entrypoint_total, + &["on_response_headers", self.method.as_ref().unwrap()], + 1, + ) + .unwrap(); + envoy_filter + .decrease_gauge(self.requests_pending, 1) + .unwrap(); + envoy_filter + .decrease_gauge_vec( + self.entrypoint_pending, + &["on_request_headers", self.method.as_ref().unwrap()], + 1, + ) + .unwrap(); + envoy_filter + .increase_gauge_vec( + self.entrypoint_pending, + &["on_response_headers", self.method.as_ref().unwrap()], + 1, + ) + .unwrap(); + + // Record histogram value to provided value in header + if let Some(header_val) = envoy_filter.get_response_header_value(self.header_to_count.as_str()) + { + let header_val = std::str::from_utf8(header_val.as_slice()) + .unwrap() + .parse() + .unwrap(); + envoy_filter + .record_histogram_value_vec( + self.entrypoint_header_values, + &["on_response_headers", self.method.as_ref().unwrap()], + header_val, + ) + .unwrap(); + } + + // Set gauges to provided value in header + if let Some(header_val) = envoy_filter.get_response_header_value(self.header_to_set.as_str()) { + let header_val = std::str::from_utf8(header_val.as_slice()) + .unwrap() + .parse() + .unwrap(); + envoy_filter + .set_gauge_vec( + self.entrypoint_set_value, + &["on_response_headers", self.method.as_ref().unwrap()], + header_val, + ) + .unwrap(); + } + + abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + } + + fn on_stream_complete(&mut self, envoy_filter: &mut EHF) { + envoy_filter + .decrease_gauge_vec( + self.entrypoint_pending, + &["on_response_headers", self.method.as_ref().unwrap()], + 1, + ) + .unwrap(); + } +} + +// Filter that blocks request and response body processing within the filter chain and +// instead injects request and response body within a scheduler callback. +struct InjectBodyHttpFilterConfig {} + +impl HttpFilterConfig for InjectBodyHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { + Box::new(InjectBodyHttpFilter {}) + } +} + +const EVENT_INJECT_REQUEST_BODY: u64 = 1; +const EVENT_INJECT_RESPONSE_BODY: u64 = 2; + +struct InjectBodyHttpFilter {} + +impl HttpFilter for InjectBodyHttpFilter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> envoy_dynamic_module_type_on_http_filter_request_headers_status { + // Schedule request body injection outside the filter lifecycle. + envoy_filter + .new_scheduler() + .commit(EVENT_INJECT_REQUEST_BODY); + return envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue; + } + + fn on_request_body( + &mut self, + _envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> envoy_dynamic_module_type_on_http_filter_request_body_status { + // Ignore downstream request body. + return envoy_dynamic_module_type_on_http_filter_request_body_status::StopIterationAndBuffer; + } + + fn on_response_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status { + // Schedule response body injection outside the filter lifecycle. + envoy_filter + .new_scheduler() + .commit(EVENT_INJECT_RESPONSE_BODY); + abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + } + + fn on_response_body( + &mut self, + _envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> envoy_dynamic_module_type_on_http_filter_response_body_status { + // Ignore upstream response body. + return envoy_dynamic_module_type_on_http_filter_response_body_status::StopIterationAndBuffer; + } + + fn on_scheduled(&mut self, envoy_filter: &mut EHF, event_id: u64) { + // Inject the request or response body based on the event type. We inject + // both without and with end_stream to ensure the full body is sent correctly. + match event_id { + EVENT_INJECT_REQUEST_BODY => { + envoy_filter.inject_request_body(b"injected_", false); + envoy_filter.inject_request_body(b"request_body", true); + }, + EVENT_INJECT_RESPONSE_BODY => { + envoy_filter.inject_response_body(b"injected_", false); + envoy_filter.inject_response_body(b"response_body", true); + }, + _ => unreachable!(), + } + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/no_op.rs b/test/extensions/dynamic_modules/test_data/rust/no_op.rs index 60ee27a30afce..eee9443417dce 100644 --- a/test/extensions/dynamic_modules/test_data/rust/no_op.rs +++ b/test/extensions/dynamic_modules/test_data/rust/no_op.rs @@ -21,7 +21,7 @@ fn new_nop_http_filter_config_fn Option>> { +) -> Option>> { let name = name.to_string(); let config = String::from_utf8(config.to_owned()).unwrap_or_default(); Some(Box::new(NopHttpFilterConfig { name, config })) @@ -35,10 +35,8 @@ struct NopHttpFilterConfig { config: String, } -impl HttpFilterConfig - for NopHttpFilterConfig -{ - fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { +impl HttpFilterConfig for NopHttpFilterConfig { + fn new_http_filter(&mut self, _envoy: &mut EHF) -> Box> { Box::new(NopHttpFilter { on_request_headers_called: false, on_request_body_called: false, diff --git a/test/extensions/filters/common/expr/context_test.cc b/test/extensions/filters/common/expr/context_test.cc index 50d9d96f253f4..b727e976c276e 100644 --- a/test/extensions/filters/common/expr/context_test.cc +++ b/test/extensions/filters/common/expr/context_test.cc @@ -841,7 +841,7 @@ TEST(Context, ConnectionAttributes) { TEST(Context, FilterStateAttributes) { StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::FilterChain); - ProtobufWkt::Arena arena; + Protobuf::Arena arena; FilterStateWrapper wrapper(arena, filter_state); auto status_or = wrapper.ListKeys(&arena); EXPECT_EQ(status_or.status().message(), "ListKeys() is not implemented"); @@ -874,7 +874,7 @@ TEST(Context, FilterStateAttributes) { "type.googleapis.com/google.protobuf.DoubleValue", StreamInfo::FilterState::LifeSpan::FilterChain); auto cel_state = std::make_shared(prototype); - ProtobufWkt::DoubleValue v; + Protobuf::DoubleValue v; v.set_value(1.0); cel_state->setValue(v.SerializeAsString()); EXPECT_TRUE(cel_state->serializeAsString().has_value()); diff --git a/test/extensions/filters/common/expr/evaluator_fuzz_test.cc b/test/extensions/filters/common/expr/evaluator_fuzz_test.cc index a09c51bb4a2ad..8ae0147154077 100644 --- a/test/extensions/filters/common/expr/evaluator_fuzz_test.cc +++ b/test/extensions/filters/common/expr/evaluator_fuzz_test.cc @@ -8,6 +8,7 @@ #include "test/test_common/network_utility.h" #include "test/test_common/utility.h" +#include "cel/expr/syntax.pb.h" #include "gtest/gtest.h" namespace Envoy { @@ -19,7 +20,7 @@ namespace { DEFINE_PROTO_FUZZER(const test::extensions::filters::common::expr::EvaluatorTestCase& input) { // Create builder without constant folding. - static Expr::BuilderPtr builder = Expr::createBuilder(nullptr); + static auto builder = Expr::createBuilder(nullptr); MockTimeSystem time_source; std::unique_ptr stream_info; @@ -39,12 +40,26 @@ DEFINE_PROTO_FUZZER(const test::extensions::filters::common::expr::EvaluatorTest auto response_trailers = Fuzz::fromHeaders(input.trailers()); try { - // Create the CEL expression. - Expr::ExpressionPtr expr = Expr::createExpression(*builder, input.expression()); + // Create the CEL expression with boundary conversion. + std::string serialized; + if (!input.expression().SerializeToString(&serialized)) { + ENVOY_LOG_MISC(debug, "Failed to serialize expression"); + return; + } + cel::expr::Expr new_expr; + if (!new_expr.ParseFromString(serialized)) { + ENVOY_LOG_MISC(debug, "Failed to convert expression to new format"); + return; + } + auto expr = Expr::CompiledExpression::Create(builder, new_expr); + if (!expr.ok()) { + ENVOY_LOG_MISC(debug, "Failed to compile"); + return; + } // Evaluate the CEL expression. Protobuf::Arena arena; - Expr::evaluate(*expr, arena, nullptr, *stream_info, &request_headers, &response_headers, + expr->evaluate(arena, nullptr, *stream_info, &request_headers, &response_headers, &response_trailers); } catch (const CelException& e) { ENVOY_LOG_MISC(debug, "CelException: {}", e.what()); diff --git a/test/extensions/filters/common/expr/evaluator_test.cc b/test/extensions/filters/common/expr/evaluator_test.cc index 6358a08b3f6b7..beba03eaa2bfc 100644 --- a/test/extensions/filters/common/expr/evaluator_test.cc +++ b/test/extensions/filters/common/expr/evaluator_test.cc @@ -27,7 +27,7 @@ TEST(Evaluator, Print) { EXPECT_EQ(print(CelValue::CreateString(&test)), "test"); EXPECT_EQ(print(CelValue::CreateBytes(&test)), "test"); - ProtobufWkt::Arena arena; + Protobuf::Arena arena; envoy::config::core::v3::Node node; std::string node_yaml = "id: test"; TestUtility::loadFromYaml(node_yaml, node); @@ -48,7 +48,7 @@ TEST(Evaluator, Activation) { auto filter_state = std::make_shared(StreamInfo::FilterState::LifeSpan::FilterChain); info.upstreamInfo()->setUpstreamFilterState(filter_state); - ProtobufWkt::Arena arena; + Protobuf::Arena arena; const auto activation = createActivation(nullptr, info, nullptr, nullptr, nullptr); EXPECT_TRUE(activation->FindValue("filter_state", &arena).has_value()); EXPECT_TRUE(activation->FindValue("upstream_filter_state", &arena).has_value()); diff --git a/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc b/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc index 88ebcae33dcfb..75d89e50439cb 100644 --- a/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc +++ b/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc @@ -69,7 +69,7 @@ TEST_F(ExtAuthzGrpcClientTest, AuthorizationOk) { auto check_response = std::make_unique(); auto status = check_response->mutable_status(); - ProtobufWkt::Struct expected_dynamic_metadata; + Protobuf::Struct expected_dynamic_metadata; auto* metadata_fields = expected_dynamic_metadata.mutable_fields(); (*metadata_fields)["foo"] = ValueUtil::stringValue("ok"); (*metadata_fields)["bar"] = ValueUtil::numberValue(1); @@ -344,12 +344,12 @@ TEST_F(ExtAuthzGrpcClientTest, AuthorizationOkWithDynamicMetadata) { auto check_response = std::make_unique(); auto status = check_response->mutable_status(); - ProtobufWkt::Struct expected_dynamic_metadata; + Protobuf::Struct expected_dynamic_metadata; auto* metadata_fields = expected_dynamic_metadata.mutable_fields(); (*metadata_fields)["original"] = ValueUtil::stringValue("true"); check_response->mutable_dynamic_metadata()->MergeFrom(expected_dynamic_metadata); - ProtobufWkt::Struct overridden_dynamic_metadata; + Protobuf::Struct overridden_dynamic_metadata; metadata_fields = overridden_dynamic_metadata.mutable_fields(); (*metadata_fields)["original"] = ValueUtil::stringValue("false"); @@ -445,6 +445,10 @@ TEST_F(ExtAuthzGrpcClientTest, AuthorizationOkWithAppendActions) { key: overwrite-if-exists-or-add value: overwrite-if-exists-or-add-value append_action: OVERWRITE_IF_EXISTS_OR_ADD + - header: + key: invalid-append-action + value: invalid-append-action-value + append_action: 404 )EOF", check_response); @@ -458,6 +462,7 @@ TEST_F(ExtAuthzGrpcClientTest, AuthorizationOkWithAppendActions) { UnsafeHeaderVector{{"add-if-absent", "add-if-absent-value"}}, .response_headers_to_overwrite_if_exists = UnsafeHeaderVector{{"overwrite-if-exists", "overwrite-if-exists-value"}}, + .saw_invalid_append_actions = true, .status_code = Http::Code::OK, .grpc_status = Grpc::Status::WellKnownGrpcStatus::Ok, }; diff --git a/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc b/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc index b472848ee087d..92cc6cddc5e62 100644 --- a/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc +++ b/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc @@ -119,7 +119,7 @@ class ExtAuthzHttpClientTest : public testing::Test { envoy::service::auth::v3::CheckRequest request; client_->check(request_callbacks_, request, parent_span_, stream_info_); - ProtobufWkt::Struct expected_dynamic_metadata; + Protobuf::Struct expected_dynamic_metadata; auto* metadata_fields = expected_dynamic_metadata.mutable_fields(); (*metadata_fields)["x-metadata-header-0"] = ValueUtil::stringValue("zero"); (*metadata_fields)["x-metadata-header-1"] = ValueUtil::stringValue("2"); @@ -209,6 +209,27 @@ class ExtAuthzHttpClientTest : public testing::Test { NiceMock stream_info_; }; +// Verify ClientConfig could be built directly from HttpService and that the +// fields get wired correctly. +TEST_F(ExtAuthzHttpClientTest, ClientConfigFromHttpService) { + envoy::extensions::filters::http::ext_authz::v3::HttpService http_service; + http_service.mutable_server_uri()->set_uri("ext_authz:9000"); + http_service.mutable_server_uri()->set_cluster("ext_authz"); + http_service.mutable_server_uri()->mutable_timeout()->set_seconds(0); + http_service.set_path_prefix("/prefix"); + // Add one header to add to request to exercise header parser creation. + auto* add = http_service.mutable_authorization_request()->add_headers_to_add(); + add->set_key("x-added"); + add->set_value("v"); + + auto cfg = std::make_shared(http_service, /*encode_raw_headers=*/true, + /*timeout_ms=*/123, factory_context_); + EXPECT_EQ(cfg->cluster(), "ext_authz"); + EXPECT_EQ(cfg->pathPrefix(), "/prefix"); + EXPECT_EQ(cfg->timeout(), std::chrono::milliseconds{123}); + EXPECT_TRUE(cfg->encodeRawHeaders()); +} + TEST_F(ExtAuthzHttpClientTest, StreamInfo) { envoy::service::auth::v3::CheckRequest request; client_->check(request_callbacks_, request, parent_span_, stream_info_); diff --git a/test/extensions/filters/common/ext_authz/test_common.h b/test/extensions/filters/common/ext_authz/test_common.h index 762d37d614344..19532ddbad6e0 100644 --- a/test/extensions/filters/common/ext_authz/test_common.h +++ b/test/extensions/filters/common/ext_authz/test_common.h @@ -133,6 +133,10 @@ MATCHER_P(AuthzOkResponse, response, "") { return false; } + if (response.saw_invalid_append_actions != arg->saw_invalid_append_actions) { + return false; + } + if (!TestCommon::compareQueryParamsVector(response.query_parameters_to_set, arg->query_parameters_to_set)) { return false; diff --git a/test/extensions/filters/common/lua/protobuf_converter_test.cc b/test/extensions/filters/common/lua/protobuf_converter_test.cc index 28b5fe58464d3..cd8991e23d95d 100644 --- a/test/extensions/filters/common/lua/protobuf_converter_test.cc +++ b/test/extensions/filters/common/lua/protobuf_converter_test.cc @@ -1139,10 +1139,10 @@ TEST_F(LuaProtobufConverterTest, NestedMessageWithRecursion) { TEST_F(LuaProtobufConverterTest, TypeURLEndingWithSlash) { // Create Any with type URL ending with slash and no type name after slash - ProtobufWkt::Any any_message; + Protobuf::Any any_message; any_message.set_type_url("type.googleapis.com/"); - Protobuf::Map typed_metadata_map; + Protobuf::Map typed_metadata_map; typed_metadata_map["test.filter"] = any_message; // Push dummy value at index 1, then filter name at index 2 (function expects index 2) @@ -1160,11 +1160,11 @@ TEST_F(LuaProtobufConverterTest, TypeURLEndingWithSlash) { TEST_F(LuaProtobufConverterTest, PrototypeNotFound) { // Create Any with a valid but invalid message type // Using a well-known type that exists in descriptor pool but might not have a prototype - ProtobufWkt::Any any_message; + Protobuf::Any any_message; any_message.set_type_url("type.googleapis.com/google.protobuf.FileDescriptorSet"); any_message.set_value("dummy_data"); - Protobuf::Map typed_metadata_map; + Protobuf::Map typed_metadata_map; typed_metadata_map["test.filter"] = any_message; // Push dummy value at index 1, then filter name at index 2 (function expects index 2) diff --git a/test/extensions/filters/common/ratelimit/mocks.h b/test/extensions/filters/common/ratelimit/mocks.h index 259fa3f08881a..48bad268d4ad7 100644 --- a/test/extensions/filters/common/ratelimit/mocks.h +++ b/test/extensions/filters/common/ratelimit/mocks.h @@ -23,10 +23,11 @@ class MockClient : public Client { // RateLimit::Client MOCK_METHOD(void, cancel, ()); + MOCK_METHOD(void, detach, ()); MOCK_METHOD(void, limit, (RequestCallbacks & callbacks, const std::string& domain, const std::vector& descriptors, - Tracing::Span& parent_span, OptRef stream_info, + Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info, uint32_t hits_addend)); }; diff --git a/test/extensions/filters/common/ratelimit/ratelimit_impl_test.cc b/test/extensions/filters/common/ratelimit/ratelimit_impl_test.cc index 4686c925ce8a8..a2cec4f89eaab 100644 --- a/test/extensions/filters/common/ratelimit/ratelimit_impl_test.cc +++ b/test/extensions/filters/common/ratelimit/ratelimit_impl_test.cc @@ -47,7 +47,7 @@ class MockRequestCallbacks : public RequestCallbacks { (LimitStatus status, const DescriptorStatusList* descriptor_statuses, const Http::ResponseHeaderMap* response_headers_to_add, const Http::RequestHeaderMap* request_headers_to_add, - const std::string& response_body, const ProtobufWkt::Struct* dynamic_metadata)); + const std::string& response_body, const Protobuf::Struct* dynamic_metadata)); }; class RateLimitGrpcClientTest : public testing::Test { @@ -61,7 +61,7 @@ class RateLimitGrpcClientTest : public testing::Test { Grpc::MockAsyncRequest async_request_; GrpcClientImpl client_; MockRequestCallbacks request_callbacks_; - Tracing::MockSpan span_; + testing::NiceMock span_; StreamInfo::MockStreamInfo stream_info_; }; @@ -231,6 +231,36 @@ TEST_F(RateLimitGrpcClientTest, RequestWithPerDescriptorHitsAddend) { client_.onSuccess(std::move(response), span_); } +TEST_F(RateLimitGrpcClientTest, SendRequestAndDetach) { + std::unique_ptr response; + + { + envoy::service::ratelimit::v3::RateLimitRequest request; + Http::TestRequestHeaderMapImpl headers; + GrpcClientImpl::createRequest(request, "foo", {{{{"foo", "bar"}}}}, 0); + EXPECT_CALL(*async_client_, sendRaw(_, _, Grpc::ProtoBufferEq(request), Ref(client_), _, _)) + .WillOnce( + Invoke([this](absl::string_view service_full_name, absl::string_view method_name, + Buffer::InstancePtr&&, Grpc::RawAsyncRequestCallbacks&, Tracing::Span&, + const Http::AsyncClient::RequestOptions&) -> Grpc::AsyncRequest* { + std::string service_name = "envoy.service.ratelimit.v3.RateLimitService"; + EXPECT_EQ(service_name, service_full_name); + EXPECT_EQ("ShouldRateLimit", method_name); + return &async_request_; + })); + + EXPECT_CALL(async_request_, detach()); + client_.limit(request_callbacks_, "foo", {{{{"foo", "bar"}}}}, Tracing::NullSpan::instance(), + stream_info_, 0); + client_.detach(); + + response = std::make_unique(); + response->set_overall_code(envoy::service::ratelimit::v3::RateLimitResponse::OK); + EXPECT_CALL(request_callbacks_, complete_(LimitStatus::OK, _, _, _, _, _)); + client_.onSuccess(std::move(response), span_); + } +} + } // namespace } // namespace RateLimit } // namespace Common diff --git a/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc b/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc index 5cfcebd105daa..903e1385dde35 100644 --- a/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc +++ b/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc @@ -1268,7 +1268,7 @@ TEST_F(RateLimitPolicyTest, RequestMatchInputSkip) { class ExtensionDescriptorFactory : public Envoy::RateLimit::DescriptorProducerFactory { public: ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "test.descriptor_producer"; } diff --git a/test/extensions/filters/common/rbac/BUILD b/test/extensions/filters/common/rbac/BUILD index c5d555e2da518..f28647786dcdc 100644 --- a/test/extensions/filters/common/rbac/BUILD +++ b/test/extensions/filters/common/rbac/BUILD @@ -26,6 +26,7 @@ envoy_extension_cc_test( "//test/mocks/network:network_mocks", "//test/mocks/server:server_factory_context_mocks", "//test/mocks/ssl:ssl_mocks", + "//test/test_common:status_utility_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/rbac/v3:pkg_cc_proto", diff --git a/test/extensions/filters/common/rbac/engine_impl_test.cc b/test/extensions/filters/common/rbac/engine_impl_test.cc index ea042abc4ca46..1bd52515017ed 100644 --- a/test/extensions/filters/common/rbac/engine_impl_test.cc +++ b/test/extensions/filters/common/rbac/engine_impl_test.cc @@ -94,7 +94,7 @@ void checkMatcherEngine( void onMetadata(NiceMock& info) { ON_CALL(info, setDynamicMetadata("envoy.common", _)) - .WillByDefault(Invoke([&info](const std::string&, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([&info](const std::string&, const Protobuf::Struct& obj) { (*info.metadata_.mutable_filter_metadata())["envoy.common"] = obj; })); } @@ -443,7 +443,7 @@ TEST(RoleBasedAccessControlEngineImpl, MetadataCondition) { auto label = MessageUtil::keyValueStruct("label", "prod"); envoy::config::core::v3::Metadata metadata; metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair("other", label)); + Protobuf::MapPair("other", label)); EXPECT_CALL(Const(info), dynamicMetadata()).WillRepeatedly(ReturnRef(metadata)); checkEngine(engine, true, LogResult::Undecided, info, Envoy::Network::MockConnection(), headers); diff --git a/test/extensions/filters/common/rbac/matchers_test.cc b/test/extensions/filters/common/rbac/matchers_test.cc index 894b7cd250a25..fa6fa08fd1003 100644 --- a/test/extensions/filters/common/rbac/matchers_test.cc +++ b/test/extensions/filters/common/rbac/matchers_test.cc @@ -14,6 +14,7 @@ #include "test/mocks/network/mocks.h" #include "test/mocks/server/server_factory_context.h" #include "test/mocks/ssl/mocks.h" +#include "test/test_common/status_utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -227,28 +228,136 @@ TEST(IPMatcher, IPMatcher) { downstream_remote_cidr.set_address_prefix("8.9.10.11"); downstream_remote_cidr.mutable_prefix_len()->set_value(32); - checkMatcher(IPMatcher(connection_remote_cidr, IPMatcher::Type::ConnectionRemote), true, conn, - headers, info); - checkMatcher(IPMatcher(downstream_local_cidr, IPMatcher::Type::DownstreamLocal), true, conn, - headers, info); - checkMatcher(IPMatcher(downstream_direct_remote_cidr, IPMatcher::Type::DownstreamDirectRemote), - true, conn, headers, info); - checkMatcher(IPMatcher(downstream_remote_cidr, IPMatcher::Type::DownstreamRemote), true, conn, - headers, info); + auto connection_remote_matcher = + IPMatcher::create(connection_remote_cidr, IPMatcher::Type::ConnectionRemote); + ASSERT_OK(connection_remote_matcher); + checkMatcher(*connection_remote_matcher.value(), true, conn, headers, info); + + auto downstream_local_matcher = + IPMatcher::create(downstream_local_cidr, IPMatcher::Type::DownstreamLocal); + ASSERT_OK(downstream_local_matcher); + checkMatcher(*downstream_local_matcher.value(), true, conn, headers, info); + + auto downstream_direct_remote_matcher = + IPMatcher::create(downstream_direct_remote_cidr, IPMatcher::Type::DownstreamDirectRemote); + ASSERT_OK(downstream_direct_remote_matcher); + checkMatcher(*downstream_direct_remote_matcher.value(), true, conn, headers, info); + + auto downstream_remote_matcher = + IPMatcher::create(downstream_remote_cidr, IPMatcher::Type::DownstreamRemote); + ASSERT_OK(downstream_remote_matcher); + checkMatcher(*downstream_remote_matcher.value(), true, conn, headers, info); connection_remote_cidr.set_address_prefix("4.5.6.7"); downstream_local_cidr.set_address_prefix("1.2.4.8"); downstream_direct_remote_cidr.set_address_prefix("4.5.6.0"); downstream_remote_cidr.set_address_prefix("4.5.6.7"); - checkMatcher(IPMatcher(connection_remote_cidr, IPMatcher::Type::ConnectionRemote), false, conn, - headers, info); - checkMatcher(IPMatcher(downstream_local_cidr, IPMatcher::Type::DownstreamLocal), false, conn, - headers, info); - checkMatcher(IPMatcher(downstream_direct_remote_cidr, IPMatcher::Type::DownstreamDirectRemote), - false, conn, headers, info); - checkMatcher(IPMatcher(downstream_remote_cidr, IPMatcher::Type::DownstreamRemote), false, conn, - headers, info); + auto connection_remote_matcher2 = + IPMatcher::create(connection_remote_cidr, IPMatcher::Type::ConnectionRemote); + ASSERT_OK(connection_remote_matcher2); + checkMatcher(*connection_remote_matcher2.value(), false, conn, headers, info); + + auto downstream_local_matcher2 = + IPMatcher::create(downstream_local_cidr, IPMatcher::Type::DownstreamLocal); + ASSERT_OK(downstream_local_matcher2); + checkMatcher(*downstream_local_matcher2.value(), false, conn, headers, info); + + auto downstream_direct_remote_matcher2 = + IPMatcher::create(downstream_direct_remote_cidr, IPMatcher::Type::DownstreamDirectRemote); + ASSERT_OK(downstream_direct_remote_matcher2); + checkMatcher(*downstream_direct_remote_matcher2.value(), false, conn, headers, info); + + auto downstream_remote_matcher2 = + IPMatcher::create(downstream_remote_cidr, IPMatcher::Type::DownstreamRemote); + ASSERT_OK(downstream_remote_matcher2); + checkMatcher(*downstream_remote_matcher2.value(), false, conn, headers, info); +} + +// Ensure non-IP addresses (e.g., pipe) do not crash IPMatcher and simply return false. +TEST(IPMatcher, NonIpAddressesReturnFalseAndDoNotCrash) { + NiceMock factory_context; + + // Principal: source_ip (ConnectionRemote) + { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_source_ip(); + cidr->set_address_prefix("::"); + cidr->mutable_prefix_len()->set_value(0); + + auto matcher = Matcher::create(principal, factory_context); + ASSERT_NE(matcher, nullptr); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + Envoy::Network::Address::InstanceConstSharedPtr pipe = + *Envoy::Network::Address::PipeInstance::create("test"); + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(pipe); + EXPECT_FALSE(matcher->matches(conn, headers, info)); + } + + // Permission: destination_ip (DownstreamLocal) + { + envoy::config::rbac::v3::Permission permission; + auto* cidr = permission.mutable_destination_ip(); + cidr->set_address_prefix("::"); + cidr->mutable_prefix_len()->set_value(0); + + auto matcher = + Matcher::create(permission, ProtobufMessage::getStrictValidationVisitor(), factory_context); + ASSERT_NE(matcher, nullptr); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + Envoy::Network::Address::InstanceConstSharedPtr pipe = + *Envoy::Network::Address::PipeInstance::create("test"); + info.downstream_connection_info_provider_->setLocalAddress(pipe); + EXPECT_FALSE(matcher->matches(conn, headers, info)); + } + + // Principal: direct_remote_ip (DownstreamDirectRemote) + { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_direct_remote_ip(); + cidr->set_address_prefix("::"); + cidr->mutable_prefix_len()->set_value(0); + + auto matcher = Matcher::create(principal, factory_context); + ASSERT_NE(matcher, nullptr); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + Envoy::Network::Address::InstanceConstSharedPtr pipe = + *Envoy::Network::Address::PipeInstance::create("test"); + info.downstream_connection_info_provider_->setDirectRemoteAddressForTest(pipe); + EXPECT_FALSE(matcher->matches(conn, headers, info)); + } + + // Principal: remote_ip (DownstreamRemote) + { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_remote_ip(); + cidr->set_address_prefix("::"); + cidr->mutable_prefix_len()->set_value(0); + + auto matcher = Matcher::create(principal, factory_context); + ASSERT_NE(matcher, nullptr); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + Envoy::Network::Address::InstanceConstSharedPtr pipe = + *Envoy::Network::Address::PipeInstance::create("test"); + info.downstream_connection_info_provider_->setRemoteAddress(pipe); + EXPECT_FALSE(matcher->matches(conn, headers, info)); + } } TEST(PortMatcher, PortMatcher) { @@ -528,9 +637,9 @@ TEST(MetadataMatcher, MetadataMatcher) { auto label = MessageUtil::keyValueStruct("label", "prod"); envoy::config::core::v3::Metadata metadata; metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair("other", label)); + Protobuf::MapPair("other", label)); metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair("rbac", label)); + Protobuf::MapPair("rbac", label)); EXPECT_CALL(Const(info), dynamicMetadata()).WillRepeatedly(ReturnRef(metadata)); envoy::type::matcher::v3::MetadataMatcher matcher; @@ -554,9 +663,9 @@ TEST(PolicyMatcher, PolicyMatcher) { policy.add_permissions()->set_destination_port(456); policy.add_principals()->mutable_authenticated()->mutable_principal_name()->set_exact("foo"); policy.add_principals()->mutable_authenticated()->mutable_principal_name()->set_exact("bar"); - Expr::BuilderPtr builder = Expr::createBuilder(nullptr); + auto builder = Expr::createBuilder(nullptr); - RBAC::PolicyMatcher matcher(policy, builder.get(), ProtobufMessage::getStrictValidationVisitor(), + RBAC::PolicyMatcher matcher(policy, builder, ProtobufMessage::getStrictValidationVisitor(), factory_context); Envoy::Network::MockConnection conn; @@ -745,14 +854,14 @@ TEST(MetadataMatcher, SourcedMetadataMatcher) { auto dynamic_label = MessageUtil::keyValueStruct("dynamic_key", "dynamic_value"); envoy::config::core::v3::Metadata dynamic_metadata; dynamic_metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair("rbac", dynamic_label)); + Protobuf::MapPair("rbac", dynamic_label)); EXPECT_CALL(Const(info), dynamicMetadata()).WillRepeatedly(ReturnRef(dynamic_metadata)); // Set up route metadata auto route_label = MessageUtil::keyValueStruct("route_key", "route_value"); envoy::config::core::v3::Metadata route_metadata; route_metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair("rbac", route_label)); + Protobuf::MapPair("rbac", route_label)); EXPECT_CALL(*route, metadata()).WillRepeatedly(ReturnRef(route_metadata)); EXPECT_CALL(info, route()).WillRepeatedly(Return(route)); @@ -938,15 +1047,15 @@ TEST(MetadataMatcher, NestedMetadata) { NiceMock info; // Create nested metadata structure - ProtobufWkt::Struct nested_struct; + Protobuf::Struct nested_struct; (*nested_struct.mutable_fields())["nested_key"] = ValueUtil::stringValue("nested_value"); - ProtobufWkt::Struct top_struct; + Protobuf::Struct top_struct; (*top_struct.mutable_fields())["top_key"] = ValueUtil::structValue(nested_struct); envoy::config::core::v3::Metadata metadata; metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair("rbac", top_struct)); + Protobuf::MapPair("rbac", top_struct)); EXPECT_CALL(Const(info), dynamicMetadata()).WillRepeatedly(ReturnRef(metadata)); // Test matching nested value @@ -1088,6 +1197,307 @@ TEST(PrincipalMatcher, OrIds) { checkMatcher(OrMatcher(principals, factory_context), false, conn, headers, info); } +// Tests to cover missing lines in coverage report +TEST(IPMatcher, PrincipalSourceIpMatching) { + // Tests lines 77-79: kSourceIp case in Principal creation + NiceMock factory_context; + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_source_ip(); + cidr->set_address_prefix("192.168.1.0"); + cidr->mutable_prefix_len()->set_value(24); + + auto matcher = Matcher::create(principal, factory_context); + ASSERT_NE(matcher, nullptr); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + // Set connection remote address that matches the CIDR + auto addr = Envoy::Network::Utility::parseInternetAddressNoThrow("192.168.1.100", 123, false); + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(addr); + + EXPECT_TRUE(matcher->matches(conn, headers, info)); + + // Set address that doesn't match + addr = Envoy::Network::Utility::parseInternetAddressNoThrow("10.0.0.1", 123, false); + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(addr); + + EXPECT_FALSE(matcher->matches(conn, headers, info)); +} + +TEST(IPMatcher, PrincipalRemoteIpMatching) { + // Tests lines 83-85: kRemoteIp case in Principal creation + NiceMock factory_context; + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_remote_ip(); + cidr->set_address_prefix("10.0.0.0"); + cidr->mutable_prefix_len()->set_value(16); + + auto matcher = Matcher::create(principal, factory_context); + ASSERT_NE(matcher, nullptr); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + // Set downstream remote address that matches the CIDR + auto addr = Envoy::Network::Utility::parseInternetAddressNoThrow("10.0.5.100", 456, false); + info.downstream_connection_info_provider_->setRemoteAddress(addr); + + EXPECT_TRUE(matcher->matches(conn, headers, info)); + + // Set address that doesn't match + addr = Envoy::Network::Utility::parseInternetAddressNoThrow("172.16.1.1", 456, false); + info.downstream_connection_info_provider_->setRemoteAddress(addr); + + EXPECT_FALSE(matcher->matches(conn, headers, info)); +} + +TEST(IPMatcher, CreateWithInvalidCidrRange) { + // Tests lines 206-208: Invalid CIDR range error handling in IPMatcher::create + Protobuf::RepeatedPtrField ranges; + + // Add valid range first + auto* valid_range = ranges.Add(); + valid_range->set_address_prefix("192.168.1.0"); + valid_range->mutable_prefix_len()->set_value(24); + + // Add invalid range (invalid IP address) + auto* invalid_range = ranges.Add(); + invalid_range->set_address_prefix("invalid.ip.address"); + invalid_range->mutable_prefix_len()->set_value(24); + + auto result = IPMatcher::create(ranges, IPMatcher::Type::ConnectionRemote); + EXPECT_FALSE(result.ok()); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Failed to create CIDR range")); +} + +TEST(IPMatcher, CreateWithEmptyRangeList) { + // Tests empty range validation + Protobuf::RepeatedPtrField empty_ranges; + + auto result = IPMatcher::create(empty_ranges, IPMatcher::Type::ConnectionRemote); + EXPECT_FALSE(result.ok()); + EXPECT_THAT(result.status().message(), testing::HasSubstr("Empty IP range list provided")); +} + +TEST(IPMatcher, MatchesWithNullIpAddress) { + // Tests line 227: null IP address check in matches method + envoy::config::core::v3::CidrRange range; + range.set_address_prefix("192.168.1.0"); + range.mutable_prefix_len()->set_value(24); + + auto matcher_result = IPMatcher::create(range, IPMatcher::Type::ConnectionRemote); + ASSERT_OK(matcher_result); + const auto& matcher = *matcher_result.value(); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + // Set null remote address + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(nullptr); + + EXPECT_FALSE(matcher.matches(conn, headers, info)); +} + +TEST(IPMatcher, MatchesWithConnectionRemoteAddress) { + // Tests that IPMatcher correctly extracts and matches connection remote addresses. + envoy::config::core::v3::CidrRange range; + range.set_address_prefix("192.168.1.0"); + range.mutable_prefix_len()->set_value(24); + + // Create matcher with a specific type + auto matcher_result = IPMatcher::create(range, IPMatcher::Type::ConnectionRemote); + ASSERT_OK(matcher_result); + const auto& matcher = *matcher_result.value(); + + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + // Set all address types to non-null to ensure we test the extraction logic + auto addr = Envoy::Network::Utility::parseInternetAddressNoThrow("192.168.1.100", 123, false); + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(addr); + info.downstream_connection_info_provider_->setLocalAddress(addr); + info.downstream_connection_info_provider_->setDirectRemoteAddressForTest(addr); + info.downstream_connection_info_provider_->setRemoteAddress(addr); + + // This should match and extract the connection remote address correctly + EXPECT_TRUE(matcher.matches(conn, headers, info)); +} + +TEST(IPMatcher, MultipleRangesCreateSuccess) { + // Tests successful creation with multiple ranges + Protobuf::RepeatedPtrField ranges; + + // Add multiple valid ranges + auto* range1 = ranges.Add(); + range1->set_address_prefix("192.168.1.0"); + range1->mutable_prefix_len()->set_value(24); + + auto* range2 = ranges.Add(); + range2->set_address_prefix("10.0.0.0"); + range2->mutable_prefix_len()->set_value(16); + + auto* range3 = ranges.Add(); + range3->set_address_prefix("2001:db8::"); + range3->mutable_prefix_len()->set_value(32); + + auto result = IPMatcher::create(ranges, IPMatcher::Type::ConnectionRemote); + EXPECT_TRUE(result.ok()); + EXPECT_NE(result.value(), nullptr); + + // Test that the created matcher works + NiceMock conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + + // Test IPv4 match + auto addr = Envoy::Network::Utility::parseInternetAddressNoThrow("192.168.1.100", 123, false); + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(addr); + EXPECT_TRUE(result.value()->matches(conn, headers, info)); + + // Test IPv4 no match + addr = Envoy::Network::Utility::parseInternetAddressNoThrow("172.16.1.1", 123, false); + conn.stream_info_.downstream_connection_info_provider_->setRemoteAddress(addr); + EXPECT_FALSE(result.value()->matches(conn, headers, info)); +} + +// Tests for kDestinationIp case in Permission matcher creation. +TEST(Matcher, CreatePermissionDestinationIp) { + envoy::config::rbac::v3::Permission permission; + auto* cidr = permission.mutable_destination_ip(); + cidr->set_address_prefix("192.168.1.0"); + cidr->mutable_prefix_len()->set_value(24); + + NiceMock validation_visitor; + NiceMock context; + + auto matcher = Matcher::create(permission, validation_visitor, context); + EXPECT_NE(matcher, nullptr); +} + +// Tests error handling in kDestinationIp case with invalid CIDR. +TEST(Matcher, CreatePermissionDestinationIpInvalidCidr) { + envoy::config::rbac::v3::Permission permission; + auto* cidr = permission.mutable_destination_ip(); + cidr->set_address_prefix("invalid.ip.address"); + cidr->mutable_prefix_len()->set_value(24); + + NiceMock validation_visitor; + NiceMock context; + + EXPECT_THROW_WITH_REGEX(Matcher::create(permission, validation_visitor, context), EnvoyException, + "Failed to create CIDR range:.*malformed IP address"); +} + +// Tests for RULE_NOT_SET case that falls through to PANIC. +TEST(Matcher, CreatePermissionRuleNotSet) { + EXPECT_DEATH( + { + envoy::config::rbac::v3::Permission permission; + + NiceMock validation_visitor; + NiceMock context; + + Matcher::create(permission, validation_visitor, context); + }, + "panic: corrupted enum"); +} + +// Tests for kSourceIp case in Principal matcher creation. +TEST(Matcher, CreatePrincipalSourceIp) { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_source_ip(); + cidr->set_address_prefix("10.0.0.0"); + cidr->mutable_prefix_len()->set_value(16); + + NiceMock context; + + auto matcher = Matcher::create(principal, context); + EXPECT_NE(matcher, nullptr); +} + +// Tests error handling in kSourceIp case with invalid CIDR. +TEST(Matcher, CreatePrincipalSourceIpInvalidCidr) { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_source_ip(); + cidr->set_address_prefix("999.999.999.999"); + cidr->mutable_prefix_len()->set_value(24); + + NiceMock context; + + EXPECT_THROW_WITH_REGEX(Matcher::create(principal, context), EnvoyException, + "Failed to create CIDR range:.*malformed IP address"); +} + +// Tests for kDirectRemoteIp case in Principal matcher creation. +TEST(Matcher, CreatePrincipalDirectRemoteIp) { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_direct_remote_ip(); + cidr->set_address_prefix("172.16.0.0"); + cidr->mutable_prefix_len()->set_value(12); + + NiceMock context; + + auto matcher = Matcher::create(principal, context); + EXPECT_NE(matcher, nullptr); +} + +// Tests error handling in kDirectRemoteIp case with invalid CIDR. +TEST(Matcher, CreatePrincipalDirectRemoteIpInvalidCidr) { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_direct_remote_ip(); + cidr->set_address_prefix(""); // Empty IP address + cidr->mutable_prefix_len()->set_value(24); + + NiceMock context; + + EXPECT_THROW_WITH_REGEX(Matcher::create(principal, context), EnvoyException, + "Failed to create CIDR range:.*malformed IP address"); +} + +// Tests for kRemoteIp case in Principal matcher creation. +TEST(Matcher, CreatePrincipalRemoteIp) { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_remote_ip(); + cidr->set_address_prefix("2001:db8::"); + cidr->mutable_prefix_len()->set_value(32); + + NiceMock context; + + auto matcher = Matcher::create(principal, context); + EXPECT_NE(matcher, nullptr); +} + +// Tests error handling in kRemoteIp case with invalid CIDR. +TEST(Matcher, CreatePrincipalRemoteIpInvalidCidr) { + envoy::config::rbac::v3::Principal principal; + auto* cidr = principal.mutable_remote_ip(); + cidr->set_address_prefix("2001:db8::gggg"); // Invalid IPv6 + cidr->mutable_prefix_len()->set_value(32); + + NiceMock context; + + EXPECT_THROW_WITH_REGEX(Matcher::create(principal, context), EnvoyException, + "Failed to create CIDR range:.*malformed IP address"); +} + +// Tests for IDENTIFIER_NOT_SET case that falls through to PANIC. +TEST(Matcher, CreatePrincipalIdentifierNotSet) { + EXPECT_DEATH( + { + envoy::config::rbac::v3::Principal principal; + + NiceMock context; + + Matcher::create(principal, context); + }, + "panic: corrupted enum"); +} + } // namespace } // namespace RBAC } // namespace Common diff --git a/test/extensions/filters/http/alternate_protocols_cache/filter_integration_test.cc b/test/extensions/filters/http/alternate_protocols_cache/filter_integration_test.cc index 8868a34c87e07..2c7cff266f837 100644 --- a/test/extensions/filters/http/alternate_protocols_cache/filter_integration_test.cc +++ b/test/extensions/filters/http/alternate_protocols_cache/filter_integration_test.cc @@ -428,7 +428,7 @@ TEST_P(FilterIntegrationTest, H3PostHandshakeFailoverToTcp) { [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) { auto* route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); - route->mutable_per_request_buffer_limit_bytes()->set_value(4096); + route->mutable_request_body_buffer_limit()->set_value(4096); }); initialize(); diff --git a/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc index 3b65b0042ce02..eb4a19c26a9b8 100644 --- a/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc +++ b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc @@ -67,7 +67,7 @@ class AwsLambdaFilterTest : public ::testing::Test { } void setupClusterMetadata() { - ProtobufWkt::Struct cluster_metadata; + Protobuf::Struct cluster_metadata; TestUtility::loadFromYaml(metadata_yaml_, cluster_metadata); metadata_.mutable_filter_metadata()->insert({"com.amazonaws.lambda", cluster_metadata}); ON_CALL(*decoder_callbacks_.cluster_info_, metadata()).WillByDefault(ReturnRef(metadata_)); @@ -204,7 +204,7 @@ TEST_F(AwsLambdaFilterTest, PerRouteConfigWrongClusterMetadata) { setupDownstreamFilter(InvocationMode::Synchronous, true /*passthrough*/, ""); - ProtobufWkt::Struct cluster_metadata; + Protobuf::Struct cluster_metadata; envoy::config::core::v3::Metadata metadata; TestUtility::loadFromYaml(metadata_yaml, cluster_metadata); metadata.mutable_filter_metadata()->insert({"WrongMetadataKey", cluster_metadata}); diff --git a/test/extensions/filters/http/aws_request_signing/BUILD b/test/extensions/filters/http/aws_request_signing/BUILD index b1d387a3b94a3..181e778ed0608 100644 --- a/test/extensions/filters/http/aws_request_signing/BUILD +++ b/test/extensions/filters/http/aws_request_signing/BUILD @@ -41,9 +41,10 @@ envoy_extension_cc_test( extension_names = ["envoy.filters.http.aws_request_signing"], rbe_pool = "6gig", deps = [ - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/filters/http/aws_request_signing:aws_request_signing_filter_lib", "//source/extensions/filters/http/aws_request_signing:config", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/extensions/common/aws:aws_mocks", "//test/integration:http_integration_lib", "//test/test_common:utility_lib", diff --git a/test/extensions/filters/http/aws_request_signing/aws_request_signing_integration_test.cc b/test/extensions/filters/http/aws_request_signing/aws_request_signing_integration_test.cc index b8501229cb8b2..da5147d3fec94 100644 --- a/test/extensions/filters/http/aws_request_signing/aws_request_signing_integration_test.cc +++ b/test/extensions/filters/http/aws_request_signing/aws_request_signing_integration_test.cc @@ -1,7 +1,10 @@ #include "envoy/extensions/filters/http/aws_request_signing/v3/aws_request_signing.pb.h" #include "envoy/upstream/load_balancer.h" -#include "source/extensions/clusters/logical_dns/logical_dns_cluster.h" +#include "source/common/common/logger.h" +#include "source/common/upstream/cluster_factory_impl.h" +#include "source/extensions/clusters/dns/dns_cluster.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/integration/http_integration.h" #include "test/test_common/registry.h" @@ -228,6 +231,8 @@ class AwsRequestSigningIntegrationTest : public testing::TestWithParamset_name("envoy.network.dns_resolver.getaddrinfo"); + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + config; + typed_dns_resolver_config->mutable_typed_config()->PackFrom(config); + }); + } + protected: bool downstream_filter_ = true; }; @@ -359,7 +374,7 @@ TEST_P(AwsRequestSigningIntegrationTest, SigV4AIntegrationUpstream) { EXPECT_FALSE( upstream_request_->headers().get(Http::LowerCaseString("x-amz-content-sha256")).empty()); } -class MockLogicalDnsClusterFactory : public Upstream::LogicalDnsClusterFactory { +class MockLogicalDnsClusterFactory : public Upstream::DnsClusterFactory { public: MockLogicalDnsClusterFactory() = default; ~MockLogicalDnsClusterFactory() override = default; @@ -384,7 +399,7 @@ class InitializeFilterTest : public ::testing::Test, public HttpIntegrationTest use_lds_ = false; } NiceMock logical_dns_cluster_factory_; - Registry::InjectFactory dns_cluster_factory_; + Registry::InjectFactory dns_cluster_factory_; NiceMock dns_resolver_factory_; Registry::InjectFactory registered_dns_factory_; @@ -865,7 +880,7 @@ class CdsInteractionTest : public testing::Test, public HttpIntegrationTest { } NiceMock logical_dns_cluster_factory_; - Registry::InjectFactory dns_cluster_factory_; + Registry::InjectFactory dns_cluster_factory_; NiceMock dns_resolver_factory_; Registry::InjectFactory registered_dns_factory_; diff --git a/test/extensions/filters/http/cache/cache_filter_integration_test.cc b/test/extensions/filters/http/cache/cache_filter_integration_test.cc index 34e318bdd4ccd..35873ec3c1553 100644 --- a/test/extensions/filters/http/cache/cache_filter_integration_test.cc +++ b/test/extensions/filters/http/cache/cache_filter_integration_test.cc @@ -150,8 +150,7 @@ TEST_P(CacheIntegrationTest, MissInsertHit) { sendHeaderOnlyRequestAwaitResponse(request_headers, serveFromCache()); EXPECT_THAT(response_decoder->headers(), IsSupersetOfHeaders(response_headers)); EXPECT_EQ(response_decoder->body(), response_body); - EXPECT_THAT(response_decoder->headers(), - HeaderHasValueRef(Http::CustomHeaders::get().Age, "10")); + EXPECT_THAT(response_decoder->headers(), ContainsHeader(Http::CustomHeaders::get().Age, "10")); // Advance time to force a log flush. simTime().advanceTimeWait(Seconds(1)); EXPECT_THAT(waitForAccessLog(access_log_name_, 1), @@ -222,8 +221,7 @@ TEST_P(CacheIntegrationTest, ExpiredValidated) { { IntegrationStreamDecoderPtr response_decoder = sendHeaderOnlyRequestAwaitResponse(request_headers, serveFromCache()); - EXPECT_THAT(response_decoder->headers(), - HeaderHasValueRef(Http::CustomHeaders::get().Age, "1")); + EXPECT_THAT(response_decoder->headers(), ContainsHeader(Http::CustomHeaders::get().Age, "1")); // Advance time to force a log flush. simTime().advanceTimeWait(Seconds(1)); @@ -355,8 +353,7 @@ TEST_P(CacheIntegrationTest, GetRequestWithResponseTrailers) { IntegrationStreamDecoderPtr response_decoder = sendHeaderOnlyRequestAwaitResponse(request_headers, serveFromCache()); EXPECT_THAT(response_decoder->headers(), IsSupersetOfHeaders(response_headers)); - EXPECT_THAT(response_decoder->headers(), - HeaderHasValueRef(Http::CustomHeaders::get().Age, "10")); + EXPECT_THAT(response_decoder->headers(), ContainsHeader(Http::CustomHeaders::get().Age, "10")); EXPECT_EQ(response_decoder->body(), response_body); ASSERT_TRUE(response_decoder->trailers() != nullptr); simTime().advanceTimeWait(Seconds(1)); @@ -434,8 +431,7 @@ TEST_P(CacheIntegrationTest, ServeHeadFromCacheAfterGetRequest) { sendHeaderOnlyRequestAwaitResponse(request_headers, serveFromCache()); EXPECT_THAT(response_decoder->headers(), IsSupersetOfHeaders(response_headers)); EXPECT_EQ(response_decoder->body().size(), 0); - EXPECT_THAT(response_decoder->headers(), - HeaderHasValueRef(Http::CustomHeaders::get().Age, "10")); + EXPECT_THAT(response_decoder->headers(), ContainsHeader(Http::CustomHeaders::get().Age, "10")); // Advance time to force a log flush. simTime().advanceTimeWait(Seconds(1)); EXPECT_THAT(waitForAccessLog(access_log_name_, 1), diff --git a/test/extensions/filters/http/cache/cache_filter_test.cc b/test/extensions/filters/http/cache/cache_filter_test.cc index fad814b0f575e..9b76553dc6e86 100644 --- a/test/extensions/filters/http/cache/cache_filter_test.cc +++ b/test/extensions/filters/http/cache/cache_filter_test.cc @@ -223,11 +223,10 @@ class CacheFilterTest : public ::testing::Test { void testDecodeRequestHitNoBody(CacheFilterSharedPtr filter) { // The filter should encode cached headers. - EXPECT_CALL( - decoder_callbacks_, - encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), - HeaderHasValueRef(Http::CustomHeaders::get().Age, age)), - true)); + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + ContainsHeader(Http::CustomHeaders::get().Age, age)), + true)); // The filter should not encode any data as the response has no body. EXPECT_CALL(decoder_callbacks_, encodeData).Times(0); @@ -250,11 +249,10 @@ class CacheFilterTest : public ::testing::Test { void testDecodeRequestHitWithBody(CacheFilterSharedPtr filter, std::string body) { // The filter should encode cached headers. - EXPECT_CALL( - decoder_callbacks_, - encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), - HeaderHasValueRef(Http::CustomHeaders::get().Age, age)), - false)); + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + ContainsHeader(Http::CustomHeaders::get().Age, age)), + false)); // The filter should encode cached data. EXPECT_CALL( @@ -1101,7 +1099,7 @@ TEST_F(CacheFilterTest, UnsuccessfulValidation) { receiveUpstreamBody(1, new_body, true); // The response headers should have the new status. - EXPECT_THAT(response_headers_, HeaderHasValueRef(Http::Headers::get().Status, "204")); + EXPECT_THAT(response_headers_, ContainsHeader(Http::Headers::get().Status, "204")); // The filter should not encode any data. EXPECT_CALL(encoder_callbacks_, addEncodedData).Times(0); @@ -1136,11 +1134,10 @@ TEST_F(CacheFilterTest, SingleSatisfiableRange) { CacheFilterSharedPtr filter = makeFilter(simple_cache_); // Decode request 2 header - EXPECT_CALL( - decoder_callbacks_, - encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), - HeaderHasValueRef(Http::CustomHeaders::get().Age, age)), - false)); + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + ContainsHeader(Http::CustomHeaders::get().Age, age)), + false)); EXPECT_CALL( decoder_callbacks_, @@ -1177,11 +1174,10 @@ TEST_F(CacheFilterTest, MultipleSatisfiableRanges) { CacheFilterSharedPtr filter = makeFilter(simple_cache_); // Decode request 2 header - EXPECT_CALL( - decoder_callbacks_, - encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), - HeaderHasValueRef(Http::CustomHeaders::get().Age, age)), - false)); + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + ContainsHeader(Http::CustomHeaders::get().Age, age)), + false)); EXPECT_CALL( decoder_callbacks_, @@ -1220,11 +1216,10 @@ TEST_F(CacheFilterTest, NotSatisfiableRange) { CacheFilterSharedPtr filter = makeFilter(simple_cache_); // Decode request 2 header - EXPECT_CALL( - decoder_callbacks_, - encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), - HeaderHasValueRef(Http::CustomHeaders::get().Age, age)), - true)); + EXPECT_CALL(decoder_callbacks_, + encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_), + ContainsHeader(Http::CustomHeaders::get().Age, age)), + true)); // 416 response should not have a body, so we don't expect a call to encodeData EXPECT_CALL(decoder_callbacks_, diff --git a/test/extensions/filters/http/cache/http_cache_test.cc b/test/extensions/filters/http/cache/http_cache_test.cc index 13de60b4011c2..bf2cb09db7389 100644 --- a/test/extensions/filters/http/cache/http_cache_test.cc +++ b/test/extensions/filters/http/cache/http_cache_test.cc @@ -199,7 +199,7 @@ TEST_P(LookupRequestTest, ResultWithoutBodyMatchesExpectation) { ASSERT_TRUE(lookup_response.headers_); EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); EXPECT_THAT(*lookup_response.headers_, - HeaderHasValueRef(Http::CustomHeaders::get().Age, GetParam().expected_age)); + ContainsHeader(Http::CustomHeaders::get().Age, GetParam().expected_age)); EXPECT_EQ(lookup_response.content_length_, 0); } @@ -217,7 +217,7 @@ TEST_P(LookupRequestTest, ResultWithUnknownContentLengthMatchesExpectation) { ASSERT_TRUE(lookup_response.headers_); EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); EXPECT_THAT(*lookup_response.headers_, - HeaderHasValueRef(Http::CustomHeaders::get().Age, GetParam().expected_age)); + ContainsHeader(Http::CustomHeaders::get().Age, GetParam().expected_age)); EXPECT_FALSE(lookup_response.content_length_.has_value()); } @@ -237,7 +237,7 @@ TEST_P(LookupRequestTest, ResultWithBodyMatchesExpectation) { ASSERT_TRUE(lookup_response.headers_); EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); EXPECT_THAT(*lookup_response.headers_, - HeaderHasValueRef(Http::CustomHeaders::get().Age, GetParam().expected_age)); + ContainsHeader(Http::CustomHeaders::get().Age, GetParam().expected_age)); EXPECT_EQ(lookup_response.content_length_, content_length); } @@ -357,7 +357,7 @@ TEST_P(LookupRequestTest, ResultWithBodyAndTrailersMatchesExpectation) { EXPECT_THAT(*lookup_response.headers_, Http::IsSupersetOfHeaders(response_headers)); // Age is populated in LookupRequest::makeLookupResult, which is called in makeLookupResult. EXPECT_THAT(*lookup_response.headers_, - HeaderHasValueRef(Http::CustomHeaders::get().Age, GetParam().expected_age)); + ContainsHeader(Http::CustomHeaders::get().Age, GetParam().expected_age)); EXPECT_EQ(lookup_response.content_length_, content_length); } diff --git a/test/extensions/filters/http/common/empty_http_filter_config.h b/test/extensions/filters/http/common/empty_http_filter_config.h index d78f9ec268ac6..43508cde792a1 100644 --- a/test/extensions/filters/http/common/empty_http_filter_config.h +++ b/test/extensions/filters/http/common/empty_http_filter_config.h @@ -30,7 +30,7 @@ class EmptyHttpFilterConfig : public Server::Configuration::NamedHttpFilterConfi ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom filter config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::set configTypes() override { return {}; } diff --git a/test/extensions/filters/http/common/fuzz/filter_corpus/clusterfuzz-testcase-minimized-filter_fuzz_test-5621461680455680 b/test/extensions/filters/http/common/fuzz/filter_corpus/clusterfuzz-testcase-minimized-filter_fuzz_test-5621461680455680 new file mode 100644 index 0000000000000..d817b33577852 --- /dev/null +++ b/test/extensions/filters/http/common/fuzz/filter_corpus/clusterfuzz-testcase-minimized-filter_fuzz_test-5621461680455680 @@ -0,0 +1,7 @@ +config { + name: "envoy.filters.http.health_check" + typed_config { + type_url: "type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck" + value: "\n\000\"\r\n\000\022\t\t\372\377\377\377\377\377\377\377" + } +} diff --git a/test/extensions/filters/http/common/fuzz/uber_per_filter.cc b/test/extensions/filters/http/common/fuzz/uber_per_filter.cc index 7890c93309338..40188b761fa80 100644 --- a/test/extensions/filters/http/common/fuzz/uber_per_filter.cc +++ b/test/extensions/filters/http/common/fuzz/uber_per_filter.cc @@ -93,7 +93,7 @@ void UberFilterFuzzer::guideAnyProtoType(test::fuzz::HttpData* mutable_data, uin "type.googleapis.com/google.protobuf.Empty", "type.googleapis.com/google.api.HttpBody", }; - ProtobufWkt::Any* mutable_any = mutable_data->mutable_proto_body()->mutable_message(); + Protobuf::Any* mutable_any = mutable_data->mutable_proto_body()->mutable_message(); const std::string& type_url = expected_types[choice % expected_types.size()]; mutable_any->set_type_url(type_url); } diff --git a/test/extensions/filters/http/common/jwks_fetcher_test.cc b/test/extensions/filters/http/common/jwks_fetcher_test.cc index 0209c5cc7631a..51eca1bbdde31 100644 --- a/test/extensions/filters/http/common/jwks_fetcher_test.cc +++ b/test/extensions/filters/http/common/jwks_fetcher_test.cc @@ -330,8 +330,7 @@ TEST_F(JwksFetcherTest, TestSchemeHeaderHttps) { .WillOnce(testing::Invoke( [](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks&, const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - EXPECT_THAT(message->headers(), - HeaderHasValueRef(Http::Headers::get().Scheme, "https")); + EXPECT_THAT(message->headers(), ContainsHeader(Http::Headers::get().Scheme, "https")); return nullptr; })); @@ -356,7 +355,7 @@ TEST_F(JwksFetcherTest, TestSchemeHeaderHttp) { .WillOnce(testing::Invoke( [](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks&, const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - EXPECT_THAT(message->headers(), HeaderHasValueRef(Http::Headers::get().Scheme, "http")); + EXPECT_THAT(message->headers(), ContainsHeader(Http::Headers::get().Scheme, "http")); return nullptr; })); diff --git a/test/extensions/filters/http/composite/BUILD b/test/extensions/filters/http/composite/BUILD index 98b3de4f8eea3..6fa15687520a9 100644 --- a/test/extensions/filters/http/composite/BUILD +++ b/test/extensions/filters/http/composite/BUILD @@ -36,6 +36,7 @@ envoy_extension_cc_test( srcs = ["composite_filter_integration_test.cc"], extension_names = ["envoy.filters.http.composite"], rbe_pool = "6gig", + shard_count = 6, deps = [ "//source/common/http:header_map_lib", "//source/extensions/filters/http/composite:config", @@ -44,6 +45,7 @@ envoy_extension_cc_test( "//test/common/grpc:grpc_client_integration_lib", "//test/common/http:common_lib", "//test/integration:http_integration_lib", + "//test/integration/filters:local_reply_during_encoding_filter_lib", "//test/integration/filters:server_factory_context_filter_config_proto_cc_proto", "//test/integration/filters:server_factory_context_filter_lib", "//test/integration/filters:set_response_code_filter_config_proto_cc_proto", diff --git a/test/extensions/filters/http/composite/composite_filter_integration_test.cc b/test/extensions/filters/http/composite/composite_filter_integration_test.cc index 3aedb41e314b3..952046adbbf1b 100644 --- a/test/extensions/filters/http/composite/composite_filter_integration_test.cc +++ b/test/extensions/filters/http/composite/composite_filter_integration_test.cc @@ -177,6 +177,51 @@ class CompositeFilterIntegrationTest : public testing::TestWithParammakeRequestWithBody(match_request_headers_, 1024); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_THAT(response->headers(), Http::HttpStatusIs("200")); + } + + { + auto response = codec_client_->makeRequestWithBody(match_request_headers_, 1024); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(match_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_THAT(response->headers(), + Http::HttpStatusIs("500")); // local-reply-during-encode sets 500. + } +} + // Verifies that if we don't match the match action the request is proxied as normal, while if the // match action is hit we apply the specified dynamic filter to the stream. TEST_P(CompositeFilterIntegrationTest, TestBasicDynamicFilter) { diff --git a/test/extensions/filters/http/composite/filter_test.cc b/test/extensions/filters/http/composite/filter_test.cc index d37bd319eae93..e8dfee81054e6 100644 --- a/test/extensions/filters/http/composite/filter_test.cc +++ b/test/extensions/filters/http/composite/filter_test.cc @@ -80,12 +80,14 @@ class CompositeFilterTest : public ::testing::Test { void expectFilterStateInfo(std::shared_ptr filter_state) { auto* info = filter_state->getDataMutable(MatchedActionsFilterStateKey); EXPECT_NE(nullptr, info); - ProtobufWkt::Struct expected; + Protobuf::Struct expected; auto& fields = *expected.mutable_fields(); fields["rootFilterName"] = ValueUtil::stringValue("actionName"); EXPECT_TRUE(MessageDifferencer::Equals(expected, *(info->serializeAsProto()))); } + testing::NiceMock context_; + testing::NiceMock decoder_callbacks_; testing::NiceMock encoder_callbacks_; Stats::MockCounter error_counter_; @@ -114,12 +116,13 @@ TEST_F(FilterTest, StreamEncoderFilterDelegation) { ON_CALL(encoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamEncoderFilter(stream_filter); }; EXPECT_CALL(*stream_filter, setEncoderFilterCallbacks(_)); - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(success_counter_, inc()); filter_.onMatchCallback(action); @@ -143,12 +146,13 @@ TEST_F(FilterTest, StreamDecoderFilterDelegation) { ON_CALL(decoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamDecoderFilter(stream_filter); }; EXPECT_CALL(*stream_filter, setDecoderFilterCallbacks(_)); - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(success_counter_, inc()); filter_.onMatchCallback(action); @@ -171,14 +175,15 @@ TEST_F(FilterTest, StreamFilterDelegation) { ON_CALL(decoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamFilter(stream_filter); }; EXPECT_CALL(*stream_filter, setDecoderFilterCallbacks(_)); EXPECT_CALL(*stream_filter, setEncoderFilterCallbacks(_)); EXPECT_CALL(success_counter_, inc()); - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); filter_.onMatchCallback(action); expectFilterStateInfo(filter_state); @@ -200,12 +205,13 @@ TEST_F(FilterTest, StreamFilterDelegationMultipleStreamFilters) { ON_CALL(decoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamFilter(stream_filter); cb.addStreamFilter(stream_filter); }; - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(error_counter_, inc()); filter_.onMatchCallback(action); @@ -225,12 +231,13 @@ TEST_F(FilterTest, StreamFilterDelegationMultipleStreamDecoderFilters) { ON_CALL(decoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamDecoderFilter(decoder_filter); cb.addStreamDecoderFilter(decoder_filter); }; - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(error_counter_, inc()); filter_.onMatchCallback(action); @@ -250,12 +257,13 @@ TEST_F(FilterTest, StreamFilterDelegationMultipleStreamEncoderFilters) { ON_CALL(encoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamEncoderFilter(encode_filter); cb.addStreamEncoderFilter(encode_filter); }; - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(error_counter_, inc()); filter_.onMatchCallback(action); @@ -278,13 +286,14 @@ TEST_F(FilterTest, StreamFilterDelegationMultipleAccessLoggers) { ON_CALL(encoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamEncoderFilter(encode_filter); cb.addAccessLogHandler(access_log_1); cb.addAccessLogHandler(access_log_2); }; - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(*encode_filter, setEncoderFilterCallbacks(_)); EXPECT_CALL(success_counter_, inc()); filter_.onMatchCallback(action); @@ -340,8 +349,7 @@ TEST(ConfigTest, TestConfig) { .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "Error: Only one of `dynamic_config` or `typed_config` can be set."); } } @@ -371,8 +379,7 @@ TEST(ConfigTest, TestDynamicConfigInDownstream) { .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "Failed to get downstream factory context or server factory context."); } @@ -400,8 +407,7 @@ TEST(ConfigTest, TestDynamicConfigInUpstream) { .server_factory_context_ = absl::nullopt}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "Failed to get upstream factory context or server factory context."); } @@ -428,8 +434,7 @@ TEST(ConfigTest, CreateFilterFromServerContextDual) { .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "DualFactoryBase: creating filter factory from server factory context is not supported"); } @@ -456,8 +461,7 @@ TEST(ConfigTest, DualFilterNoUpstreamFactoryContext) { .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "Failed to get upstream filter factory creation function"); } @@ -482,8 +486,7 @@ TEST(ConfigTest, DownstreamFilterNoFactoryContext) { .server_factory_context_ = absl::nullopt}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "Failed to get downstream filter factory creation function"); } @@ -510,8 +513,7 @@ TEST(ConfigTest, TestDownstreamFilterNoOverridingServerContext) { .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; EXPECT_THROW_WITH_MESSAGE( - factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor()), + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()), EnvoyException, "Creating filter factory from server factory context is not supported"); } @@ -526,12 +528,23 @@ TEST(ConfigTest, TestSamplePercentNotSpecifiedl) { http_status: 503 )EOF"; - testing::NiceMock server_factory_context; - NiceMock& runtime = server_factory_context.runtime_loader_; envoy::extensions::filters::http::composite::v3::ExecuteFilterAction config; TestUtility::loadFromYaml(yaml_string, config); + + testing::NiceMock server_factory_context; + testing::NiceMock factory_context; + testing::NiceMock upstream_factory_context; + Envoy::Http::Matching::HttpFilterActionContext action_context{ + .is_downstream_ = true, + .stat_prefix_ = "test", + .factory_context_ = absl::nullopt, + .upstream_factory_context_ = absl::nullopt, + .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; - EXPECT_TRUE(factory.isSampled(config, runtime)); + auto action = + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()); + + EXPECT_FALSE(action->getTyped().actionSkip()); } // Config test to check if sample_percent config is in place and feature enabled. @@ -549,15 +562,27 @@ TEST(ConfigTest, TestSamplePercentInPlaceFeatureEnabled) { denominator: HUNDRED )EOF"; - testing::NiceMock server_factory_context; - NiceMock& runtime = server_factory_context.runtime_loader_; envoy::extensions::filters::http::composite::v3::ExecuteFilterAction config; TestUtility::loadFromYaml(yaml_string, config); + + testing::NiceMock server_factory_context; + testing::NiceMock factory_context; + testing::NiceMock upstream_factory_context; + Envoy::Http::Matching::HttpFilterActionContext action_context{ + .is_downstream_ = true, + .stat_prefix_ = "test", + .factory_context_ = absl::nullopt, + .upstream_factory_context_ = absl::nullopt, + .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; - EXPECT_CALL(runtime.snapshot_, + auto action = + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()); + + EXPECT_CALL(server_factory_context.runtime_loader_.snapshot_, featureEnabled(_, testing::A())) .WillOnce(testing::Return(true)); - EXPECT_TRUE(factory.isSampled(config, runtime)); + + EXPECT_FALSE(action->getTyped().actionSkip()); } // Config test to check if sample_percent config is in place and feature not enabled. @@ -570,18 +595,32 @@ TEST(ConfigTest, TestSamplePercentInPlaceFeatureNotEnabled) { abort: http_status: 503 sample_percent: - runtime_key: + default_value: + numerator: 30 + denominator: HUNDRED )EOF"; - testing::NiceMock server_factory_context; - NiceMock& runtime = server_factory_context.runtime_loader_; envoy::extensions::filters::http::composite::v3::ExecuteFilterAction config; TestUtility::loadFromYaml(yaml_string, config); + + testing::NiceMock server_factory_context; + testing::NiceMock factory_context; + testing::NiceMock upstream_factory_context; + Envoy::Http::Matching::HttpFilterActionContext action_context{ + .is_downstream_ = true, + .stat_prefix_ = "test", + .factory_context_ = absl::nullopt, + .upstream_factory_context_ = absl::nullopt, + .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; - EXPECT_CALL(runtime.snapshot_, + auto action = + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()); + + EXPECT_CALL(server_factory_context.runtime_loader_.snapshot_, featureEnabled(_, testing::A())) .WillOnce(testing::Return(false)); - EXPECT_FALSE(factory.isSampled(config, runtime)); + + EXPECT_TRUE(action->getTyped().actionSkip()); } TEST_F(FilterTest, FilterStateShouldBeUpdatedWithTheMatchingActionForDynamicConfig) { @@ -608,8 +647,8 @@ TEST_F(FilterTest, FilterStateShouldBeUpdatedWithTheMatchingActionForDynamicConf .upstream_factory_context_ = upstream_factory_context, .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; - auto action = factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor())(); + auto action = + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()); EXPECT_EQ("actionName", action->getTyped().actionName()); } @@ -639,8 +678,8 @@ TEST_F(FilterTest, FilterStateShouldBeUpdatedWithTheMatchingActionForTypedConfig .upstream_factory_context_ = upstream_factory_context, .server_factory_context_ = server_factory_context}; ExecuteFilterActionFactory factory; - auto action = factory.createActionFactoryCb(config, action_context, - ProtobufMessage::getStrictValidationVisitor())(); + auto action = + factory.createAction(config, action_context, ProtobufMessage::getStrictValidationVisitor()); EXPECT_EQ("actionName", action->getTyped().actionName()); } @@ -658,12 +697,13 @@ TEST_F(FilterTest, FilterStateShouldBeUpdatedWithTheMatchingAction) { StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamEncoderFilter(stream_filter); }; EXPECT_CALL(*stream_filter, setEncoderFilterCallbacks(_)); - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(success_counter_, inc()); filter_.onMatchCallback(action); @@ -688,18 +728,19 @@ TEST_F(FilterTest, MatchingActionShouldNotCollitionWithOtherRootFilter) { StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::FilterChain); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamEncoderFilter(stream_filter); }; EXPECT_CALL(*stream_filter, setEncoderFilterCallbacks(_)); - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(success_counter_, inc()); filter_.onMatchCallback(action); auto* info = filter_state->getDataMutable(MatchedActionsFilterStateKey); EXPECT_NE(nullptr, info); - ProtobufWkt::Struct expected; + Protobuf::Struct expected; auto& fields = *expected.mutable_fields(); fields["otherRootFilterName"] = ValueUtil::stringValue("anyActionName"); fields["rootFilterName"] = ValueUtil::stringValue("actionName"); @@ -724,12 +765,13 @@ TEST_F(UpstreamFilterTest, StreamEncoderFilterDelegationUpstream) { ON_CALL(encoder_callbacks_.stream_info_, filterState()) .WillByDefault(testing::ReturnRef(filter_state)); - auto factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { + Http::FilterFactoryCb factory_callback = [&](Http::FilterChainFactoryCallbacks& cb) { cb.addStreamEncoderFilter(stream_filter); }; EXPECT_CALL(*stream_filter, setEncoderFilterCallbacks(_)); - ExecuteFilterAction action(factory_callback, "actionName"); + ExecuteFilterAction action([&]() -> OptRef { return factory_callback; }, + "actionName", absl::nullopt, context_.runtime_loader_); EXPECT_CALL(success_counter_, inc()); filter_.onMatchCallback(action); diff --git a/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc b/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc index 07d61d93dafa7..555b13e28d1d2 100644 --- a/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc +++ b/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc @@ -12,8 +12,8 @@ namespace Envoy { +using Envoy::Protobuf::Any; using Envoy::Protobuf::MapPair; -using Envoy::ProtobufWkt::Any; class CompressorIntegrationTest : public testing::TestWithParam, public Event::SimulatedTimeSystem, diff --git a/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_filter_test.cc b/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_filter_test.cc index 2c99607397ca9..bb347b686b989 100644 --- a/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_filter_test.cc +++ b/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_filter_test.cc @@ -27,7 +27,7 @@ class ConnectGrpcBridgeFilterTest : public testing::Test { } bool jsonEqual(const std::string& expected, const std::string& actual) { - ProtobufWkt::Value expected_value, actual_value; + Protobuf::Value expected_value, actual_value; TestUtility::loadFromJson(expected, expected_value); TestUtility::loadFromJson(actual, actual_value); return TestUtility::protoEqual(expected_value, actual_value); @@ -41,7 +41,7 @@ class ConnectGrpcBridgeFilterTest : public testing::Test { } void addStatusDetails(google::rpc::Status& status, const Protobuf::Message& message) { - ProtobufWkt::Any any; + Protobuf::Any any; any.PackFrom(message); *status.add_details() = any; } diff --git a/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_integration_test.cc b/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_integration_test.cc index 8e0f5fbae602a..1a434d4deb3ec 100644 --- a/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_integration_test.cc +++ b/test/extensions/filters/http/connect_grpc_bridge/connect_grpc_bridge_integration_test.cc @@ -100,11 +100,11 @@ TEST_P(ConnectIntegrationTest, ConnectFilterUnaryRequestE2E) { EXPECT_THAT(grpc_request, ProtoEq(connect_request)); EXPECT_THAT(upstream_request_->headers(), - HeaderHasValueRef(Http::Headers::get().ContentType, "application/grpc+proto")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc+proto")); EXPECT_THAT(upstream_request_->headers(), - HeaderHasValueRef(Http::Headers::get().Path, "/Service/Method")); + ContainsHeader(Http::Headers::get().Path, "/Service/Method")); EXPECT_THAT(upstream_request_->headers(), - HeaderHasValueRef(Http::CustomHeaders::get().GrpcTimeout, "10000m")); + ContainsHeader(Http::CustomHeaders::get().GrpcTimeout, "10000m")); helloworld::HelloReply grpc_response; grpc_response.set_message("success"); @@ -119,7 +119,7 @@ TEST_P(ConnectIntegrationTest, ConnectFilterUnaryRequestE2E) { EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), Http::HttpStatusIs("200")); EXPECT_THAT(response->headers(), - HeaderHasValueRef(Http::Headers::get().ContentType, "application/proto")); + ContainsHeader(Http::Headers::get().ContentType, "application/proto")); helloworld::HelloReply connect_response; ASSERT_TRUE(connect_response.ParseFromString(response->body())); @@ -150,11 +150,11 @@ TEST_P(ConnectIntegrationTest, ConnectFilterStreamingRequestE2E) { EXPECT_THAT(grpc_request, ProtoEq(connect_request)); EXPECT_THAT(upstream_request_->headers(), - HeaderHasValueRef(Http::Headers::get().ContentType, "application/grpc+proto")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc+proto")); EXPECT_THAT(upstream_request_->headers(), - HeaderHasValueRef(Http::Headers::get().Path, "/Service/Method")); + ContainsHeader(Http::Headers::get().Path, "/Service/Method")); EXPECT_THAT(upstream_request_->headers(), - HeaderHasValueRef(Http::CustomHeaders::get().GrpcTimeout, "10000m")); + ContainsHeader(Http::CustomHeaders::get().GrpcTimeout, "10000m")); helloworld::HelloReply grpc_response; grpc_response.set_message("success"); @@ -170,7 +170,7 @@ TEST_P(ConnectIntegrationTest, ConnectFilterStreamingRequestE2E) { EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), Http::HttpStatusIs("200")); EXPECT_THAT(response->headers(), - HeaderHasValueRef(Http::Headers::get().ContentType, "application/connect+proto")); + ContainsHeader(Http::Headers::get().ContentType, "application/connect+proto")); Buffer::OwnedImpl response_body{response->body()}; ASSERT_THAT(response_body.length(), testing::Gt(5)); diff --git a/test/extensions/filters/http/connect_grpc_bridge/end_stream_response_test.cc b/test/extensions/filters/http/connect_grpc_bridge/end_stream_response_test.cc index 5927c10680187..259e43316e0b1 100644 --- a/test/extensions/filters/http/connect_grpc_bridge/end_stream_response_test.cc +++ b/test/extensions/filters/http/connect_grpc_bridge/end_stream_response_test.cc @@ -18,7 +18,7 @@ using GrpcStatus = Grpc::Status::WellKnownGrpcStatus; class EndStreamResponseTest : public testing::Test { protected: void compareJson(const std::string& expected, const std::string& actual) { - ProtobufWkt::Value expected_value, actual_value; + Protobuf::Value expected_value, actual_value; TestUtility::loadFromJson(expected, expected_value); TestUtility::loadFromJson(actual, actual_value); EXPECT_TRUE(TestUtility::protoEqual(expected_value, actual_value)); @@ -43,7 +43,7 @@ TEST_F(EndStreamResponseTest, StatusCodeToConnectUnaryStatus) { } TEST_F(EndStreamResponseTest, SerializeJsonError) { - ProtobufWkt::Any detail; + Protobuf::Any detail; detail.set_type_url("type.url"); detail.set_value("protobuf"); const std::vector> test_set = { diff --git a/test/extensions/filters/http/custom_response/custom_response_integration_test.cc b/test/extensions/filters/http/custom_response/custom_response_integration_test.cc index 1991be48d2dce..45147af783731 100644 --- a/test/extensions/filters/http/custom_response/custom_response_integration_test.cc +++ b/test/extensions/filters/http/custom_response/custom_response_integration_test.cc @@ -24,8 +24,8 @@ using LocalResponsePolicyProto = using RedirectPolicyProto = envoy::extensions::http::custom_response::redirect_policy::v3::RedirectPolicy; using RedirectActionProto = envoy::config::route::v3::RedirectAction; +using Envoy::Protobuf::Any; using Envoy::Protobuf::MapPair; -using Envoy::ProtobufWkt::Any; namespace { diff --git a/test/extensions/filters/http/custom_response/utility.h b/test/extensions/filters/http/custom_response/utility.h index 63f23766b98ec..0f433fea5326a 100644 --- a/test/extensions/filters/http/custom_response/utility.h +++ b/test/extensions/filters/http/custom_response/utility.h @@ -268,7 +268,7 @@ class TestModifyRequestHeadersActionFactory ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom filter config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "modify-request-headers-action"; } diff --git a/test/extensions/filters/http/dynamic_forward_proxy/BUILD b/test/extensions/filters/http/dynamic_forward_proxy/BUILD index f5e2d9ac10372..2a0946b675689 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/BUILD +++ b/test/extensions/filters/http/dynamic_forward_proxy/BUILD @@ -66,6 +66,20 @@ envoy_cc_test_library( alwayslink = 1, ) +envoy_cc_test_library( + name = "modify_host_filter_lib", + srcs = ["modify_host_filter.cc"], + deps = [ + "//envoy/http:filter_interface", + "//envoy/registry", + "//envoy/server:filter_config_interface", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "//test/extensions/filters/http/common:empty_http_filter_config_lib", + "//test/integration/filters:common_lib", + ], +) + envoy_extension_cc_test( name = "proxy_filter_integration_test", size = "large", @@ -79,15 +93,17 @@ envoy_extension_cc_test( # https://gist.github.com/wrowe/a152cb1d12c2f751916122aed39d8517 tags = ["fails_on_clang_cl"], deps = [ + ":modify_host_filter_lib", ":test_resolver_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/dynamic_forward_proxy:cluster", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/filters/http/dynamic_forward_proxy:config", "//source/extensions/filters/http/set_filter_state:config", "//source/extensions/key_value/file_based:config_lib", "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:http_integration_lib", "//test/integration/filters:stream_info_to_headers_filter_lib", + "//test/test_common:test_runtime_lib", "//test/test_common:threadsafe_singleton_injector_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", @@ -113,15 +129,17 @@ envoy_extension_cc_test( # https://gist.github.com/wrowe/a152cb1d12c2f751916122aed39d8517 tags = ["fails_on_clang_cl"], deps = [ + ":modify_host_filter_lib", ":test_resolver_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/dynamic_forward_proxy:cluster", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/filters/http/dynamic_forward_proxy:config", "//source/extensions/filters/http/set_filter_state:config", "//source/extensions/key_value/file_based:config_lib", "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:http_integration_lib", "//test/integration/filters:stream_info_to_headers_filter_lib", + "//test/test_common:test_runtime_lib", "//test/test_common:threadsafe_singleton_injector_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/dynamic_forward_proxy/modify_host_filter.cc b/test/extensions/filters/http/dynamic_forward_proxy/modify_host_filter.cc new file mode 100644 index 0000000000000..c20815cfb7ae4 --- /dev/null +++ b/test/extensions/filters/http/dynamic_forward_proxy/modify_host_filter.cc @@ -0,0 +1,37 @@ +#include "envoy/http/filter.h" +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +#include "source/extensions/filters/http/common/factory_base.h" +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "test/integration/filters/common.h" + +namespace Envoy { + +class ModifyHostFilter : public Http::PassThroughFilter { +public: + ModifyHostFilter() = default; + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override { + headers.setHost("non-existing.foo.bar.bats.com"); + return Http::FilterHeadersStatus::Continue; + } +}; + +class ModifyHostFilterFactory : public Extensions::HttpFilters::Common::EmptyHttpDualFilterConfig { +public: + ModifyHostFilterFactory() : EmptyHttpDualFilterConfig("modify-host-filter") {} + absl::StatusOr + createDualFilter(const std::string&, Server::Configuration::ServerFactoryContext&) override { + return [](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared<::Envoy::ModifyHostFilter>()); + }; + } +}; + +// perform static registration +static Registry::RegisterFactory + register_; + +} // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc index f8d259e268936..ad7ae5dcf4d9a 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc +++ b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc @@ -12,6 +12,7 @@ #include "test/integration/http_integration.h" #include "test/integration/ssl_utility.h" #include "test/test_common/registry.h" +#include "test/test_common/test_runtime.h" #include "test/test_common/threadsafe_singleton_injector.h" using testing::HasSubstr; @@ -111,7 +112,8 @@ class ProxyFilterIntegrationTest : public testing::TestWithParam& prepend_custom_filter_config_yaml = absl::nullopt) { + const absl::optional& prepend_custom_filter_config_yaml = absl::nullopt, + bool use_dfp_even_when_cluster_resolves_hosts = false) { const std::string filter_use_sub_cluster = R"EOF( name: dynamic_forward_proxy typed_config: @@ -146,7 +148,8 @@ name: stream-info-to-headers-filter if (prepend_custom_filter_config_yaml.has_value()) { // Prepend DFP filter. - if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.dfp_cluster_resolves_hosts")) { + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.dfp_cluster_resolves_hosts") || + use_dfp_even_when_cluster_resolves_hosts) { config_helper_.prependFilter(use_sub_cluster ? filter_use_sub_cluster : filter_use_dns_cache); } else if (use_sub_cluster) { @@ -161,7 +164,8 @@ name: stream-info-to-headers-filter // Prepend stream_info_filter. config_helper_.prependFilter(stream_info_filter_config_str); } else { - if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.dfp_cluster_resolves_hosts")) { + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.dfp_cluster_resolves_hosts") || + use_dfp_even_when_cluster_resolves_hosts) { config_helper_.prependFilter(use_sub_cluster ? filter_use_sub_cluster : filter_use_dns_cache); } else if (use_sub_cluster) { @@ -1277,6 +1281,8 @@ TEST_P(ProxyFilterIntegrationTest, UseCacheFileAndTestHappyEyeballs) { #if defined(ENVOY_ENABLE_QUIC) TEST_P(ProxyFilterIntegrationTest, UseCacheFileAndHttp3) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.http3_happy_eyeballs", "true"}}); upstream_cert_name_ = ""; // Force standard TLS dns_hostname_ = "sni.lyft.com"; autonomous_upstream_ = true; @@ -1515,68 +1521,6 @@ TEST_P(ProxyFilterIntegrationTest, SubClusterReloadCluster) { test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 0); } -// Verify that we expire sub clusters and not remove on CDS. -TEST_P(ProxyFilterWithSimtimeIntegrationTest, RemoveViaTTLAndDFPUpdateWithoutAvoidCDSRemoval) { - const std::string cluster_yaml = R"EOF( - name: fake_cluster - connect_timeout: 0.250s - type: STATIC - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: fake_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: 127.0.0.1 - port_value: 11001 - )EOF"; - auto cluster = Upstream::parseClusterFromV3Yaml(cluster_yaml); - // make runtime guard false - config_helper_.addRuntimeOverride( - "envoy.reloadable_features.avoid_dfp_cluster_removal_on_cds_update", "false"); - initializeWithArgs(1024, 1024, "", typed_dns_resolver_config_, true); - codec_client_ = makeHttpConnection(lookupPort("http")); - const Http::TestRequestHeaderMapImpl request_headers{ - {":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", - fmt::format("localhost:{}", fake_upstreams_[0]->localAddress()->ip()->port())}}; - - auto response = - sendRequestAndWaitForResponse(request_headers, 1024, default_response_headers_, 1024); - checkSimpleRequestSuccess(1024, 1024, response.get()); - // one more cluster - test_server_->waitForCounterEq("cluster_manager.cluster_added", 2); - test_server_->waitForCounterEq("cluster_manager.cluster_removed", 0); - cleanupUpstreamAndDownstream(); - - // Sub cluster expected to be removed after ttl - // > 5m - simTime().advanceTimeWait(std::chrono::milliseconds(300001)); - test_server_->waitForCounterEq("cluster_manager.cluster_added", 2); - test_server_->waitForCounterEq("cluster_manager.cluster_removed", 1); - - codec_client_ = makeHttpConnection(lookupPort("http")); - response = sendRequestAndWaitForResponse(request_headers, 1024, default_response_headers_, 1024); - checkSimpleRequestSuccess(1024, 1024, response.get()); - - // sub cluster added again - test_server_->waitForCounterEq("cluster_manager.cluster_added", 3); - test_server_->waitForCounterEq("cluster_manager.cluster_removed", 1); - cleanupUpstreamAndDownstream(); - - // Make update to DFP cluster - cluster_.mutable_circuit_breakers()->add_thresholds()->mutable_max_connections()->set_value(100); - cds_helper_.setCds({cluster_}); - - // sub cluster removed due to dfp cluster update - test_server_->waitForCounterEq("cluster_manager.cluster_added", 3); - test_server_->waitForCounterEq("cluster_manager.cluster_removed", 2); -} - // Verify that we expire sub clusters. TEST_P(ProxyFilterWithSimtimeIntegrationTest, RemoveSubClusterViaTTL) { initializeWithArgs(1024, 1024, "", typed_dns_resolver_config_, true); @@ -1655,8 +1599,6 @@ TEST_P(ProxyFilterIntegrationTest, SubClusterWithIpHost) { // Verify that no DFP clusters are removed when CDS Reload is triggered. TEST_P(ProxyFilterIntegrationTest, CDSReloadNotRemoveDFPCluster) { - config_helper_.addRuntimeOverride( - "envoy.reloadable_features.avoid_dfp_cluster_removal_on_cds_update", "true"); const std::string cluster_yaml = R"EOF( name: fake_cluster connect_timeout: 0.250s @@ -1738,5 +1680,31 @@ TEST_P(ProxyFilterIntegrationTest, ResetStreamDuringDnsLookup) { EXPECT_EQ("504", response->headers().getStatusValue()); } +// This test validates that processing of DNS resolutions on worker threads is handled correctly. +// The test uses specific scenario where DFP filter AND async resolution in DFP cluster are enabled. +// Normally DFP filter is not needed, however this configuration can occur as the +// envoy.reloadable_features.dfp_cluster_resolves_hosts flag is now enabled by default. The test +// also requires the Host header to be modified between DFP and Router filters to trigger abnormal +// behavior in the DNS resolution processing loop. +TEST_P(ProxyFilterIntegrationTest, DoubleResolution) { + config_helper_.addRuntimeOverride("envoy.reloadable_features.dfp_cluster_resolves_hosts", "true"); + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.skip_dns_lookup_for_proxied_requests", "true"); + upstream_tls_ = false; + autonomous_upstream_ = true; + // Add DFP filter even if async DNS resolution is enabled. + config_helper_.prependFilter("{ name: modify-host-filter }"); + initializeWithArgs(1024, 1024, "", typed_dns_resolver_config_, false, 5, false, false, + absl::nullopt, true); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + + ASSERT_TRUE(response->waitForEndStream()); + // The host modification filter sets a non-existing host which should result in a 503. + EXPECT_EQ("503", response->headers().getStatusValue()); +} + } // namespace } // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc index 047f893ff26fa..f1bfd6b5e0447 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc +++ b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc @@ -813,8 +813,6 @@ TEST_F(ProxyFilterWithFilterStateHostTest, NoFilterStatePresent) { .Times(AnyNumber()) .WillRepeatedly(Return(false)); - // Use empty filter state (no filter state values set). - // Should use "foo" from host header when no filter state found. Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = new Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle(); @@ -1089,6 +1087,91 @@ TEST_F(ProxyFilterWithFilterStateHostDisabledTest, IgnoresFilterStatePortWhenFla filter_->onDestroy(); } +// Test for IPv6. +TEST_F(ProxyFilterTest, IPv6BracketStrippingBug) { + Upstream::ResourceAutoIncDec* circuit_breakers_( + new Upstream::ResourceAutoIncDec(pending_requests_)); + InSequence s; + + EXPECT_CALL(callbacks_, route()); + EXPECT_CALL(factory_context_.server_factory_context_.cluster_manager_, getThreadLocalCluster(_)); + EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(false)); + EXPECT_CALL(callbacks_, route()); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) + .WillOnce(Return(circuit_breakers_)); + EXPECT_CALL(callbacks_, streamInfo()); + EXPECT_CALL(callbacks_, dispatcher()); + EXPECT_CALL(callbacks_, streamInfo()); + + // We expect IPv6 address with brackets to be preserved and port to be detected. + Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = + new Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle(); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, loadDnsCacheEntry_(Eq("[::1]"), 8080, _, _)) + .WillOnce(Return( + MockLoadDnsCacheEntryResult{LoadDnsCacheEntryStatus::Loading, handle, absl::nullopt})); + + // Test with IPv6 literal host header. + Http::TestRequestHeaderMapImpl headers{{":authority", "[::1]:8080"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(headers, false)); + + EXPECT_CALL(*handle, onDestroy()); + filter_->onDestroy(); +} + +// Parameterized test for IPv6 bracket variations. +struct IPv6TestCase { + std::string host_header; + std::string expected_host; + uint16_t expected_port; + std::string test_name; +}; + +class ProxyFilterIPv6ParameterizedTest : public ProxyFilterTest, + public testing::WithParamInterface {}; + +TEST_P(ProxyFilterIPv6ParameterizedTest, IPv6BracketVariations) { + const auto& test_case = GetParam(); + + Upstream::ResourceAutoIncDec* circuit_breakers_( + new Upstream::ResourceAutoIncDec(pending_requests_)); + InSequence s; + + EXPECT_CALL(callbacks_, route()); + EXPECT_CALL(factory_context_.server_factory_context_.cluster_manager_, getThreadLocalCluster(_)); + EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(false)); + EXPECT_CALL(callbacks_, route()); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) + .WillOnce(Return(circuit_breakers_)); + EXPECT_CALL(callbacks_, streamInfo()); + EXPECT_CALL(callbacks_, dispatcher()); + EXPECT_CALL(callbacks_, streamInfo()); + + Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = + new Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle(); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, + loadDnsCacheEntry_(Eq(test_case.expected_host), test_case.expected_port, _, _)) + .WillOnce(Return( + MockLoadDnsCacheEntryResult{LoadDnsCacheEntryStatus::Loading, handle, absl::nullopt})); + + Http::TestRequestHeaderMapImpl headers{{":authority", test_case.host_header}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(headers, false)); + + EXPECT_CALL(*handle, onDestroy()); + filter_->onDestroy(); +} + +INSTANTIATE_TEST_SUITE_P( + IPv6Formats, ProxyFilterIPv6ParameterizedTest, + testing::Values( + IPv6TestCase{"[::1]:8080", "[::1]", 8080, "IPv6LoopbackWithPort"}, + IPv6TestCase{"[2001:db8::1]:443", "[2001:db8::1]", 443, "IPv6AddressWithHTTPSPort"}, + IPv6TestCase{"[::1]", "[::1]", 80, "IPv6LoopbackDefaultPort"}, + IPv6TestCase{"[2001:db8:85a3::8a2e:370:7334]:9999", "[2001:db8:85a3::8a2e:370:7334]", 9999, + "IPv6FullAddressWithCustomPort"}), + [](const testing::TestParamInfo& info) { return info.param.test_name; }); + } // namespace } // namespace DynamicForwardProxy } // namespace HttpFilters diff --git a/test/extensions/filters/http/ext_authz/BUILD b/test/extensions/filters/http/ext_authz/BUILD index 694c9a635f2b9..b622affef674d 100644 --- a/test/extensions/filters/http/ext_authz/BUILD +++ b/test/extensions/filters/http/ext_authz/BUILD @@ -79,15 +79,17 @@ envoy_extension_cc_test( deps = [ ":ext_authz_fuzz_proto_cc_proto", ":logging_test_filter_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/filters/http/ext_authz:config", "//source/extensions/listener_managers/validation_listener_manager:validation_listener_manager_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//source/server/config_validation:server_lib", "//test/integration:http_integration_lib", "//test/mocks/server:options_mocks", "//test/test_common:test_runtime_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/ext_authz/v3:pkg_cc_proto", "@envoy_api//envoy/service/auth/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/ext_authz/config_test.cc b/test/extensions/filters/http/ext_authz/config_test.cc index bfb2a593b4114..9a84ee2e73deb 100644 --- a/test/extensions/filters/http/ext_authz/config_test.cc +++ b/test/extensions/filters/http/ext_authz/config_test.cc @@ -8,6 +8,7 @@ #include "source/common/network/address_impl.h" #include "source/common/thread_local/thread_local_impl.h" #include "source/extensions/filters/http/ext_authz/config.h" +#include "source/extensions/filters/http/ext_authz/ext_authz.h" #include "test/mocks/server/factory_context.h" #include "test/test_common/real_threads_test_helper.h" @@ -28,6 +29,10 @@ namespace Extensions { namespace HttpFilters { namespace ExtAuthz { +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + class TestAsyncClientManagerImpl : public Grpc::AsyncClientManagerImpl { public: TestAsyncClientManagerImpl(Upstream::ClusterManager& cm, ThreadLocal::Instance& tls, @@ -217,6 +222,412 @@ TEST_F(ExtAuthzFilterHttpTest, FilterWithServerContext) { cb(filter_callback); } +TEST_F(ExtAuthzFilterHttpTest, PerRouteGrpcServiceConfiguration) { + const std::string per_route_config_yaml = R"EOF( + check_settings: + context_extensions: + virtual_host: "my_virtual_host" + route_type: "high_qps" + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_high_qps" + )EOF"; + + ExtAuthzFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(per_route_config_yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + auto route_config = factory.createRouteSpecificFilterConfig(*proto_config, context, + context.messageValidationVisitor()); + EXPECT_TRUE(route_config.ok()); + + const auto& typed_config = dynamic_cast(*route_config.value()); + EXPECT_FALSE(typed_config.disabled()); + EXPECT_TRUE(typed_config.grpcService().has_value()); + + const auto& grpc_service = typed_config.grpcService().value(); + EXPECT_TRUE(grpc_service.has_envoy_grpc()); + EXPECT_EQ(grpc_service.envoy_grpc().cluster_name(), "ext_authz_high_qps"); + + const auto& context_extensions = typed_config.contextExtensions(); + EXPECT_EQ(context_extensions.at("virtual_host"), "my_virtual_host"); + EXPECT_EQ(context_extensions.at("route_type"), "high_qps"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteHttpServiceConfiguration) { + const std::string per_route_config_yaml = R"EOF( + check_settings: + context_extensions: + virtual_host: "my_virtual_host" + route_type: "high_qps" + http_service: + server_uri: + uri: "https://ext-authz-http.example.com" + cluster: "ext_authz_http_cluster" + timeout: 2s + path_prefix: "/api/auth" + )EOF"; + + ExtAuthzFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(per_route_config_yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + auto route_config = factory.createRouteSpecificFilterConfig(*proto_config, context, + context.messageValidationVisitor()); + EXPECT_TRUE(route_config.ok()); + + const auto& typed_config = dynamic_cast(*route_config.value()); + EXPECT_FALSE(typed_config.disabled()); + EXPECT_TRUE(typed_config.httpService().has_value()); + EXPECT_FALSE(typed_config.grpcService().has_value()); + + const auto& http_service = typed_config.httpService().value(); + EXPECT_EQ(http_service.server_uri().uri(), "https://ext-authz-http.example.com"); + EXPECT_EQ(http_service.server_uri().cluster(), "ext_authz_http_cluster"); + EXPECT_EQ(http_service.server_uri().timeout().seconds(), 2); + EXPECT_EQ(http_service.path_prefix(), "/api/auth"); + + const auto& context_extensions = typed_config.contextExtensions(); + EXPECT_EQ(context_extensions.at("virtual_host"), "my_virtual_host"); + EXPECT_EQ(context_extensions.at("route_type"), "high_qps"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteServiceTypeSwitching) { + // Test that we can switch service types - e.g., have gRPC in less specific and HTTP in more + // specific + const std::string less_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + base_setting: "from_base" + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_grpc_cluster" + )EOF"; + + const std::string more_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + override_setting: "from_override" + http_service: + server_uri: + uri: "https://ext-authz-http.example.com" + cluster: "ext_authz_http_cluster" + timeout: 3s + path_prefix: "/auth/check" + )EOF"; + + ExtAuthzFilterConfig factory; + + // Create less specific configuration with gRPC service + ProtobufTypes::MessagePtr less_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(less_specific_config_yaml, *less_specific_proto); + FilterConfigPerRoute less_specific_config( + *dynamic_cast( + less_specific_proto.get())); + + // Create more specific configuration with HTTP service + ProtobufTypes::MessagePtr more_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(more_specific_config_yaml, *more_specific_proto); + FilterConfigPerRoute more_specific_config( + *dynamic_cast( + more_specific_proto.get())); + + // Merge configurations - should use HTTP service from more specific config + FilterConfigPerRoute merged_config(less_specific_config, more_specific_config); + + // Verify that HTTP service from more specific config is used (service type switching) + EXPECT_TRUE(merged_config.httpService().has_value()); + EXPECT_FALSE(merged_config.grpcService().has_value()); + + const auto& http_service = merged_config.httpService().value(); + EXPECT_EQ(http_service.server_uri().uri(), "https://ext-authz-http.example.com"); + EXPECT_EQ(http_service.server_uri().cluster(), "ext_authz_http_cluster"); + EXPECT_EQ(http_service.path_prefix(), "/auth/check"); + + // Verify context extensions are properly merged (less specific preserved, more specific + // overrides) + const auto& context_extensions = merged_config.contextExtensions(); + EXPECT_EQ(context_extensions.size(), 2); + EXPECT_EQ(context_extensions.at("base_setting"), "from_base"); + EXPECT_EQ(context_extensions.at("override_setting"), "from_override"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteServiceTypeSwitchingHttpToGrpc) { + // Test that we can switch from HTTP service to gRPC service (reverse of the other test) + const std::string less_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + base_setting: "from_base" + http_service: + server_uri: + uri: "https://ext-authz-http.example.com" + cluster: "ext_authz_http_cluster" + timeout: 1s + path_prefix: "/auth" + )EOF"; + + const std::string more_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + override_setting: "from_override" + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_grpc_cluster" + authority: "ext-authz.example.com" + timeout: 5s + )EOF"; + + ExtAuthzFilterConfig factory; + + // Create less specific configuration with HTTP service + ProtobufTypes::MessagePtr less_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(less_specific_config_yaml, *less_specific_proto); + FilterConfigPerRoute less_specific_config( + *dynamic_cast( + less_specific_proto.get())); + + // Create more specific configuration with gRPC service + ProtobufTypes::MessagePtr more_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(more_specific_config_yaml, *more_specific_proto); + FilterConfigPerRoute more_specific_config( + *dynamic_cast( + more_specific_proto.get())); + + // Merge configurations - should use gRPC service from more specific config + FilterConfigPerRoute merged_config(less_specific_config, more_specific_config); + + // Verify that gRPC service from more specific config is used (service type switching) + EXPECT_TRUE(merged_config.grpcService().has_value()); + EXPECT_FALSE(merged_config.httpService().has_value()); + + const auto& grpc_service = merged_config.grpcService().value(); + EXPECT_TRUE(grpc_service.has_envoy_grpc()); + EXPECT_EQ(grpc_service.envoy_grpc().cluster_name(), "ext_authz_grpc_cluster"); + EXPECT_EQ(grpc_service.envoy_grpc().authority(), "ext-authz.example.com"); + EXPECT_EQ(grpc_service.timeout().seconds(), 5); + + // Verify context extensions are properly merged + const auto& context_extensions = merged_config.contextExtensions(); + EXPECT_EQ(context_extensions.size(), 2); + EXPECT_EQ(context_extensions.at("base_setting"), "from_base"); + EXPECT_EQ(context_extensions.at("override_setting"), "from_override"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteHttpServiceWithTimeout) { + // Test HTTP service configuration with custom timeout + const std::string per_route_config_yaml = R"EOF( + check_settings: + http_service: + server_uri: + uri: "https://ext-authz-custom.example.com" + cluster: "ext_authz_custom_cluster" + timeout: 10s + path_prefix: "/custom/auth" + )EOF"; + + ExtAuthzFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(per_route_config_yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + auto route_config = factory.createRouteSpecificFilterConfig(*proto_config, context, + context.messageValidationVisitor()); + EXPECT_TRUE(route_config.ok()); + + const auto& typed_config = dynamic_cast(*route_config.value()); + EXPECT_TRUE(typed_config.httpService().has_value()); + EXPECT_FALSE(typed_config.grpcService().has_value()); + + const auto& http_service = typed_config.httpService().value(); + EXPECT_EQ(http_service.server_uri().uri(), "https://ext-authz-custom.example.com"); + EXPECT_EQ(http_service.server_uri().cluster(), "ext_authz_custom_cluster"); + EXPECT_EQ(http_service.server_uri().timeout().seconds(), 10); + EXPECT_EQ(http_service.path_prefix(), "/custom/auth"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteGrpcServiceWithTimeout) { + // Test gRPC service configuration with custom timeout + const std::string per_route_config_yaml = R"EOF( + check_settings: + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_custom_grpc" + authority: "custom-ext-authz.example.com" + timeout: 15s + )EOF"; + + ExtAuthzFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(per_route_config_yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + auto route_config = factory.createRouteSpecificFilterConfig(*proto_config, context, + context.messageValidationVisitor()); + EXPECT_TRUE(route_config.ok()); + + const auto& typed_config = dynamic_cast(*route_config.value()); + EXPECT_TRUE(typed_config.grpcService().has_value()); + EXPECT_FALSE(typed_config.httpService().has_value()); + + const auto& grpc_service = typed_config.grpcService().value(); + EXPECT_TRUE(grpc_service.has_envoy_grpc()); + EXPECT_EQ(grpc_service.envoy_grpc().cluster_name(), "ext_authz_custom_grpc"); + EXPECT_EQ(grpc_service.envoy_grpc().authority(), "custom-ext-authz.example.com"); + EXPECT_EQ(grpc_service.timeout().seconds(), 15); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteEmptyContextExtensionsMerging) { + // Test merging when one config has empty context extensions + const std::string less_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + base_key: "base_value" + shared_key: "base_shared" + grpc_service: + envoy_grpc: + cluster_name: "base_cluster" + )EOF"; + + const std::string more_specific_config_yaml = R"EOF( + check_settings: + grpc_service: + envoy_grpc: + cluster_name: "specific_cluster" + )EOF"; + + ExtAuthzFilterConfig factory; + + ProtobufTypes::MessagePtr less_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(less_specific_config_yaml, *less_specific_proto); + FilterConfigPerRoute less_specific_config( + *dynamic_cast( + less_specific_proto.get())); + + ProtobufTypes::MessagePtr more_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(more_specific_config_yaml, *more_specific_proto); + FilterConfigPerRoute more_specific_config( + *dynamic_cast( + more_specific_proto.get())); + + FilterConfigPerRoute merged_config(less_specific_config, more_specific_config); + + // Should use gRPC service from more specific + EXPECT_TRUE(merged_config.grpcService().has_value()); + EXPECT_EQ(merged_config.grpcService().value().envoy_grpc().cluster_name(), "specific_cluster"); + + // Should preserve context extensions from less specific since more specific has none + const auto& context_extensions = merged_config.contextExtensions(); + EXPECT_EQ(context_extensions.size(), 2); + EXPECT_EQ(context_extensions.at("base_key"), "base_value"); + EXPECT_EQ(context_extensions.at("shared_key"), "base_shared"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteGrpcServiceConfigurationMerging) { + // Test merging of per-route configurations + const std::string less_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + virtual_host: "my_virtual_host" + shared_setting: "from_less_specific" + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_default" + )EOF"; + + const std::string more_specific_config_yaml = R"EOF( + check_settings: + context_extensions: + route_type: "high_qps" + shared_setting: "from_more_specific" + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_high_qps" + )EOF"; + + ExtAuthzFilterConfig factory; + + // Create less specific configuration + ProtobufTypes::MessagePtr less_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(less_specific_config_yaml, *less_specific_proto); + FilterConfigPerRoute less_specific_config( + *dynamic_cast( + less_specific_proto.get())); + + // Create more specific configuration + ProtobufTypes::MessagePtr more_specific_proto = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(more_specific_config_yaml, *more_specific_proto); + FilterConfigPerRoute more_specific_config( + *dynamic_cast( + more_specific_proto.get())); + + // Merge configurations + FilterConfigPerRoute merged_config(less_specific_config, more_specific_config); + + // Check that more specific gRPC service is used + EXPECT_TRUE(merged_config.grpcService().has_value()); + const auto& grpc_service = merged_config.grpcService().value(); + EXPECT_TRUE(grpc_service.has_envoy_grpc()); + EXPECT_EQ(grpc_service.envoy_grpc().cluster_name(), "ext_authz_high_qps"); + + // Check that context extensions are properly merged + const auto& context_extensions = merged_config.contextExtensions(); + EXPECT_EQ(context_extensions.at("virtual_host"), "my_virtual_host"); + EXPECT_EQ(context_extensions.at("route_type"), "high_qps"); + EXPECT_EQ(context_extensions.at("shared_setting"), "from_more_specific"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteGrpcServiceConfigurationWithoutGrpcService) { + const std::string per_route_config_yaml = R"EOF( + check_settings: + context_extensions: + virtual_host: "my_virtual_host" + disable_request_body_buffering: true + )EOF"; + + ExtAuthzFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(per_route_config_yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + auto route_config = factory.createRouteSpecificFilterConfig(*proto_config, context, + context.messageValidationVisitor()); + EXPECT_TRUE(route_config.ok()); + + const auto& typed_config = dynamic_cast(*route_config.value()); + EXPECT_FALSE(typed_config.disabled()); + EXPECT_FALSE(typed_config.grpcService().has_value()); + + const auto& context_extensions = typed_config.contextExtensions(); + EXPECT_EQ(context_extensions.at("virtual_host"), "my_virtual_host"); +} + +TEST_F(ExtAuthzFilterHttpTest, PerRouteGrpcServiceConfigurationDisabled) { + const std::string per_route_config_yaml = R"EOF( + disabled: true + )EOF"; + + ExtAuthzFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(per_route_config_yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + auto route_config = factory.createRouteSpecificFilterConfig(*proto_config, context, + context.messageValidationVisitor()); + EXPECT_TRUE(route_config.ok()); + + const auto& typed_config = dynamic_cast(*route_config.value()); + EXPECT_TRUE(typed_config.disabled()); + EXPECT_FALSE(typed_config.grpcService().has_value()); +} + class ExtAuthzFilterGrpcTest : public ExtAuthzFilterTest { public: void testFilterFactoryAndFilterWithGrpcClient(const std::string& ext_authz_config_yaml) { diff --git a/test/extensions/filters/http/ext_authz/ext_authz.yaml b/test/extensions/filters/http/ext_authz/ext_authz.yaml index 148b47e42ed75..2dce2f248dd18 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz.yaml +++ b/test/extensions/filters/http/ext_authz/ext_authz.yaml @@ -43,6 +43,10 @@ static_resources: - name: local_service connect_timeout: 30s type: STRICT_DNS + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig lb_policy: ROUND_ROBIN load_assignment: cluster_name: local_service @@ -55,6 +59,10 @@ static_resources: port_value: 8080 - name: ext_authz-service type: STRICT_DNS + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig lb_policy: ROUND_ROBIN typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: diff --git a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc index 82aeac4ddd807..0d0368ce57149 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc @@ -1,10 +1,13 @@ #include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/base.pb.h" #include "envoy/config/listener/v3/listener_components.pb.h" #include "envoy/extensions/filters/http/ext_authz/v3/ext_authz.pb.h" #include "envoy/service/auth/v3/external_auth.pb.h" #include "source/common/common/macros.h" #include "source/extensions/filters/common/ext_authz/ext_authz.h" +#include "source/extensions/filters/http/ext_authz/ext_authz.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "source/server/config_validation/server.h" #include "test/common/grpc/grpc_client_integration.h" @@ -143,11 +146,11 @@ class ExtAuthzGrpcIntegrationTest labels["label_1"] = "value_1"; labels["label_2"] = "value_2"; - ProtobufWkt::Struct metadata; - ProtobufWkt::Value val; - ProtobufWkt::Struct* labels_obj = val.mutable_struct_value(); + Protobuf::Struct metadata; + Protobuf::Value val; + Protobuf::Struct* labels_obj = val.mutable_struct_value(); for (const auto& pair : labels) { - ProtobufWkt::Value val; + Protobuf::Value val; val.set_string_value(pair.second); (*labels_obj->mutable_fields())[pair.first] = val; } @@ -188,10 +191,10 @@ class ExtAuthzGrpcIntegrationTest config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* layer = bootstrap.mutable_layered_runtime()->add_layers(); layer->set_name("enable layer"); - ProtobufWkt::Struct& runtime = *layer->mutable_static_layer(); + Protobuf::Struct& runtime = *layer->mutable_static_layer(); bootstrap.mutable_layered_runtime()->mutable_layers(0)->set_name("base layer"); - ProtobufWkt::Struct& enable = + Protobuf::Struct& enable = *(*runtime.mutable_fields())["envoy.ext_authz.enable"].mutable_struct_value(); (*enable.mutable_fields())["numerator"].set_number_value(0); }); @@ -380,17 +383,17 @@ class ExtAuthzGrpcIntegrationTest if (opts.failure_mode_allowed_header) { EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf("x-envoy-auth-failure-mode-allowed", "true")); + ContainsHeader("x-envoy-auth-failure-mode-allowed", "true")); } // Check that ext_authz didn't remove this downstream header which should be immune to // mutations. EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf("disallow-mutation-downstream-req", - "authz resp cannot set or append to this header")); + ContainsHeader("disallow-mutation-downstream-req", + "authz resp cannot set or append to this header")); for (const auto& header_to_add : opts.headers_to_add) { EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(header_to_add.first, header_to_add.second)); + ContainsHeader(header_to_add.first, header_to_add.second)); // For headers_to_add (with append = false), the original request headers have no "-replaced" // suffix, but the ones from the authorization server have it. EXPECT_TRUE(absl::EndsWith(header_to_add.second, "-replaced")); @@ -403,7 +406,7 @@ class ExtAuthzGrpcIntegrationTest // header is existed in the original request headers). EXPECT_THAT( upstream_request_->headers(), - Http::HeaderValueOf( + ContainsHeader( header_to_append.first, // In this test, the keys and values of the original request headers have the same // string value. Hence for "header2" key, the value is "header2,header2-appended". @@ -430,7 +433,7 @@ class ExtAuthzGrpcIntegrationTest // headers_to_append_multiple has append = false for the first entry of multiple entries, and // append = true for the rest entries. EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf("multiple", "multiple-first,multiple-second")); + ContainsHeader("multiple", "multiple-first,multiple-second")); } for (const auto& header_to_remove : opts.headers_to_remove) { @@ -456,7 +459,8 @@ class ExtAuthzGrpcIntegrationTest const Headers& response_headers_to_append, const Headers& response_headers_to_set, const Headers& response_headers_to_append_if_absent, - const Headers& response_headers_to_set_if_exists = {}) { + const Headers& response_headers_to_set_if_exists = {}, + bool add_sentinel_header_append_action = false) { ext_authz_request_->startGrpcStream(); envoy::service::auth::v3::CheckResponse check_response; check_response.mutable_status()->set_code(Grpc::Status::WellKnownGrpcStatus::Ok); @@ -517,6 +521,20 @@ class ExtAuthzGrpcIntegrationTest ENVOY_LOG_MISC(trace, "sendExtAuthzResponse: set response_header_to_add {}={}", key, value); } + if (add_sentinel_header_append_action) { + auto* entry = check_response.mutable_ok_response()->mutable_response_headers_to_add()->Add(); + entry->set_append_action( + static_cast( + std::numeric_limits::max())); + const auto key = std::string("invalid-append-action"); + const auto value = std::string("invalid-append-action-value"); + entry->mutable_header()->set_key(key); + entry->mutable_header()->set_value(value); + ENVOY_LOG_MISC(trace, + "sendExtAuthzResponse: set response header with invalid append action {}={}", + key, value); + } + for (const auto& response_header_to_set : response_headers_to_set) { auto* entry = check_response.mutable_ok_response()->mutable_response_headers_to_add()->Add(); const auto key = std::string(response_header_to_set.first); @@ -746,11 +764,11 @@ class ExtAuthzHttpIntegrationTest result = ext_authz_request_->waitForEndStream(*dispatcher_); RELEASE_ASSERT(result, result.message()); - EXPECT_THAT(ext_authz_request_->headers(), Http::HeaderValueOf("allowed-prefix-one", "one")); - EXPECT_THAT(ext_authz_request_->headers(), Http::HeaderValueOf("allowed-prefix-two", "two")); - EXPECT_THAT(ext_authz_request_->headers(), Http::HeaderValueOf("authorization", "legit")); - EXPECT_THAT(ext_authz_request_->headers(), Http::HeaderValueOf("regex-food", "food")); - EXPECT_THAT(ext_authz_request_->headers(), Http::HeaderValueOf("regex-fool", "fool")); + EXPECT_THAT(ext_authz_request_->headers(), ContainsHeader("allowed-prefix-one", "one")); + EXPECT_THAT(ext_authz_request_->headers(), ContainsHeader("allowed-prefix-two", "two")); + EXPECT_THAT(ext_authz_request_->headers(), ContainsHeader("authorization", "legit")); + EXPECT_THAT(ext_authz_request_->headers(), ContainsHeader("regex-food", "food")); + EXPECT_THAT(ext_authz_request_->headers(), ContainsHeader("regex-fool", "fool")); EXPECT_TRUE(ext_authz_request_->headers() .get(Http::LowerCaseString(std::string("not-allowed"))) @@ -859,7 +877,7 @@ class ExtAuthzHttpIntegrationTest // The original client request header value of "baz" is "foo". Since we configure to "override" // the value of "baz", we expect the request headers to be sent to upstream contain only one // "baz" with value "baz" (set by the authorization server). - EXPECT_THAT(upstream_request_->headers(), Http::HeaderValueOf("baz", "baz")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("baz", "baz")); // The original client request header value of "bat" is "foo". Since we configure to "append" // the value of "bat", we expect the request headers to be sent to upstream contain two "bat"s, @@ -983,6 +1001,32 @@ INSTANTIATE_TEST_SUITE_P(IpVersionsCientType, ExtAuthzGrpcIntegrationTest, testing::Bool()), ExtAuthzGrpcIntegrationTest::testParamsToString); +// Test per-route gRPC service configuration parsing +TEST_P(ExtAuthzGrpcIntegrationTest, PerRouteGrpcServiceConfigurationParsing) { + // Create a simple per-route configuration with gRPC service + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("per_route_cluster"); + (*per_route_config.mutable_check_settings()->mutable_context_extensions())["route_type"] = + "special"; + + // Test configuration parsing and validation + Envoy::Extensions::HttpFilters::ExtAuthz::FilterConfigPerRoute config_per_route(per_route_config); + + // Verify the configuration was parsed correctly + ASSERT_TRUE(config_per_route.grpcService().has_value()); + EXPECT_TRUE(config_per_route.grpcService().value().has_envoy_grpc()); + EXPECT_EQ(config_per_route.grpcService().value().envoy_grpc().cluster_name(), + "per_route_cluster"); + + // Verify context extensions are present + const auto& check_settings = config_per_route.checkSettings(); + ASSERT_TRUE(check_settings.context_extensions().contains("route_type")); + EXPECT_EQ(check_settings.context_extensions().at("route_type"), "special"); +} + // Verifies that the request body is included in the CheckRequest when the downstream protocol is // HTTP/1.1. TEST_P(ExtAuthzGrpcIntegrationTest, HTTP1DownstreamRequestWithBody) { @@ -1267,6 +1311,49 @@ TEST_P(ExtAuthzGrpcIntegrationTest, ValidateMutations) { cleanup(); } +TEST_P(ExtAuthzGrpcIntegrationTest, ValidateMutationsSentinelAppendAction) { + GrpcInitializeConfigOpts opts; + opts.validate_mutations = true; + initializeConfig(opts); + + // Use h1, set up the test. + setDownstreamProtocol(Http::CodecType::HTTP1); + HttpIntegrationTest::initialize(); + + // Start a client connection and request. + initiateClientConnection(0); + waitForExtAuthzRequest(expectedCheckRequest(Http::CodecType::HTTP1)); + sendExtAuthzResponse({}, {}, {}, {}, {}, {}, {}, {}, {}, + /*add_sentinel_header_append_action=*/true); + + ASSERT_TRUE(response_->waitForEndStream()); + EXPECT_TRUE(response_->complete()); + EXPECT_EQ("500", response_->headers().getStatusValue()); + test_server_->waitForCounterEq("cluster.cluster_0.ext_authz.invalid", 1); + + cleanup(); +} + +// Ignore invalid header append actions when validate_mutations is false. +TEST_P(ExtAuthzGrpcIntegrationTest, NoValidateMutationsSentinelAppendAction) { + GrpcInitializeConfigOpts opts; + opts.validate_mutations = false; + initializeConfig(opts); + + // Use h1, set up the test. + setDownstreamProtocol(Http::CodecType::HTTP1); + HttpIntegrationTest::initialize(); + + // Start a client connection and request. + initiateClientConnection(0); + + waitForExtAuthzRequest(expectedCheckRequest(Http::CodecType::HTTP1)); + sendExtAuthzResponse({}, {}, {}, {}, {}, {}, {}, {}, {}, + /*add_sentinel_header_append_action=*/true); + waitForSuccessfulUpstreamResponse("200"); + cleanup(); +} + TEST_P(ExtAuthzGrpcIntegrationTest, TimeoutFailOpen) { GrpcInitializeConfigOpts init_opts; init_opts.stats_expect_response_bytes = false; @@ -1428,7 +1515,7 @@ TEST_P(ExtAuthzHttpIntegrationTest, DefaultCaseSensitiveStringMatcher) { // Verifies that "X-Forwarded-For" header is unmodified. TEST_P(ExtAuthzHttpIntegrationTest, UnmodifiedForwardedForHeader) { setup(false); - EXPECT_THAT(ext_authz_request_->headers(), Http::HeaderValueOf("x-forwarded-for", "1.2.3.4")); + EXPECT_THAT(ext_authz_request_->headers(), ContainsHeader("x-forwarded-for", "1.2.3.4")); } // Verifies that by default HTTP service uses the case-sensitive string matcher @@ -1548,7 +1635,7 @@ TEST_P(ExtAuthzHttpIntegrationTest, TimeoutFailOpen) { RELEASE_ASSERT(result, result.message()); EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf("x-envoy-auth-failure-mode-allowed", "true")); + ContainsHeader("x-envoy-auth-failure-mode-allowed", "true")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response_->waitForEndStream()); diff --git a/test/extensions/filters/http/ext_authz/ext_authz_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_test.cc index 5f24a87dd33ee..871f6eeb7be99 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_test.cc @@ -53,6 +53,50 @@ namespace HttpFilters { namespace ExtAuthz { namespace { +// Matcher to convert a Buffer::Instance to its string representation for composition. +MATCHER_P(BufferString, m, "") { + return testing::ExplainMatchResult(m, arg->toString(), result_listener); +} + +// Matcher to parse a buffer string into a CheckRequest proto. +MATCHER_P(AsCheckRequest, m, "") { + envoy::service::auth::v3::CheckRequest check_request; + if (!check_request.ParseFromString(arg)) { + *result_listener << "failed to parse CheckRequest from buffer"; + return false; + } + return testing::ExplainMatchResult(m, check_request, result_listener); +} + +// Matcher to verify CheckRequest has specific context extension. +MATCHER_P2(HasContextExtension, key, value, "") { + const auto& context_extensions = arg.attributes().context_extensions(); + if (context_extensions.find(key) == context_extensions.end()) { + *result_listener << "context extension '" << key << "' not found"; + return false; + } + if (context_extensions.at(key) != value) { + *result_listener << "context extension '" << key << "' has value '" + << context_extensions.at(key) << "', expected '" << value << "'"; + return false; + } + return true; +} + +// Matcher to verify RequestOptions has specific timeout value. +MATCHER_P(HasTimeout, expected_timeout_ms, "") { + if (!arg.timeout.has_value()) { + *result_listener << "timeout not set"; + return false; + } + if (arg.timeout->count() != expected_timeout_ms) { + *result_listener << "timeout is " << arg.timeout->count() << "ms, expected " + << expected_timeout_ms << "ms"; + return false; + } + return true; +} + constexpr char FilterConfigName[] = "ext_authz_filter"; template class HttpFilterTestBase : public T { @@ -71,7 +115,8 @@ template class HttpFilterTestBase : public T { config_ = std::make_shared(proto_config, *stats_store_.rootScope(), "ext_authz_prefix", factory_context_); client_ = new NiceMock(); - filter_ = std::make_unique(config_, Filters::Common::ExtAuthz::ClientPtr{client_}); + filter_ = std::make_unique(config_, Filters::Common::ExtAuthz::ClientPtr{client_}, + factory_context_); ON_CALL(decoder_filter_callbacks_, filterConfigName()).WillByDefault(Return(FilterConfigName)); filter_->setDecoderFilterCallbacks(decoder_filter_callbacks_); filter_->setEncoderFilterCallbacks(encoder_filter_callbacks_); @@ -80,7 +125,7 @@ template class HttpFilterTestBase : public T { static envoy::extensions::filters::http::ext_authz::v3::ExtAuthz getFilterConfig(bool failure_mode_allow, bool http_client, bool emit_filter_state_stats = false, - absl::optional filter_metadata = absl::nullopt) { + absl::optional filter_metadata = absl::nullopt) { const std::string http_config = R"EOF( failure_mode_allow_header_add: true http_service: @@ -270,12 +315,12 @@ class EmitFilterStateTest public: EmitFilterStateTest() : expected_output_(filterMetadata()) {} - absl::optional filterMetadata() const { + absl::optional filterMetadata() const { if (!std::get<2>(GetParam())) { return absl::nullopt; } - auto filter_metadata = Envoy::ProtobufWkt::Struct(); + auto filter_metadata = Envoy::Protobuf::Struct(); *(*filter_metadata.mutable_fields())["foo"].mutable_string_value() = "bar"; return filter_metadata; } @@ -439,9 +484,13 @@ class InvalidMutationTest : public HttpFilterTestBase { EXPECT_EQ(1U, config_->stats().invalid_.value()); } - const std::string invalid_key_ = "invalid-\nkey"; - const uint8_t invalid_value_bytes_[3]{0x7f, 0x7f, 0}; + static constexpr const char* invalid_key_ = "invalid-\nkey"; + static constexpr uint8_t invalid_value_bytes_[3]{0x7f, 0x7f, 0}; const std::string invalid_value_; + + static std::string getInvalidValue() { + return std::string(reinterpret_cast(invalid_value_bytes_)); + } }; TEST_F(HttpFilterTest, DisableDynamicMetadataIngestion) { @@ -484,139 +533,156 @@ TEST_F(HttpFilterTest, DisableDynamicMetadataIngestion) { // Tests that the filter rejects authz responses with mutations with an invalid key when // validate_authz_response is set to true in config. -TEST_F(InvalidMutationTest, HeadersToSetKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.headers_to_set = {{invalid_key_, "bar"}}; - testResponse(response); -} - -// Same as above, setting a different field... -TEST_F(InvalidMutationTest, HeadersToAddKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.headers_to_add = {{invalid_key_, "bar"}}; - testResponse(response); -} - -// headers_to_set is also used when the authz response has status denied. -TEST_F(InvalidMutationTest, HeadersToSetKeyDenied) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::Denied; - response.headers_to_set = {{invalid_key_, "bar"}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, HeadersToAppendKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.headers_to_append = {{invalid_key_, "bar"}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToAddKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.response_headers_to_set = {{invalid_key_, "bar"}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToSetKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.response_headers_to_set = {{invalid_key_, "bar"}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToAddIfAbsentKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.response_headers_to_add_if_absent = {{invalid_key_, "bar"}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToOverwriteIfExistsKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.response_headers_to_overwrite_if_exists = {{invalid_key_, "bar"}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, QueryParametersToSetKey) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.query_parameters_to_set = {{"f o o", "bar"}}; - testResponse(response); -} - -// Test that the filter rejects mutations with an invalid value -TEST_F(InvalidMutationTest, HeadersToSetValueOk) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.headers_to_set = {{"foo", invalid_value_}}; - testResponse(response); -} - -// Same as above, setting a different field... -TEST_F(InvalidMutationTest, HeadersToAddValue) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.headers_to_add = {{"foo", invalid_value_}}; - testResponse(response); -} - -// headers_to_set is also used when the authz response has status denied. -TEST_F(InvalidMutationTest, HeadersToSetValueDenied) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::Denied; - response.headers_to_set = {{"foo", invalid_value_}}; - testResponse(response); -} +// Parameterized test for invalid mutation scenarios to reduce redundancy. +class InvalidMutationParamTest + : public InvalidMutationTest, + public testing::WithParamInterface< + std::tuple, // setup func + Filters::Common::ExtAuthz::CheckStatus // status + >> {}; + +TEST_P(InvalidMutationParamTest, InvalidMutationFields) { + const auto& [test_name, setup_func, status] = GetParam(); -TEST_F(InvalidMutationTest, HeadersToAppendValue) { Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - response.headers_to_append = {{"foo", invalid_value_}}; + response.status = status; + setup_func(response); testResponse(response); } -TEST_F(InvalidMutationTest, ResponseHeadersToAddValue) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - // Add a valid header to see if it gets added to the downstream response. - response.response_headers_to_set = {{"foo", invalid_value_}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToSetValue) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - // Add a valid header to see if it gets added to the downstream response. - response.response_headers_to_set = {{"foo", invalid_value_}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToAddIfAbsentValue) { - Filters::Common::ExtAuthz::Response response; - response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - // Add a valid header to see if it gets added to the downstream response. - response.response_headers_to_add_if_absent = {{"foo", invalid_value_}}; - testResponse(response); -} - -TEST_F(InvalidMutationTest, ResponseHeadersToOverwriteIfExistsValue) { +INSTANTIATE_TEST_SUITE_P( + InvalidMutationScenarios, InvalidMutationParamTest, + testing::Values( + // Invalid key tests + std::make_tuple( + "HeadersToSetKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_set = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "HeadersToAddKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_add = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "HeadersToSetKeyDenied", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_set = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::Denied), + std::make_tuple( + "HeadersToAppendKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_append = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToAddKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_set = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToSetKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_set = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToAddIfAbsentKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_add_if_absent = {{InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToOverwriteIfExistsKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_overwrite_if_exists = { + {InvalidMutationTest::invalid_key_, "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "QueryParametersToSetKey", + [](Filters::Common::ExtAuthz::Response& r) { + r.query_parameters_to_set = {{"f o o", "bar"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + // Invalid value tests + std::make_tuple( + "HeadersToSetValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_set = {{"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "HeadersToAddValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_add = {{"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "HeadersToSetValueDenied", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_set = {{"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::Denied), + std::make_tuple( + "HeadersToAppendValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.headers_to_append = {{"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToAddValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_set = {{"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToSetValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_set = {{"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToAddIfAbsentValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_add_if_absent = { + {"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "ResponseHeadersToOverwriteIfExistsValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.response_headers_to_overwrite_if_exists = { + {"foo", InvalidMutationTest::getInvalidValue()}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK), + std::make_tuple( + "QueryParametersToSetValue", + [](Filters::Common::ExtAuthz::Response& r) { + r.query_parameters_to_set = {{"foo", "b a r"}}; + }, + Filters::Common::ExtAuthz::CheckStatus::OK)), + [](const testing::TestParamInfo& info) { + return std::get<0>(info.param); + }); + +// Keep one simple focused test to ensure backward compatibility. +TEST_F(InvalidMutationTest, BasicInvalidKey) { Filters::Common::ExtAuthz::Response response; response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - // Add a valid header to see if it gets added to the downstream response. - response.response_headers_to_overwrite_if_exists = {{"foo", invalid_value_}}; + response.headers_to_set = {{invalid_key_, "bar"}}; testResponse(response); } -TEST_F(InvalidMutationTest, QueryParametersToSetValue) { +TEST_F(InvalidMutationTest, InvalidHeaderAppendAction) { Filters::Common::ExtAuthz::Response response; response.status = Filters::Common::ExtAuthz::CheckStatus::OK; - // Add a valid header to see if it gets added to the downstream response. - response.query_parameters_to_set = {{"foo", "b a r"}}; + response.saw_invalid_append_actions = true; testResponse(response); } @@ -824,68 +890,55 @@ TEST_F(DecoderHeaderMutationRulesTest, DisallowAll) { runTest(opts); } -TEST_F(DecoderHeaderMutationRulesTest, RejectResponseAdd) { - DecoderHeaderMutationRulesTestOpts opts; - opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); - opts.rules->mutable_disallow_all()->set_value(true); - opts.rules->mutable_disallow_is_error()->set_value(true); - opts.expect_reject_response = true; - - opts.disallowed_headers_to_add = {{"cant-add-me", "sad"}}; - runTest(opts); -} - -TEST_F(DecoderHeaderMutationRulesTest, RejectResponseAppend) { - DecoderHeaderMutationRulesTestOpts opts; - opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); - opts.rules->mutable_disallow_all()->set_value(true); - opts.rules->mutable_disallow_is_error()->set_value(true); - opts.expect_reject_response = true; - - opts.disallowed_headers_to_append = {{"cant-append-to-me", "fail"}}; - runTest(opts); -} - -TEST_F(DecoderHeaderMutationRulesTest, RejectResponseAppendPseudoheader) { - DecoderHeaderMutationRulesTestOpts opts; - opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); - opts.rules->mutable_disallow_is_error()->set_value(true); - opts.expect_reject_response = true; - - opts.disallowed_headers_to_append = {{":fake-pseudo-header", "fail"}}; - runTest(opts); -} - -TEST_F(DecoderHeaderMutationRulesTest, RejectResponseSet) { - DecoderHeaderMutationRulesTestOpts opts; - opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); - opts.rules->mutable_disallow_all()->set_value(true); - opts.rules->mutable_disallow_is_error()->set_value(true); - opts.expect_reject_response = true; - - opts.disallowed_headers_to_set = {{"cant-override-me", "nope"}}; - runTest(opts); -} - -TEST_F(DecoderHeaderMutationRulesTest, RejectResponseRemove) { - DecoderHeaderMutationRulesTestOpts opts; - opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); - opts.rules->mutable_disallow_all()->set_value(true); - opts.rules->mutable_disallow_is_error()->set_value(true); - opts.expect_reject_response = true; - - opts.disallowed_headers_to_remove = {"cant-delete-me"}; - runTest(opts); -} +// Consolidated rejection test that covers all the scenarios previously tested individually. +TEST_F(DecoderHeaderMutationRulesTest, RejectResponseOperations) { + // Test data structure for all rejection scenarios + struct TestCase { + std::string name; + bool use_disallow_all; + std::function setup_func; + }; -TEST_F(DecoderHeaderMutationRulesTest, RejectResponseRemovePseudoHeader) { - DecoderHeaderMutationRulesTestOpts opts; - opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); - opts.rules->mutable_disallow_is_error()->set_value(true); - opts.expect_reject_response = true; + std::vector test_cases = { + {"RejectResponseAdd", true, + [](DecoderHeaderMutationRulesTestOpts& opts) { + opts.disallowed_headers_to_add = {{"cant-add-me", "sad"}}; + }}, + {"RejectResponseAppend", true, + [](DecoderHeaderMutationRulesTestOpts& opts) { + opts.disallowed_headers_to_append = {{"cant-append-to-me", "fail"}}; + }}, + {"RejectResponseAppendPseudoheader", false, + [](DecoderHeaderMutationRulesTestOpts& opts) { + opts.disallowed_headers_to_append = {{":fake-pseudo-header", "fail"}}; + }}, + {"RejectResponseSet", true, + [](DecoderHeaderMutationRulesTestOpts& opts) { + opts.disallowed_headers_to_set = {{"cant-override-me", "nope"}}; + }}, + {"RejectResponseRemove", true, + [](DecoderHeaderMutationRulesTestOpts& opts) { + opts.disallowed_headers_to_remove = {"cant-delete-me"}; + }}, + {"RejectResponseRemovePseudoHeader", false, [](DecoderHeaderMutationRulesTestOpts& opts) { + opts.disallowed_headers_to_remove = {":fake-pseudo-header"}; + }}}; + + // Run all test cases + for (const auto& test_case : test_cases) { + SCOPED_TRACE(test_case.name); + + DecoderHeaderMutationRulesTestOpts opts; + opts.rules = envoy::config::common::mutation_rules::v3::HeaderMutationRules(); + if (test_case.use_disallow_all) { + opts.rules->mutable_disallow_all()->set_value(true); + } + opts.rules->mutable_disallow_is_error()->set_value(true); + opts.expect_reject_response = true; - opts.disallowed_headers_to_remove = {":fake-pseudo-header"}; - runTest(opts); + test_case.setup_func(opts); + runTest(opts); + } } TEST_F(DecoderHeaderMutationRulesTest, DisallowExpression) { @@ -2828,19 +2881,20 @@ TEST_P(HttpFilterTestParam, ContextExtensions) { // Test that filter can be disabled with route config. TEST_P(HttpFilterTestParam, DisabledOnRoute) { envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute settings; - FilterConfigPerRoute auth_per_route(settings); + std::unique_ptr auth_per_route = + std::make_unique(settings); prepareCheck(); - ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) - .WillByDefault(Return(&auth_per_route)); - auto test_disable = [&](bool disabled) { initialize(""); // Set disabled settings.set_disabled(disabled); // Initialize the route's per filter config. - auth_per_route = FilterConfigPerRoute(settings); + auth_per_route = std::make_unique(settings); + // Update the mock to return the new pointer + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(auth_per_route.get())); }; // baseline: make sure that when not disabled, check is called @@ -2861,10 +2915,8 @@ TEST_P(HttpFilterTestParam, DisabledOnRoute) { // Test that filter can be disabled with route config. TEST_P(HttpFilterTestParam, DisabledOnRouteWithRequestBody) { envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute settings; - FilterConfigPerRoute auth_per_route(settings); - - ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) - .WillByDefault(Return(&auth_per_route)); + std::unique_ptr auth_per_route = + std::make_unique(settings); auto test_disable = [&](bool disabled) { initialize(R"EOF( @@ -2880,7 +2932,10 @@ TEST_P(HttpFilterTestParam, DisabledOnRouteWithRequestBody) { // Set the filter disabled setting. settings.set_disabled(disabled); // Initialize the route's per filter config. - auth_per_route = FilterConfigPerRoute(settings); + auth_per_route = std::make_unique(settings); + // Update the mock to return the new pointer. + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(auth_per_route.get())); }; test_disable(false); @@ -3660,7 +3715,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadata) { decoder_filter_callbacks_.dispatcher_.globalTimeSystem().advanceTimeWait( std::chrono::milliseconds(10)); - ProtobufWkt::Value ext_authz_duration_value; + Protobuf::Value ext_authz_duration_value; ext_authz_duration_value.set_number_value(10); Filters::Common::ExtAuthz::Response response{}; @@ -3672,7 +3727,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadata) { EXPECT_CALL(decoder_filter_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce(Invoke([&response](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.ext_authz"); // Check timing metadata correctness EXPECT_TRUE(returned_dynamic_metadata.fields().at("ext_authz_duration").has_number_value()); @@ -3726,7 +3781,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadataWhenDenied) { EXPECT_CALL(decoder_filter_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce(Invoke([&response](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.ext_authz"); // Check timing metadata correctness EXPECT_FALSE(returned_dynamic_metadata.fields().contains("ext_authz_duration")); @@ -3914,6 +3969,31 @@ TEST_F(HttpFilterTest, PerRouteCheckSettingsWorks) { } // Checks that the per-route filter can override the check_settings set on the main filter. +TEST_F(HttpFilterTest, NullRouteSkipsCheck) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + failure_mode_allow: false + stat_prefix: "ext_authz" + )EOF"); + + prepareCheck(); + + // Set up a null route return value. + ON_CALL(decoder_filter_callbacks_, route()).WillByDefault(Return(nullptr)); + + // With null route, no authorization check should be performed. + EXPECT_CALL(*client_, check(_, _, _, _)).Times(0); + + // Call the filter directly. + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {"host", "example.com"}}; + + // With null route, the filter should continue without an auth check. + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); +} + TEST_F(HttpFilterTest, PerRouteCheckSettingsOverrideWorks) { InSequence s; @@ -3975,10 +4055,8 @@ TEST_F(HttpFilterTest, PerRouteCheckSettingsOverrideWorks) { // Verify that request body buffering can be skipped per route. TEST_P(HttpFilterTestParam, DisableRequestBodyBufferingOnRoute) { envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute settings; - FilterConfigPerRoute auth_per_route(settings); - - ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) - .WillByDefault(Return(&auth_per_route)); + std::unique_ptr auth_per_route = + std::make_unique(settings); auto test_disable_request_body_buffering = [&](bool bypass) { initialize(R"EOF( @@ -3994,7 +4072,10 @@ TEST_P(HttpFilterTestParam, DisableRequestBodyBufferingOnRoute) { // Set bypass request body buffering for this route. settings.mutable_check_settings()->set_disable_request_body_buffering(bypass); // Initialize the route's per filter config. - auth_per_route = FilterConfigPerRoute(settings); + auth_per_route = std::make_unique(settings); + // Update the mock to return the new pointer. + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(auth_per_route.get())); }; test_disable_request_body_buffering(false); @@ -4159,6 +4240,824 @@ TEST_P(EmitFilterStateTest, PreexistingFilterStateSameTypeMutable) { TEST_P(ExtAuthzLoggingInfoTest, FieldTest) { test(); } +// Test per-route gRPC service override with null server context (fallback to default client) +TEST_P(HttpFilterTestParam, PerRouteGrpcServiceOverrideWithNullServerContext) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - per-route gRPC service only applies to gRPC clients + return; + } + + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("per_route_ext_authz_cluster"); + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + // Set up route to return per-route config + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(per_route_filter_config.get())); + + prepareCheck(); + + // Mock the default client check call (should fall back to default since server context is null) + EXPECT_CALL(*client_, check(_, _, _, _)) + .WillOnce(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks, + const envoy::service::auth::v3::CheckRequest&, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { + Filters::Common::ExtAuthz::Response response{}; + response.status = Filters::Common::ExtAuthz::CheckStatus::OK; + callbacks.onComplete(std::make_unique(response)); + })); + + EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); +} + +// Test per-route configuration merging with context extensions +TEST_P(HttpFilterTestParam, PerRouteConfigurationMergingWithContextExtensions) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - configuration merging applies to gRPC clients + return; + } + + // Create base configuration with context extensions + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + base_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"base_key", "base_value"}); + base_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "base_shared_value"}); + + // Create more specific configuration with context extensions + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute specific_config; + specific_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"specific_key", "specific_value"}); + specific_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "specific_shared_value"}); + + // Test merging using the merge constructor + FilterConfigPerRoute base_filter_config(base_config); + FilterConfigPerRoute specific_filter_config(specific_config); + FilterConfigPerRoute merged_config(base_filter_config, specific_filter_config); + + // Verify merged context extensions + const auto& merged_extensions = merged_config.contextExtensions(); + EXPECT_EQ(merged_extensions.size(), 3); + EXPECT_EQ(merged_extensions.at("base_key"), "base_value"); + EXPECT_EQ(merged_extensions.at("specific_key"), "specific_value"); + EXPECT_EQ(merged_extensions.at("shared_key"), "specific_shared_value"); // More specific wins +} + +// Test per-route configuration merging with gRPC service override +TEST_P(HttpFilterTestParam, PerRouteConfigurationMergingWithGrpcServiceOverride) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - gRPC service override applies to gRPC clients + return; + } + + // Create base configuration without gRPC service + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + base_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"base_key", "base_value"}); + + // Create more specific configuration with gRPC service + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute specific_config; + specific_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("specific_cluster"); + specific_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"specific_key", "specific_value"}); + + // Test merging using the merge constructor + FilterConfigPerRoute base_filter_config(base_config); + FilterConfigPerRoute specific_filter_config(specific_config); + FilterConfigPerRoute merged_config(base_filter_config, specific_filter_config); + + // Verify gRPC service override is from more specific config + EXPECT_TRUE(merged_config.grpcService().has_value()); + EXPECT_EQ(merged_config.grpcService().value().envoy_grpc().cluster_name(), "specific_cluster"); + + // Verify context extensions are merged + const auto& merged_extensions = merged_config.contextExtensions(); + EXPECT_EQ(merged_extensions.size(), 2); + EXPECT_EQ(merged_extensions.at("base_key"), "base_value"); + EXPECT_EQ(merged_extensions.at("specific_key"), "specific_value"); +} + +// Test per-route configuration merging with request body settings +TEST_P(HttpFilterTestParam, PerRouteConfigurationMergingWithRequestBodySettings) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - request body settings apply to gRPC clients + return; + } + + // Create base configuration with request body settings + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + base_config.mutable_check_settings()->mutable_with_request_body()->set_max_request_bytes(1000); + base_config.mutable_check_settings()->mutable_with_request_body()->set_allow_partial_message( + true); + + // Create more specific configuration with different request body settings + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute specific_config; + specific_config.mutable_check_settings()->mutable_with_request_body()->set_max_request_bytes( + 2000); + specific_config.mutable_check_settings()->mutable_with_request_body()->set_allow_partial_message( + false); + + // Test merging using the merge constructor + FilterConfigPerRoute base_filter_config(base_config); + FilterConfigPerRoute specific_filter_config(specific_config); + FilterConfigPerRoute merged_config(base_filter_config, specific_filter_config); + + // Verify request body settings are from more specific config + const auto& merged_check_settings = merged_config.checkSettings(); + EXPECT_TRUE(merged_check_settings.has_with_request_body()); + EXPECT_EQ(merged_check_settings.with_request_body().max_request_bytes(), 2000); + EXPECT_EQ(merged_check_settings.with_request_body().allow_partial_message(), false); +} + +// Test per-route configuration merging with disable_request_body_buffering +TEST_P(HttpFilterTestParam, PerRouteConfigurationMergingWithDisableRequestBodyBuffering) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - disable request body buffering applies to gRPC clients + return; + } + + // Create base configuration without disable_request_body_buffering + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + base_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"base_key", "base_value"}); + + // Create more specific configuration with disable_request_body_buffering + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute specific_config; + specific_config.mutable_check_settings()->set_disable_request_body_buffering(true); + + // Test merging using the merge constructor + FilterConfigPerRoute base_filter_config(base_config); + FilterConfigPerRoute specific_filter_config(specific_config); + FilterConfigPerRoute merged_config(base_filter_config, specific_filter_config); + + // Verify disable_request_body_buffering is from more specific config + const auto& merged_check_settings = merged_config.checkSettings(); + EXPECT_TRUE(merged_check_settings.disable_request_body_buffering()); +} + +// Test per-route configuration merging with multiple levels +TEST_P(HttpFilterTestParam, PerRouteConfigurationMergingMultipleLevels) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - configuration merging applies to gRPC clients + return; + } + + // Create virtual host level configuration + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute vh_config; + vh_config.mutable_check_settings()->mutable_context_extensions()->insert({"vh_key", "vh_value"}); + vh_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "vh_shared_value"}); + + // Create route level configuration + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute route_config; + route_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"route_key", "route_value"}); + route_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "route_shared_value"}); + route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("route_cluster"); + + // Create weighted cluster level configuration + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute wc_config; + wc_config.mutable_check_settings()->mutable_context_extensions()->insert({"wc_key", "wc_value"}); + wc_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "wc_shared_value"}); + + // Test merging from least specific to most specific + FilterConfigPerRoute vh_filter_config(vh_config); + FilterConfigPerRoute route_filter_config(route_config); + FilterConfigPerRoute wc_filter_config(wc_config); + + // First merge: vh + route + FilterConfigPerRoute vh_route_merged(vh_filter_config, route_filter_config); + + // Second merge: (vh + route) + weighted cluster + FilterConfigPerRoute final_merged(vh_route_merged, wc_filter_config); + + // Verify final merged context extensions + const auto& merged_extensions = final_merged.contextExtensions(); + EXPECT_EQ(merged_extensions.size(), 4); + EXPECT_EQ(merged_extensions.at("vh_key"), "vh_value"); + EXPECT_EQ(merged_extensions.at("route_key"), "route_value"); + EXPECT_EQ(merged_extensions.at("wc_key"), "wc_value"); + EXPECT_EQ(merged_extensions.at("shared_key"), "wc_shared_value"); // Most specific wins + + // Verify gRPC service override is NOT inherited from less specific levels. + EXPECT_FALSE(final_merged.grpcService().has_value()); +} + +// Test per-route context extensions take precedence over check_settings context extensions. +TEST_P(HttpFilterTestParam, PerRouteContextExtensionsPrecedence) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test as context extensions apply to gRPC clients. + return; + } + + // Create configuration with context extensions in both places. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + base_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"check_key", "check_value"}); + base_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "check_shared_value"}); + + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute specific_config; + specific_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"specific_check_key", "specific_check_value"}); + specific_config.mutable_check_settings()->mutable_context_extensions()->insert( + {"shared_key", "specific_check_shared_value"}); + + // Test merging using the merge constructor. + FilterConfigPerRoute base_filter_config(base_config); + FilterConfigPerRoute specific_filter_config(specific_config); + FilterConfigPerRoute merged_config(base_filter_config, specific_filter_config); + + // Verify context extensions are properly merged. + const auto& merged_extensions = merged_config.contextExtensions(); + EXPECT_EQ(merged_extensions.size(), 3); + EXPECT_EQ(merged_extensions.at("check_key"), "check_value"); + EXPECT_EQ(merged_extensions.at("specific_check_key"), "specific_check_value"); + EXPECT_EQ(merged_extensions.at("shared_key"), + "specific_check_shared_value"); // More specific wins +} + +// Test per-route Google gRPC service configuration. +TEST_P(HttpFilterTestParam, PerRouteGoogleGrpcServiceConfiguration) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test as per-route gRPC service only applies to gRPC clients. + return; + } + + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_google_grpc() + ->set_target_uri("https://ext-authz.googleapis.com"); + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + // Verify Google gRPC service is properly configured + EXPECT_TRUE(per_route_filter_config->grpcService().has_value()); + EXPECT_TRUE(per_route_filter_config->grpcService().value().has_google_grpc()); + EXPECT_EQ(per_route_filter_config->grpcService().value().google_grpc().target_uri(), + "https://ext-authz.googleapis.com"); +} + +// Test existing functionality still works with new logic. +TEST_P(HttpFilterTestParam, ExistingFunctionalityWithNewLogic) { + // Test that the existing functionality still works with our new per-route merging logic. + prepareCheck(); + + // Mock the default client check call (no per-route config). + EXPECT_CALL(*client_, check(_, _, _, _)) + .WillOnce(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks, + const envoy::service::auth::v3::CheckRequest&, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { + Filters::Common::ExtAuthz::Response response{}; + response.status = Filters::Common::ExtAuthz::CheckStatus::OK; + callbacks.onComplete(std::make_unique(response)); + })); + + EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); +} + +// Test per-route configuration merging with empty configurations. +TEST_P(HttpFilterTestParam, PerRouteConfigurationMergingWithEmptyConfigurations) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test as configuration merging applies to gRPC clients. + return; + } + + // Create empty base configuration. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + + // Create empty specific configuration. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute specific_config; + + // Test merging using the merge constructor. + FilterConfigPerRoute base_filter_config(base_config); + FilterConfigPerRoute specific_filter_config(specific_config); + FilterConfigPerRoute merged_config(base_filter_config, specific_filter_config); + + // Verify merged configuration has empty context extensions. + const auto& merged_extensions = merged_config.contextExtensions(); + EXPECT_EQ(merged_extensions.size(), 0); + + // Verify no gRPC service override + EXPECT_FALSE(merged_config.grpcService().has_value()); +} + +// Test per-route gRPC service configuration merging functionality. +TEST_P(HttpFilterTestParam, PerRouteGrpcServiceMergingWithBaseConfiguration) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test as per-route gRPC service only applies to gRPC clients. + return; + } + + // Create base per-route configuration. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute base_config; + (*base_config.mutable_check_settings()->mutable_context_extensions())["base"] = "value"; + FilterConfigPerRoute base_filter_config(base_config); + + // Create per-route configuration with gRPC service. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("per_route_cluster"); + (*per_route_config.mutable_check_settings()->mutable_context_extensions())["route"] = "override"; + + // Test merging constructor. + FilterConfigPerRoute merged_config(base_filter_config, per_route_config); + + // Verify the merged configuration has the gRPC service from the per-route config. + EXPECT_TRUE(merged_config.grpcService().has_value()); + EXPECT_TRUE(merged_config.grpcService().value().has_envoy_grpc()); + EXPECT_EQ(merged_config.grpcService().value().envoy_grpc().cluster_name(), "per_route_cluster"); + + // Verify that context extensions are properly merged. + const auto& merged_settings = merged_config.checkSettings(); + EXPECT_TRUE(merged_settings.context_extensions().contains("route")); + EXPECT_EQ(merged_settings.context_extensions().at("route"), "override"); +} + +// Test focused integration test to verify per-route configuration is processed correctly. +TEST_P(HttpFilterTestParam, PerRouteConfigurationIntegrationTest) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - per-route gRPC service only applies to gRPC clients. + return; + } + + // This test covers the per-route configuration processing in initiateCall + // which exercises the lines where getAllPerFilterConfig is called and processed. + + // Set up per-route configuration with gRPC service override + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("per_route_cluster"); + + // Add context extensions to test that path too. + (*per_route_config.mutable_check_settings()->mutable_context_extensions())["test_key"] = + "test_value"; + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + // Mock decoder callbacks to return per-route config. + ON_CALL(decoder_filter_callbacks_, mostSpecificPerFilterConfig()) + .WillByDefault(Return(per_route_filter_config.get())); + + // Mock perFilterConfigs to return the per-route config vector. + Router::RouteSpecificFilterConfigs per_route_configs; + per_route_configs.push_back(per_route_filter_config.get()); + ON_CALL(decoder_filter_callbacks_, perFilterConfigs()).WillByDefault(Return(per_route_configs)); + + // Set up basic request headers. + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "https"}, {"host", "example.com"}}; + + prepareCheck(); + + // Create a new filter with server context to enable per-route client creation. + // We'll mock the gRPC client manager to return a controlled mock client. + auto new_client = std::make_unique(); + auto* new_client_ptr = new_client.get(); + auto new_filter = std::make_unique(config_, std::move(new_client), factory_context_); + new_filter->setDecoderFilterCallbacks(decoder_filter_callbacks_); + + // Mock the cluster manager to successfully create a per-route gRPC client + // but use a mock raw gRPC client that we can control. + ON_CALL(factory_context_, clusterManager()).WillByDefault(ReturnRef(cm_)); + auto mock_grpc_client_manager = std::make_shared(); + ON_CALL(cm_, grpcAsyncClientManager()).WillByDefault(ReturnRef(*mock_grpc_client_manager)); + + // Return a mock raw gRPC client for per-route client creation. + auto mock_raw_grpc_client = std::make_shared(); + EXPECT_CALL(*mock_grpc_client_manager, getOrCreateRawAsyncClientWithHashKey(_, _, true)) + .WillOnce(Return(absl::StatusOr(mock_raw_grpc_client))); + + // Mock the sendRaw call with matcher-based validation for the gRPC authorization check. + EXPECT_CALL(*mock_raw_grpc_client, + sendRaw(_, _, + BufferString(AsCheckRequest(HasContextExtension("test_key", "test_value"))), + _, _, _)) + .WillOnce([&](absl::string_view /*service_full_name*/, absl::string_view /*method_name*/, + Buffer::InstancePtr&& /*request*/, Grpc::RawAsyncRequestCallbacks& callbacks, + Tracing::Span& parent_span, + const Http::AsyncClient::RequestOptions& /*options*/) -> Grpc::AsyncRequest* { + // Create and send successful response. + envoy::service::auth::v3::CheckResponse check_response; + check_response.mutable_status()->set_code(Grpc::Status::WellKnownGrpcStatus::Ok); + check_response.mutable_ok_response(); + + std::string serialized_response; + check_response.SerializeToString(&serialized_response); + auto response = std::make_unique(serialized_response); + + callbacks.onSuccessRaw(std::move(response), parent_span); + return nullptr; // No async request handle needed for immediate response. + }); + + // Since we're using the per-route client, the default client should not be called. + EXPECT_CALL(*new_client_ptr, check(_, _, _, _)).Times(0); + + // This exercises the per-route configuration processing logic which includes + // the getAllPerFilterConfig call and per-route gRPC service detection. + EXPECT_EQ(Http::FilterHeadersStatus::Continue, new_filter->decodeHeaders(headers, true)); +} + +// Test per-route gRPC client creation and usage. +TEST_P(HttpFilterTestParam, PerRouteGrpcClientCreationAndUsage) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test as per-route gRPC service only applies to gRPC clients. + return; + } + + // Create per-route configuration with valid gRPC service. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("per_route_ext_authz_cluster"); + + // Add context extensions to test merging. + (*per_route_config.mutable_check_settings()->mutable_context_extensions())["test_key"] = + "test_value"; + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + // Set up route to return per-route config. + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(per_route_filter_config.get())); + + // Mock perFilterConfigs to return the per-route config vector which exercises + // getAllPerFilterConfig. + Router::RouteSpecificFilterConfigs per_route_configs; + per_route_configs.push_back(per_route_filter_config.get()); + ON_CALL(decoder_filter_callbacks_, perFilterConfigs()).WillByDefault(Return(per_route_configs)); + + prepareCheck(); + + // Create a filter with server context for per-route gRPC client creation. + auto new_client = std::make_unique(); + auto* new_client_ptr = new_client.get(); + auto new_filter = std::make_unique(config_, std::move(new_client), factory_context_); + new_filter->setDecoderFilterCallbacks(decoder_filter_callbacks_); + + // Mock successful gRPC async client manager access. + auto mock_grpc_client_manager = std::make_shared(); + ON_CALL(factory_context_, clusterManager()).WillByDefault(ReturnRef(cm_)); + ON_CALL(cm_, grpcAsyncClientManager()).WillByDefault(ReturnRef(*mock_grpc_client_manager)); + + // Mock successful raw gRPC client creation which exercises createPerRouteGrpcClient. + auto mock_raw_grpc_client = std::make_shared(); + EXPECT_CALL(*mock_grpc_client_manager, getOrCreateRawAsyncClientWithHashKey(_, _, true)) + .WillOnce(Return(absl::StatusOr(mock_raw_grpc_client))); + + // Set up expectations for the sendRaw call that will be made by the GrpcClientImpl. + EXPECT_CALL(*mock_raw_grpc_client, sendRaw(_, _, _, _, _, _)) + .WillOnce([](absl::string_view /*service_full_name*/, absl::string_view /*method_name*/, + Buffer::InstancePtr&& /*request*/, Grpc::RawAsyncRequestCallbacks& callbacks, + Tracing::Span& parent_span, + const Http::AsyncClient::RequestOptions& /*options*/) -> Grpc::AsyncRequest* { + envoy::service::auth::v3::CheckResponse check_response; + check_response.mutable_status()->set_code(Grpc::Status::WellKnownGrpcStatus::Ok); + check_response.mutable_ok_response(); + + // Serialize the response to a buffer. + std::string serialized_response; + check_response.SerializeToString(&serialized_response); + auto response = std::make_unique(serialized_response); + + callbacks.onSuccessRaw(std::move(response), parent_span); + return nullptr; // No async request handle needed for immediate response. + }); + + // Since per-route gRPC client creation succeeds, the per-route client should be used + // instead of the default client. We won't see a call to new_client_ptr. + EXPECT_CALL(*new_client_ptr, check(_, _, _, _)).Times(0); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + new_filter->decodeHeaders(request_headers_, false)); +} + +// Test per-route HTTP service configuration parsing. +TEST_P(HttpFilterTestParam, PerRouteHttpServiceConfigurationParsing) { + if (!std::get<1>(GetParam())) { + // Skip gRPC client test as per-route HTTP service only applies to HTTP clients. + return; + } + + // Create per-route configuration with valid HTTP service. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings()->mutable_http_service()->mutable_server_uri()->set_uri( + "https://per-route-ext-authz.example.com"); + per_route_config.mutable_check_settings() + ->mutable_http_service() + ->mutable_server_uri() + ->set_cluster("per_route_http_cluster"); + per_route_config.mutable_check_settings()->mutable_http_service()->set_path_prefix( + "/api/v2/auth"); + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + // Verify the per-route HTTP service configuration is correctly parsed + EXPECT_TRUE(per_route_filter_config->httpService().has_value()); + EXPECT_FALSE(per_route_filter_config->grpcService().has_value()); + + const auto& http_service = per_route_filter_config->httpService().value(); + EXPECT_EQ(http_service.server_uri().uri(), "https://per-route-ext-authz.example.com"); + EXPECT_EQ(http_service.server_uri().cluster(), "per_route_http_cluster"); + EXPECT_EQ(http_service.path_prefix(), "/api/v2/auth"); +} + +// Test error handling when server context is not available for per-route gRPC client. +TEST_P(HttpFilterTestParam, PerRouteGrpcClientCreationNoServerContext) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test - per-route gRPC service only applies to gRPC clients. + return; + } + + // Create per-route configuration with gRPC service. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings() + ->mutable_grpc_service() + ->mutable_envoy_grpc() + ->set_cluster_name("per_route_grpc_cluster"); + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(per_route_filter_config.get())); + + Router::RouteSpecificFilterConfigs per_route_configs; + per_route_configs.push_back(per_route_filter_config.get()); + ON_CALL(decoder_filter_callbacks_, perFilterConfigs()).WillByDefault(Return(per_route_configs)); + + prepareCheck(); + + // Create filter without server context. This should cause per-route client creation to fail. + auto new_client = std::make_unique(); + auto* new_client_ptr = new_client.get(); + auto new_filter = std::make_unique(config_, std::move(new_client)); // No server context + new_filter->setDecoderFilterCallbacks(decoder_filter_callbacks_); + + // Since per-route client creation fails (no server context), should fall back to default client. + EXPECT_CALL(*new_client_ptr, check(_, _, _, _)) + .WillOnce(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks, + const envoy::service::auth::v3::CheckRequest&, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { + // Verify this is using the default client. + auto response = std::make_unique(); + response->status = Filters::Common::ExtAuthz::CheckStatus::OK; + callbacks.onComplete(std::move(response)); + })); + + Http::TestRequestHeaderMapImpl request_headers_{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {"host", "example.com"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + new_filter->decodeHeaders(request_headers_, false)); +} + +// Test error handling when server context is not available for per-route HTTP client. +TEST_P(HttpFilterTestParam, PerRouteHttpClientCreationNoServerContext) { + if (!std::get<1>(GetParam())) { + // Skip gRPC client test as per-route HTTP service only applies to HTTP clients. + return; + } + + // Create per-route configuration with HTTP service. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + per_route_config.mutable_check_settings()->mutable_http_service()->mutable_server_uri()->set_uri( + "https://per-route-ext-authz.example.com"); + per_route_config.mutable_check_settings() + ->mutable_http_service() + ->mutable_server_uri() + ->set_cluster("per_route_http_cluster"); + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(per_route_filter_config.get())); + + Router::RouteSpecificFilterConfigs per_route_configs; + per_route_configs.push_back(per_route_filter_config.get()); + ON_CALL(decoder_filter_callbacks_, perFilterConfigs()).WillByDefault(Return(per_route_configs)); + + prepareCheck(); + + // Create filter without server context. + auto new_client = std::make_unique(); + auto* new_client_ptr = new_client.get(); + auto new_filter = std::make_unique(config_, std::move(new_client)); // No server context + new_filter->setDecoderFilterCallbacks(decoder_filter_callbacks_); + + // Since per-route client creation fails, should fall back to default client. + EXPECT_CALL(*new_client_ptr, check(_, _, _, _)) + .WillOnce(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks, + const envoy::service::auth::v3::CheckRequest&, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { + auto response = std::make_unique(); + response->status = Filters::Common::ExtAuthz::CheckStatus::OK; + callbacks.onComplete(std::move(response)); + })); + + Http::TestRequestHeaderMapImpl request_headers_{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {"host", "example.com"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + new_filter->decodeHeaders(request_headers_, false)); +} + +// Test gRPC client error handling for per-route config. +TEST_F(HttpFilterTest, GrpcClientPerRouteError) { + // Initialize with gRPC client configuration. + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + failure_mode_allow: false + stat_prefix: "ext_authz" + )EOF"); + + prepareCheck(); + + // Create per-route configuration with gRPC service override. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + auto* grpc_service = per_route_config.mutable_check_settings()->mutable_grpc_service(); + grpc_service->mutable_envoy_grpc()->set_cluster_name("nonexistent_cluster"); + + FilterConfigPerRoute per_route_filter_config(per_route_config); + + // Set up route config to use the per-route configuration. + ON_CALL(decoder_filter_callbacks_, mostSpecificPerFilterConfig()) + .WillByDefault(Return(&per_route_filter_config)); + + // Since cluster doesn't exist, per-route client creation should fail + // and we'll use the default client instead. + EXPECT_CALL(*client_, check(_, _, _, _)) + .WillOnce(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks, + const envoy::service::auth::v3::CheckRequest&, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { + Filters::Common::ExtAuthz::Response response{}; + response.status = Filters::Common::ExtAuthz::CheckStatus::OK; + callbacks.onComplete(std::make_unique(response)); + })); + + // Verify filter processes the request with the default client. + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {"host", "example.com"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); +} + +// Test HTTP client with per-route configuration. +TEST_F(HttpFilterTest, HttpClientPerRouteOverride) { + // Initialize with HTTP client configuration. + initialize(R"EOF( + http_service: + server_uri: + uri: "https://ext-authz.example.com" + cluster: "ext_authz_server" + path_prefix: "/api/v1/auth" + failure_mode_allow: false + stat_prefix: "ext_authz" + )EOF"); + + prepareCheck(); + + // Create per-route configuration with HTTP service override. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + auto* http_service = per_route_config.mutable_check_settings()->mutable_http_service(); + http_service->mutable_server_uri()->set_uri("https://per-route-ext-authz.example.com"); + http_service->mutable_server_uri()->set_cluster("per_route_http_cluster"); + http_service->set_path_prefix("/api/v2/auth"); + + FilterConfigPerRoute per_route_filter_config(per_route_config); + + // Set up route config to use the per-route configuration. + ON_CALL(decoder_filter_callbacks_, mostSpecificPerFilterConfig()) + .WillByDefault(Return(&per_route_filter_config)); + + // Set up a check expectation that will be satisfied by the default client. + EXPECT_CALL(*client_, check(_, _, _, _)) + .WillOnce(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks, + const envoy::service::auth::v3::CheckRequest&, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { + Filters::Common::ExtAuthz::Response response{}; + response.status = Filters::Common::ExtAuthz::CheckStatus::OK; + callbacks.onComplete(std::make_unique(response)); + })); + + // Verify filter processes the request. + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {"host", "example.com"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); +} + +// Test invalid response header validation via response_headers_to_add. +TEST_F(InvalidMutationTest, InvalidResponseHeadersToAddName) { + Filters::Common::ExtAuthz::Response r; + r.status = Filters::Common::ExtAuthz::CheckStatus::OK; + r.response_headers_to_add = {{"invalid header name", "value"}}; + testResponse(r); +} + +// Test invalid response header validation via response_headers_to_add value. +TEST_F(InvalidMutationTest, InvalidResponseHeadersToAddValue) { + Filters::Common::ExtAuthz::Response r; + r.status = Filters::Common::ExtAuthz::CheckStatus::OK; + r.response_headers_to_add = {{"valid-name", getInvalidValue()}}; + testResponse(r); +} + +// Test per-route timeout configuration is correctly used in gRPC client creation. +TEST_P(HttpFilterTestParam, PerRouteGrpcClientTimeoutConfiguration) { + if (std::get<1>(GetParam())) { + // Skip HTTP client test as per-route gRPC service only applies to gRPC clients. + return; + } + + // Create per-route configuration with custom timeout. + envoy::extensions::filters::http::ext_authz::v3::ExtAuthzPerRoute per_route_config; + auto* grpc_service = per_route_config.mutable_check_settings()->mutable_grpc_service(); + grpc_service->mutable_envoy_grpc()->set_cluster_name("per_route_grpc_cluster"); + grpc_service->mutable_timeout()->set_seconds(30); // Custom 30s timeout + + std::unique_ptr per_route_filter_config = + std::make_unique(per_route_config); + + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(Return(per_route_filter_config.get())); + + Router::RouteSpecificFilterConfigs per_route_configs; + per_route_configs.push_back(per_route_filter_config.get()); + ON_CALL(decoder_filter_callbacks_, perFilterConfigs()).WillByDefault(Return(per_route_configs)); + + prepareCheck(); + + auto new_client = std::make_unique(); + auto* new_client_ptr = new_client.get(); + auto new_filter = std::make_unique(config_, std::move(new_client), factory_context_); + new_filter->setDecoderFilterCallbacks(decoder_filter_callbacks_); + + // Mock gRPC client manager. + auto mock_grpc_client_manager = std::make_shared(); + ON_CALL(factory_context_, clusterManager()).WillByDefault(ReturnRef(cm_)); + ON_CALL(cm_, grpcAsyncClientManager()).WillByDefault(ReturnRef(*mock_grpc_client_manager)); + + auto mock_raw_grpc_client = std::make_shared(); + EXPECT_CALL(*mock_grpc_client_manager, getOrCreateRawAsyncClientWithHashKey(_, _, true)) + .WillOnce(Return(absl::StatusOr(mock_raw_grpc_client))); + + // Mock the sendRaw call with matcher-based validation for timeout verification. + EXPECT_CALL(*mock_raw_grpc_client, sendRaw(_, _, _, _, _, HasTimeout(30000))) + .WillOnce([](absl::string_view /*service_full_name*/, absl::string_view /*method_name*/, + Buffer::InstancePtr&& /*request*/, Grpc::RawAsyncRequestCallbacks& callbacks, + Tracing::Span& parent_span, + const Http::AsyncClient::RequestOptions& /*options*/) -> Grpc::AsyncRequest* { + envoy::service::auth::v3::CheckResponse check_response; + check_response.mutable_status()->set_code(Grpc::Status::WellKnownGrpcStatus::Ok); + check_response.mutable_ok_response(); + + std::string serialized_response; + check_response.SerializeToString(&serialized_response); + auto response = std::make_unique(serialized_response); + + callbacks.onSuccessRaw(std::move(response), parent_span); + return nullptr; + }); + + EXPECT_CALL(*new_client_ptr, check(_, _, _, _)).Times(0); + + Http::TestRequestHeaderMapImpl request_headers_{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {"host", "example.com"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + new_filter->decodeHeaders(request_headers_, false)); +} + } // namespace } // namespace ExtAuthz } // namespace HttpFilters diff --git a/test/extensions/filters/http/ext_authz/logging_test_filter.cc b/test/extensions/filters/http/ext_authz/logging_test_filter.cc index 3222cfceb4754..5b33ab7079459 100644 --- a/test/extensions/filters/http/ext_authz/logging_test_filter.cc +++ b/test/extensions/filters/http/ext_authz/logging_test_filter.cc @@ -123,7 +123,7 @@ class LoggingTestFilter : public Http::PassThroughFilter { const bool expect_stats_; const bool expect_envoy_grpc_specific_stats_; const bool expect_response_bytes_; - const absl::optional filter_metadata_; + const absl::optional filter_metadata_; // The gRPC status returned by the authorization server when it is making a gRPC call. const LoggingTestFilterConfig::GrpcStatus expect_grpc_status_; }; diff --git a/test/extensions/filters/http/ext_proc/BUILD b/test/extensions/filters/http/ext_proc/BUILD index 10b362abc390f..3071935302330 100644 --- a/test/extensions/filters/http/ext_proc/BUILD +++ b/test/extensions/filters/http/ext_proc/BUILD @@ -206,6 +206,7 @@ envoy_extension_cc_test( "@envoy_api//envoy/extensions/filters/http/set_metadata/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/upstream_codec/v3:pkg_cc_proto", "@envoy_api//envoy/service/ext_proc/v3:pkg_cc_proto", + "@envoy_api//envoy/type/v3:pkg_cc_proto", "@ocp//ocpdiag/core/testing:status_matchers", ], ) diff --git a/test/extensions/filters/http/ext_proc/config_test.cc b/test/extensions/filters/http/ext_proc/config_test.cc index cafb704ecca08..02ae87c54f224 100644 --- a/test/extensions/filters/http/ext_proc/config_test.cc +++ b/test/extensions/filters/http/ext_proc/config_test.cc @@ -256,54 +256,6 @@ TEST(HttpExtProcConfigTest, InvalidFullDuplexStreamedConfig) { "then the request_trailer_mode has to be set to SEND"); } -TEST(HttpExtProcConfigTest, InvalidRequestFullDuplexStreamedFailureModeAllowConfig) { - std::string yaml = R"EOF( - grpc_service: - envoy_grpc: - cluster_name: ext_proc_server - failure_mode_allow: true - processing_mode: - request_body_mode: FULL_DUPLEX_STREAMED - request_trailer_mode: SEND - )EOF"; - - ExternalProcessingFilterConfig factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); - - testing::NiceMock context; - auto result = factory.createFilterFactoryFromProto(*proto_config, "stats", context); - EXPECT_FALSE(result.ok()); - EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); - EXPECT_EQ(result.status().message(), - "If the ext_proc filter has either the request_body_mode or the response_body_mode set " - "to FULL_DUPLEX_STREAMED, then the failure_mode_allow has to be left as false"); -} - -TEST(HttpExtProcConfigTest, InvalidResponseFullDuplexStreamedFailureModeAllowConfig) { - std::string yaml = R"EOF( - grpc_service: - envoy_grpc: - cluster_name: ext_proc_server - failure_mode_allow: true - processing_mode: - response_body_mode: FULL_DUPLEX_STREAMED - response_trailer_mode: SEND - )EOF"; - - ExternalProcessingFilterConfig factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); - - testing::NiceMock context; - auto result = factory.createFilterFactoryFromProto(*proto_config, "stats", context); - EXPECT_FALSE(result.ok()); - EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); - EXPECT_EQ(result.status().message(), - "If the ext_proc filter has either the request_body_mode or the response_body_mode set " - "to FULL_DUPLEX_STREAMED, then the failure_mode_allow has to be left as false"); -} - TEST(HttpExtProcConfigTest, GrpcServiceHttpServiceBothSet) { std::string yaml = R"EOF( grpc_service: @@ -567,6 +519,50 @@ TEST(HttpExtProcConfigTest, FullDuplexStreamedValidation) { EXPECT_TRUE(other_result.ok()); } +TEST(HttpExtProcConfigTest, StatusOnErrorConfig) { + std::string yaml = R"EOF( + grpc_service: + google_grpc: + target_uri: ext_proc_server + stat_prefix: google + status_on_error: + code: 503 + )EOF"; + + ExternalProcessingFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyConfigProto(); + TestUtility::loadFromYaml(yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + Http::FilterFactoryCb cb = + factory.createFilterFactoryFromProto(*proto_config, "stats", context).value(); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); +} + +TEST(HttpExtProcConfigTest, StatusOnErrorDefaultConfig) { + std::string yaml = R"EOF( + grpc_service: + google_grpc: + target_uri: ext_proc_server + stat_prefix: google + )EOF"; + + ExternalProcessingFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyConfigProto(); + TestUtility::loadFromYaml(yaml, *proto_config); + + testing::NiceMock context; + EXPECT_CALL(context, messageValidationVisitor()); + Http::FilterFactoryCb cb = + factory.createFilterFactoryFromProto(*proto_config, "stats", context).value(); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); +} + } // namespace } // namespace ExternalProcessing } // namespace HttpFilters diff --git a/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc b/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc index 0d16ea738a721..d81503a53a83a 100644 --- a/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc +++ b/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc @@ -9,12 +9,14 @@ #include "envoy/extensions/filters/http/upstream_codec/v3/upstream_codec.pb.h" #include "envoy/network/address.h" #include "envoy/service/ext_proc/v3/external_processor.pb.h" +#include "envoy/type/v3/http_status.pb.h" #include "source/common/json/json_loader.h" #include "source/extensions/filters/http/ext_proc/config.h" #include "source/extensions/filters/http/ext_proc/ext_proc.h" #include "source/extensions/filters/http/ext_proc/on_processing_response.h" +#include "test/common/grpc/grpc_client_integration.h" #include "test/common/http/common.h" #include "test/extensions/filters/http/ext_proc/logging_test_filter.pb.h" #include "test/extensions/filters/http/ext_proc/logging_test_filter.pb.validate.h" @@ -39,8 +41,8 @@ using envoy::extensions::filters::http::ext_proc::v3::ExtProcPerRoute; using envoy::extensions::filters::http::ext_proc::v3::ProcessingMode; using envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager; using Envoy::Extensions::HttpFilters::ExternalProcessing::verifyMultipleHeaderValues; +using Envoy::Protobuf::Any; using Envoy::Protobuf::MapPair; -using Envoy::ProtobufWkt::Any; using envoy::service::ext_proc::v3::BodyResponse; using envoy::service::ext_proc::v3::CommonResponse; using envoy::service::ext_proc::v3::HeadersResponse; @@ -53,14 +55,14 @@ using envoy::service::ext_proc::v3::ProcessingResponse; using envoy::service::ext_proc::v3::ProtocolConfiguration; using envoy::service::ext_proc::v3::TrailersResponse; using Extensions::HttpFilters::ExternalProcessing::DEFAULT_DEFERRED_CLOSE_TIMEOUT_MS; -using Extensions::HttpFilters::ExternalProcessing::HasNoHeader; using Extensions::HttpFilters::ExternalProcessing::HeaderProtosEqual; using Extensions::HttpFilters::ExternalProcessing::makeHeaderValue; using Extensions::HttpFilters::ExternalProcessing::OnProcessingResponseFactory; -using Extensions::HttpFilters::ExternalProcessing::SingleHeaderValueIs; using Extensions::HttpFilters::ExternalProcessing::TestOnProcessingResponseFactory; using Http::LowerCaseString; using test::integration::filters::LoggingTestFilterConfig; +using testing::_; +using testing::Not; using namespace std::chrono_literals; @@ -187,7 +189,7 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, auto* untyped_md = set_metadata_config.add_metadata(); untyped_md->set_metadata_namespace("forwarding_ns_untyped"); untyped_md->set_allow_overwrite(true); - ProtobufWkt::Struct test_md_val; + Protobuf::Struct test_md_val; (*test_md_val.mutable_fields())["foo"].set_string_value("value from set_metadata"); (*untyped_md->mutable_value()) = test_md_val; @@ -254,7 +256,7 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, processing_response_factory_registration_ = std::make_unique>( *processing_response_factory_); - ProtobufWkt::Struct config; + Protobuf::Struct config; proto_config_.mutable_on_processing_response()->set_name("test-on-processing-response"); proto_config_.mutable_on_processing_response()->mutable_typed_config()->PackFrom(config); } @@ -360,8 +362,8 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, void verifyChunkedEncoding(const Http::RequestOrResponseHeaderMap& headers) { EXPECT_EQ(headers.ContentLength(), nullptr); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().TransferEncoding, - Http::Headers::get().TransferEncodingValues.Chunked)); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().TransferEncoding, + Http::Headers::get().TransferEncodingValues.Chunked)); } void handleUpstreamRequestWithTrailer() { @@ -721,10 +723,10 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, } void testSendDyanmicMetadata() { - ProtobufWkt::Struct test_md_struct; + Protobuf::Struct test_md_struct; (*test_md_struct.mutable_fields())["foo"].set_string_value("value from ext_proc"); - ProtobufWkt::Value md_val; + Protobuf::Value md_val; *(md_val.mutable_struct_value()) = test_md_struct; processGenericMessage( @@ -732,7 +734,7 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, [md_val](const ProcessingRequest& req, ProcessingResponse& resp) { // Verify the processing request contains the untyped metadata we injected. EXPECT_TRUE(req.metadata_context().filter_metadata().contains("forwarding_ns_untyped")); - const ProtobufWkt::Struct& fwd_metadata = + const Protobuf::Struct& fwd_metadata = req.metadata_context().filter_metadata().at("forwarding_ns_untyped"); EXPECT_EQ(1, fwd_metadata.fields_size()); EXPECT_TRUE(fwd_metadata.fields().contains("foo")); @@ -741,7 +743,7 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, // Verify the processing request contains the typed metadata we injected. EXPECT_TRUE( req.metadata_context().typed_filter_metadata().contains("forwarding_ns_typed")); - const ProtobufWkt::Any& fwd_typed_metadata = + const Protobuf::Any& fwd_typed_metadata = req.metadata_context().typed_filter_metadata().at("forwarding_ns_typed"); EXPECT_EQ("type.googleapis.com/envoy.extensions.filters.http.set_metadata.v3.Metadata", fwd_typed_metadata.type_url()); @@ -788,9 +790,7 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, verifyDownstreamResponse(*response, 200); } - IntegrationStreamDecoderPtr initAndSendDataDuplexStreamedMode(absl::string_view body_sent, - bool end_of_stream, - bool both_direction = false) { + void initializeConfigDuplexStreamed(bool both_direction = false) { config_helper_.setBufferLimits(1024, 1024); auto* processing_mode = proto_config_.mutable_processing_mode(); processing_mode->set_request_header_mode(ProcessingMode::SEND); @@ -806,6 +806,12 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, initializeConfig(); HttpIntegrationTest::initialize(); + } + + IntegrationStreamDecoderPtr initAndSendDataDuplexStreamedMode(absl::string_view body_sent, + bool end_of_stream, + bool both_direction = false) { + initializeConfigDuplexStreamed(both_direction); codec_client_ = makeHttpConnection(lookupPort("http")); Http::TestRequestHeaderMapImpl default_headers; HttpTestUtility::addDefaultHeaders(default_headers); @@ -879,7 +885,7 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, } void serverSendBodyRespDuplexStreamed(uint32_t total_resp_body_msg, bool end_of_stream = true, - bool response = false) { + bool response = false, absl::string_view body_sent = "") { for (uint32_t i = 0; i < total_resp_body_msg; i++) { ProcessingResponse response_body; BodyResponse* body_resp; @@ -891,7 +897,11 @@ class ExtProcIntegrationTest : public HttpIntegrationTest, auto* body_mut = body_resp->mutable_response()->mutable_body_mutation(); auto* streamed_response = body_mut->mutable_streamed_response(); - streamed_response->set_body("r"); + if (!body_sent.empty()) { + streamed_response->set_body(body_sent); + } else { + streamed_response->set_body("r"); + } if (end_of_stream) { const bool end_of_stream = (i == total_resp_body_msg - 1) ? true : false; streamed_response->set_end_of_stream(end_of_stream); @@ -942,6 +952,7 @@ TEST_P(ExtProcIntegrationTest, GetAndCloseStream) { } TEST_P(ExtProcIntegrationTest, GetAndCloseStreamWithTracing) { + proto_config_.mutable_message_timeout()->set_seconds(1); // Turn on debug to troubleshoot possible flaky test. // TODO(cainelli): Remove this and the debug logs in the tracer test filter after a test failure // occurs. @@ -1226,7 +1237,7 @@ TEST_P(ExtProcIntegrationTest, OnlyRequestHeadersResetOnServerMessage) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); EXPECT_EQ(upstream_request_->bodyLength(), 4); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1269,7 +1280,7 @@ TEST_P(ExtProcIntegrationTest, OnlyRequestHeadersGracefulClose) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); EXPECT_EQ(upstream_request_->bodyLength(), 4); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1318,7 +1329,7 @@ TEST_P(ExtProcIntegrationTest, OnlyRequestHeadersServerHalfClosesFirst) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); EXPECT_EQ(upstream_request_->bodyLength(), 4); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1391,8 +1402,8 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeaders) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-remove-this", _))); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1479,7 +1490,7 @@ TEST_P(ExtProcIntegrationTest, SetHostHeaderRoutingSucceeded) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); // Host header is updated when `allow_all_routing` mutation rule is true. - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs(":authority", "new_host")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(":authority", "new_host")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1584,12 +1595,12 @@ TEST_P(ExtProcIntegrationTest, GetAndSetPathHeader) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); // Path header is updated. - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs(":path", "/mutated_path/bluh")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(":path", "/mutated_path/bluh")); // Routing headers are not updated by ext_proc when `allow_all_routing` mutation rule is false // (default value). - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs(":scheme", "http")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs(":authority", "host")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs(":method", "GET")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(":scheme", "http")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(":authority", "host")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(":method", "GET")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1626,7 +1637,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersWithLogging) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1676,7 +1687,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersNonUtf8WithValueInString) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-bad-utf8")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-bad-utf8", _))); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -1738,9 +1749,9 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersNonUtf8WithValueInBytes) { ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-bad-utf8")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-bad-utf8", _))); EXPECT_THAT(upstream_request_->headers(), - SingleHeaderValueIs("x-new-utf8", "valid_prefix\303(valid_suffix")); + ContainsHeader("x-new-utf8", "valid_prefix\303(valid_suffix")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); verifyDownstreamResponse(*response, 200); } @@ -1963,7 +1974,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponse) { }); verifyDownstreamResponse(*response, 201); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1")); + EXPECT_THAT(response->headers(), ContainsHeader("x-response-processed", "1")); // Verify that the response processor added headers to dynamic metadata verifyMultipleHeaderValues( response->headers(), @@ -1998,7 +2009,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponseBadStatus) { // Invalid status code should be ignored, but the other header mutation // should still have been processed. verifyDownstreamResponse(*response, 200); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1")); + EXPECT_THAT(response->headers(), ContainsHeader("x-response-processed", "1")); } // Test the filter using the default configuration by connecting to @@ -2033,7 +2044,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponseTwoStatuses) { // Invalid status code should be ignored, but the other header mutation // should still have been processed. verifyDownstreamResponse(*response, 201); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1")); + EXPECT_THAT(response->headers(), ContainsHeader("x-response-processed", "1")); } // Test the filter using the default configuration by connecting to @@ -2064,11 +2075,10 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersAndTrailersOnResponse) { verifyDownstreamResponse(*response, 200); ASSERT_TRUE(response->trailers()); - EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-test-trailers", "Yes")); - EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-modified-trailers", "xxx")); - EXPECT_THAT( - response->headers(), - SingleHeaderValueIs("envoy-test-ext_proc-response_trailers_response", "x-modified-trailers")); + EXPECT_THAT(*(response->trailers()), ContainsHeader("x-test-trailers", "Yes")); + EXPECT_THAT(*(response->trailers()), ContainsHeader("x-modified-trailers", "xxx")); + EXPECT_THAT(response->headers(), ContainsHeader("envoy-test-ext_proc-response_trailers_response", + "x-modified-trailers")); } // Test the filter using the default configuration by connecting to @@ -2123,8 +2133,8 @@ TEST_P(ExtProcIntegrationTest, GetAndSetOnlyTrailersOnResponse) { verifyDownstreamResponse(*response, 200); ASSERT_TRUE(response->trailers()); - EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-test-trailers", "Yes")); - EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-modified-trailers", "xxx")); + EXPECT_THAT(*(response->trailers()), ContainsHeader("x-test-trailers", "Yes")); + EXPECT_THAT(*(response->trailers()), ContainsHeader("x-modified-trailers", "xxx")); } // Test the filter with a response body callback enabled using an @@ -2165,13 +2175,12 @@ TEST_P(ExtProcIntegrationTest, GetAndSetBodyAndHeadersOnResponse) { }); verifyDownstreamResponse(*response, 200); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-testing-response-header", "Yes")); + EXPECT_THAT(response->headers(), ContainsHeader("x-testing-response-header", "Yes")); // Verify that the content length header in the response is set by external processor, EXPECT_EQ(response->headers().getContentLengthValue(), "13"); EXPECT_EQ("Hello, World!", response->body()); - EXPECT_THAT( - response->headers(), - SingleHeaderValueIs("envoy-test-ext_proc-response_headers_response", "content-length")); + EXPECT_THAT(response->headers(), + ContainsHeader("envoy-test-ext_proc-response_headers_response", "content-length")); } TEST_P(ExtProcIntegrationTest, GetAndSetBodyOnResponse) { @@ -2230,7 +2239,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetBodyAndHeadersOnResponsePartialBuffered) verifyDownstreamResponse(*response, 200); // Verify that the content length header is removed in BUFFERED_PARTIAL BodySendMode. EXPECT_EQ(response->headers().ContentLength(), nullptr); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-testing-response-header", "Yes")); + EXPECT_THAT(response->headers(), ContainsHeader("x-testing-response-header", "Yes")); } // Test the filter with a response body callback enabled using an @@ -2267,8 +2276,8 @@ TEST_P(ExtProcIntegrationTest, GetAndSetBodyAndHeadersAndTrailersOnResponse) { verifyDownstreamResponse(*response, 200); ASSERT_TRUE(response->trailers()); - EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-test-trailers", "Yes")); - EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-modified-trailers", "xxx")); + EXPECT_THAT(*(response->trailers()), ContainsHeader("x-test-trailers", "Yes")); + EXPECT_THAT(*(response->trailers()), ContainsHeader("x-modified-trailers", "xxx")); } // Test the filter using a configuration that sends response headers and trailers, @@ -2330,7 +2339,7 @@ TEST_P(ExtProcIntegrationTest, GetAndSetBodyAndHeadersOnBigResponse) { }); verifyDownstreamResponse(*response, 200); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-testing-response-header", "Yes")); + EXPECT_THAT(response->headers(), ContainsHeader("x-testing-response-header", "Yes")); } // Test the filter with both body callbacks enabled and have the @@ -2401,7 +2410,7 @@ TEST_P(ExtProcIntegrationTest, ProcessingModeResponseOnly) { }); verifyDownstreamResponse(*response, 200); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1")); + EXPECT_THAT(response->headers(), ContainsHeader("x-response-processed", "1")); } // Test the filter using the default configuration by connecting to @@ -2432,8 +2441,8 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediately) { EXPECT_TRUE(processor_stream_->waitForReset()); verifyDownstreamResponse(*response, 401); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-failure-reason", "testing")); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("content-type", "application/json")); + EXPECT_THAT(response->headers(), ContainsHeader("x-failure-reason", "testing")); + EXPECT_THAT(response->headers(), ContainsHeader("content-type", "application/json")); EXPECT_EQ("{\"reason\": \"Not authorized\"}", response->body()); } @@ -2466,8 +2475,8 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyGracefulClose) { processor_stream_->finishGrpcStream(Grpc::Status::Ok); verifyDownstreamResponse(*response, 401); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-failure-reason", "testing")); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("content-type", "application/json")); + EXPECT_THAT(response->headers(), ContainsHeader("x-failure-reason", "testing")); + EXPECT_THAT(response->headers(), ContainsHeader("content-type", "application/json")); EXPECT_EQ("{\"reason\": \"Not authorized\"}", response->body()); if (IsEnvoyGrpc()) { // There should be no resets @@ -2503,8 +2512,8 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyGracefulCloseNoServerTrai // However server fails to send trailers verifyDownstreamResponse(*response, 401); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-failure-reason", "testing")); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("content-type", "application/json")); + EXPECT_THAT(response->headers(), ContainsHeader("x-failure-reason", "testing")); + EXPECT_THAT(response->headers(), ContainsHeader("content-type", "application/json")); EXPECT_EQ("{\"reason\": \"Not authorized\"}", response->body()); // Since the server did not send trailers, gRPC client will reset the stream after remote close @@ -2534,8 +2543,8 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyWithLogging) { }); verifyDownstreamResponse(*response, 401); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-failure-reason", "testing")); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("content-type", "application/json")); + EXPECT_THAT(response->headers(), ContainsHeader("x-failure-reason", "testing")); + EXPECT_THAT(response->headers(), ContainsHeader("content-type", "application/json")); EXPECT_EQ("{\"reason\": \"Not authorized\"}", response->body()); } @@ -2602,9 +2611,9 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyOnStreamedRequestBody) { verifyDownstreamResponse(*response, 400); EXPECT_EQ("{\"reason\": \"Request too evil\"}", response->body()); // The previously added request header is not sent to the client. - EXPECT_THAT(response->headers(), HasNoHeader("foo")); + EXPECT_THAT(response->headers(), Not(ContainsHeader("foo", _))); EXPECT_THAT(response->headers(), - SingleHeaderValueIs("envoy-test-ext_proc-request_headers_response", "foo")); + ContainsHeader("envoy-test-ext_proc-request_headers_response", "foo")); } // Test immediate_response behavior with STREAMED response body. @@ -2636,7 +2645,7 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyOnStreamedResponseBody) { verifyDownstreamResponse(*response, 400); EXPECT_EQ("{\"reason\": \"Response too evil\"}", response->body()); // The previously added response header is not sent to the client. - EXPECT_THAT(response->headers(), HasNoHeader("foo")); + EXPECT_THAT(response->headers(), Not(ContainsHeader("foo", _))); } // Test the filter with request body buffering enabled using @@ -2798,7 +2807,7 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyWithSystemHeaderMutation) }); verifyDownstreamResponse(*response, 401); // The added system header is not sent to the client. - EXPECT_THAT(response->headers(), HasNoHeader(":foo")); + EXPECT_THAT(response->headers(), Not(ContainsHeader(":foo", _))); } // Test the filter using an ext_proc server that responds to the request_header message @@ -2818,7 +2827,7 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyWithEnvoyHeaderMutation) hdr->mutable_header()->set_raw_value("bar"); }); verifyDownstreamResponse(*response, 401); - EXPECT_THAT(response->headers(), HasNoHeader("x-envoy-foo")); + EXPECT_THAT(response->headers(), Not(ContainsHeader("x-envoy-foo", _))); } TEST_P(ExtProcIntegrationTest, GetAndImmediateRespondMutationAllowEnvoy) { @@ -2841,8 +2850,8 @@ TEST_P(ExtProcIntegrationTest, GetAndImmediateRespondMutationAllowEnvoy) { }); verifyDownstreamResponse(*response, 401); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("host", "test")); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-envoy-foo", "bar")); + EXPECT_THAT(response->headers(), ContainsHeader("host", "test")); + EXPECT_THAT(response->headers(), ContainsHeader("x-envoy-foo", "bar")); } // Test the filter with request body buffering enabled using @@ -2918,8 +2927,8 @@ TEST_P(ExtProcIntegrationTest, ConvertGetToPost) { handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs(":method", "POST")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("content-type", "text/plain")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(":method", "POST")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("content-type", "text/plain")); EXPECT_EQ(upstream_request_->bodyLength(), 14); EXPECT_EQ(upstream_request_->body().toString(), "Hello, Server!"); @@ -3559,7 +3568,7 @@ TEST_P(ExtProcIntegrationTest, PerRouteGrpcService) { return true; }); verifyDownstreamResponse(*response, 201); - EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1")); + EXPECT_THAT(response->headers(), ContainsHeader("x-response-processed", "1")); } // Set up per-route configuration that extends original metadata. @@ -3638,8 +3647,8 @@ TEST_P(ExtProcIntegrationTest, RequestAndResponseMessageNewTimeoutWithHeaderMuta ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-remove-this", _))); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -4441,7 +4450,7 @@ TEST_P(ExtProcIntegrationTest, SendAndReceiveDynamicMetadataObservabilityMode) { ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(response->complete()); // No headers from dynamic metadata response as the response is ignored in observability mode. - EXPECT_THAT(response->headers(), HasNoHeader(Http::LowerCaseString("receiving_ns_untyped.foo"))); + EXPECT_THAT(response->headers(), Not(ContainsHeader("receiving_ns_untyped.foo", _))); verifyDownstreamResponse(*response, 200); } @@ -4587,8 +4596,8 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersUpstream) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-remove-this", _))); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -4765,7 +4774,7 @@ TEST_P(ExtProcIntegrationTest, ObservabilityModeWithHeader) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); // Header mutation response has been ignored. - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-remove-this", _))); Http::TestResponseHeaderMapImpl response_headers = Http::TestResponseHeaderMapImpl{{":status", "200"}}; @@ -4876,7 +4885,7 @@ TEST_P(ExtProcIntegrationTest, ObservabilityModeWithTrailer) { }); verifyDownstreamResponse(*response, 200); - EXPECT_THAT(*(response->trailers()), HasNoHeader("x-modified-trailers")); + EXPECT_THAT(*(response->trailers()), Not(ContainsHeader("x-modified-trailers", _))); timeSystem().advanceTimeWaitImpl(std::chrono::milliseconds(DEFAULT_DEFERRED_CLOSE_TIMEOUT_MS)); } @@ -5324,8 +5333,8 @@ TEST_P(ExtProcIntegrationTest, SendBodyBeforeHeaderRespStreamedBasicTest) { ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-remove-this", _))); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), "replaced body"); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); upstream_request_->encodeData(100, true); @@ -5434,7 +5443,7 @@ TEST_P(ExtProcIntegrationTest, ServerWaitForBodyBeforeSendsHeaderRespStreamedTes } handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), body_upstream); verifyDownstreamResponse(*response, 200); } @@ -5510,11 +5519,50 @@ TEST_P(ExtProcIntegrationTest, ServerWaitForBodyBeforeSendsHeaderRespDuplexStrea serverSendBodyRespDuplexStreamed(total_resp_body_msg); handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), body_upstream); verifyDownstreamResponse(*response, 200); } +TEST_P(ExtProcIntegrationTest, LargeBodyTestDuplexStreamed) { + const std::string body_sent(2 * 1024 * 1024, 's'); + initializeConfigDuplexStreamed(false); + + // Sends 30 consecutive request, each carrying 2MB data. + for (int i = 0; i < 30; i++) { + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl default_headers; + HttpTestUtility::addDefaultHeaders(default_headers); + + std::pair encoder_decoder = + codec_client_->startRequest(default_headers); + request_encoder_ = &encoder_decoder.first; + IntegrationStreamDecoderPtr response = std::move(encoder_decoder.second); + codec_client_->sendData(*request_encoder_, body_sent, true); + // The ext_proc server receives the headers. + ProcessingRequest header_request; + serverReceiveHeaderDuplexStreamed(header_request); + // The ext_proc server receives the body. + uint32_t total_req_body_msg = serverReceiveBodyDuplexStreamed(body_sent); + EXPECT_GT(total_req_body_msg, 0); + // The ext_proc server sends back the header response. + serverSendHeaderRespDuplexStreamed(); + // The ext_proc server sends back body responses, which include 50 chunks, + // and each chunk contains 64KB data, thus totally ~3MB per request. + uint32_t total_resp_body_msg = 50; + const std::string body_response(64 * 1024, 'r'); + const std::string body_upstream(total_resp_body_msg * 64 * 1024, 'r'); + serverSendBodyRespDuplexStreamed(total_resp_body_msg, /*end_of_stream*/ true, + /*response*/ false, body_response); + + handleUpstreamRequest(); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); + EXPECT_EQ(upstream_request_->body().toString(), body_upstream); + verifyDownstreamResponse(*response, 200); + TearDown(); + } +} + // Buffer the whole message including header, body and trailer before sending response. TEST_P(ExtProcIntegrationTest, ServerWaitForBodyAndTrailerBeforeSendsHeaderRespDuplexStreamedSmallBody) { @@ -5558,7 +5606,7 @@ TEST_P(ExtProcIntegrationTest, serverSendTrailerRespDuplexStreamed(); handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), body_upstream); verifyDownstreamResponse(*response, 200); } @@ -5638,7 +5686,7 @@ TEST_P(ExtProcIntegrationTest, ServerSendBodyRespWithouRecvEntireBodyDuplexStrea serverSendTrailerRespDuplexStreamed(); handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), body_upstream); verifyDownstreamResponse(*response, 200); } @@ -5659,7 +5707,7 @@ TEST_P(ExtProcIntegrationTest, DuplexStreamedInBothDirection) { serverSendBodyRespDuplexStreamed(total_resp_body_msg); handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), body_upstream); // The ext_proc server receives the responses from backend server. @@ -5738,7 +5786,7 @@ TEST_P(ExtProcIntegrationTest, DuplexStreamedServerResponseWithSynthesizedTraile serverSendTrailerRespDuplexStreamed(); handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), body_upstream); EXPECT_EQ(upstream_request_->trailers(), nullptr); verifyDownstreamResponse(*response, 200); @@ -5845,6 +5893,89 @@ TEST_P(ExtProcIntegrationTest, RequestHeaderModeIgnoredInModeOverrideComparison) verifyDownstreamResponse(*response, 200); } +TEST_P(ExtProcIntegrationTest, ModeOverrideNoneToFullDuplex) { + proto_config_.mutable_processing_mode()->set_response_header_mode(ProcessingMode::SKIP); + proto_config_.set_allow_mode_override(true); + initializeConfig(); + HttpIntegrationTest::initialize(); + + std::string body_str = std::string(10, 'a'); + std::string upstream_body_str = std::string(5, 'b'); + auto response = sendDownstreamRequestWithBody(body_str, absl::nullopt); + // Process request header message. + processGenericMessage( + *grpc_upstreams_[0], true, [](const ProcessingRequest&, ProcessingResponse& resp) { + resp.mutable_request_headers(); + resp.mutable_mode_override()->set_request_body_mode(ProcessingMode::FULL_DUPLEX_STREAMED); + return true; + }); + + processRequestBodyMessage( + *grpc_upstreams_[0], false, + [&body_str, &upstream_body_str](const HttpBody& body, BodyResponse& resp) { + EXPECT_TRUE(body.end_of_stream()); + EXPECT_EQ(body.body(), body_str); + auto* streamed_response = + resp.mutable_response()->mutable_body_mutation()->mutable_streamed_response(); + streamed_response->set_body(upstream_body_str); + streamed_response->set_end_of_stream(true); + return true; + }); + handleUpstreamRequest(); + EXPECT_EQ(upstream_request_->body().toString(), upstream_body_str); + verifyDownstreamResponse(*response, 200); +} + +TEST_P(ExtProcIntegrationTest, NoneToFullDuplexMoreDataAfterModeOverride) { + proto_config_.mutable_processing_mode()->set_response_header_mode(ProcessingMode::SKIP); + proto_config_.set_allow_mode_override(true); + initializeConfig(); + HttpIntegrationTest::initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + auto encoder_decoder = codec_client_->startRequest(headers); + request_encoder_ = &encoder_decoder.first; + codec_client_->sendData(*request_encoder_, 10, false); + IntegrationStreamDecoderPtr response = std::move(encoder_decoder.second); + // Process request header message. + processGenericMessage( + *grpc_upstreams_[0], true, [](const ProcessingRequest&, ProcessingResponse& resp) { + resp.mutable_request_headers(); + resp.mutable_mode_override()->set_request_body_mode(ProcessingMode::FULL_DUPLEX_STREAMED); + return true; + }); + + processRequestBodyMessage( + *grpc_upstreams_[0], false, [](const HttpBody& body, BodyResponse& resp) { + EXPECT_FALSE(body.end_of_stream()); + EXPECT_EQ(body.body().size(), 10); + auto* streamed_response = + resp.mutable_response()->mutable_body_mutation()->mutable_streamed_response(); + streamed_response->set_body("bbbbb"); + streamed_response->set_end_of_stream(false); + return true; + }); + + codec_client_->sendData(*request_encoder_, 20, true); + + processRequestBodyMessage( + *grpc_upstreams_[0], false, [](const HttpBody& body, BodyResponse& resp) { + EXPECT_TRUE(body.end_of_stream()); + EXPECT_EQ(body.body().size(), 20); + auto* streamed_response = + resp.mutable_response()->mutable_body_mutation()->mutable_streamed_response(); + streamed_response->set_body("0123456789"); + streamed_response->set_end_of_stream(true); + return true; + }); + + handleUpstreamRequest(); + EXPECT_EQ(upstream_request_->body().toString(), "bbbbb0123456789"); + verifyDownstreamResponse(*response, 200); +} + TEST_P(ExtProcIntegrationTest, BufferedModeOverSizeRequestLocalReply) { proto_config_.mutable_processing_mode()->set_request_body_mode(ProcessingMode::BUFFERED); proto_config_.mutable_processing_mode()->set_response_header_mode(ProcessingMode::SKIP); @@ -6053,4 +6184,249 @@ TEST_P(ExtProcIntegrationTest, FilterStateAccessLogSerialization) { ENVOY_LOG_MISC(info, "Sample FIELD: bytes_sent={}", *bytes_sent); } +TEST_P(ExtProcIntegrationTest, ExtProcLoggingInfoGRPCTimeout) { + auto access_log_path = TestEnvironment::temporaryPath("ext_proc_filter_state_access.log"); + + config_helper_.addConfigModifier([&](HttpConnectionManager& cm) { + auto* access_log = cm.add_access_log(); + access_log->set_name("accesslog"); + envoy::extensions::access_loggers::file::v3::FileAccessLog access_log_config; + access_log_config.set_path(access_log_path); + auto* json_format = access_log_config.mutable_log_format()->mutable_json_format(); + + (*json_format->mutable_fields())["field_request_header_call_status"].set_string_value( + "%FILTER_STATE(envoy.filters.http.ext_proc:FIELD:request_header_call_status)%"); + + access_log->mutable_typed_config()->PackFrom(access_log_config); + }); + proto_config_.set_failure_mode_allow(true); + proto_config_.mutable_message_timeout()->set_nanos(200000000); + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(absl::nullopt); + processRequestHeadersMessage(*grpc_upstreams_[0], true, + [this](const HttpHeaders&, HeadersResponse&) { + // Travel forward 400 ms + timeSystem().advanceTimeWaitImpl(400ms); + return false; + }); + + // We should be able to continue from here since the error was ignored + handleUpstreamRequest(); + // Since we are ignoring errors, the late response to the request headers + // message meant that subsequent messages are spurious and the response + // headers message was never sent. + // Despite the timeout the request should succeed. + verifyDownstreamResponse(*response, 200); + std::string log_result = waitForAccessLog(access_log_path, 0, true); + auto json_log = Json::Factory::loadFromString(log_result).value(); + auto field_request_header_status = json_log->getString("field_request_header_call_status"); + // Should be 4:DEADLINE_EXCEEDED instead of 10:ABORTED + EXPECT_EQ(*field_request_header_status, "4"); +} + +// Tests for status_on_error functionality. +class ExtProcStatusOnErrorIntegrationTest : public HttpIntegrationTest, + public Grpc::GrpcClientIntegrationParamTest { +protected: + ExtProcStatusOnErrorIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP2, ipVersion()) {} + + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + + // Create separate "upstreams" for ExtProc gRPC servers + for (int i = 0; i < grpc_upstream_count_; ++i) { + grpc_upstreams_.push_back(&addFakeUpstream(Http::CodecType::HTTP2)); + } + } + + void TearDown() override { + if (processor_connection_) { + ASSERT_TRUE(processor_connection_->close()); + ASSERT_TRUE(processor_connection_->waitForDisconnect()); + } + cleanupUpstreamAndDownstream(); + } + + void initializeConfig(uint32_t status_code) { + config_helper_.addConfigModifier([this, status_code]( + envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* server_cluster = bootstrap.mutable_static_resources()->add_clusters(); + server_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + server_cluster->set_name("ext_proc_server"); + server_cluster->mutable_load_assignment()->set_cluster_name("ext_proc_server"); + + setGrpcService(*proto_config_.mutable_grpc_service(), "ext_proc_server", + grpc_upstreams_[0]->localAddress()); + + proto_config_.mutable_status_on_error()->set_code( + static_cast(status_code)); + proto_config_.set_failure_mode_allow(false); + + envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter ext_proc_filter; + ext_proc_filter.set_name("envoy.filters.http.ext_proc"); + ext_proc_filter.mutable_typed_config()->PackFrom(proto_config_); + config_helper_.prependFilter(MessageUtil::getJsonStringFromMessageOrError(ext_proc_filter)); + }); + + setUpstreamProtocol(Http::CodecType::HTTP1); + setDownstreamProtocol(Http::CodecType::HTTP1); + } + + IntegrationStreamDecoderPtr sendDownstreamRequest( + absl::optional> modify_headers) { + auto conn = makeClientConnection(lookupPort("http")); + codec_client_ = makeHttpConnection(std::move(conn)); + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + if (modify_headers) { + (*modify_headers)(headers); + } + return codec_client_->makeHeaderOnlyRequest(headers); + } + + void waitForFirstMessage(FakeUpstream& grpc_upstream, ProcessingRequest& request) { + ASSERT_TRUE(grpc_upstream.waitForHttpConnection(*dispatcher_, processor_connection_)); + ASSERT_TRUE(processor_connection_->waitForNewStream(*dispatcher_, processor_stream_)); + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, request)); + } + + bool IsEnvoyGrpc() { return std::get<1>(GetParam()) == Envoy::Grpc::ClientType::EnvoyGrpc; } + + envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor proto_config_{}; + FakeStreamPtr processor_stream_; + FakeHttpConnectionPtr processor_connection_; + std::vector grpc_upstreams_; + int grpc_upstream_count_ = 1; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, ExtProcStatusOnErrorIntegrationTest, + GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::GrpcClientIntegrationParamTest::protocolTestParamsToString); + +// Test that status_on_error is used when gRPC stream encounters an error. +TEST_P(ExtProcStatusOnErrorIntegrationTest, GrpcStreamErrorCustomStatus) { + SKIP_IF_GRPC_CLIENT(Grpc::ClientType::EnvoyGrpc); + + initializeConfig(503); // Use 503 Service Unavailable. + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(absl::nullopt); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(*grpc_upstreams_[0], request_headers_msg); + processor_stream_->startGrpcStream(); + + // Send successful response first, then simulate gRPC stream error. + ProcessingResponse resp; + resp.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp); + + // Now trigger gRPC stream error which should use status_on_error. + processor_stream_->finishGrpcStream(Grpc::Status::Internal); + + // Should get custom status code 503 instead of default 500. + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("503", response->headers().getStatusValue()); +} + +// Test that default status (500) is used when status_on_error is not configured. +TEST_P(ExtProcStatusOnErrorIntegrationTest, GrpcStreamErrorDefaultStatus) { + SKIP_IF_GRPC_CLIENT(Grpc::ClientType::EnvoyGrpc); + + // Initialize without setting status_on_error, should default to 500. + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* server_cluster = bootstrap.mutable_static_resources()->add_clusters(); + server_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + server_cluster->set_name("ext_proc_server"); + server_cluster->mutable_load_assignment()->set_cluster_name("ext_proc_server"); + + setGrpcService(*proto_config_.mutable_grpc_service(), "ext_proc_server", + grpc_upstreams_[0]->localAddress()); + + proto_config_.set_failure_mode_allow(false); + + envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter ext_proc_filter; + ext_proc_filter.set_name("envoy.filters.http.ext_proc"); + ext_proc_filter.mutable_typed_config()->PackFrom(proto_config_); + config_helper_.prependFilter(MessageUtil::getJsonStringFromMessageOrError(ext_proc_filter)); + }); + + setUpstreamProtocol(Http::CodecType::HTTP1); + setDownstreamProtocol(Http::CodecType::HTTP1); + + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(absl::nullopt); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(*grpc_upstreams_[0], request_headers_msg); + processor_stream_->startGrpcStream(); + + // Send successful response first, then simulate gRPC stream error. + ProcessingResponse resp; + resp.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp); + + // Trigger gRPC stream error without status_on_error configured. + processor_stream_->finishGrpcStream(Grpc::Status::Internal); + + // Should get default status code 500. + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("500", response->headers().getStatusValue()); +} + +// Test that message timeout returns 504 Gateway Timeout regardless of status_on_error. +TEST_P(ExtProcStatusOnErrorIntegrationTest, MessageTimeoutReturnsGatewayTimeout) { + SKIP_IF_GRPC_CLIENT(Grpc::ClientType::EnvoyGrpc); + + initializeConfig(502); // Configure 502, but timeout should return 504. + proto_config_.mutable_message_timeout()->set_nanos(100000000); // 100ms timeout. + + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(absl::nullopt); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(*grpc_upstreams_[0], request_headers_msg); + processor_stream_->startGrpcStream(); + + // Don't send a response to trigger timeout - just wait for the timeout to occur. + + // Let timeout occur. + test_server_->waitForCounterGe("http.config_test.ext_proc.message_timeouts", 1); + + // Should return 504 Gateway Timeout instead of configured status_on_error. + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("504", response->headers().getStatusValue()); +} + +// Test that status_on_error is used when processing/mutation errors occur. +TEST_P(ExtProcStatusOnErrorIntegrationTest, ProcessingErrorCustomStatus) { + SKIP_IF_GRPC_CLIENT(Grpc::ClientType::EnvoyGrpc); + + initializeConfig(422); // Use 422 Unprocessable Entity. + // Enable strict mutation rules to trigger processing errors. + proto_config_.mutable_mutation_rules()->mutable_disallow_is_error()->set_value(true); + proto_config_.mutable_mutation_rules()->mutable_disallow_system()->set_value(true); + + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(absl::nullopt); + + // Process the request and send back invalid system header mutation. + ProcessingRequest request_headers_msg; + waitForFirstMessage(*grpc_upstreams_[0], request_headers_msg); + processor_stream_->startGrpcStream(); + + ProcessingResponse resp; + auto* header_mut = resp.mutable_request_headers()->mutable_response()->mutable_header_mutation(); + auto* header = header_mut->add_set_headers(); + header->mutable_append()->set_value(false); + header->mutable_header()->set_key(":system-header"); // This should trigger error. + header->mutable_header()->set_raw_value("invalid"); + processor_stream_->sendGrpcMessage(resp); + + // Should get custom status code 422 for processing error. + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("422", response->headers().getStatusValue()); +} + } // namespace Envoy diff --git a/test/extensions/filters/http/ext_proc/ext_proc_misc_test.cc b/test/extensions/filters/http/ext_proc/ext_proc_misc_test.cc index 64e6cdf5dcfb6..c940a71adcbd9 100644 --- a/test/extensions/filters/http/ext_proc/ext_proc_misc_test.cc +++ b/test/extensions/filters/http/ext_proc/ext_proc_misc_test.cc @@ -213,11 +213,11 @@ class ExtProcMiscIntegrationTest : public HttpIntegrationTest, ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); - upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "401"}}, true); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.skip_ext_proc_on_local_reply")) { processResponseHeadersMessage(*grpc_upstreams_[0], false, absl::nullopt); } - verifyDownstreamResponse(*response, 401); + verifyDownstreamResponse(*response, 200); } envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor proto_config_{}; diff --git a/test/extensions/filters/http/ext_proc/filter_test.cc b/test/extensions/filters/http/ext_proc/filter_test.cc index 1d5d02a8d2dc1..ff7dc4469e8fe 100644 --- a/test/extensions/filters/http/ext_proc/filter_test.cc +++ b/test/extensions/filters/http/ext_proc/filter_test.cc @@ -140,9 +140,7 @@ class HttpFilterTest : public testing::Test { } config_ = std::make_shared( proto_config, 200ms, 10000, *stats_store_.rootScope(), "", is_upstream_filter, - std::make_shared( - Envoy::Extensions::Filters::Common::Expr::createBuilder(nullptr)), - factory_context_); + Envoy::Extensions::Filters::Common::Expr::createBuilder(nullptr), factory_context_); filter_ = std::make_unique(config_, std::move(client_)); filter_->setEncoderFilterCallbacks(encoder_callbacks_); EXPECT_CALL(encoder_callbacks_, encoderBufferLimit()).WillRepeatedly(Return(BufferSize)); @@ -206,7 +204,7 @@ class HttpFilterTest : public testing::Test { return stream; } - void doSetDynamicMetadata(const std::string& ns, const ProtobufWkt::Struct& val) { + void doSetDynamicMetadata(const std::string& ns, const Protobuf::Struct& val) { (*dynamic_metadata_.mutable_filter_metadata())[ns] = val; }; @@ -627,13 +625,13 @@ class HttpFilterTest : public testing::Test { // The metadata configured as part of ext_proc filter should be in the filter state. // In addition, bytes sent/received should also be stored. - void expectFilterState(const Envoy::ProtobufWkt::Struct& expected_metadata) { + void expectFilterState(const Envoy::Protobuf::Struct& expected_metadata) { const auto* filterState = stream_info_.filterState() ->getDataReadOnly< Envoy::Extensions::HttpFilters::ExternalProcessing::ExtProcLoggingInfo>( filter_config_name); - const Envoy::ProtobufWkt::Struct& loggedMetadata = filterState->filterMetadata(); + const Envoy::Protobuf::Struct& loggedMetadata = filterState->filterMetadata(); EXPECT_THAT(loggedMetadata, ProtoEq(expected_metadata)); } @@ -735,7 +733,7 @@ TEST_F(HttpFilterTest, SimplestPost) { checkGrpcCallHeaderOnlyStats(envoy::config::core::v3::TrafficDirection::INBOUND); checkGrpcCallHeaderOnlyStats(envoy::config::core::v3::TrafficDirection::OUTBOUND); - Envoy::ProtobufWkt::Struct filter_metadata; + Envoy::Protobuf::Struct filter_metadata; (*filter_metadata.mutable_fields())["scooby"].set_string_value("doo"); expectFilterState(filter_metadata); } @@ -885,7 +883,7 @@ TEST_F(HttpFilterTest, PostAndRespondImmediately) { checkGrpcCallHeaderOnlyStats(envoy::config::core::v3::TrafficDirection::INBOUND); expectNoGrpcCall(envoy::config::core::v3::TrafficDirection::OUTBOUND); - expectFilterState(Envoy::ProtobufWkt::Struct()); + expectFilterState(Envoy::Protobuf::Struct()); } TEST_F(HttpFilterTest, PostAndRespondImmediatelyWithDisabledConfig) { @@ -1720,6 +1718,37 @@ failure_mode_allow: true)EOF"; EXPECT_EQ(config_->stats().failure_mode_allowed_.value(), 1); } +// Verify that when status_on_error is configured, gRPC errors use the configured HTTP status. +TEST_F(HttpFilterTest, GrpcErrorUsesConfiguredStatusOnError) { + std::string yaml_config = R"EOF( +grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" +status_on_error: + code: 503 +)EOF"; + initialize(std::move(yaml_config)); + + // Start header processing to open the stream and arm timers. + EXPECT_EQ(filter_->decodeHeaders(request_headers_, false), FilterHeadersStatus::StopIteration); + test_time_->advanceTimeWait(std::chrono::microseconds(10)); + + // Expect a 503 Service Unavailable local reply instead of the previous default 500. + TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, + sendLocalReply(::Envoy::Http::Code::ServiceUnavailable, "", _, Eq(absl::nullopt), + "ext_proc_error_gRPC_error_13{error_message}")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, Unused, + Unused) { modify_headers(immediate_response_headers); })); + + // Simulate a gRPC error from the external processor. + server_closed_stream_ = true; + stream_callbacks_->onGrpcError(Grpc::Status::Internal, "error_message"); + filter_->onDestroy(); +} + TEST_F(FailureModeAllowOverrideTest, FilterDisallowNoRouteOverride) { std::string yaml_config = R"EOF( grpc_service: @@ -4188,7 +4217,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadata) { EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->encodeHeaders(response_headers_, false)); processResponseHeaders(false, [](const HttpHeaders&, ProcessingResponse& resp, HeadersResponse&) { - ProtobufWkt::Struct foobar; + Protobuf::Struct foobar; (*foobar.mutable_fields())["foo"].set_string_value("bar"); auto metadata_mut = resp.mutable_dynamic_metadata()->mutable_fields(); auto mut_struct = (*metadata_mut)["envoy.filters.http.ext_proc"].mutable_struct_value(); @@ -4237,7 +4266,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadataArbitraryNamespace) { EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->encodeHeaders(response_headers_, false)); processResponseHeaders(false, [](const HttpHeaders&, ProcessingResponse& resp, HeadersResponse&) { - ProtobufWkt::Struct foobar; + Protobuf::Struct foobar; (*foobar.mutable_fields())["foo"].set_string_value("bar"); auto metadata_mut = resp.mutable_dynamic_metadata()->mutable_fields(); auto mut_struct = (*metadata_mut)["envoy.filters.http.ext_authz"].mutable_struct_value(); @@ -4283,7 +4312,7 @@ TEST_F(HttpFilterTest, DisableEmitDynamicMetadata) { EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->encodeHeaders(response_headers_, false)); processResponseHeaders(false, [](const HttpHeaders&, ProcessingResponse& resp, HeadersResponse&) { - ProtobufWkt::Struct foobar; + Protobuf::Struct foobar; (*foobar.mutable_fields())["foo"].set_string_value("bar"); auto metadata_mut = resp.mutable_dynamic_metadata()->mutable_fields(); auto mut_struct = (*metadata_mut)["envoy.filters.http.ext_proc"].mutable_struct_value(); @@ -4329,7 +4358,7 @@ TEST_F(HttpFilterTest, DisableEmittingDynamicMetadataToDisallowedNamespaces) { EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->encodeHeaders(response_headers_, false)); processResponseHeaders(false, [](const HttpHeaders&, ProcessingResponse& resp, HeadersResponse&) { - ProtobufWkt::Struct foobar; + Protobuf::Struct foobar; (*foobar.mutable_fields())["foo"].set_string_value("bar"); auto metadata_mut = resp.mutable_dynamic_metadata()->mutable_fields(); auto mut_struct = (*metadata_mut)["envoy.filters.http.ext_authz"].mutable_struct_value(); @@ -4369,7 +4398,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadataUseLast) { EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); processRequestHeaders(false, [](const HttpHeaders&, ProcessingResponse& resp, HeadersResponse&) { - ProtobufWkt::Struct batbaz; + Protobuf::Struct batbaz; (*batbaz.mutable_fields())["bat"].set_string_value("baz"); auto metadata_mut = resp.mutable_dynamic_metadata()->mutable_fields(); auto mut_struct = (*metadata_mut)["envoy.filters.http.ext_proc"].mutable_struct_value(); @@ -4381,7 +4410,7 @@ TEST_F(HttpFilterTest, EmitDynamicMetadataUseLast) { EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->encodeHeaders(response_headers_, false)); processResponseHeaders(false, [](const HttpHeaders&, ProcessingResponse& resp, HeadersResponse&) { - ProtobufWkt::Struct foobar; + Protobuf::Struct foobar; (*foobar.mutable_fields())["foo"].set_string_value("bar"); auto metadata_mut = resp.mutable_dynamic_metadata()->mutable_fields(); auto mut_struct = (*metadata_mut)["envoy.filters.http.ext_proc"].mutable_struct_value(); @@ -5648,7 +5677,7 @@ TEST_F(HttpFilterTest, OnProcessingResponseHeaders) { dynamic_metadata_.filter_metadata().contains("envoy-test-ext_proc-request_headers_response")); const auto& request_headers_struct_metadata = dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-request_headers_response"); - ProtobufWkt::Struct expected_request_headers; + Protobuf::Struct expected_request_headers; TestUtility::loadFromJson(R"EOF( { "x-do-we-want-this": "remove", @@ -5694,7 +5723,7 @@ TEST_F(HttpFilterTest, OnProcessingResponseHeaders) { "envoy-test-ext_proc-response_headers_response")); const auto& response_headers_struct_metadata = dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-response_headers_response"); - ProtobufWkt::Struct expected_response_headers; + Protobuf::Struct expected_response_headers; TestUtility::loadFromJson(R"EOF( { "x-new-header": "new", @@ -5915,7 +5944,7 @@ TEST_F(HttpFilterTest, OnProcessingResponseBodies) { dynamic_metadata_.filter_metadata().contains("envoy-test-ext_proc-request_body_response")); const auto& request_body_struct_metadata = dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-request_body_response"); - ProtobufWkt::Struct expected_request_body; + Protobuf::Struct expected_request_body; TestUtility::loadFromJson(R"EOF( { "clear_body": "1" @@ -5949,7 +5978,7 @@ TEST_F(HttpFilterTest, OnProcessingResponseBodies) { dynamic_metadata_.filter_metadata().contains("envoy-test-ext_proc-response_body_response")); const auto& response_body_struct_metadata = dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-response_body_response"); - ProtobufWkt::Struct expected_response_body; + Protobuf::Struct expected_response_body; TestUtility::loadFromJson(R"EOF( { "body": "Hello, World!" @@ -6075,7 +6104,7 @@ TEST_F(HttpFilterTest, SaveImmediateResponse) { checkGrpcCallHeaderOnlyStats(envoy::config::core::v3::TrafficDirection::INBOUND); expectNoGrpcCall(envoy::config::core::v3::TrafficDirection::OUTBOUND); - expectFilterState(Envoy::ProtobufWkt::Struct()); + expectFilterState(Envoy::Protobuf::Struct()); } TEST_F(HttpFilterTest, DontSaveImmediateResponse) { @@ -6140,7 +6169,7 @@ TEST_F(HttpFilterTest, DontSaveImmediateResponse) { checkGrpcCallHeaderOnlyStats(envoy::config::core::v3::TrafficDirection::INBOUND); expectNoGrpcCall(envoy::config::core::v3::TrafficDirection::OUTBOUND); - expectFilterState(Envoy::ProtobufWkt::Struct()); + expectFilterState(Envoy::Protobuf::Struct()); } TEST_F(HttpFilterTest, DontSaveImmediateResponseOnError) { @@ -6197,7 +6226,7 @@ TEST_F(HttpFilterTest, DontSaveImmediateResponseOnError) { expectNoGrpcCall(envoy::config::core::v3::TrafficDirection::OUTBOUND); - expectFilterState(Envoy::ProtobufWkt::Struct()); + expectFilterState(Envoy::Protobuf::Struct()); } TEST_F(HttpFilterTest, SaveResponseTrailers) { diff --git a/test/extensions/filters/http/ext_proc/http_client/ext_proc_http_integration_test.cc b/test/extensions/filters/http/ext_proc/http_client/ext_proc_http_integration_test.cc index 4636a0166609b..0871751683866 100644 --- a/test/extensions/filters/http/ext_proc/http_client/ext_proc_http_integration_test.cc +++ b/test/extensions/filters/http/ext_proc/http_client/ext_proc_http_integration_test.cc @@ -31,10 +31,11 @@ using envoy::service::ext_proc::v3::HttpTrailers; using envoy::service::ext_proc::v3::ProcessingRequest; using envoy::service::ext_proc::v3::ProcessingResponse; using envoy::service::ext_proc::v3::TrailersResponse; -using Extensions::HttpFilters::ExternalProcessing::HasHeader; -using Extensions::HttpFilters::ExternalProcessing::HasNoHeader; using Extensions::HttpFilters::ExternalProcessing::HeaderProtosEqual; -using Extensions::HttpFilters::ExternalProcessing::SingleHeaderValueIs; + +using ::testing::_; +using ::testing::AllOf; +using ::testing::Not; using Http::LowerCaseString; @@ -178,9 +179,8 @@ class ExtProcHttpClientIntegrationTest : public testing::TestWithParamwaitForNewStream(*dispatcher_, processor_stream_)); ASSERT_TRUE(processor_stream_->waitForEndStream(*dispatcher_)); EXPECT_THAT(processor_stream_->headers(), - SingleHeaderValueIs("content-type", "application/json")); - EXPECT_THAT(processor_stream_->headers(), SingleHeaderValueIs(":method", "POST")); - EXPECT_THAT(processor_stream_->headers(), HasHeader("x-request-id")); + AllOf(ContainsHeader("content-type", "application/json"), + ContainsHeader(":method", "POST"), ContainsHeader("x-request-id", _))); } void sendHttpResponse(ProcessingResponse& response) { @@ -312,7 +312,7 @@ TEST_P(ExtProcHttpClientIntegrationTest, ServerNoRequestHeaderMutation) { // The request is sent to the upstream. handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("foo", "yes")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("foo", "yes")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); verifyDownstreamResponse(*response, 200); @@ -329,7 +329,7 @@ TEST_P(ExtProcHttpClientIntegrationTest, ServerNoResponseHeaderMutation) { // The request is sent to the upstream. handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("foo", "yes")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("foo", "yes")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); processResponseHeadersMessage(http_side_upstreams_[0], true, absl::nullopt); verifyDownstreamResponse(*response, 200); @@ -361,8 +361,8 @@ TEST_P(ExtProcHttpClientIntegrationTest, GetAndSetHeadersWithMutation) { // The request is sent to the upstream. handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); - EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("x-remove-this", "_"))); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); verifyDownstreamResponse(*response, 200); @@ -472,7 +472,7 @@ TEST_P(ExtProcHttpClientIntegrationTest, SentHeadersInBothDirection) { // The request is sent to the upstream. handleUpstreamRequestWithTrailer(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("x-new-header", "new")); EXPECT_EQ(upstream_request_->body().toString(), "foo"); processResponseHeadersMessage(http_side_upstreams_[0], false, absl::nullopt); @@ -523,7 +523,7 @@ TEST_P(ExtProcHttpClientIntegrationTest, StatsTestOnSuccess) { // The request is sent to the upstream. handleUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("foo", "yes")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("foo", "yes")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); verifyDownstreamResponse(*response, 200); diff --git a/test/extensions/filters/http/ext_proc/matching_utils_test.cc b/test/extensions/filters/http/ext_proc/matching_utils_test.cc index 32372a68ce090..724549dd2fc32 100644 --- a/test/extensions/filters/http/ext_proc/matching_utils_test.cc +++ b/test/extensions/filters/http/ext_proc/matching_utils_test.cc @@ -21,64 +21,62 @@ using ::Envoy::Http::TestRequestTrailerMapImpl; using ::Envoy::Http::TestResponseHeaderMapImpl; using ::Envoy::Http::TestResponseTrailerMapImpl; +#ifdef USE_CEL_PARSER + class ExpressionManagerTest : public testing::Test { -public: - void initialize() { builder_ = Envoy::Extensions::Filters::Common::Expr::getBuilder(context_); } +protected: + ExpressionManagerTest() { + auto builder = Filters::Common::Expr::getBuilder(context_); + Protobuf::RepeatedPtrField request_matchers; + Protobuf::RepeatedPtrField response_matchers; + expression_manager_ = std::make_unique(builder, context_.local_info_, + request_matchers, response_matchers); + } - std::shared_ptr builder_; NiceMock context_; - testing::NiceMock stream_info_; - testing::NiceMock local_info_; - TestRequestHeaderMapImpl request_headers_; - TestResponseHeaderMapImpl response_headers_; - TestRequestTrailerMapImpl request_trailers_; - TestResponseTrailerMapImpl response_trailers_; - Protobuf::RepeatedPtrField req_matchers_; - Protobuf::RepeatedPtrField resp_matchers_; + std::unique_ptr expression_manager_; }; -#if defined(USE_CEL_PARSER) -TEST_F(ExpressionManagerTest, DuplicateAttributesIgnored) { - initialize(); - req_matchers_.Add("request.path"); - req_matchers_.Add("request.method"); - req_matchers_.Add("request.method"); - req_matchers_.Add("request.path"); - const auto expr_mgr = ExpressionManager(builder_, local_info_, req_matchers_, resp_matchers_); - - request_headers_.setMethod("GET"); - request_headers_.setPath("/foo"); - const auto activation_ptr = Filters::Common::Expr::createActivation( - &expr_mgr.localInfo(), stream_info_, &request_headers_, &response_headers_, - &response_trailers_); - - auto result = expr_mgr.evaluateRequestAttributes(*activation_ptr); - EXPECT_EQ(2, result.fields_size()); - EXPECT_NE(result.fields().end(), result.fields().find("request.path")); - EXPECT_NE(result.fields().end(), result.fields().find("request.method")); +TEST_F(ExpressionManagerTest, SimpleExpression) { + EXPECT_FALSE(expression_manager_->hasRequestExpr()); + EXPECT_FALSE(expression_manager_->hasResponseExpr()); } -TEST_F(ExpressionManagerTest, UnparsableExpressionThrowsException) { - initialize(); - req_matchers_.Add("++"); - EXPECT_THROW_WITH_REGEX(ExpressionManager(builder_, local_info_, req_matchers_, resp_matchers_), - EnvoyException, "Unable to parse descriptor expression.*"); +TEST_F(ExpressionManagerTest, InvalidExpression) { + Protobuf::RepeatedPtrField request_matchers; + request_matchers.Add("undefined_func()"); + auto builder = Filters::Common::Expr::getBuilder(context_); + EXPECT_THROW( + { ExpressionManager test_manager(builder, context_.local_info_, request_matchers, {}); }, + EnvoyException); } -TEST_F(ExpressionManagerTest, EmptyExpressionReturnsEmptyStruct) { - initialize(); - const auto expr_mgr = ExpressionManager(builder_, local_info_, req_matchers_, resp_matchers_); +TEST_F(ExpressionManagerTest, ComplexExpressionWithSourceInfo) { + // Create a complex expression that would test source info handling + Protobuf::RepeatedPtrField request_matchers; + request_matchers.Add("request.headers.contains('x-test') && " + "request.headers['x-test'].startsWith('value')"); + + // This should create successfully without throwing + auto builder = Filters::Common::Expr::getBuilder(context_); + ExpressionManager test_manager(builder, context_.local_info_, request_matchers, {}); + EXPECT_TRUE(test_manager.hasRequestExpr()); +} - request_headers_ = TestRequestHeaderMapImpl(); - request_headers_.setMethod("GET"); - request_headers_.setPath("/foo"); - const auto activation_ptr = Filters::Common::Expr::createActivation( - &expr_mgr.localInfo(), stream_info_, &request_headers_, &response_headers_, - &response_trailers_); +#else - EXPECT_EQ(0, expr_mgr.evaluateRequestAttributes(*activation_ptr).fields_size()); +TEST(ExpressionManagerTest, CelUnavailableTest) { + NiceMock context; + auto builder = Filters::Common::Expr::getBuilder(context); + Protobuf::RepeatedPtrField request_matchers; + request_matchers.Add("true"); + + // When CEL is not available, this should log a warning but not throw + ExpressionManager manager(builder, context.local_info_, request_matchers, {}); + EXPECT_FALSE(manager.hasRequestExpr()); } -#endif + +#endif // USE_CEL_PARSER } // namespace } // namespace ExternalProcessing diff --git a/test/extensions/filters/http/ext_proc/ordering_test.cc b/test/extensions/filters/http/ext_proc/ordering_test.cc index 57302cb103700..51375b583c881 100644 --- a/test/extensions/filters/http/ext_proc/ordering_test.cc +++ b/test/extensions/filters/http/ext_proc/ordering_test.cc @@ -73,9 +73,7 @@ class OrderingTest : public testing::Test { } config_ = std::make_shared( proto_config, kMessageTimeout, kMaxMessageTimeoutMs, *stats_store_.rootScope(), "", false, - std::make_shared( - Envoy::Extensions::Filters::Common::Expr::createBuilder(nullptr)), - factory_context_); + Envoy::Extensions::Filters::Common::Expr::createBuilder(nullptr), factory_context_); filter_ = std::make_unique(config_, std::move(client_)); filter_->setEncoderFilterCallbacks(encoder_callbacks_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); diff --git a/test/extensions/filters/http/ext_proc/tracer_test_filter.cc b/test/extensions/filters/http/ext_proc/tracer_test_filter.cc index ffc38344660e1..3d2b1ad673bbd 100644 --- a/test/extensions/filters/http/ext_proc/tracer_test_filter.cc +++ b/test/extensions/filters/http/ext_proc/tracer_test_filter.cc @@ -74,6 +74,13 @@ class Span : public Tracing::Span { ENVOY_LOG_MISC(trace, "TestTracer setSampled: {}", do_sample); sampled_ = do_sample; } + bool useLocalDecision() const override { + // NOTE: the trace decision from Envoy will be ignored in the startSpan() method + // of this test implementation. So, the useLocalDecision() method is only for logging + // and will also ignore the decision value. + ENVOY_LOG_MISC(trace, "TestTracer useLocalDecision"); + return false; + } void injectContext(Tracing::TraceContext& trace_context, const Tracing::UpstreamContext&) override { diff --git a/test/extensions/filters/http/ext_proc/unit_test_fuzz/ext_proc_unit_test_fuzz.cc b/test/extensions/filters/http/ext_proc/unit_test_fuzz/ext_proc_unit_test_fuzz.cc index f8635e18598d0..08eb648ab65f5 100644 --- a/test/extensions/filters/http/ext_proc/unit_test_fuzz/ext_proc_unit_test_fuzz.cc +++ b/test/extensions/filters/http/ext_proc/unit_test_fuzz/ext_proc_unit_test_fuzz.cc @@ -88,9 +88,7 @@ DEFINE_PROTO_FUZZER( try { config = std::make_shared( proto_config, std::chrono::milliseconds(200), 200, *stats_store.rootScope(), "", false, - std::make_shared( - Envoy::Extensions::Filters::Common::Expr::createBuilder(nullptr)), - mocks.factory_context_); + Envoy::Extensions::Filters::Common::Expr::createBuilder(nullptr), mocks.factory_context_); } catch (const EnvoyException& e) { ENVOY_LOG_MISC(debug, "EnvoyException during ext_proc filter config validation: {}", e.what()); return; diff --git a/test/extensions/filters/http/ext_proc/utils.h b/test/extensions/filters/http/ext_proc/utils.h index a0bf78e78b6ed..1b724297d978c 100644 --- a/test/extensions/filters/http/ext_proc/utils.h +++ b/test/extensions/filters/http/ext_proc/utils.h @@ -33,23 +33,6 @@ MATCHER_P(HeaderProtosEqual, expected, "HTTP header protos match") { return ExtProcTestUtility::headerProtosEqualIgnoreOrder(expected, arg); } -MATCHER_P(HasNoHeader, key, absl::StrFormat("Headers have no value for \"%s\"", key)) { - return arg.get(::Envoy::Http::LowerCaseString(std::string(key))).empty(); -} - -MATCHER_P(HasHeader, key, absl::StrFormat("There exists a header for \"%s\"", key)) { - return !arg.get(::Envoy::Http::LowerCaseString(std::string(key))).empty(); -} - -MATCHER_P2(SingleHeaderValueIs, key, value, - absl::StrFormat("Header \"%s\" equals \"%s\"", key, value)) { - const auto hdr = arg.get(::Envoy::Http::LowerCaseString(std::string(key))); - if (hdr.size() != 1) { - return false; - } - return hdr[0]->value() == value; -} - template inline void verifyMultipleHeaderValues(const Envoy::Http::HeaderMap& headers, Envoy::Http::LowerCaseString const& key, Args... values) { @@ -104,30 +87,30 @@ class TestOnProcessingResponse : public OnProcessingResponse { Envoy::StreamInfo::StreamInfo&) override; private: - Envoy::ProtobufWkt::Struct + Envoy::Protobuf::Struct getHeaderMutations(const envoy::service::ext_proc::v3::HeaderMutation& header_mutation) { - Envoy::ProtobufWkt::Struct struct_metadata; + Envoy::Protobuf::Struct struct_metadata; for (auto& header : header_mutation.set_headers()) { - Envoy::ProtobufWkt::Value value; + Envoy::Protobuf::Value value; value.mutable_string_value()->assign(header.header().raw_value()); struct_metadata.mutable_fields()->insert(std::make_pair(header.header().key(), value)); } for (auto& header : header_mutation.remove_headers()) { - Envoy::ProtobufWkt::Value value; + Envoy::Protobuf::Value value; value.mutable_string_value()->assign("remove"); struct_metadata.mutable_fields()->insert(std::make_pair(header, value)); } return struct_metadata; } - Envoy::ProtobufWkt::Struct + Envoy::Protobuf::Struct getBodyMutation(const envoy::service::ext_proc::v3::BodyMutation& body_mutation) { - Envoy::ProtobufWkt::Struct struct_metadata; + Envoy::Protobuf::Struct struct_metadata; if (body_mutation.has_body()) { - Envoy::ProtobufWkt::Value value; + Envoy::Protobuf::Value value; value.mutable_string_value()->assign(body_mutation.body()); struct_metadata.mutable_fields()->insert(std::make_pair("body", value)); } else { - Envoy::ProtobufWkt::Value value; + Envoy::Protobuf::Value value; value.mutable_string_value()->assign(absl::StrCat(body_mutation.clear_body())); struct_metadata.mutable_fields()->insert(std::make_pair("clear_body", value)); } @@ -147,7 +130,7 @@ class TestOnProcessingResponseFactory : public OnProcessingResponseFactory { ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom filter config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "on_processing_response"; } diff --git a/test/extensions/filters/http/fault/fault_filter_integration_test.cc b/test/extensions/filters/http/fault/fault_filter_integration_test.cc index a43df1d668ad8..3ea5f7efa64fb 100644 --- a/test/extensions/filters/http/fault/fault_filter_integration_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_integration_test.cc @@ -329,10 +329,10 @@ TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultAbortGrpcConfig) { EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), Envoy::Http::HttpStatusIs("200")); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "5")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().GrpcStatus, "5")); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().GrpcMessage, "fault filter abort")); + ContainsHeader(Http::Headers::get().GrpcMessage, "fault filter abort")); EXPECT_EQ(nullptr, response->trailers()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); @@ -380,10 +380,10 @@ TEST_P(FaultIntegrationTestAllProtocols, FaultAbortGrpcConfig) { EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), Envoy::Http::HttpStatusIs("200")); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "5")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().GrpcStatus, "5")); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().GrpcMessage, "fault filter abort")); + ContainsHeader(Http::Headers::get().GrpcMessage, "fault filter abort")); EXPECT_EQ(nullptr, response->trailers()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); diff --git a/test/extensions/filters/http/gcp_authn/gcp_authn_filter_integration_test.cc b/test/extensions/filters/http/gcp_authn/gcp_authn_filter_integration_test.cc index 425233d190ca5..92bd8dd29c97c 100644 --- a/test/extensions/filters/http/gcp_authn/gcp_authn_filter_integration_test.cc +++ b/test/extensions/filters/http/gcp_authn/gcp_authn_filter_integration_test.cc @@ -231,29 +231,6 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, GcpAuthnFilterIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); -TEST_P(GcpAuthnFilterIntegrationTest, DEPRECATED_FEATURE_TEST(Basicflow)) { - use_new_config_ = false; - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.gcp_authn_use_fixed_url", "false"}}); - initializeConfig(/*add_audience=*/true); - HttpIntegrationTest::initialize(); - int num = 2; - // Send multiple requests. - for (int i = 0; i < num; ++i) { - initiateClientConnection(); - // Send the request to cluster `gcp_authn`. - waitForGcpAuthnServerResponse(); - // Send the request to cluster `cluster_0` and validate the response. - sendRequestToDestinationAndValidateResponse(/*with_audience=*/true); - // Clean up the codec and connections. - cleanup(); - } - - // Verify request has been routed to both upstream clusters. - EXPECT_GE(test_server_->counter("cluster.gcp_authn.upstream_cx_total")->value(), num); - EXPECT_GE(test_server_->counter("cluster.cluster_0.upstream_cx_total")->value(), num); -} - TEST_P(GcpAuthnFilterIntegrationTest, BasicflowWithNewConfig) { initializeConfig(/*add_audience=*/true); HttpIntegrationTest::initialize(); diff --git a/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc b/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc index dd4515de406d2..9fa8eb429863c 100644 --- a/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc +++ b/test/extensions/filters/http/geoip/geoip_filter_integration_test.cc @@ -99,6 +99,22 @@ const std::string ConfigIspAndCity = R"EOF( isp_db_path: "{{ test_rundir }}/test/extensions/geoip_providers/maxmind/test_data/GeoIP2-ISP-Test.mmdb" )EOF"; +const std::string ConfigIsApplePrivateRelayOnly = R"EOF( + name: envoy.filters.http.geoip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.geoip.v3.Geoip + xff_config: + xff_num_trusted_hops: 1 + provider: + name: envoy.geoip_providers.maxmind + typed_config: + "@type": type.googleapis.com/envoy.extensions.geoip_providers.maxmind.v3.MaxMindConfig + common_provider_config: + geo_headers_to_add: + apple_private_relay: "x-geo-apple-private-relay" + isp_db_path: "{{ test_rundir }}/test/extensions/geoip_providers/maxmind/test_data/GeoIP2-ISP-Test.mmdb" + )EOF"; + class GeoipFilterIntegrationTest : public testing::TestWithParam, public HttpIntegrationTest { public: @@ -116,27 +132,29 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, GeoipFilterIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); -TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedNoXff) { +TEST_P(GeoipFilterIntegrationTest, GeoDataDontPopulatedWhenCalledFromLocalhosNoXff) { config_helper_.prependFilter(TestEnvironment::substitute(DefaultConfig)); initialize(); codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - EXPECT_EQ("Boxford", headerValue("x-geo-city")); - EXPECT_EQ("ENG", headerValue("x-geo-region")); - EXPECT_EQ("GB", headerValue("x-geo-country")); - EXPECT_EQ("15169", headerValue("x-geo-asn")); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-city")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-region")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-country")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-asn")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-anon-vpn")).empty()); + ASSERT_TRUE(response->headers().get(Http::LowerCaseString("x-geo-anon")).empty()); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); test_server_->waitForCounterEq("http.config_test.geoip.total", 1); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.total")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.asn_db.total")->value()); - EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.hit")->value()); - EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.asn_db.hit")->value()); + EXPECT_EQ(nullptr, test_server_->counter("http.config_test.maxmind.city_db.hit")); + EXPECT_EQ(nullptr, test_server_->counter("http.config_test.maxmind.asn_db.hit")); } -TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseXff) { +TEST_P(GeoipFilterIntegrationTest, GeoAnonDataPopulatedUseXff) { config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithXff)); initialize(); codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); @@ -144,12 +162,8 @@ TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseXff) { {":path", "/"}, {":scheme", "http"}, {":authority", "host"}, - {"x-forwarded-for", "1.2.0.0,9.10.11.12"}}; + {"x-forwarded-for", "::81.2.69.0,9.10.11.12"}}; auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - EXPECT_EQ("Boxford", headerValue("x-geo-city")); - EXPECT_EQ("ENG", headerValue("x-geo-region")); - EXPECT_EQ("GB", headerValue("x-geo-country")); - EXPECT_EQ("15169", headerValue("x-geo-asn")); EXPECT_EQ("true", headerValue("x-geo-anon")); EXPECT_EQ("true", headerValue("x-geo-anon-vpn")); ASSERT_TRUE(response->complete()); @@ -157,6 +171,25 @@ TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseXff) { test_server_->waitForCounterEq("http.config_test.geoip.total", 1); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.anon_db.total")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.anon_db.hit")->value()); +} + +TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseXff) { + config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithXff)); + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "216.160.83.56"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_EQ("Milton", headerValue("x-geo-city")); + EXPECT_EQ("WA", headerValue("x-geo-region")); + EXPECT_EQ("US", headerValue("x-geo-country")); + EXPECT_EQ("209", headerValue("x-geo-asn")); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + test_server_->waitForCounterEq("http.config_test.geoip.total", 1); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.total")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.hit")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.asn_db.total")->value()); @@ -171,13 +204,13 @@ TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseXffWithIspAndAsn) { {":path", "/"}, {":scheme", "http"}, {":authority", "host"}, - {"x-forwarded-for", "::12.96.16.1,9.10.11.12"}}; + {"x-forwarded-for", "216.160.83.56,9.10.11.12"}}; auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - EXPECT_EQ("Boxford", headerValue("x-geo-city")); - EXPECT_EQ("ENG", headerValue("x-geo-region")); - EXPECT_EQ("GB", headerValue("x-geo-country")); - EXPECT_EQ("7018", headerValue("x-geo-asn")); - EXPECT_EQ("AT&T Services", headerValue("x-geo-isp")); + EXPECT_EQ("Milton", headerValue("x-geo-city")); + EXPECT_EQ("WA", headerValue("x-geo-region")); + EXPECT_EQ("US", headerValue("x-geo-country")); + EXPECT_EQ("209", headerValue("x-geo-asn")); + EXPECT_EQ("Century Link", headerValue("x-geo-isp")); EXPECT_EQ("false", headerValue("x-geo-apple-private-relay")); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); @@ -198,13 +231,13 @@ TEST_P(GeoipFilterIntegrationTest, GeoDataPopulatedUseXffWithIsp) { {":path", "/"}, {":scheme", "http"}, {":authority", "host"}, - {"x-forwarded-for", "::12.96.16.1,9.10.11.12"}}; + {"x-forwarded-for", "216.160.83.56,9.10.11.12"}}; auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - EXPECT_EQ("Boxford", headerValue("x-geo-city")); - EXPECT_EQ("ENG", headerValue("x-geo-region")); - EXPECT_EQ("GB", headerValue("x-geo-country")); - EXPECT_EQ("7018", headerValue("x-geo-asn")); - EXPECT_EQ("AT&T Services", headerValue("x-geo-isp")); + EXPECT_EQ("Milton", headerValue("x-geo-city")); + EXPECT_EQ("WA", headerValue("x-geo-region")); + EXPECT_EQ("US", headerValue("x-geo-country")); + EXPECT_EQ("209", headerValue("x-geo-asn")); + EXPECT_EQ("Century Link", headerValue("x-geo-isp")); EXPECT_EQ("false", headerValue("x-geo-apple-private-relay")); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); @@ -226,17 +259,14 @@ TEST_P(GeoipFilterIntegrationTest, GeoHeadersOverridenInRequest) { {":path", "/"}, {":scheme", "http"}, {":authority", "host"}, - {"x-forwarded-for", "81.2.69.142,9.10.11.12"}, + {"x-forwarded-for", "216.160.83.56,9.10.11.12"}, {"x-geo-city", "Berlin"}, {"x-geo-country", "Germany"}}; auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - EXPECT_EQ("London", headerValue("x-geo-city")); - EXPECT_EQ("GB", headerValue("x-geo-country")); + EXPECT_EQ("Milton", headerValue("x-geo-city")); + EXPECT_EQ("US", headerValue("x-geo-country")); ASSERT_TRUE(response->complete()); - EXPECT_EQ("200", response->headers().getStatusValue()); test_server_->waitForCounterEq("http.config_test.geoip.total", 1); - EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.anon_db.total")->value()); - EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.anon_db.hit")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.total")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.city_db.hit")->value()); EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.asn_db.total")->value()); @@ -262,7 +292,7 @@ TEST_P(GeoipFilterIntegrationTest, GeoDataNotPopulatedOnEmptyLookupResult) { } TEST_P(GeoipFilterIntegrationTest, GeoipFilterNoCrashOnLdsUpdate) { - config_helper_.prependFilter(TestEnvironment::substitute(DefaultConfig)); + config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithXff)); initialize(); // LDS update to modify the listener and corresponding drain. @@ -279,11 +309,14 @@ TEST_P(GeoipFilterIntegrationTest, GeoipFilterNoCrashOnLdsUpdate) { test_server_->waitForGaugeEq("listener_manager.total_listeners_draining", 0); } codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); - Http::TestRequestHeaderMapImpl request_headers{ - {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "216.160.83.56,9.10.11.12"}}; auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - EXPECT_EQ("Boxford", headerValue("x-geo-city")); - EXPECT_EQ("ENG", headerValue("x-geo-region")); + EXPECT_EQ("Milton", headerValue("x-geo-city")); + EXPECT_EQ("WA", headerValue("x-geo-region")); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); @@ -295,6 +328,34 @@ TEST_P(GeoipFilterIntegrationTest, GeoipFilterNoCrashOnLdsUpdate) { EXPECT_EQ(2, test_server_->counter("http.config_test.maxmind.city_db.hit")->value()); } +TEST_P(GeoipFilterIntegrationTest, OnlyApplePrivateRelayHeaderIsPopulated) { + config_helper_.prependFilter(TestEnvironment::substitute(ConfigIsApplePrivateRelayOnly)); + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "::65.116.3.80,9.10.11.12"}, + {"x-geo-city", "Berlin"}, + {"x-geo-country", "Germany"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + + EXPECT_EQ("false", headerValue("x-geo-apple-private-relay")); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + test_server_->waitForCounterEq("http.config_test.geoip.total", 1); + EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.isp_db.total")->value()); + EXPECT_EQ(1, test_server_->counter("http.config_test.maxmind.isp_db.hit")->value()); +} + +TEST_P(GeoipFilterIntegrationTest, MetricForDbBuildEpochIsEmitted) { + config_helper_.prependFilter(TestEnvironment::substitute(ConfigWithXff)); + initialize(); + EXPECT_EQ(1671567063, + test_server_->gauge("http.config_test.maxmind.city_db.db_build_epoch")->value()); +} + } // namespace } // namespace Geoip } // namespace HttpFilters diff --git a/test/extensions/filters/http/grpc_field_extraction/filter_test.cc b/test/extensions/filters/http/grpc_field_extraction/filter_test.cc index b4cf646a959e9..71d0a220b2383 100644 --- a/test/extensions/filters/http/grpc_field_extraction/filter_test.cc +++ b/test/extensions/filters/http/grpc_field_extraction/filter_test.cc @@ -90,8 +90,8 @@ CreateApiKeyRequest makeCreateApiKeyRequest(absl::string_view pb = R"pb( return request; } -void checkProtoStruct(ProtobufWkt::Struct got, absl::string_view expected_in_pbtext) { - ProtobufWkt::Struct expected; +void checkProtoStruct(Protobuf::Struct got, absl::string_view expected_in_pbtext) { + Protobuf::Struct expected; ASSERT_TRUE(Protobuf::TextFormat::ParseFromString(expected_in_pbtext, &expected)); EXPECT_TRUE(TestUtility::protoEqual(got, expected, true)) << "got:\n" << got.DebugString() << "expected:\n" @@ -111,7 +111,7 @@ TEST_F(FilterTestExtractOk, UnarySingleBuffer) { CreateApiKeyRequest request = makeCreateApiKeyRequest(); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, expected_metadata); })); @@ -133,7 +133,7 @@ TEST_F(FilterTestExtractOk, EmptyFields) { CreateApiKeyRequest request = makeCreateApiKeyRequest(""); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -183,7 +183,7 @@ TEST_F(FilterTestExtractOk, UnaryMultipeBuffers) { EXPECT_EQ(middle_request_data.length(), 0); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, expected_metadata); })); @@ -235,7 +235,7 @@ extractions_by_method: { EXPECT_EQ(request_data3->length(), 0); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, expected_metadata); })); @@ -354,7 +354,7 @@ supported_types: { )pb"); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb(fields { key: "supported_types.double" @@ -539,7 +539,7 @@ repeated_intermediate: { )pb"); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -689,7 +689,7 @@ repeated_supported_types: { )pb"); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.grpc_field_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { diff --git a/test/extensions/filters/http/grpc_http1_bridge/grpc_http1_bridge_integration_test.cc b/test/extensions/filters/http/grpc_http1_bridge/grpc_http1_bridge_integration_test.cc index f8721b0c3121e..a9e4d470c7139 100644 --- a/test/extensions/filters/http/grpc_http1_bridge/grpc_http1_bridge_integration_test.cc +++ b/test/extensions/filters/http/grpc_http1_bridge/grpc_http1_bridge_integration_test.cc @@ -42,7 +42,7 @@ TEST_P(GrpcIntegrationTest, HittingGrpcFilterLimitBufferingHeaders) { EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), Http::HttpStatusIs("200")); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().GrpcStatus, "2")); // Unknown gRPC error + ContainsHeader(Http::Headers::get().GrpcStatus, "2")); // Unknown gRPC error } INSTANTIATE_TEST_SUITE_P(IpVersions, GrpcIntegrationTest, diff --git a/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_integration_test.cc b/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_integration_test.cc index a1ce6eaf34a85..4f1c988f9307c 100644 --- a/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_integration_test.cc +++ b/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_integration_test.cc @@ -12,8 +12,6 @@ #include "fmt/printf.h" #include "gtest/gtest.h" -using Envoy::Http::HeaderValueOf; - // for ::operator""s (which Windows compiler does not support): using namespace std::string_literals; @@ -87,7 +85,7 @@ TEST_P(ReverseBridgeIntegrationTest, DisabledRoute) { // Ensure that we don't do anything EXPECT_EQ("abcdef", upstream_request_->body().toString()); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); // Respond to the request. Http::TestResponseHeaderMapImpl response_headers; @@ -107,8 +105,8 @@ TEST_P(ReverseBridgeIntegrationTest, DisabledRoute) { EXPECT_EQ(response->body(), response_data.toString()); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -138,9 +136,9 @@ TEST_P(ReverseBridgeIntegrationTest, EnabledRoute) { EXPECT_EQ("f", upstream_request_->body().toString()); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); // Respond to the request. Http::TestResponseHeaderMapImpl response_headers; @@ -165,8 +163,8 @@ TEST_P(ReverseBridgeIntegrationTest, EnabledRoute) { EXPECT_TRUE( std::equal(response->body().begin(), response->body().begin() + 5, expected_prefix.begin())); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -195,8 +193,8 @@ TEST_P(ReverseBridgeIntegrationTest, EnabledRouteBadContentType) { // The response should indicate an error. EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "2")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().GrpcStatus, "2")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -229,9 +227,9 @@ TEST_P(ReverseBridgeIntegrationTest, EnabledRouteStreamResponse) { EXPECT_EQ("f", upstream_request_->body().toString()); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); // Respond to the request. Http::TestResponseHeaderMapImpl response_headers; @@ -265,8 +263,8 @@ TEST_P(ReverseBridgeIntegrationTest, EnabledRouteStreamResponse) { EXPECT_TRUE( std::equal(response->body().begin(), response->body().begin() + 5, expected_prefix.begin())); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -298,9 +296,9 @@ TEST_P(ReverseBridgeIntegrationTest, EnabledRouteStreamWithholdResponse) { EXPECT_EQ("f", upstream_request_->body().toString()); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); // Respond to the request. Http::TestResponseHeaderMapImpl response_headers; diff --git a/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_test.cc b/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_test.cc index f3b5f36c90b17..613804ca609dd 100644 --- a/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_test.cc +++ b/test/extensions/filters/http/grpc_http1_reverse_bridge/reverse_bridge_test.cc @@ -18,7 +18,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -using Envoy::Http::HeaderValueOf; using testing::_; using testing::ReturnRef; @@ -57,10 +56,11 @@ TEST_F(ReverseBridgeTest, InvalidGrpcRequest) { {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -69,11 +69,11 @@ TEST_F(ReverseBridgeTest, InvalidGrpcRequest) { buffer.add("abc", 3); EXPECT_CALL(decoder_callbacks_, sendLocalReply(_, _, _, _, _)); EXPECT_CALL(decoder_callbacks_, encodeHeaders_(_, _)).WillOnce(Invoke([](auto& headers, auto) { - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().Status, "200")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().GrpcStatus, "2")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().Status, "200")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().GrpcStatus, "2")); EXPECT_THAT(headers, - HeaderValueOf(Http::Headers::get().GrpcMessage, - Http::Utility::PercentEncoding::encode("invalid request body"))); + ContainsHeader(Http::Headers::get().GrpcMessage, + Http::Utility::PercentEncoding::encode("invalid request body"))); })); EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, filter_->decodeData(buffer, false)); EXPECT_EQ(decoder_callbacks_.details(), "grpc_bridge_data_too_small"); @@ -92,8 +92,8 @@ TEST_F(ReverseBridgeTest, HeaderOnlyGrpcRequest) { EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, true)); // Verify that headers are unmodified. - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "25")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "25")); } // Verify no modification on encoding path as well. @@ -101,8 +101,8 @@ TEST_F(ReverseBridgeTest, HeaderOnlyGrpcRequest) { {{"content-type", "application/grpc"}, {"content-length", "20"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, true)); // Ensure we didn't mutate content type or length. - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); // We should not drain the buffer, nor stop iteration. Envoy::Buffer::OwnedImpl buffer; @@ -121,8 +121,8 @@ TEST_F(ReverseBridgeTest, NoGrpcRequest) { {{"content-type", "application/json"}, {"content-length", "10"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); // Ensure we didn't mutate content type or length. - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/json")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "10")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/json")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "10")); } { @@ -140,8 +140,8 @@ TEST_F(ReverseBridgeTest, NoGrpcRequest) { {{"content-type", "application/json"}, {"content-length", "20"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); // Ensure we didn't mutate content type or length. - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/json")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/json")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); } Envoy::Buffer::OwnedImpl buffer; @@ -154,8 +154,8 @@ TEST_F(ReverseBridgeTest, NoGrpcRequest) { {{"content-type", "application/grpc"}, {"content-length", "20"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, true)); // Ensure we didn't mutate content type or length. - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); } // Verifies that if we receive a gRPC request but have configured the filter to not handle the gRPC @@ -172,10 +172,11 @@ TEST_F(ReverseBridgeTest, GrpcRequestNoManageFrameHeader) { {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "25")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "25")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -195,8 +196,8 @@ TEST_F(ReverseBridgeTest, GrpcRequestNoManageFrameHeader) { Http::TestResponseHeaderMapImpl headers( {{":status", "200"}, {"content-length", "30"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "30")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "30")); { // We should not drain the buffer, nor stop iteration. @@ -215,7 +216,7 @@ TEST_F(ReverseBridgeTest, GrpcRequestNoManageFrameHeader) { buffer.add("ghj", 3); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(buffer, true)); EXPECT_EQ(3, buffer.length()); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); } } @@ -233,10 +234,11 @@ TEST_F(ReverseBridgeTest, GrpcRequest) { {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -263,8 +265,8 @@ TEST_F(ReverseBridgeTest, GrpcRequest) { Http::TestResponseHeaderMapImpl headers( {{":status", "200"}, {"content-length", "30"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "35")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "35")); { // First few calls should drain the buffer @@ -289,7 +291,7 @@ TEST_F(ReverseBridgeTest, GrpcRequest) { buffer.add("ghj", 4); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(buffer, true)); EXPECT_EQ(17, buffer.length()); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); Grpc::Decoder decoder; std::vector frames; @@ -314,9 +316,10 @@ TEST_F(ReverseBridgeTest, GrpcRequestNoContentLength) { {{"content-type", "application/grpc"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); // Ensure that we don't insert a content-length header. EXPECT_EQ(nullptr, headers.ContentLength()); } @@ -345,7 +348,7 @@ TEST_F(ReverseBridgeTest, GrpcRequestNoContentLength) { Http::TestResponseHeaderMapImpl headers( {{":status", "200"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); // Ensure that we don't insert a content-length header. EXPECT_EQ(nullptr, headers.ContentLength()); @@ -372,7 +375,7 @@ TEST_F(ReverseBridgeTest, GrpcRequestNoContentLength) { buffer.add("ghj", 4); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(buffer, true)); EXPECT_EQ(17, buffer.length()); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); Grpc::Decoder decoder; std::vector frames; @@ -398,10 +401,11 @@ TEST_F(ReverseBridgeTest, GrpcRequestHeaderOnlyResponse) { {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -434,8 +438,8 @@ TEST_F(ReverseBridgeTest, GrpcRequestHeaderOnlyResponse) { Http::TestResponseHeaderMapImpl headers( {{":status", "200"}, {"content-length", "0"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, true)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "5")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "5")); } // Tests that a gRPC is downgraded to application/x-protobuf and upgraded back @@ -451,9 +455,10 @@ TEST_F(ReverseBridgeTest, GrpcRequestInternalError) { Http::TestRequestHeaderMapImpl headers( {{"content-type", "application/grpc"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -480,8 +485,8 @@ TEST_F(ReverseBridgeTest, GrpcRequestInternalError) { Http::TestResponseHeaderMapImpl headers( {{":status", "400"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().Status, "200")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().Status, "200")); { // First few calls should drain the buffer @@ -505,7 +510,7 @@ TEST_F(ReverseBridgeTest, GrpcRequestInternalError) { Envoy::Buffer::OwnedImpl buffer; buffer.add("ghj", 4); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(buffer, true)); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "13")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "13")); Grpc::Decoder decoder; std::vector frames; @@ -528,9 +533,10 @@ TEST_F(ReverseBridgeTest, GrpcRequestBadResponseNoContentType) { Http::TestRequestHeaderMapImpl headers( {{"content-type", "application/grpc"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -575,9 +581,10 @@ TEST_F(ReverseBridgeTest, GrpcRequestBadResponse) { Http::TestRequestHeaderMapImpl headers( {{"content-type", "application/grpc"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -634,10 +641,10 @@ TEST_F(ReverseBridgeTest, FilterConfigPerRouteDisabled) { EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); // Verify that headers are unmodified. - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "25")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "25")); EXPECT_THAT(headers, - HeaderValueOf(Http::Headers::get().Path, "/testing.ExampleService/SendData")); + ContainsHeader(Http::Headers::get().Path, "/testing.ExampleService/SendData")); } // Tests that a gRPC is downgraded to application/x-protobuf and upgraded back @@ -662,10 +669,11 @@ TEST_F(ReverseBridgeTest, FilterConfigPerRouteEnabled) { {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -692,8 +700,8 @@ TEST_F(ReverseBridgeTest, FilterConfigPerRouteEnabled) { Http::TestResponseHeaderMapImpl headers( {{":status", "200"}, {"content-length", "30"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "35")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "35")); { // First few calls should drain the buffer @@ -718,7 +726,7 @@ TEST_F(ReverseBridgeTest, FilterConfigPerRouteEnabled) { buffer.add("ghj", 4); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(buffer, true)); EXPECT_EQ(17, buffer.length()); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); Grpc::Decoder decoder; std::vector frames; @@ -748,10 +756,11 @@ TEST_F(ReverseBridgeTest, RouteWithTrailers) { {"content-length", "25"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -770,8 +779,8 @@ TEST_F(ReverseBridgeTest, RouteWithTrailers) { Http::TestResponseHeaderMapImpl headers( {{":status", "200"}, {"content-length", "30"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "35")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "35")); { // First few calls should drain the buffer @@ -795,7 +804,7 @@ TEST_F(ReverseBridgeTest, RouteWithTrailers) { .WillOnce(Invoke([&](Envoy::Buffer::Instance& buf, bool) -> void { buffer.move(buf); })); Http::TestResponseTrailerMapImpl trailers({{"foo", "bar"}, {"one", "two"}, {"three", "four"}}); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(trailers)); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); Grpc::Decoder decoder; std::vector frames; @@ -821,10 +830,11 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponse) { {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "20")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "20")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -854,8 +864,8 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponse) { {"custom-content-length", "8"}, {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "13")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "13")); { // The response data should be streamed to the client instead of buffered. Additionally, the @@ -875,7 +885,7 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponse) { buffer.add("ghj", 4); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(buffer, true)); EXPECT_EQ(4, buffer.length()); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); } } @@ -891,9 +901,10 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponseNoContentLength) { Http::TestRequestHeaderMapImpl headers( {{"content-type", "application/grpc"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -938,9 +949,10 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponseWrongContentLength) { Http::TestRequestHeaderMapImpl headers( {{"content-type", "application/grpc"}, {":path", "/testing.ExampleService/SendData"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/x-protobuf")); EXPECT_THAT(headers, - HeaderValueOf(Http::CustomHeaders::get().Accept, "application/x-protobuf")); + ContainsHeader(Http::Headers::get().ContentType, "application/x-protobuf")); + EXPECT_THAT(headers, + ContainsHeader(Http::CustomHeaders::get().Accept, "application/x-protobuf")); } { @@ -970,8 +982,8 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponseWrongContentLength) { {"content-type", "application/x-protobuf"}}); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentType, "application/grpc")); - EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().ContentLength, "35")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentType, "application/grpc")); + EXPECT_THAT(headers, ContainsHeader(Http::Headers::get().ContentLength, "35")); { // The response data should be streamed to the client instead of buffered. Additionally, the @@ -996,7 +1008,7 @@ TEST_F(ReverseBridgeTest, WithholdGrpcStreamResponseWrongContentLength) { absl::make_optional(static_cast(Grpc::Status::Internal)), _)); EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, filter_->encodeData(buffer, true)); EXPECT_EQ(4, buffer.length()); - EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(trailers, ContainsHeader(Http::Headers::get().GrpcStatus, "0")); } } } // namespace diff --git a/test/extensions/filters/http/grpc_json_reverse_transcoder/grpc_json_reverse_transcoder_integration_test.cc b/test/extensions/filters/http/grpc_json_reverse_transcoder/grpc_json_reverse_transcoder_integration_test.cc index 1558258ab5526..a14f61b37a2ca 100644 --- a/test/extensions/filters/http/grpc_json_reverse_transcoder/grpc_json_reverse_transcoder_integration_test.cc +++ b/test/extensions/filters/http/grpc_json_reverse_transcoder/grpc_json_reverse_transcoder_integration_test.cc @@ -16,8 +16,8 @@ using absl::Status; using absl::StatusCode; +using Envoy::Protobuf::Empty; using Envoy::Protobuf::TextFormat; -using Envoy::ProtobufWkt::Empty; using Envoy::Protobuf::util::MessageDifferencer; @@ -99,14 +99,14 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, SimpleRequest) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); std::string expected_request = "{\"author\":\"John Doe\",\"id\":\"123\",\"title\":\"Kids book\"}"; + EXPECT_THAT(upstream_request_->headers(), + ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Json)); EXPECT_THAT( upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, Http::Headers::get().ContentTypeValues.Json)); - EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().ContentLength, - std::to_string(expected_request.size()))); + ContainsHeader(Http::Headers::get().ContentLength, std::to_string(expected_request.size()))); EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().Path, "/shelves/12345/books/123")); + ContainsHeader(Http::Headers::get().Path, "/shelves/12345/books/123")); EXPECT_EQ(upstream_request_->body().toString(), expected_request); Http::TestResponseHeaderMapImpl response_headers; @@ -124,8 +124,8 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, SimpleRequest) { ASSERT_TRUE(response->waitForEndStream()); EXPECT_TRUE(response->complete()); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().ContentType, - Http::Headers::get().ContentTypeValues.Grpc)); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Grpc)); bookstore::Book expected_book; expected_book.set_id(123); @@ -143,7 +143,7 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, SimpleRequest) { EXPECT_TRUE(MessageDifferencer::Equals(expected_book, book)); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -176,14 +176,13 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, HttpBodyRequestResponse) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT( - upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, Http::Headers::get().ContentTypeValues.Text)); - EXPECT_THAT( - upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().ContentLength, std::to_string(request_str.size()))); EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().Path, "/echoRawBody")); + ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Text)); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Http::Headers::get().ContentLength, + std::to_string(request_str.size()))); + EXPECT_THAT(upstream_request_->headers(), + ContainsHeader(Http::Headers::get().Path, "/echoRawBody")); EXPECT_EQ(upstream_request_->body().toString(), request_str); Http::TestResponseHeaderMapImpl response_headers; @@ -202,8 +201,8 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, HttpBodyRequestResponse) { ASSERT_TRUE(response->waitForEndStream()); EXPECT_TRUE(response->complete()); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().ContentType, - Http::Headers::get().ContentTypeValues.Grpc)); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Grpc)); google::api::HttpBody expected_res; expected_res.set_content_type(Http::Headers::get().ContentTypeValues.Html); @@ -219,7 +218,7 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, HttpBodyRequestResponse) { EXPECT_TRUE(MessageDifferencer::Equals(expected_res, transcoded_res)); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -261,13 +260,13 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, NestedHttpBodyRequest) { ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - EXPECT_THAT( - upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().ContentType, Http::Headers::get().ContentTypeValues.Json)); - EXPECT_THAT(upstream_request_->headers(), Http::HeaderValueOf(Http::Headers::get().ContentLength, - std::to_string(book_str.size()))); EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().Path, "/v2/shelves/12345/books")); + ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Json)); + EXPECT_THAT(upstream_request_->headers(), + ContainsHeader(Http::Headers::get().ContentLength, std::to_string(book_str.size()))); + EXPECT_THAT(upstream_request_->headers(), + ContainsHeader(Http::Headers::get().Path, "/v2/shelves/12345/books")); EXPECT_EQ(upstream_request_->body().toString(), book_str); Http::TestResponseHeaderMapImpl response_headers; @@ -286,8 +285,8 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, NestedHttpBodyRequest) { ASSERT_TRUE(response->waitForEndStream()); EXPECT_TRUE(response->complete()); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().ContentType, - Http::Headers::get().ContentTypeValues.Grpc)); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Grpc)); google::api::HttpBody expected_res; expected_res.set_content_type(Http::Headers::get().ContentTypeValues.Html); @@ -303,7 +302,7 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, NestedHttpBodyRequest) { EXPECT_TRUE(MessageDifferencer::Equals(expected_res, transcoded_res)); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -337,10 +336,10 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, RequestWithQueryParams) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().Method, Http::Headers::get().MethodValues.Get)); + ContainsHeader(Http::Headers::get().Method, Http::Headers::get().MethodValues.Get)); EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().Path, - "/shelves/12345/books:unary?author=567&theme=Science%20Fiction")); + ContainsHeader(Http::Headers::get().Path, + "/shelves/12345/books:unary?author=567&theme=Science%20Fiction")); Http::TestResponseHeaderMapImpl response_headers; response_headers.setStatus(200); @@ -359,8 +358,8 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, RequestWithQueryParams) { ASSERT_TRUE(response->waitForEndStream()); EXPECT_TRUE(response->complete()); - EXPECT_THAT(response->headers(), HeaderValueOf(Http::Headers::get().ContentType, - Http::Headers::get().ContentTypeValues.Grpc)); + EXPECT_THAT(response->headers(), ContainsHeader(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Grpc)); bookstore::ListBooksResponse expected_res; auto* book = expected_res.add_books(); @@ -378,7 +377,7 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, RequestWithQueryParams) { EXPECT_TRUE(MessageDifferencer::Equals(expected_res, transcoded_res)); - EXPECT_THAT(*response->trailers(), HeaderValueOf(Http::Headers::get().GrpcStatus, "0")); + EXPECT_THAT(*response->trailers(), ContainsHeader(Http::Headers::get().GrpcStatus, "0")); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); @@ -410,9 +409,9 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, ErrorFromBackend) { ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); EXPECT_THAT(upstream_request_->headers(), - HeaderValueOf(Http::Headers::get().Method, Http::Headers::get().MethodValues.Put)); + ContainsHeader(Http::Headers::get().Method, Http::Headers::get().MethodValues.Put)); EXPECT_THAT(upstream_request_->headers(), - Http::HeaderValueOf(Http::Headers::get().Path, "/shelves/12345/books")); + ContainsHeader(Http::Headers::get().Path, "/shelves/12345/books")); Http::TestResponseHeaderMapImpl response_headers; response_headers.setStatus(400); @@ -431,10 +430,10 @@ TEST_P(GrpcJsonReverseTranscoderIntegrationTest, ErrorFromBackend) { ASSERT_TRUE(response->trailers()); EXPECT_THAT(*response->trailers(), - Http::HeaderValueOf(Http::Headers::get().GrpcStatus, - std::to_string(Grpc::Status::WellKnownGrpcStatus::Internal))); + ContainsHeader(Http::Headers::get().GrpcStatus, + std::to_string(Grpc::Status::WellKnownGrpcStatus::Internal))); EXPECT_THAT(*response->trailers(), - Http::HeaderValueOf(Http::Headers::get().GrpcMessage, response_str)); + ContainsHeader(Http::Headers::get().GrpcMessage, response_str)); codec_client_->close(); ASSERT_TRUE(fake_upstream_connection_->close()); diff --git a/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc b/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc index 9b3e8d930b662..c0dddba0d1b45 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc @@ -15,8 +15,8 @@ using absl::Status; using absl::StatusCode; +using Envoy::Protobuf::Empty; using Envoy::Protobuf::TextFormat; -using Envoy::ProtobufWkt::Empty; namespace Envoy { namespace { @@ -1059,7 +1059,7 @@ std::string createDeepJson(int level, bool valid) { } std::string jsonStrToPbStrucStr(std::string json) { - Envoy::ProtobufWkt::Struct message; + Envoy::Protobuf::Struct message; std::string structStr; TestUtility::loadFromJson(json, message); TextFormat::PrintToString(message, &structStr); @@ -1106,12 +1106,12 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, DeepStruct) { } std::string createLargeJson(int level) { - std::shared_ptr cur = std::make_shared(); + std::shared_ptr cur = std::make_shared(); for (int i = 0; i < level - 1; ++i) { - std::shared_ptr next = std::make_shared(); - ProtobufWkt::Value val = ProtobufWkt::Value(); - ProtobufWkt::Value left = ProtobufWkt::Value(*cur); - ProtobufWkt::Value right = ProtobufWkt::Value(*cur); + std::shared_ptr next = std::make_shared(); + Protobuf::Value val = Protobuf::Value(); + Protobuf::Value left = Protobuf::Value(*cur); + Protobuf::Value right = Protobuf::Value(*cur); val.mutable_list_value()->add_values()->Swap(&left); val.mutable_list_value()->add_values()->Swap(&right); (*next->mutable_struct_value()->mutable_fields())["k"] = val; diff --git a/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc b/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc index e9d0bd66a0a0a..010a7f093d650 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc @@ -26,7 +26,7 @@ class HttpBodyUtilsTest : public testing::Test { void setBodyFieldPath(const std::vector& body_field_path) { for (int field_number : body_field_path) { - ProtobufWkt::Field field; + Protobuf::Field field; field.set_number(field_number); raw_body_field_path_.emplace_back(std::move(field)); } @@ -82,8 +82,8 @@ class HttpBodyUtilsTest : public testing::Test { EXPECT_FALSE(HttpBodyUtils::parseMessageByFieldPath(&stream, body_field_path_, &http_body)); } - std::vector raw_body_field_path_; - std::vector body_field_path_; + std::vector raw_body_field_path_; + std::vector body_field_path_; }; TEST_F(HttpBodyUtilsTest, UnknownQueryParamsAppearInExtension) { diff --git a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc index ba2de188e3eb7..25807762f5dad 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc @@ -592,6 +592,47 @@ TEST_F(GrpcJsonTranscoderFilterTest, NoTranscoding) { EXPECT_EQ(expected_response_trailers, response_trailers); } +TEST_F(GrpcJsonTranscoderFilterTest, NoTranscodingWithStreamedRequest) { + // It should not be an error to pass-through a non-transcoded request/response when + // the downstream sends more data after the upstream headers have been sent. + // This is a bit of an odd thing to test for specifically, but there was a bug here + // that caused debug builds to assert, so this is an anti-regression test. + Http::TestRequestHeaderMapImpl request_headers{{"content-type", "application/grpc"}, + {":method", "POST"}, + {":path", "/grpc.service/UnknownGrpcMethod"}}; + + Http::TestRequestHeaderMapImpl expected_request_headers{ + {"content-type", "application/grpc"}, + {":method", "POST"}, + {":path", "/grpc.service/UnknownGrpcMethod"}}; + + EXPECT_CALL(decoder_callbacks_.downstream_callbacks_, clearRouteCache()).Times(0); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(expected_request_headers, request_headers); + Http::MetadataMap metadata_map{{"metadata", "metadata"}}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_.decodeMetadata(metadata_map)); + + // Not grpc response. + Http::TestResponseHeaderMapImpl response_headers{{"content-type", "application/json"}, + {":status", "200"}}; + + Http::TestResponseHeaderMapImpl expected_response_headers{{"content-type", "application/json"}, + {":status", "200"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.encodeHeaders(response_headers, false)); + EXPECT_EQ(expected_response_headers, response_headers); + + // decodeData after pass-through encodeHeaders should not provoke an ASSERT. + Buffer::OwnedImpl request_data{"{}"}; + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_data, true)); + EXPECT_EQ(2, request_data.length()); + + Buffer::OwnedImpl response_data{"{}"}; + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.encodeData(response_data, true)); + EXPECT_EQ(2, response_data.length()); +} + TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryPost) { Http::TestRequestHeaderMapImpl request_headers{ {"content-type", "application/json"}, {":method", "POST"}, {":path", "/shelf"}}; @@ -1256,6 +1297,68 @@ TEST_F(GrpcJsonTranscoderFilterTest, TranscodingStreamPostWithHttpBody) { } } +class GrpcJsonTranscoderFilterTestWithLargerBuffer : public GrpcJsonTranscoderFilterTest { +public: + GrpcJsonTranscoderFilterTestWithLargerBuffer() + : GrpcJsonTranscoderFilterTest(modifiedBookstoreProtoConfig()) {} + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder + modifiedBookstoreProtoConfig() { + auto proto_config = bookstoreProtoConfig(); + proto_config.mutable_max_request_body_size()->set_value(1024 * 1024 * 2); + return proto_config; + } +}; + +TEST_F(GrpcJsonTranscoderFilterTestWithLargerBuffer, TranscodingStreamPostWithLargeBufferHttpBody) { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", "/streamBody?arg=hi"}, {"content-type", "text/plain"}}; + + EXPECT_CALL(decoder_callbacks_.downstream_callbacks_, clearRouteCache()); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ("application/grpc", request_headers.get_("content-type")); + EXPECT_EQ("/streamBody?arg=hi", request_headers.get_("x-envoy-original-path")); + EXPECT_EQ("POST", request_headers.get_("x-envoy-original-method")); + EXPECT_EQ("/bookstore.Bookstore/StreamBody", request_headers.get_(":path")); + EXPECT_EQ("trailers", request_headers.get_("te")); + + // For client_streaming, a large buffer should be packaged into multiple grpc frames. + // Test this with a buffer of 2MB plus 512 bytes. + std::string text(JsonTranscoderConfig::MaxStreamedPieceSize * 2 + 512, 'X'); + Buffer::OwnedImpl buffer; + buffer.add(text); + EXPECT_CALL(decoder_callbacks_, sendLocalReply).Times(0); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(buffer, true)); + + Grpc::Decoder decoder; + std::vector frames; + std::ignore = decoder.decode(buffer, frames); + ASSERT_EQ(frames.size(), 3); + + // First frame should include non-streamed content, plus 1MB of streamed content. + bookstore::EchoBodyRequest expected_first_request; + expected_first_request.set_arg("hi"); + expected_first_request.mutable_nested()->mutable_content()->set_content_type("text/plain"); + expected_first_request.mutable_nested()->mutable_content()->set_data( + std::string(JsonTranscoderConfig::MaxStreamedPieceSize, 'X')); + bookstore::EchoBodyRequest request; + request.ParseFromString(frames[0].data_->toString()); + EXPECT_THAT(request, ProtoEq(expected_first_request)); + + // Second frame should have only 1MB of streamed content. + bookstore::EchoBodyRequest expected_second_request; + expected_second_request.mutable_nested()->mutable_content()->set_data( + std::string(JsonTranscoderConfig::MaxStreamedPieceSize, 'X')); + request.ParseFromString(frames[1].data_->toString()); + EXPECT_THAT(request, ProtoEq(expected_second_request)); + + // Third frame should have the remaining 512 bytes of streamed content. + bookstore::EchoBodyRequest expected_third_request; + expected_third_request.mutable_nested()->mutable_content()->set_data(std::string(512, 'X')); + request.ParseFromString(frames[2].data_->toString()); + EXPECT_THAT(request, ProtoEq(expected_third_request)); +} + TEST_F(GrpcJsonTranscoderFilterTest, TranscodingStreamSSE) { envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder proto_config = bookstoreProtoConfig(); @@ -1296,7 +1399,7 @@ TEST_F(GrpcJsonTranscoderFilterTest, TranscodingStreamSSE) { // The configured buffer limits will not apply. TEST_F(GrpcJsonTranscoderFilterTest, TranscodingStreamPostWithHttpBodyNoBuffer) { EXPECT_CALL(decoder_callbacks_, decoderBufferLimit()) - .Times(testing::AtLeast(3)) + .Times(testing::AtLeast(1)) .WillRepeatedly(Return(8)); Http::TestRequestHeaderMapImpl request_headers{ @@ -1490,7 +1593,7 @@ bookstore::EchoStructReqResp createDeepStruct(int level) { auto* field_map = msg.mutable_content()->mutable_fields(); for (int i = 0; i < level; ++i) { (*field_map)["level"] = ValueUtil::numberValue(i); - Envoy::ProtobufWkt::Struct s; + Envoy::Protobuf::Struct s; (*field_map)["struct"] = ValueUtil::structValue(s); field_map = (*field_map)["struct"].mutable_struct_value()->mutable_fields(); } diff --git a/test/extensions/filters/http/grpc_stats/config_test.cc b/test/extensions/filters/http/grpc_stats/config_test.cc index 4346ec23aee88..5fb0ce40d6a2c 100644 --- a/test/extensions/filters/http/grpc_stats/config_test.cc +++ b/test/extensions/filters/http/grpc_stats/config_test.cc @@ -504,11 +504,11 @@ TEST_F(GrpcStatsFilterConfigTest, MessageCounts) { {":path", "/lyft.users.BadCompanions/GetBadCompanions"}}; EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); - ProtobufWkt::Value v1; + Protobuf::Value v1; v1.set_string_value("v1"); auto b1 = Grpc::Common::serializeToGrpcFrame(v1); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(*b1, false)); - ProtobufWkt::Value v2; + Protobuf::Value v2; v2.set_string_value("v2"); auto b2 = Grpc::Common::serializeToGrpcFrame(v2); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(*b2, true)); diff --git a/test/extensions/filters/http/grpc_web/grpc_web_filter_test.cc b/test/extensions/filters/http/grpc_web/grpc_web_filter_test.cc index 8476cfebb8d1d..deaecf95dd4cd 100644 --- a/test/extensions/filters/http/grpc_web/grpc_web_filter_test.cc +++ b/test/extensions/filters/http/grpc_web/grpc_web_filter_test.cc @@ -502,6 +502,17 @@ TEST_P(GrpcWebFilterTest, MediaTypeWithParameter) { EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.encodeData(data, false)); } +TEST_P(GrpcWebFilterTest, RemoveResponseContentLength) { + Http::TestRequestHeaderMapImpl request_headers{ + {"content-type", Http::Headers::get().ContentTypeValues.GrpcWeb}, {":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "200"}, {"content-type", "application/grpc"}, {"content-length", "123"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.encodeHeaders(response_headers, false)); + EXPECT_EQ(nullptr, response_headers.ContentLength()); +} + TEST_P(GrpcWebFilterTest, Unary) { // Tests request headers. request_headers_.addCopy(Http::Headers::get().ContentType, requestContentType()); diff --git a/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc b/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc index 9da8b14b6515e..eeb89c585ab0d 100644 --- a/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc +++ b/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc @@ -70,7 +70,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParammutable_typed_per_filter_config()->insert( {"downstream-header-mutation", per_route_config}); @@ -87,7 +87,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParammutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; per_route_config.PackFrom(header_mutation); route->mutable_typed_per_filter_config()->insert( {absl::StrCat(prefix, "-header-mutation"), per_route_config}); @@ -106,7 +106,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParammutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config_vhost; + Protobuf::Any per_route_config_vhost; per_route_config_vhost.PackFrom(header_mutation_vhost); auto* vhost = hcm.mutable_route_config()->mutable_virtual_hosts(0); @@ -124,7 +124,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParammutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config_rt; + Protobuf::Any per_route_config_rt; per_route_config_rt.PackFrom(header_mutation_rt); auto* route_table = hcm.mutable_route_config(); @@ -222,7 +222,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParamPackFrom(header_mutation); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; per_route_config.PackFrom(per_route_filter_config); route->mutable_typed_per_filter_config()->insert( {"upstream-header-mutation", per_route_config}); @@ -234,7 +234,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParamPackFrom(header_mutation_vhost); - ProtobufWkt::Any per_route_config_vhost; + Protobuf::Any per_route_config_vhost; per_route_config_vhost.PackFrom(per_route_filter_config_vhost); auto* vhost = hcm.mutable_route_config()->mutable_virtual_hosts(0); @@ -248,7 +248,7 @@ class HeaderMutationIntegrationTest : public testing::TestWithParamPackFrom(header_mutation_rt); - ProtobufWkt::Any per_route_config_rt; + Protobuf::Any per_route_config_rt; per_route_config_rt.PackFrom(per_route_filter_config_rt); auto* route_table = hcm.mutable_route_config(); @@ -388,7 +388,7 @@ disabled: true request_mutation->mutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; per_route_config.PackFrom(header_mutation); route->mutable_typed_per_filter_config()->insert( {"downstream-header-mutation", per_route_config}); @@ -405,7 +405,7 @@ disabled: true "upstream-per-route-flag-header-value"); response_mutation->mutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; per_route_config.PackFrom(header_mutation); route->mutable_typed_per_filter_config()->insert( {"upstream-header-mutation", per_route_config}); @@ -416,7 +416,7 @@ disabled: true envoy::config::route::v3::FilterConfig filter_config; filter_config.mutable_config()->PackFrom(PerRouteProtoConfig()); filter_config.set_disabled(false); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; per_route_config.PackFrom(filter_config); // Try enable the filter that is disabled by default. route->mutable_typed_per_filter_config()->insert( @@ -429,7 +429,7 @@ disabled: true // Per route disable downstream and upstream header mutation. envoy::config::route::v3::FilterConfig filter_config; filter_config.set_disabled(true); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; per_route_config.PackFrom(filter_config); another_route->mutable_typed_per_filter_config()->insert( {"downstream-header-mutation", per_route_config}); @@ -451,7 +451,7 @@ disabled: true response_mutation_vhost->mutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config_vhost; + Protobuf::Any per_route_config_vhost; per_route_config_vhost.PackFrom(header_mutation_vhost); auto* vhost = hcm.mutable_route_config()->mutable_virtual_hosts(0); @@ -470,7 +470,7 @@ disabled: true response_mutation_vhost->mutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config_vhost; + Protobuf::Any per_route_config_vhost; per_route_config_vhost.PackFrom(header_mutation_vhost); auto* vhost = hcm.mutable_route_config()->mutable_virtual_hosts(0); @@ -492,7 +492,7 @@ disabled: true response_mutation_rt->mutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config_rt; + Protobuf::Any per_route_config_rt; per_route_config_rt.PackFrom(header_mutation_rt); auto* route_table = hcm.mutable_route_config(); @@ -511,7 +511,7 @@ disabled: true response_mutation_rt->mutable_append()->set_append_action( envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); - ProtobufWkt::Any per_route_config_rt; + Protobuf::Any per_route_config_rt; per_route_config_rt.PackFrom(header_mutation_rt); auto* route_table = hcm.mutable_route_config(); diff --git a/test/extensions/filters/http/header_mutation/header_mutation_test.cc b/test/extensions/filters/http/header_mutation/header_mutation_test.cc index a37d1fa58db51..3b2d07b43ce63 100644 --- a/test/extensions/filters/http/header_mutation/header_mutation_test.cc +++ b/test/extensions/filters/http/header_mutation/header_mutation_test.cc @@ -1,6 +1,7 @@ #include "source/extensions/filters/http/header_mutation/header_mutation.h" #include "test/mocks/http/mocks.h" +#include "test/mocks/server/server_factory_context.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -48,6 +49,11 @@ TEST(HeaderMutationFilterTest, RequestMutationTest) { key: "flag-header-6" value: "flag-header-6-value" append_action: "OVERWRITE_IF_EXISTS" + - append: + header: + key: "flag-header-7" + value: "%TRACE_ID%" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" )EOF"; const std::string config_yaml = R"EOF( @@ -60,17 +66,19 @@ TEST(HeaderMutationFilterTest, RequestMutationTest) { append_action: "ADD_IF_ABSENT" )EOF"; + Server::Configuration::MockServerFactoryContext context; + PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config, creation_status); + std::make_shared(per_route_proto_config, context, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config, creation_status); + std::make_shared(proto_config, context, creation_status); { NiceMock decoder_callbacks; @@ -80,6 +88,10 @@ TEST(HeaderMutationFilterTest, RequestMutationTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); + EXPECT_CALL(decoder_callbacks, activeSpan()); + EXPECT_CALL(decoder_callbacks.active_span_, getTraceId()) + .WillOnce(testing::Return("trace-id-value")); + EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { return {config.get()}; @@ -113,6 +125,8 @@ TEST(HeaderMutationFilterTest, RequestMutationTest) { EXPECT_FALSE(headers.has("flag-header-5")); // 'flag-header-6' was present and should be overwritten. EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); + // 'flag-header-7' should the value extracted from the trace ID. + EXPECT_EQ("trace-id-value", headers.get_("flag-header-7")); // global header is added. EXPECT_EQ("global-flag-header-value", headers.get_("global-flag-header")); } @@ -153,6 +167,11 @@ TEST(HeaderMutationFilterTest, ResponseMutationTest) { key: "flag-header-6" value: "flag-header-6-value" append_action: "OVERWRITE_IF_EXISTS" + - append: + header: + key: "flag-header-7" + value: "%TRACE_ID%" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" )EOF"; const std::string config_yaml = R"EOF( @@ -161,17 +180,19 @@ TEST(HeaderMutationFilterTest, ResponseMutationTest) { - remove: "global-flag-header" )EOF"; + Server::Configuration::MockServerFactoryContext context; + PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config, creation_status); + std::make_shared(per_route_proto_config, context, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config, creation_status); + std::make_shared(proto_config, context, creation_status); // Case where the decodeHeaders() is not called and the encodeHeaders() is called. { @@ -182,6 +203,10 @@ TEST(HeaderMutationFilterTest, ResponseMutationTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); + EXPECT_CALL(encoder_callbacks, activeSpan()); + EXPECT_CALL(encoder_callbacks.active_span_, getTraceId()) + .WillOnce(testing::Return("trace-id-value")); + EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { return {config.get()}; @@ -219,6 +244,8 @@ TEST(HeaderMutationFilterTest, ResponseMutationTest) { EXPECT_FALSE(headers.has("flag-header-5")); // 'flag-header-6' was present and should be overwritten. EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); + // 'flag-header-7' should the value extracted from the trace ID. + EXPECT_EQ("trace-id-value", headers.get_("flag-header-7")); // global header is removed. EXPECT_FALSE(headers.has("global-flag-header")); } @@ -318,6 +345,11 @@ TEST(HeaderMutationFilterTest, ResponseTrailerMutationTest) { key: "flag-header-6" value: "flag-header-6-value" append_action: "OVERWRITE_IF_EXISTS" + - append: + header: + key: "flag-header-7" + value: "%TRACE_ID%" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" )EOF"; const std::string config_yaml = R"EOF( @@ -326,17 +358,19 @@ TEST(HeaderMutationFilterTest, ResponseTrailerMutationTest) { - remove: "global-flag-header" )EOF"; + Server::Configuration::MockServerFactoryContext context; + PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config, creation_status); + std::make_shared(per_route_proto_config, context, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config, creation_status); + std::make_shared(proto_config, context, creation_status); // Case where the decodeHeaders() is not called and the encodeHeaders() is called. { @@ -347,6 +381,10 @@ TEST(HeaderMutationFilterTest, ResponseTrailerMutationTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); + EXPECT_CALL(encoder_callbacks, activeSpan()); + EXPECT_CALL(encoder_callbacks.active_span_, getTraceId()) + .WillOnce(testing::Return("trace-id-value")); + EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { return {config.get()}; @@ -393,6 +431,8 @@ TEST(HeaderMutationFilterTest, ResponseTrailerMutationTest) { EXPECT_FALSE(trailers.has("flag-header-5")); // 'flag-header-6' was present and should be overwritten. EXPECT_EQ("flag-header-6-value", trailers.get_("flag-header-6")); + // 'flag-header-7' should the value extracted from the trace ID. + EXPECT_EQ("trace-id-value", trailers.get_("flag-header-7")); // global header is removed. EXPECT_FALSE(trailers.has("global-flag-header")); } @@ -498,17 +538,19 @@ TEST(HeaderMutationFilterTest, HybridMutationTest) { - remove: "global-flag-header" )EOF"; + Server::Configuration::MockServerFactoryContext context; + PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config, creation_status); + std::make_shared(per_route_proto_config, context, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config, creation_status); + std::make_shared(proto_config, context, creation_status); { NiceMock decoder_callbacks; @@ -628,17 +670,19 @@ TEST(HeaderMutationFilterTest, QueryParameterMutationTest) { - remove: "global-flag-header" )EOF"; + Server::Configuration::MockServerFactoryContext context; + PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config, creation_status); + std::make_shared(per_route_proto_config, context, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config, creation_status); + std::make_shared(proto_config, context, creation_status); { NiceMock decoder_callbacks; @@ -727,6 +771,11 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { key: "flag-header-6" value: "flag-header-6-value" append_action: "OVERWRITE_IF_EXISTS" + - append: + header: + key: "flag-header-7" + value: "%TRACE_ID%" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" )EOF"; const std::string config_yaml = R"EOF( @@ -735,17 +784,19 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { - remove: "global-flag-header" )EOF"; + Server::Configuration::MockServerFactoryContext context; + PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config, creation_status); + std::make_shared(per_route_proto_config, context, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config, creation_status); + std::make_shared(proto_config, context, creation_status); // Case where the decodeHeaders() is not called and the encodeHeaders() is called. { @@ -756,7 +807,11 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); - EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) + EXPECT_CALL(decoder_callbacks, activeSpan()); + EXPECT_CALL(decoder_callbacks.active_span_, getTraceId()) + .WillOnce(testing::Return("trace-id-value")); + + EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { return {config.get()}; })); @@ -774,7 +829,7 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { Http::RequestHeaderMapPtr request_headers_pointer{ new Envoy::Http::TestRequestHeaderMapImpl{{"req-flag-header", "req-header-value"}}}; - EXPECT_CALL(encoder_callbacks, requestHeaders()) + EXPECT_CALL(decoder_callbacks, requestHeaders()) .WillOnce(testing::Return(makeOptRefFromPtr(request_headers_pointer.get()))); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter.decodeTrailers(trailers)); @@ -795,6 +850,8 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { EXPECT_FALSE(trailers.has("flag-header-5")); // 'flag-header-6' was present and should be overwritten. EXPECT_EQ("flag-header-6-value", trailers.get_("flag-header-6")); + // 'flag-header-7' should the value extracted from the trace ID. + EXPECT_EQ("trace-id-value", trailers.get_("flag-header-7")); // global header is removed. EXPECT_FALSE(trailers.has("global-flag-header")); } @@ -809,7 +866,7 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); - EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) + EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { return {config.get()}; })); @@ -825,7 +882,7 @@ TEST(HeaderMutationFilterTest, RequestTrailerMutationTest) { {":status", "200"}, }; - EXPECT_CALL(encoder_callbacks, requestHeaders()) + EXPECT_CALL(decoder_callbacks, requestHeaders()) .WillOnce(testing::Return(Http::RequestHeaderMapOptRef{})); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter.decodeTrailers(trailers)); diff --git a/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc b/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc index 10890d6eb071a..6dd58a2358d84 100644 --- a/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc +++ b/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc @@ -9,9 +9,11 @@ #include "source/extensions/filters/http/well_known_names.h" #include "test/mocks/http/mocks.h" +#include "test/mocks/stats/mocks.h" #include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" +#include "absl/container/flat_hash_map.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -26,7 +28,7 @@ namespace HeaderToMetadataFilter { namespace { MATCHER_P(MapEq, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); @@ -35,7 +37,7 @@ MATCHER_P(MapEq, rhs, "") { } MATCHER_P(MapEqNum, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).number_value(), entry.second); @@ -44,7 +46,7 @@ MATCHER_P(MapEqNum, rhs, "") { } MATCHER_P(MapEqValue, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_TRUE(TestUtility::protoEqual(obj.fields().at(entry.first), entry.second)); @@ -73,7 +75,8 @@ class HeaderToMetadataTest : public testing::Test { absl::Status initializeFilter(const std::string& yaml) { envoy::extensions::filters::http::header_to_metadata::v3::Config config; TestUtility::loadFromYaml(yaml, config); - absl::StatusOr config_or = Config::create(config, regex_engine_); + absl::StatusOr config_or = + Config::create(config, regex_engine_, *stats_.rootScope()); RETURN_IF_NOT_OK_REF(config_or.status()); config_ = std::move(*config_or); filter_ = std::make_shared(config_); @@ -82,9 +85,15 @@ class HeaderToMetadataTest : public testing::Test { return absl::OkStatus(); } + uint64_t findCounter(const std::string& name) { + const auto counter = TestUtility::findCounter(stats_, name); + return counter != nullptr ? counter->value() : 0; + } + const Config* getConfig() { return filter_->getConfig(); } Regex::GoogleReEngine regex_engine_; + Stats::IsolatedStoreImpl stats_; ConfigSharedPtr config_; std::shared_ptr filter_; NiceMock decoder_callbacks_; @@ -139,7 +148,8 @@ TEST_F(HeaderToMetadataTest, PerRouteOverride) { // Setup per route config. envoy::extensions::filters::http::header_to_metadata::v3::Config config_proto; TestUtility::loadFromYaml(request_config_yaml, config_proto); - ConfigSharedPtr per_route_config = *Config::create(config_proto, regex_engine_, true); + ConfigSharedPtr per_route_config = + *Config::create(config_proto, regex_engine_, *stats_.rootScope(), true); EXPECT_CALL(*decoder_callbacks_.route_, mostSpecificPerFilterConfig(_)) .WillOnce(Return(per_route_config.get())); @@ -164,7 +174,8 @@ TEST_F(HeaderToMetadataTest, ConfigIsCached) { // Setup per route config. envoy::extensions::filters::http::header_to_metadata::v3::Config config_proto; TestUtility::loadFromYaml(request_config_yaml, config_proto); - ConfigSharedPtr per_route_config = *Config::create(config_proto, regex_engine_, true); + ConfigSharedPtr per_route_config = + *Config::create(config_proto, regex_engine_, *stats_.rootScope(), true); EXPECT_CALL(*decoder_callbacks_.route_, mostSpecificPerFilterConfig(_)) .WillOnce(Return(per_route_config.get())); @@ -278,10 +289,10 @@ TEST_F(HeaderToMetadataTest, ProtobufValueTypeInBase64UrlTest) { )EOF"; EXPECT_TRUE(initializeFilter(response_config_yaml).ok()); - ProtobufWkt::Value value; + Protobuf::Value value; auto* s = value.mutable_struct_value(); - ProtobufWkt::Value v; + Protobuf::Value v; v.set_string_value("blafoo"); (*s->mutable_fields())["k1"] = v; v.set_number_value(2019.07); @@ -293,7 +304,7 @@ TEST_F(HeaderToMetadataTest, ProtobufValueTypeInBase64UrlTest) { ASSERT_TRUE(value.SerializeToString(&data)); const auto encoded = Base64::encode(data.c_str(), data.size()); Http::TestResponseHeaderMapImpl incoming_headers{{"x-authenticated", encoded}}; - std::map expected = {{"auth", value}}; + std::map expected = {{"auth", value}}; EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); EXPECT_CALL(req_info_, @@ -475,7 +486,7 @@ TEST_F(HeaderToMetadataTest, PerRouteEmtpyRules) { envoy::extensions::filters::http::header_to_metadata::v3::Config config_proto; auto expected = "header_to_metadata_filter: Per filter configs must at " "least specify either request or response rules"; - auto create_or = Config::create(config_proto, regex_engine_, true); + auto create_or = Config::create(config_proto, regex_engine_, *stats_.rootScope(), true); EXPECT_FALSE(create_or.ok()); EXPECT_EQ(create_or.status().message(), expected); } @@ -792,6 +803,239 @@ TEST_F(HeaderToMetadataTest, CookieRegexSubstitution) { } } +/** + * Test that stats are not collected when stat_prefix is not configured. + */ +TEST_F(HeaderToMetadataTest, NoStatsWithoutPrefix) { + const std::string config_yaml = R"EOF( +request_rules: + - header: x-version + on_header_present: + metadata_namespace: envoy.lb + key: version + type: STRING +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + EXPECT_FALSE(getConfig()->stats().has_value()); +} + +/** + * Test that stats are collected when stat_prefix is configured. + */ +TEST_F(HeaderToMetadataTest, StatsCollectedWithPrefix) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +request_rules: + - header: x-version + on_header_present: + metadata_namespace: envoy.lb + key: version + type: STRING +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + EXPECT_TRUE(getConfig()->stats().has_value()); +} + +/** + * Test that rules_processed and metadata_added stats are incremented correctly. + */ +TEST_F(HeaderToMetadataTest, StatsRulesProcessedAndMetadataAdded) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +request_rules: + - header: x-version + on_header_present: + metadata_namespace: envoy.lb + key: version + type: STRING + - header: x-custom + on_header_present: + metadata_namespace: envoy.lb + key: custom + type: STRING +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + + Http::TestRequestHeaderMapImpl headers{{"x-version", "1.0"}, {"x-custom", "test"}}; + absl::flat_hash_map expected = {{"version", "1.0"}, {"custom", "test"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(testing::ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + + // Verify stats were collected correctly. + EXPECT_EQ(2U, findCounter("http_filter_name.test_prefix.request_rules_processed")); + EXPECT_EQ(2U, findCounter("http_filter_name.test_prefix.request_metadata_added")); + EXPECT_EQ(0U, findCounter("http_filter_name.test_prefix.response_rules_processed")); + EXPECT_EQ(0U, findCounter("http_filter_name.test_prefix.response_metadata_added")); +} + +/** + * Test that header_not_found stat is incremented when header is missing. + */ +TEST_F(HeaderToMetadataTest, StatsHeaderNotFound) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +request_rules: + - header: x-missing + on_header_missing: + metadata_namespace: envoy.lb + key: default + value: 'missing' + type: STRING +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + + Http::TestRequestHeaderMapImpl headers{}; // No headers present. + absl::flat_hash_map expected = {{"default", "missing"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(testing::ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + + // Verify stats were collected correctly. + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.request_rules_processed")); + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.request_header_not_found")); + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.request_metadata_added")); +} + +/** + * Test that value_too_long stat is incremented when header value exceeds limit. + */ +TEST_F(HeaderToMetadataTest, StatsValueTooLong) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +request_rules: + - header: x-long + on_header_present: + metadata_namespace: envoy.lb + key: long_value + type: STRING +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + + // Create a header value that exceeds MAX_HEADER_VALUE_LEN (8KB). + std::string long_value(9000, 'a'); + Http::TestRequestHeaderMapImpl headers{{"x-long", long_value}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(testing::ReturnRef(req_info_)); + // No metadata should be set due to value being too long. + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + + // Verify stats were collected correctly. + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.request_rules_processed")); + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.header_value_too_long")); + EXPECT_EQ( + 0U, findCounter("http_filter_name.test_prefix.request_metadata_added")); // No metadata added. +} + +/** + * Test that value_decode_failed stat is incremented when Base64 decode fails. + */ +TEST_F(HeaderToMetadataTest, StatsValueDecodeFailed) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +request_rules: + - header: x-encoded + on_header_present: + metadata_namespace: envoy.lb + key: decoded_value + type: STRING + encode: BASE64 +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + + // Invalid Base64 string. + Http::TestRequestHeaderMapImpl headers{{"x-encoded", "invalid_base64!@#$"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(testing::ReturnRef(req_info_)); + // No metadata should be set due to decode failure. + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + + // Verify stats were collected correctly. + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.request_rules_processed")); + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.base64_decode_failed")); + EXPECT_EQ( + 0U, findCounter("http_filter_name.test_prefix.request_metadata_added")); // No metadata added. +} + +/** + * Test response rule processing and stats. + */ +TEST_F(HeaderToMetadataTest, StatsResponseRules) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +response_rules: + - header: x-response-header + on_header_present: + metadata_namespace: envoy.lb + key: response_value + type: STRING +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + + Http::TestResponseHeaderMapImpl headers{{"x-response-header", "response_data"}}; + absl::flat_hash_map expected = {{"response_value", "response_data"}}; + + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillRepeatedly(testing::ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(headers, false)); + + // Verify stats were collected correctly. + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.response_rules_processed")); + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.response_metadata_added")); + EXPECT_EQ(0U, findCounter("http_filter_name.test_prefix.request_rules_processed")); + EXPECT_EQ(0U, findCounter("http_filter_name.test_prefix.request_metadata_added")); +} + +/** + * Test that regex_substitution_failed stat is incremented when regex results in empty value. + */ +TEST_F(HeaderToMetadataTest, StatsRegexSubstitutionFailed) { + const std::string config_yaml = R"EOF( +stat_prefix: test_prefix +request_rules: + - header: x-test + on_header_present: + metadata_namespace: envoy.lb + key: transformed_value + type: STRING + regex_value_rewrite: + pattern: + google_re2: {} + regex: "^([a-z]+)$" + substitution: "" +)EOF"; + + EXPECT_TRUE(initializeFilter(config_yaml).ok()); + + // Header value that matches pattern but substitution results in empty string. + Http::TestRequestHeaderMapImpl headers{{"x-test", "validinput"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(testing::ReturnRef(req_info_)); + // No metadata should be set due to empty substitution result. + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + + // Verify stats were collected correctly. + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.request_rules_processed")); + EXPECT_EQ(1U, findCounter("http_filter_name.test_prefix.regex_substitution_failed")); + EXPECT_EQ( + 0U, findCounter("http_filter_name.test_prefix.request_metadata_added")); // No metadata added. +} + } // namespace HeaderToMetadataFilter } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/json_to_metadata/filter_test.cc b/test/extensions/filters/http/json_to_metadata/filter_test.cc index f2a684b593494..d5b3268df8904 100644 --- a/test/extensions/filters/http/json_to_metadata/filter_test.cc +++ b/test/extensions/filters/http/json_to_metadata/filter_test.cc @@ -22,7 +22,7 @@ namespace HttpFilters { namespace JsonToMetadata { MATCHER_P(MapEq, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); @@ -31,7 +31,7 @@ MATCHER_P(MapEq, rhs, "") { } MATCHER_P2(MapEqType, rhs, getter, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(getter(obj.fields().at(entry.first)), entry.second); @@ -208,11 +208,10 @@ TEST_F(FilterTest, BasicBoolMatch) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.bool_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.bool_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -230,11 +229,10 @@ TEST_F(FilterTest, BasicIntegerMatch) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -252,11 +250,10 @@ TEST_F(FilterTest, BasicDoubleMatch) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -374,11 +371,10 @@ TEST_F(FilterTest, StringToNumber) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -409,11 +405,10 @@ TEST_F(FilterTest, BadStringToNumber) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -467,11 +462,10 @@ TEST_F(FilterTest, NumberToNumber) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -525,11 +519,10 @@ TEST_F(FilterTest, IntegerToNumber) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); @@ -583,11 +576,10 @@ TEST_F(FilterTest, BoolToNumber) { filter_->decodeHeaders(incoming_headers_, false)); EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); - EXPECT_CALL( - stream_info_, - setDynamicMetadata("envoy.lb", MapEqType(expected, [](const ProtobufWkt::Value& value) { - return value.number_value(); - }))); + EXPECT_CALL(stream_info_, + setDynamicMetadata("envoy.lb", MapEqType(expected, [](const Protobuf::Value& value) { + return value.number_value(); + }))); testRequestWithBody(request_body); EXPECT_EQ(getCounterValue("json_to_metadata.rq.success"), 1); diff --git a/test/extensions/filters/http/jwt_authn/authenticator_test.cc b/test/extensions/filters/http/jwt_authn/authenticator_test.cc index 0202d588358b9..946b3bb839e19 100644 --- a/test/extensions/filters/http/jwt_authn/authenticator_test.cc +++ b/test/extensions/filters/http/jwt_authn/authenticator_test.cc @@ -65,7 +65,7 @@ class AuthenticatorTest : public testing::Test { google::jwt_verify::getStatusString(expected_status).c_str()); }; auto set_extracted_jwt_data_cb = [this](const std::string& name, - const ProtobufWkt::Struct& extracted_data) { + const Protobuf::Struct& extracted_data) { this->addExtractedData(name, extracted_data); }; initTokenExtractor(); @@ -86,7 +86,7 @@ class AuthenticatorTest : public testing::Test { // This is like ContextImpl::addExtractedData in // source/extensions/filters/http/jwt_authn/verifier.cc. - void addExtractedData(const std::string& name, const ProtobufWkt::Struct& extracted_data) { + void addExtractedData(const std::string& name, const Protobuf::Struct& extracted_data) { *(*out_extracted_data_.mutable_fields())[name].mutable_struct_value() = extracted_data; } @@ -98,7 +98,7 @@ class AuthenticatorTest : public testing::Test { JwksFetcherPtr fetcher_; AuthenticatorPtr auth_; ::google::jwt_verify::JwksPtr jwks_; - ProtobufWkt::Struct out_extracted_data_; + Protobuf::Struct out_extracted_data_; NiceMock parent_span_; }; @@ -318,7 +318,7 @@ TEST_F(AuthenticatorTest, TestSetPayload) { // Only one field is set. EXPECT_EQ(1, out_extracted_data_.fields().size()); - ProtobufWkt::Value expected_payload; + Protobuf::Value expected_payload; TestUtility::loadFromJson(ExpectedPayloadJSON, expected_payload); EXPECT_TRUE( TestUtility::protoEqual(expected_payload, out_extracted_data_.fields().at("my_payload"))); @@ -350,7 +350,7 @@ TEST_F(AuthenticatorTest, TestSetPayloadWithSpaces) { // Only one field is set. EXPECT_EQ(1, out_extracted_data_.fields().size()); - ProtobufWkt::Value expected_payload; + Protobuf::Value expected_payload; TestUtility::loadFromJson(ExpectedPayloadJSONWithSpaces, expected_payload); EXPECT_TRUE( TestUtility::protoEqual(expected_payload, out_extracted_data_.fields().at("my_payload"))); @@ -377,7 +377,7 @@ TEST_F(AuthenticatorTest, TestSetHeader) { EXPECT_EQ(1, out_extracted_data_.fields().size()); // We should expect empty JWT payload. - ProtobufWkt::Value expected_payload; + Protobuf::Value expected_payload; TestUtility::loadFromJson(ExpectedHeaderJSON, expected_payload); EXPECT_TRUE( TestUtility::protoEqual(expected_payload, out_extracted_data_.fields().at("my_header"))); @@ -604,12 +604,12 @@ TEST_F(AuthenticatorTest, TestSetPayloadAndHeader) { EXPECT_EQ(2, out_extracted_data_.fields().size()); // We should expect both JWT payload and header are set. - ProtobufWkt::Value expected_payload; + Protobuf::Value expected_payload; TestUtility::loadFromJson(ExpectedPayloadJSON, expected_payload); EXPECT_TRUE( TestUtility::protoEqual(expected_payload, out_extracted_data_.fields().at("my_payload"))); - ProtobufWkt::Value expected_header; + Protobuf::Value expected_header; TestUtility::loadFromJson(ExpectedHeaderJSON, expected_header); EXPECT_TRUE( TestUtility::protoEqual(expected_header, out_extracted_data_.fields().at("my_header"))); @@ -1102,7 +1102,7 @@ class AuthenticatorJwtCacheTest : public testing::Test { ASSERT_EQ(status, expected_status); }; auto set_extracted_jwt_data_cb = [this](const std::string& name, - const ProtobufWkt::Struct& extracted_data) { + const Protobuf::Struct& extracted_data) { out_name_ = name; out_extracted_data_ = extracted_data; }; @@ -1120,7 +1120,7 @@ class AuthenticatorJwtCacheTest : public testing::Test { ExtractorConstPtr extractor_; NiceMock parent_span_; std::string out_name_; - ProtobufWkt::Struct out_extracted_data_; + Protobuf::Struct out_extracted_data_; }; TEST_F(AuthenticatorJwtCacheTest, TestNonProvider) { @@ -1183,7 +1183,7 @@ TEST_F(AuthenticatorJwtCacheTest, TestCacheHit) { // Payload is set EXPECT_EQ(out_name_, "my_payload"); - ProtobufWkt::Struct expected_payload; + Protobuf::Struct expected_payload; TestUtility::loadFromJson(ExpectedPayloadJSON, expected_payload); EXPECT_TRUE(TestUtility::protoEqual(out_extracted_data_, expected_payload)); } diff --git a/test/extensions/filters/http/jwt_authn/extractor_test.cc b/test/extensions/filters/http/jwt_authn/extractor_test.cc index 75109af4fcb1b..9bf2c0d2abb87 100644 --- a/test/extensions/filters/http/jwt_authn/extractor_test.cc +++ b/test/extensions/filters/http/jwt_authn/extractor_test.cc @@ -205,26 +205,10 @@ TEST_F(ExtractorTest, TestDefaultParamLocation) { EXPECT_FALSE(tokens[0]->isIssuerAllowed("unknown_issuer")); // Test token remove from the query parameter - { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.jwt_authn_remove_jwt_from_query_params", "false"}}); - - tokens[0]->removeJwt(headers); - Http::Utility::QueryParamsMulti query_params = - Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); - EXPECT_EQ(query_params.getFirstValue("access_token").has_value(), true); - } - { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.jwt_authn_remove_jwt_from_query_params", "true"}}); - - tokens[0]->removeJwt(headers); - Http::Utility::QueryParamsMulti query_params = - Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); - EXPECT_EQ(query_params.getFirstValue("access_token").has_value(), false); - } + tokens[0]->removeJwt(headers); + Http::Utility::QueryParamsMulti query_params = + Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); + EXPECT_EQ(query_params.getFirstValue("access_token").has_value(), false); } // Test extracting token from the custom header: "token-header" @@ -341,26 +325,10 @@ TEST_F(ExtractorTest, TestCustomParamToken) { EXPECT_FALSE(tokens[0]->isIssuerAllowed("issuer5")); EXPECT_FALSE(tokens[0]->isIssuerAllowed("unknown_issuer")); - { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.jwt_authn_remove_jwt_from_query_params", "false"}}); - - tokens[0]->removeJwt(headers); - Http::Utility::QueryParamsMulti query_params = - Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); - EXPECT_EQ(query_params.getFirstValue("token_param").has_value(), true); - } - { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.jwt_authn_remove_jwt_from_query_params", "true"}}); - - tokens[0]->removeJwt(headers); - Http::Utility::QueryParamsMulti query_params = - Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); - EXPECT_EQ(query_params.getFirstValue("token_param").has_value(), false); - } + tokens[0]->removeJwt(headers); + Http::Utility::QueryParamsMulti query_params = + Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); + EXPECT_EQ(query_params.getFirstValue("token_param").has_value(), false); } // Test extracting token from a cookie diff --git a/test/extensions/filters/http/jwt_authn/filter_config_test.cc b/test/extensions/filters/http/jwt_authn/filter_config_test.cc index 0d9e703c49d66..e4c52c39690cf 100644 --- a/test/extensions/filters/http/jwt_authn/filter_config_test.cc +++ b/test/extensions/filters/http/jwt_authn/filter_config_test.cc @@ -340,26 +340,6 @@ TEST(HttpJwtAuthnFilterConfigTest, RemoteJwksInvalidUri) { HasSubstr("invalid URI")); } -TEST(HttpJwtAuthnFilterConfigTest, RemoteJwksInvalidUriValidationDisabled) { - // Disabling this runtime feature should allow invalid URIs. - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.jwt_authn_validate_uri", "false"}}); - const char config[] = R"( -providers: - provider1: - issuer: issuer1 - remote_jwks: - http_uri: - uri: http://www.not\nvalid.com -)"; - - JwtAuthentication proto_config; - TestUtility::loadFromYaml(config, proto_config); - - NiceMock context; - EXPECT_NO_THROW(FilterConfigImpl(proto_config, "", context)); -} - TEST(HttpJwtAuthnFilterConfigTest, RemoteJwksValidUri) { // Valid URI should not fail config validation. const char config[] = R"( diff --git a/test/extensions/filters/http/jwt_authn/filter_test.cc b/test/extensions/filters/http/jwt_authn/filter_test.cc index 404b52a6997fe..6870ce1bcb6e9 100644 --- a/test/extensions/filters/http/jwt_authn/filter_test.cc +++ b/test/extensions/filters/http/jwt_authn/filter_test.cc @@ -147,7 +147,7 @@ TEST_F(FilterTest, CorsPreflightMssingAccessControlRequestMethod) { // This test verifies the setExtractedData call is handled correctly TEST_F(FilterTest, TestSetExtractedData) { setupMockConfig(); - ProtobufWkt::Struct extracted_data; + Protobuf::Struct extracted_data; // A successful authentication completed inline: callback is called inside verify(). EXPECT_CALL(*mock_verifier_, verify(_)) .WillOnce(Invoke([&extracted_data](ContextSharedPtr context) { @@ -157,7 +157,7 @@ TEST_F(FilterTest, TestSetExtractedData) { EXPECT_CALL(filter_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([&extracted_data](const std::string& ns, const ProtobufWkt::Struct& out_payload) { + Invoke([&extracted_data](const std::string& ns, const Protobuf::Struct& out_payload) { EXPECT_EQ(ns, "envoy.filters.http.jwt_authn"); EXPECT_TRUE(TestUtility::protoEqual(out_payload, extracted_data)); })); diff --git a/test/extensions/filters/http/jwt_authn/group_verifier_test.cc b/test/extensions/filters/http/jwt_authn/group_verifier_test.cc index a54a747481c56..97e61234afef7 100644 --- a/test/extensions/filters/http/jwt_authn/group_verifier_test.cc +++ b/test/extensions/filters/http/jwt_authn/group_verifier_test.cc @@ -88,7 +88,7 @@ class GroupVerifierTest : public testing::Test { SetExtractedJwtDataCallback set_extracted_jwt_data_cb, AuthenticatorCallback callback) { if (status == Status::Ok) { - ProtobufWkt::Struct empty_struct; + Protobuf::Struct empty_struct; set_extracted_jwt_data_cb(issuer, empty_struct); } callback(status); @@ -101,11 +101,11 @@ class GroupVerifierTest : public testing::Test { // This expected extracted data is only for createSyncMockAuthsAndVerifier() function // which set an empty extracted data struct for each issuer. - static ProtobufWkt::Struct getExpectedExtractedData(const std::vector& issuers) { - ProtobufWkt::Struct struct_obj; + static Protobuf::Struct getExpectedExtractedData(const std::vector& issuers) { + Protobuf::Struct struct_obj; auto* fields = struct_obj.mutable_fields(); for (const auto& issuer : issuers) { - ProtobufWkt::Struct empty_struct; + Protobuf::Struct empty_struct; *(*fields)[issuer].mutable_struct_value() = empty_struct; } return struct_obj; @@ -174,7 +174,7 @@ TEST_F(GroupVerifierTest, DeeplyNestedAnys) { createSyncMockAuthsAndVerifier(StatusMap{{"example_provider", Status::Ok}}); EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& extracted_data) { + .WillOnce(Invoke([](const Protobuf::Struct& extracted_data) { EXPECT_TRUE(TestUtility::protoEqual(extracted_data, getExpectedExtractedData({"example_provider"}))); })); @@ -231,7 +231,7 @@ TEST_F(GroupVerifierTest, TestRequiresAll) { StatusMap{{"example_provider", Status::Ok}, {"other_provider", Status::Ok}}); EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& extracted_data) { + .WillOnce(Invoke([](const Protobuf::Struct& extracted_data) { EXPECT_TRUE(TestUtility::protoEqual( extracted_data, getExpectedExtractedData({"example_provider", "other_provider"}))); })); @@ -319,7 +319,7 @@ TEST_F(GroupVerifierTest, TestRequiresAnyFirstAuthOK) { createSyncMockAuthsAndVerifier(StatusMap{{"example_provider", Status::Ok}}); EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& extracted_data) { + .WillOnce(Invoke([](const Protobuf::Struct& extracted_data) { EXPECT_TRUE(TestUtility::protoEqual(extracted_data, getExpectedExtractedData({"example_provider"}))); })); @@ -342,7 +342,7 @@ TEST_F(GroupVerifierTest, TestRequiresAnyLastAuthOk) { StatusMap{{"example_provider", Status::JwtUnknownIssuer}, {"other_provider", Status::Ok}}); EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& extracted_data) { + .WillOnce(Invoke([](const Protobuf::Struct& extracted_data) { EXPECT_TRUE( TestUtility::protoEqual(extracted_data, getExpectedExtractedData({"other_provider"}))); })); @@ -431,7 +431,7 @@ TEST_F(GroupVerifierTest, TestAnyInAllFirstAnyIsOk) { createSyncMockAuthsAndVerifier(StatusMap{{"provider_1", Status::Ok}, {"provider_3", Status::Ok}}); EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& extracted_data) { + .WillOnce(Invoke([](const Protobuf::Struct& extracted_data) { EXPECT_TRUE(TestUtility::protoEqual( extracted_data, getExpectedExtractedData({"provider_1", "provider_3"}))); })); @@ -451,7 +451,7 @@ TEST_F(GroupVerifierTest, TestAnyInAllLastAnyIsOk) { {"provider_3", Status::Ok}}); EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& extracted_data) { + .WillOnce(Invoke([](const Protobuf::Struct& extracted_data) { EXPECT_TRUE(TestUtility::protoEqual( extracted_data, getExpectedExtractedData({"provider_2", "provider_3"}))); })); diff --git a/test/extensions/filters/http/jwt_authn/mock.h b/test/extensions/filters/http/jwt_authn/mock.h index b9e340d0f7876..92dcf2ad88bc3 100644 --- a/test/extensions/filters/http/jwt_authn/mock.h +++ b/test/extensions/filters/http/jwt_authn/mock.h @@ -47,7 +47,7 @@ class MockAuthenticator : public Authenticator { class MockVerifierCallbacks : public Verifier::Callbacks { public: - MOCK_METHOD(void, setExtractedData, (const ProtobufWkt::Struct& payload)); + MOCK_METHOD(void, setExtractedData, (const Protobuf::Struct& payload)); MOCK_METHOD(void, clearRouteCache, ()); MOCK_METHOD(void, onComplete, (const Status& status)); }; diff --git a/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc b/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc index dacbefbdd6137..0c27591c6f98a 100644 --- a/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc +++ b/test/extensions/filters/http/jwt_authn/provider_verifier_test.cc @@ -22,11 +22,11 @@ namespace HttpFilters { namespace JwtAuthn { namespace { -ProtobufWkt::Struct getExpectedPayload(const std::string& name) { - ProtobufWkt::Struct expected_payload; +Protobuf::Struct getExpectedPayload(const std::string& name) { + Protobuf::Struct expected_payload; TestUtility::loadFromJson(ExpectedPayloadJSON, expected_payload); - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; *(*struct_obj.mutable_fields())[name].mutable_struct_value() = expected_payload; return struct_obj; } @@ -60,10 +60,9 @@ TEST_F(ProviderVerifierTest, TestOkJWT) { createVerifier(); MockUpstream mock_pubkey(mock_factory_ctx_.server_factory_context_.cluster_manager_, PublicKey); - EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& payload) { - EXPECT_TRUE(TestUtility::protoEqual(payload, getExpectedPayload("my_payload"))); - })); + EXPECT_CALL(mock_cb_, setExtractedData(_)).WillOnce(Invoke([](const Protobuf::Struct& payload) { + EXPECT_TRUE(TestUtility::protoEqual(payload, getExpectedPayload("my_payload"))); + })); EXPECT_CALL(mock_cb_, onComplete(Status::Ok)); @@ -91,14 +90,13 @@ TEST_F(ProviderVerifierTest, TestOkJWTWithExtractedHeaderAndPayload) { createVerifier(); MockUpstream mock_pubkey(mock_factory_ctx_.server_factory_context_.cluster_manager_, PublicKey); - EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& payload) { - // The expected payload is a merged struct of the extracted (from the JWT) payload and - // header data with "my_payload" and "my_header" as the keys. - ProtobufWkt::Struct expected_payload; - MessageUtil::loadFromJson(ExpectedPayloadAndHeaderJSON, expected_payload); - EXPECT_TRUE(TestUtility::protoEqual(payload, expected_payload)); - })); + EXPECT_CALL(mock_cb_, setExtractedData(_)).WillOnce(Invoke([](const Protobuf::Struct& payload) { + // The expected payload is a merged struct of the extracted (from the JWT) payload and + // header data with "my_payload" and "my_header" as the keys. + Protobuf::Struct expected_payload; + MessageUtil::loadFromJson(ExpectedPayloadAndHeaderJSON, expected_payload); + EXPECT_TRUE(TestUtility::protoEqual(payload, expected_payload)); + })); EXPECT_CALL(mock_cb_, onComplete(Status::Ok)); @@ -119,13 +117,12 @@ TEST_F(ProviderVerifierTest, TestExpiredJWTWithFailedStatusInMetadata) { createVerifier(); MockUpstream mock_pubkey(mock_factory_ctx_.server_factory_context_.cluster_manager_, PublicKey); - EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& payload) { - ProtobufWkt::Struct expected_payload; - MessageUtil::loadFromJson(ExpectedJWTExpiredStatusJSON, expected_payload); + EXPECT_CALL(mock_cb_, setExtractedData(_)).WillOnce(Invoke([](const Protobuf::Struct& payload) { + Protobuf::Struct expected_payload; + MessageUtil::loadFromJson(ExpectedJWTExpiredStatusJSON, expected_payload); - EXPECT_TRUE(TestUtility::protoEqual(payload, expected_payload)); - })); + EXPECT_TRUE(TestUtility::protoEqual(payload, expected_payload)); + })); EXPECT_CALL(mock_cb_, onComplete(Status::JwtExpired)); @@ -142,10 +139,9 @@ TEST_F(ProviderVerifierTest, TestSpanPassedDown) { createVerifier(); MockUpstream mock_pubkey(mock_factory_ctx_.server_factory_context_.cluster_manager_, PublicKey); - EXPECT_CALL(mock_cb_, setExtractedData(_)) - .WillOnce(Invoke([](const ProtobufWkt::Struct& payload) { - EXPECT_TRUE(TestUtility::protoEqual(payload, getExpectedPayload("my_payload"))); - })); + EXPECT_CALL(mock_cb_, setExtractedData(_)).WillOnce(Invoke([](const Protobuf::Struct& payload) { + EXPECT_TRUE(TestUtility::protoEqual(payload, getExpectedPayload("my_payload"))); + })); EXPECT_CALL(mock_cb_, onComplete(Status::Ok)); diff --git a/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc b/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc index 60c0b2885b6e8..39a257ed0fd28 100644 --- a/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc +++ b/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc @@ -54,10 +54,10 @@ class LocalRateLimitFilterIntegrationTest : public Event::TestUsingSimulatedTime RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {route_config_name}, true)); sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml( initial_route_config)}, "1"); @@ -86,10 +86,10 @@ class LocalRateLimitFilterIntegrationTest : public Event::TestUsingSimulatedTime RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"local_cluster"}, true)); sendSotwDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {TestUtility::parseYaml( initial_local_cluster_endpoints)}, "1"); @@ -171,17 +171,16 @@ class LocalRateLimitFilterIntegrationTest : public Event::TestUsingSimulatedTime EXPECT_EQ(expected_body_size, response->body().size()); EXPECT_THAT( response->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitLimit, expected_limit)); + EXPECT_THAT(response->headers(), + ContainsHeader(Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get() + .XRateLimitRemaining, + expected_remaining)); EXPECT_THAT( response->headers(), - Http::HeaderValueOf(Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get() - .XRateLimitRemaining, - expected_remaining)); - EXPECT_THAT( - response->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitReset, expected_reset)); } @@ -633,7 +632,7 @@ TEST_P(LocalRateLimitFilterIntegrationTest, BasicTestPerRouteAndRds) { // Update route config by RDS when request is sending. Test whether RDS can work normally. sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml(update_route_config_)}, "2"); test_server_->waitForCounterGe("http.config_test.rds.basic_routes.update_success", 2); @@ -683,7 +682,7 @@ TEST_P(LocalRateLimitFilterIntegrationTest, TestLocalClusterRateLimit) { EXPECT_EQ(1.0, share_provider->getTokensShareFactor()); sendSotwDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {TestUtility::parseYaml( update_local_cluster_endpoints_)}, "2"); diff --git a/test/extensions/filters/http/lua/BUILD b/test/extensions/filters/http/lua/BUILD index cc595699d2647..b67aeb7ba83cb 100644 --- a/test/extensions/filters/http/lua/BUILD +++ b/test/extensions/filters/http/lua/BUILD @@ -41,9 +41,13 @@ envoy_extension_cc_test( rbe_pool = "6gig", deps = [ "//source/common/network:address_lib", + "//source/common/router:string_accessor_lib", + "//source/common/stream_info:bool_accessor_lib", "//source/common/stream_info:stream_info_lib", + "//source/common/stream_info:uint64_accessor_lib", "//source/extensions/filters/http/lua:wrappers_lib", "//test/extensions/filters/common/lua:lua_wrappers_lib", + "//test/mocks/router:router_mocks", "//test/mocks/stream_info:stream_info_mocks", "//test/test_common:utility_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/lua/lua_filter_test.cc b/test/extensions/filters/http/lua/lua_filter_test.cc index 78ea58e31b3d9..985ad9d2b29ff 100644 --- a/test/extensions/filters/http/lua/lua_filter_test.cc +++ b/test/extensions/filters/http/lua/lua_filter_test.cc @@ -116,6 +116,43 @@ class LuaHttpFilterTest : public testing::Test { ON_CALL(*decoder_callbacks_.route_, metadata()).WillByDefault(testing::ReturnRef(metadata_)); } + void setupVirtualHostMetadata(const std::string& yaml) { + TestUtility::loadFromYaml(yaml, virtual_host_metadata_); + + auto virtual_host = std::make_shared>(); + stream_info_.virtual_host_ = virtual_host; + + ON_CALL(*virtual_host, metadata()).WillByDefault(ReturnRef(virtual_host_metadata_)); + + ON_CALL(decoder_callbacks_.stream_info_, virtualHost()).WillByDefault(ReturnRef(virtual_host)); + ON_CALL(encoder_callbacks_.stream_info_, virtualHost()).WillByDefault(ReturnRef(virtual_host)); + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillOnce(ReturnRef(stream_info_)); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillOnce(ReturnRef(stream_info_)); + + const std::string filter_name = "lua-filter-config-name"; + ON_CALL(decoder_callbacks_, filterConfigName()).WillByDefault(Return(filter_name)); + ON_CALL(encoder_callbacks_, filterConfigName()).WillByDefault(Return(filter_name)); + + EXPECT_EQ(0, stats_store_.counter("test.lua.errors").value()); + } + + void setupRouteMetadata(const std::string& yaml) { + auto route = std::make_shared>(); + TestUtility::loadFromYaml(yaml, route->metadata_); + + ON_CALL(stream_info_, route()).WillByDefault(Return(route)); + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillOnce(ReturnRef(stream_info_)); + EXPECT_CALL(encoder_callbacks_, streamInfo()).WillOnce(ReturnRef(stream_info_)); + + const std::string filter_name = "lua-filter-config-name"; + ON_CALL(decoder_callbacks_, filterConfigName()).WillByDefault(Return(filter_name)); + ON_CALL(encoder_callbacks_, filterConfigName()).WillByDefault(Return(filter_name)); + + EXPECT_EQ(0, stats_store_.counter("test.lua.errors").value()); + } + NiceMock server_factory_context_; NiceMock tls_; NiceMock api_; @@ -126,6 +163,7 @@ class LuaHttpFilterTest : public testing::Test { NiceMock decoder_callbacks_; NiceMock encoder_callbacks_; envoy::config::core::v3::Metadata metadata_; + envoy::config::core::v3::Metadata virtual_host_metadata_; std::shared_ptr> ssl_; NiceMock connection_; NiceMock stream_info_; @@ -2266,17 +2304,17 @@ TEST_F(LuaHttpFilterTest, GetConnectionDynamicMetadata) { )EOF"}; // Proxy Protocol Filter Metadata - ProtobufWkt::Value tlv_ea_value; + Protobuf::Value tlv_ea_value; tlv_ea_value.set_string_value("vpce-064c279a4001a055f"); - ProtobufWkt::Struct proxy_protocol_metadata; + Protobuf::Struct proxy_protocol_metadata; proxy_protocol_metadata.mutable_fields()->insert({"tlv_ea", tlv_ea_value}); (*stream_info_.metadata_.mutable_filter_metadata())["envoy.proxy_protocol"] = proxy_protocol_metadata; // LB Filter Metadata - ProtobufWkt::Value lb_version_value; + Protobuf::Value lb_version_value; lb_version_value.set_string_value("v1.0"); - ProtobufWkt::Struct lb_metadata; + Protobuf::Struct lb_metadata; lb_metadata.mutable_fields()->insert({"version", lb_version_value}); (*stream_info_.metadata_.mutable_filter_metadata())["envoy.lb"] = lb_metadata; @@ -2325,10 +2363,10 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadata) { setup(SCRIPT); // Create a simple Struct for testing typed metadata - ProtobufWkt::Struct main_struct; + Protobuf::Struct main_struct; // Create a nested struct for typed_metadata - ProtobufWkt::Struct typed_metadata_struct; + Protobuf::Struct typed_metadata_struct; (*typed_metadata_struct.mutable_fields())["tlv_ea"].set_string_value("vpce-1234567890abcdef"); (*typed_metadata_struct.mutable_fields())["pp2_type"].set_string_value("PROXY"); @@ -2336,7 +2374,7 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadata) { auto* typed_meta_value = &(*main_struct.mutable_fields())["typed_metadata"]; typed_meta_value->mutable_struct_value()->MergeFrom(typed_metadata_struct); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(main_struct); // Add the typed metadata to the stream info @@ -2393,14 +2431,14 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadataComplex) { setup(SCRIPT); // Create a complex Struct for testing - ProtobufWkt::Struct main_struct; + Protobuf::Struct main_struct; // Add simple key/value pairs (*main_struct.mutable_fields())["tlv_ea"].set_string_value("vpce-1234567890abcdef"); (*main_struct.mutable_fields())["pp2_type"].set_string_value("PROXY"); // Create a nested struct for SSL info - ProtobufWkt::Struct ssl_info; + Protobuf::Struct ssl_info; (*ssl_info.mutable_fields())["version"].set_string_value("TLSv1.3"); (*ssl_info.mutable_fields())["cipher"].set_string_value("ECDHE-RSA-AES128-GCM-SHA256"); @@ -2409,7 +2447,7 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadataComplex) { ssl_value->mutable_struct_value()->MergeFrom(ssl_info); // Create an array of addresses - ProtobufWkt::ListValue addresses; + Protobuf::ListValue addresses; addresses.add_values()->set_string_value("192.168.1.1"); addresses.add_values()->set_string_value("10.0.0.1"); addresses.add_values()->set_string_value("172.16.0.1"); @@ -2418,7 +2456,7 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadataComplex) { auto* addresses_value = &(*main_struct.mutable_fields())["addresses"]; addresses_value->mutable_list_value()->MergeFrom(addresses); - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(main_struct); // Add the typed metadata to the stream info @@ -2486,7 +2524,7 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadataInvalidType) { setup(SCRIPT); // Pack an invalid/unknown message type - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/unknown.type"); typed_config.set_value("invalid data"); @@ -2517,7 +2555,7 @@ TEST_F(LuaHttpFilterTest, GetConnectionTypedMetadataUnpackFailure) { setup(SCRIPT); // Pack invalid data that will fail to unpack - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/envoy.data.core.v3.TlvsMetadata"); typed_config.set_value("invalid protobuf data"); @@ -2548,10 +2586,10 @@ TEST_F(LuaHttpFilterTest, GetDynamicMetadataBinaryData) { end )EOF"}; - ProtobufWkt::Value metadata_value; + Protobuf::Value metadata_value; constexpr uint8_t buffer[] = {'h', 'e', 0x00, 'l', 'l', 'o'}; metadata_value.set_string_value(reinterpret_cast(buffer), sizeof(buffer)); - ProtobufWkt::Struct metadata; + Protobuf::Struct metadata; metadata.mutable_fields()->insert({"bin_data", metadata_value}); (*stream_info_.metadata_.mutable_filter_metadata())["envoy.pp"] = metadata; @@ -2603,12 +2641,12 @@ TEST_F(LuaHttpFilterTest, SetGetDynamicMetadata) { .at("foo") .string_value()); - const ProtobufWkt::Struct& meta_complex = stream_info.dynamicMetadata() - .filter_metadata() - .at("envoy.lb") - .fields() - .at("complex") - .struct_value(); + const Protobuf::Struct& meta_complex = stream_info.dynamicMetadata() + .filter_metadata() + .at("envoy.lb") + .fields() + .at("complex") + .struct_value(); EXPECT_EQ("abcd", meta_complex.fields().at("x").string_value()); EXPECT_EQ(1234.0, meta_complex.fields().at("y").number_value()); EXPECT_EQ(0, stats_store_.counter("test.lua.errors").value()); @@ -3693,14 +3731,14 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoTypedMetadata) { setup(SCRIPT); // Create a Struct for testing typed metadata using the set_metadata filter's proto - ProtobufWkt::Struct main_struct; + Protobuf::Struct main_struct; // Add simple key/value pairs (*main_struct.mutable_fields())["metadata_namespace"].set_string_value("test.namespace"); (*main_struct.mutable_fields())["allow_overwrite"].set_bool_value(true); // Pack the Struct into an Any - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/google.protobuf.Struct"); typed_config.PackFrom(main_struct); @@ -3756,14 +3794,14 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoComplexTypedMetadata) { setup(SCRIPT); // Create a complex Struct for testing - ProtobufWkt::Struct main_struct; + Protobuf::Struct main_struct; // Add simple key/value pairs (*main_struct.mutable_fields())["filter_name"].set_string_value("complex_metadata"); (*main_struct.mutable_fields())["version"].set_string_value("v1.2.3"); // Create a nested struct for config - ProtobufWkt::Struct config_struct; + Protobuf::Struct config_struct; (*config_struct.mutable_fields())["version"].set_string_value("v2.0.0"); (*config_struct.mutable_fields())["enabled"].set_bool_value(true); @@ -3772,7 +3810,7 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoComplexTypedMetadata) { *config_value->mutable_struct_value() = config_struct; // Create a list for servers - ProtobufWkt::ListValue servers_list; + Protobuf::ListValue servers_list; servers_list.add_values()->set_string_value("server1.example.com"); servers_list.add_values()->set_string_value("server2.example.com"); @@ -3781,7 +3819,7 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoComplexTypedMetadata) { *servers_value->mutable_list_value() = servers_list; // Pack the Struct into an Any - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/google.protobuf.Struct"); typed_config.PackFrom(main_struct); @@ -3846,7 +3884,7 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoTypedMetadataInvalidType) { setup(SCRIPT); // Pack an invalid/unknown message type - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/unknown.type"); typed_config.set_value("invalid data"); @@ -3878,7 +3916,7 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoTypedMetadataUnpackFailure) { setup(SCRIPT); // Pack invalid data that will fail to unpack - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/google.protobuf.Struct"); typed_config.set_value("invalid protobuf data"); @@ -3893,6 +3931,351 @@ TEST_F(LuaHttpFilterTest, GetStreamInfoTypedMetadataUnpackFailure) { EXPECT_EQ(0, stats_store_.counter("test.lua.errors").value()); } +// Test that handle:virtualHost():metadata() works when both virtual host and route match. +// This verifies that when a virtual host is matched and a route is found for the request, +// the virtualHost() function returns a valid object and metadata can be accessed +// successfully from both request and response handles. +TEST_F(LuaHttpFilterTest, GetVirtualHostMetadataFromHandle) { + const std::string SCRIPT{R"EOF( + function envoy_on_request(request_handle) + local metadata = request_handle:virtualHost():metadata() + request_handle:logTrace(metadata:get("foo.bar")["name"]) + request_handle:logTrace(metadata:get("foo.bar")["prop"]) + end + function envoy_on_response(response_handle) + local metadata = response_handle:virtualHost():metadata() + response_handle:logTrace(metadata:get("baz.bat")["name"]) + response_handle:logTrace(metadata:get("baz.bat")["prop"]) + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + lua-filter-config-name: + foo.bar: + name: foo + prop: bar + baz.bat: + name: baz + prop: bat + )EOF"}; + + InSequence s; + setup(SCRIPT); + setupVirtualHostMetadata(METADATA); + + // Request path + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "foo"}, + {"trace", "bar"}, + }), + { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "baz"}, + {"trace", "bat"}, + }), + { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->encodeHeaders(response_headers, true)); + }); +} + +// Test that handle:virtualHost():metadata() returns empty metadata when no filter-specific metadata +// exists. This verifies that when a virtual host has metadata for other filters but not for the +// current one, the metadata object is empty. +TEST_F(LuaHttpFilterTest, GetVirtualHostMetadataFromHandleNoLuaMetadata) { + const std::string SCRIPT{R"EOF( + function is_metadata_empty(metadata) + for _, _ in pairs(metadata) do + return false + end + return true + end + function envoy_on_request(request_handle) + if is_metadata_empty(request_handle:virtualHost():metadata()) then + request_handle:logTrace("No metadata found on request") + end + end + function envoy_on_response(response_handle) + if is_metadata_empty(response_handle:virtualHost():metadata()) then + response_handle:logTrace("No metadata found on response") + end + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + envoy.some_filter: + foo.bar: + name: foo + prop: bar + )EOF"}; + + InSequence s; + setup(SCRIPT); + setupVirtualHostMetadata(METADATA); + + // Request path + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found on request", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found on response", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers, true)); + }); +} + +// Test that handle:virtualHost() returns a valid virtual host wrapper object that can be +// safely accessed when no virtual host matches the request authority. +// This verifies that calling metadata() returns an empty metadata object. +TEST_F(LuaHttpFilterTest, GetVirtualHostFromHandleNoVirtualHost) { + const std::string SCRIPT{R"EOF( + function envoy_on_request(request_handle) + local virtual_host = request_handle:virtualHost() + for _, _ in pairs(virtual_host:metadata()) do + return + end + request_handle:logTrace("No metadata found during request handling") + end + function envoy_on_response(response_handle) + local virtual_host = response_handle:virtualHost() + for _, _ in pairs(virtual_host:metadata()) do + return + end + response_handle:logTrace("No metadata found during response handling") + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + // Request path + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found during request handling", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found during response handling", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers, true)); + }); + + EXPECT_EQ(0, stats_store_.counter("test.lua.errors").value()); +} + +// Test that handle:virtualHost():metadata() still works when there is no route. +// This verifies that when a virtual host is matched but no route is found for the request, +// the virtualHost() function returns a valid object and metadata can still be accessed +// successfully from both request and response handles. +TEST_F(LuaHttpFilterTest, GetVirtualHostMetadataFromHandleNoRoute) { + const std::string SCRIPT{R"EOF( + function envoy_on_request(request_handle) + local metadata = request_handle:virtualHost():metadata() + request_handle:logTrace(metadata:get("foo.bar")["name"]) + request_handle:logTrace(metadata:get("foo.bar")["prop"]) + end + function envoy_on_response(response_handle) + local metadata = response_handle:virtualHost():metadata() + response_handle:logTrace(metadata:get("baz.bat")["name"]) + response_handle:logTrace(metadata:get("baz.bat")["prop"]) + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + lua-filter-config-name: + foo.bar: + name: foo + prop: bar + baz.bat: + name: baz + prop: bat + )EOF"}; + + InSequence s; + setup(SCRIPT); + setupVirtualHostMetadata(METADATA); + + // Request path + ON_CALL(decoder_callbacks_, route()).WillByDefault(Return(nullptr)); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "foo"}, + {"trace", "bar"}, + }), + { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + ON_CALL(encoder_callbacks_, route()).WillByDefault(Return(nullptr)); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "baz"}, + {"trace", "bat"}, + }), + { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->encodeHeaders(response_headers, true)); + }); +} + +// Test that handle:route():metadata() returns metadata when route matches the request. +// This verifies that when a route is found for the request, the route() function returns +// a valid object and metadata can be successfully accessed from both request and response handles. +TEST_F(LuaHttpFilterTest, GetRouteMetadataFromHandle) { + const std::string SCRIPT{R"EOF( + function envoy_on_request(request_handle) + local metadata = request_handle:route():metadata() + request_handle:logTrace(metadata:get("foo.bar")["name"]) + request_handle:logTrace(metadata:get("foo.bar")["prop"]) + end + function envoy_on_response(response_handle) + local metadata = response_handle:route():metadata() + response_handle:logTrace(metadata:get("baz.bat")["name"]) + response_handle:logTrace(metadata:get("baz.bat")["prop"]) + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + lua-filter-config-name: + foo.bar: + name: foo + prop: bar + baz.bat: + name: baz + prop: bat + )EOF"}; + + InSequence s; + setup(SCRIPT); + setupRouteMetadata(METADATA); + + // Request path + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "foo"}, + {"trace", "bar"}, + }), + { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "baz"}, + {"trace", "bat"}, + }), + { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->encodeHeaders(response_headers, true)); + }); +} + +// Test that handle:route():metadata() returns empty metadata when no filter-specific metadata +// exists. This verifies that when a route has metadata for other filters but not for the +// current one, the metadata object is empty. +TEST_F(LuaHttpFilterTest, GetRouteMetadataFromHandleNoLuaMetadata) { + const std::string SCRIPT{R"EOF( + function is_metadata_empty(metadata) + for _, _ in pairs(metadata) do + return false + end + return true + end + function envoy_on_request(request_handle) + if is_metadata_empty(request_handle:route():metadata()) then + request_handle:logTrace("No metadata found during request handling") + end + end + function envoy_on_response(response_handle) + if is_metadata_empty(response_handle:route():metadata()) then + response_handle:logTrace("No metadata found during response handling") + end + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + envoy.some_filter: + foo.bar: + name: foo + prop: bar + )EOF"}; + + InSequence s; + setup(SCRIPT); + setupRouteMetadata(METADATA); + + // Request path + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found during request handling", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found during response handling", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers, true)); + }); +} + +// Test that handle:route() returns a valid route wrapper object that can be +// safely accessed when no route matches the request. +// This verifies that calling metadata() returns an empty metadata object. +TEST_F(LuaHttpFilterTest, GetRouteFromHandleNoRoute) { + const std::string SCRIPT{R"EOF( + function envoy_on_request(request_handle) + local route = request_handle:route() + for _, _ in pairs(route:metadata()) do + return + end + request_handle:logTrace("No metadata found during request handling") + end + function envoy_on_response(response_handle) + local route = response_handle:route() + for _, _ in pairs(route:metadata()) do + return + end + response_handle:logTrace("No metadata found during response handling") + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + // Request path + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found during request handling", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + }); + + // Response path + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_LOG_CONTAINS("trace", "No metadata found during response handling", { + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers, true)); + }); + + EXPECT_EQ(0, stats_store_.counter("test.lua.errors").value()); +} + } // namespace } // namespace Lua } // namespace HttpFilters diff --git a/test/extensions/filters/http/lua/lua_integration_test.cc b/test/extensions/filters/http/lua/lua_integration_test.cc index 9cb250c1b2e1d..7eac57b65ce92 100644 --- a/test/extensions/filters/http/lua/lua_integration_test.cc +++ b/test/extensions/filters/http/lua/lua_integration_test.cc @@ -11,8 +11,6 @@ #include "gtest/gtest.h" -using Envoy::Http::HeaderValueOf; - namespace Envoy { namespace { @@ -75,8 +73,27 @@ class LuaIntegrationTest : public UpstreamDownstreamIntegrationTest { response_header->set_key("fake_header"); response_header->set_value("fake_value"); - const std::string key = "envoy.filters.http.lua"; - const std::string yaml = + // Metadata variables for the virtual host and route. + const std::string key = "lua"; + Protobuf::Struct value; + std::string yaml; + + // Sets the virtual host's metadata. + yaml = + R"EOF( + foo.bar: + foo: vhost_bar + baz: vhost_bat + )EOF"; + TestUtility::loadFromYaml(yaml, value); + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_metadata() + ->mutable_filter_metadata() + ->insert(Protobuf::MapPair(key, value)); + + // Sets the route's metadata. + yaml = R"EOF( foo.bar: foo: bar @@ -84,17 +101,13 @@ class LuaIntegrationTest : public UpstreamDownstreamIntegrationTest { keyset: foo: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0cSZtAdFgMI1zQJwG8ujTXFMcRY0+SA6fMZGEfQYuxcz/e8UelJ1fLDVAwYmk7KHoYzpizy0JIxAcJ+OAE+cd6a6RpwSEm/9/vizlv0vWZv2XMRAqUxk/5amlpQZE/4sRg/qJdkZZjKrSKjf5VEUQg2NytExYyYWG+3FEYpzYyUeVktmW0y/205XAuEQuxaoe+AUVKeoON1iDzvxywE42C0749XYGUFicqBSRj2eO7jm4hNWvgTapYwpswM3hV9yOAPOVQGKNXzNbLDbFTHyLw3OKayGs/4FUBa+ijlGD9VDawZq88RRaf5ztmH22gOSiKcrHXe40fsnrzh/D27uwIDAQAB )EOF"; - - ProtobufWkt::Struct value; TestUtility::loadFromYaml(yaml, value); - - // Sets the route's metadata. hcm.mutable_route_config() ->mutable_virtual_hosts(0) ->mutable_routes(0) ->mutable_metadata() ->mutable_filter_metadata() - ->insert(Protobuf::MapPair(key, value)); + ->insert(Protobuf::MapPair(key, value)); }); // This filter is not compatible with the async load balancer, as httpCall with data will @@ -184,10 +197,10 @@ class LuaIntegrationTest : public UpstreamDownstreamIntegrationTest { RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {route_config_name}, true)); sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml( initial_route_config)}, "1"); @@ -329,6 +342,80 @@ name: lua EXPECT_TRUE(response.find("HTTP/1.1 400 Bad Request\r\n") == 0); } +// Test that handle:metadata() falls back to metadata under the filter canonical name +// (envoy.filters.http.lua) when no metadata is present under the filter configured name. +TEST_P(LuaIntegrationTest, MetadataFallbackToCanonicalName) { + const std::string filter_config = + R"EOF( +name: lua-filter-custom-name +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + local foo_bar = request_handle:metadata():get("foo.bar") + request_handle:logTrace(foo_bar["name"]) + request_handle:logTrace(foo_bar["prop"]) + end + function envoy_on_response(response_handle) + local baz_bat = response_handle:metadata():get("baz.bat") + response_handle:logTrace(baz_bat["name"]) + response_handle:logTrace(baz_bat["prop"]) + end +)EOF"; + + const std::string route_config = + R"EOF( +name: test_routes +virtual_hosts: +- name: test_vhost + domains: ["foo.lyft.com"] + routes: + - match: + path: "/test/long/url" + metadata: + filter_metadata: + envoy.filters.http.lua: + foo.bar: + name: foo + prop: bar + baz.bat: + name: baz + prop: bat + route: + cluster: cluster_0 +)EOF"; + + initializeWithYaml(filter_config, route_config); + + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "foo.lyft.com"}, + {"x-forwarded-for", "10.0.0.1"}}; + + IntegrationStreamDecoderPtr response; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "foo"}, + {"trace", "bar"}, + {"trace", "baz"}, + {"trace", "bat"}, + }), + { + response = codec_client_->makeHeaderOnlyRequest(request_headers); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + }); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + + cleanup(); +} + // Basic request and response. TEST_P(LuaIntegrationTest, RequestAndResponse) { const std::string FILTER_AND_CODE = @@ -346,6 +433,8 @@ name: lua request_handle:logErr("log test") request_handle:logCritical("log test") + local vhost_metadata = request_handle:virtualHost():metadata():get("foo.bar") + local route_metadata = request_handle:route():metadata():get("foo.bar") local metadata = request_handle:metadata():get("foo.bar") local body_length = request_handle:body():length() @@ -367,6 +456,10 @@ name: lua request_handle:headers():add("cookie_size", request_handle:headers():getNumValues("set-cookie")) request_handle:headers():add("request_body_size", body_length) + request_handle:headers():add("request_vhost_metadata_foo", vhost_metadata["foo"]) + request_handle:headers():add("request_vhost_metadata_baz", vhost_metadata["baz"]) + request_handle:headers():add("request_route_metadata_foo", route_metadata["foo"]) + request_handle:headers():add("request_route_metadata_baz", route_metadata["baz"]) request_handle:headers():add("request_metadata_foo", metadata["foo"]) request_handle:headers():add("request_metadata_baz", metadata["baz"]) if request_handle:connection():ssl() == nil then @@ -389,8 +482,14 @@ name: lua end function envoy_on_response(response_handle) + local vhost_metadata = response_handle:virtualHost():metadata():get("foo.bar") + local route_metadata = response_handle:route():metadata():get("foo.bar") local metadata = response_handle:metadata():get("foo.bar") local body_length = response_handle:body():length() + response_handle:headers():add("response_vhost_metadata_foo", vhost_metadata["foo"]) + response_handle:headers():add("response_vhost_metadata_baz", vhost_metadata["baz"]) + response_handle:headers():add("response_route_metadata_foo", route_metadata["foo"]) + response_handle:headers():add("response_route_metadata_baz", route_metadata["baz"]) response_handle:headers():add("response_metadata_foo", metadata["foo"]) response_handle:headers():add("response_metadata_baz", metadata["baz"]) response_handle:headers():add("response_body_size", body_length) @@ -469,6 +568,26 @@ name: lua ->value() .getStringView()); + EXPECT_EQ("vhost_bar", upstream_request_->headers() + .get(Http::LowerCaseString("request_vhost_metadata_foo"))[0] + ->value() + .getStringView()); + + EXPECT_EQ("vhost_bat", upstream_request_->headers() + .get(Http::LowerCaseString("request_vhost_metadata_baz"))[0] + ->value() + .getStringView()); + + EXPECT_EQ("bar", upstream_request_->headers() + .get(Http::LowerCaseString("request_route_metadata_foo"))[0] + ->value() + .getStringView()); + + EXPECT_EQ("bat", upstream_request_->headers() + .get(Http::LowerCaseString("request_route_metadata_baz"))[0] + ->value() + .getStringView()); + EXPECT_EQ("bar", upstream_request_->headers() .get(Http::LowerCaseString("request_metadata_foo"))[0] ->value() @@ -543,6 +662,22 @@ name: lua .get(Http::LowerCaseString("response_body_size"))[0] ->value() .getStringView()); + EXPECT_EQ("vhost_bar", response->headers() + .get(Http::LowerCaseString("response_vhost_metadata_foo"))[0] + ->value() + .getStringView()); + EXPECT_EQ("vhost_bat", response->headers() + .get(Http::LowerCaseString("response_vhost_metadata_baz"))[0] + ->value() + .getStringView()); + EXPECT_EQ("bar", response->headers() + .get(Http::LowerCaseString("response_route_metadata_foo"))[0] + ->value() + .getStringView()); + EXPECT_EQ("bat", response->headers() + .get(Http::LowerCaseString("response_route_metadata_baz"))[0] + ->value() + .getStringView()); EXPECT_EQ("bar", response->headers() .get(Http::LowerCaseString("response_metadata_foo"))[0] ->value() @@ -748,8 +883,8 @@ name: envoy.filters.http.lua ASSERT_TRUE(fake_lua_connection_->waitForNewStream(*dispatcher_, lua_request_)); ASSERT_TRUE(lua_request_->waitForEndStream(*dispatcher_)); // Sanity checking that we sent the expected data. - EXPECT_THAT(lua_request_->headers(), HeaderValueOf(Http::Headers::get().Method, "POST")); - EXPECT_THAT(lua_request_->headers(), HeaderValueOf(Http::Headers::get().Path, "/")); + EXPECT_THAT(lua_request_->headers(), ContainsHeader(Http::Headers::get().Method, "POST")); + EXPECT_THAT(lua_request_->headers(), ContainsHeader(Http::Headers::get().Path, "/")); waitForNextUpstreamRequest(); @@ -1244,7 +1379,7 @@ TEST_P(LuaIntegrationTest, RdsTestOfLuaPerRoute) { // Update route config by RDS. Test whether RDS can work normally. sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml(UPDATE_ROUTE_CONFIG)}, "2"); test_server_->waitForCounterGe("http.config_test.rds.basic_lua_routes.update_success", 2); @@ -1476,7 +1611,7 @@ class TestTypedMetadataFilter final : public Network::ReadFilter { (*typed_metadata_map)["ssl_cn"] = "client.example.com"; // Pack metadata into Any - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(metadata); typed_filter_metadata.insert({metadata_key, typed_config}); @@ -1504,7 +1639,7 @@ class TestTypedMetadataFilterConfig final } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.typed_metadata"; } @@ -1552,7 +1687,7 @@ class PPV2TypedMetadataFilter final : public Network::ReadFilter { (*typed_metadata_map)["ssl_cipher"] = "ECDHE-RSA-AES128-GCM-SHA256"; // Pack metadata into Any - ProtobufWkt::Any typed_config; + Protobuf::Any typed_config; typed_config.PackFrom(metadata); typed_filter_metadata.insert({metadata_key, typed_config}); @@ -1580,7 +1715,7 @@ class PPV2TypedMetadataFilterConfig final } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.test.ppv2.typed_metadata"; } @@ -2026,6 +2161,277 @@ name: lua cleanup(); } +// Test ``filterState()`` functionality with simple string values. +TEST_P(LuaIntegrationTest, FilterStateBasic) { + const std::string FILTER_AND_CODE = R"EOF( +name: lua +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + local stream_info = request_handle:streamInfo() + + -- Test filterState() function with non-existent key + local missing_state = stream_info:filterState():get("nonexistent_key") + if missing_state == nil then + request_handle:headers():add("missing_state", "nil") + else + request_handle:headers():add("missing_state", "unexpected") + end + + -- Test with another non-existent key + local another_missing = stream_info:filterState():get("another_missing") + if another_missing == nil then + request_handle:headers():add("another_missing", "nil") + else + request_handle:headers():add("another_missing", "unexpected") + end + end +)EOF"; + + initializeFilter(FILTER_AND_CODE); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test/long/url"}, {":scheme", "http"}, {":authority", "host"}}; + + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + waitForNextUpstreamRequest(); + + // Verify both calls return nil as expected for non-existent keys. + EXPECT_EQ("nil", upstream_request_->headers() + .get(Http::LowerCaseString("missing_state"))[0] + ->value() + .getStringView()); + + EXPECT_EQ("nil", upstream_request_->headers() + .get(Http::LowerCaseString("another_missing"))[0] + ->value() + .getStringView()); + + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + + cleanup(); +} + +// Test that handle:virtualHost():metadata() returns valid metadata when virtual host matches +// but route doesn't, ensuring metadata access works correctly. +TEST_P(LuaIntegrationTest, VirtualHostValidWhenNoRouteMatch) { + if (!testing_downstream_filter_) { + GTEST_SKIP() << "This is a local reply test that does not go upstream"; + } + + const std::string filter_config = + R"EOF( +name: lua +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + local metadata = request_handle:virtualHost():metadata() + request_handle:logTrace(metadata:get("foo.bar")["name"]) + request_handle:logTrace(metadata:get("foo.bar")["prop"]) + end + function envoy_on_response(response_handle) + local metadata = response_handle:virtualHost():metadata() + response_handle:logTrace(metadata:get("baz.bat")["name"]) + response_handle:logTrace(metadata:get("baz.bat")["prop"]) + end +)EOF"; + + const std::string route_config = + R"EOF( +name: test_routes +virtual_hosts: +- name: test_vhost + domains: ["foo.lyft.com"] + metadata: + filter_metadata: + lua: + foo.bar: + name: foo + prop: bar + baz.bat: + name: baz + prop: bat + routes: + - match: + path: "/existing/route" + route: + cluster: cluster_0 +)EOF"; + + initializeWithYaml(filter_config, route_config); + codec_client_ = makeHttpConnection(lookupPort("http")); + + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/non/existing/path"}, + {":scheme", "http"}, + {":authority", "foo.lyft.com"}, + {"x-forwarded-for", "10.0.0.1"}}; + + IntegrationStreamDecoderPtr response; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "foo"}, + {"trace", "bar"}, + {"trace", "baz"}, + {"trace", "bat"}, + }), + { + auto encoder_decoder = codec_client_->startRequest(request_headers); + response = std::move(encoder_decoder.second); + + ASSERT_TRUE(response->waitForEndStream()); + }); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("404", response->headers().getStatusValue()); + cleanup(); +} + +// Test that handle:virtualHost() returns a valid object when no virtual host matches the request +// authority. This verifies that metadata() returns an empty metadata object that can be safely +// iterated. +TEST_P(LuaIntegrationTest, VirtualHostValidWhenNoVirtualHostMatch) { + if (!testing_downstream_filter_) { + GTEST_SKIP() << "This is a local reply test that does not go upstream"; + } + + const std::string FILTER_AND_CODE = + R"EOF( +name: lua +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + local virtual_host = request_handle:virtualHost() + for _, _ in pairs(virtual_host:metadata()) do + return + end + request_handle:logTrace("No metadata found during request handling") + end + function envoy_on_response(response_handle) + local virtual_host = response_handle:virtualHost() + for _, _ in pairs(virtual_host:metadata()) do + return + end + response_handle:logTrace("No metadata found during response handling") + end + +)EOF"; + + initializeFilter(FILTER_AND_CODE, "foo.lyft.com"); + codec_client_ = makeHttpConnection(lookupPort("http")); + + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "bar.lyft.com"}, + {"x-forwarded-for", "10.0.0.1"}}; + + IntegrationStreamDecoderPtr response; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "No metadata found during request handling"}, + {"trace", "No metadata found during response handling"}, + }), + { + auto encoder_decoder = codec_client_->startRequest(request_headers); + response = std::move(encoder_decoder.second); + + ASSERT_TRUE(response->waitForEndStream()); + }); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("404", response->headers().getStatusValue()); + cleanup(); +} + +// Test that handle:route() returns a valid object when no route matches the request. +// This verifies that metadata() returns an empty metadata object that can be safely +// iterated. +TEST_P(LuaIntegrationTest, RouteValidWhenNoRouteMatch) { + if (!testing_downstream_filter_) { + GTEST_SKIP() << "This is a local reply test that does not go upstream"; + } + + const std::string filter_config = + R"EOF( +name: lua +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + local route = request_handle:route() + for _, _ in pairs(route:metadata()) do + return + end + request_handle:logTrace("No metadata found during request handling") + end + function envoy_on_response(response_handle) + local route = response_handle:route() + for _, _ in pairs(route:metadata()) do + return + end + response_handle:logTrace("No metadata found during response handling") + end +)EOF"; + + const std::string route_config = + R"EOF( +name: test_routes +virtual_hosts: +- name: test_vhost + domains: ["foo.lyft.com"] + routes: + - match: + path: "/existing/route" + metadata: + filter_metadata: + lua: + foo.bar: + name: foo + prop: bar + baz.bat: + name: baz + prop: bat + route: + cluster: cluster_0 +)EOF"; + + initializeWithYaml(filter_config, route_config); + codec_client_ = makeHttpConnection(lookupPort("http")); + + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/non/existing/path"}, + {":scheme", "http"}, + {":authority", "foo.lyft.com"}, + {"x-forwarded-for", "10.0.0.1"}}; + + IntegrationStreamDecoderPtr response; + EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ + {"trace", "No metadata found during request handling"}, + {"trace", "No metadata found during response handling"}, + }), + { + auto encoder_decoder = codec_client_->startRequest(request_headers); + response = std::move(encoder_decoder.second); + + ASSERT_TRUE(response->waitForEndStream()); + }); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("404", response->headers().getStatusValue()); + + cleanup(); +} + #ifdef NDEBUG // This test is only run in release mode because in debug mode, // the code reaches ENVOY_BUG() which triggers a forced abort diff --git a/test/extensions/filters/http/lua/wrappers_test.cc b/test/extensions/filters/http/lua/wrappers_test.cc index 239589cdd52b5..30f53ef1d87c3 100644 --- a/test/extensions/filters/http/lua/wrappers_test.cc +++ b/test/extensions/filters/http/lua/wrappers_test.cc @@ -2,16 +2,22 @@ #include "source/common/http/utility.h" #include "source/common/network/address_impl.h" +#include "source/common/router/string_accessor_impl.h" +#include "source/common/stream_info/bool_accessor_impl.h" #include "source/common/stream_info/stream_info_impl.h" +#include "source/common/stream_info/uint64_accessor_impl.h" #include "source/extensions/filters/http/lua/wrappers.h" #include "test/extensions/filters/common/lua/lua_wrappers.h" +#include "test/mocks/router/mocks.h" #include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" using testing::Expectation; using testing::InSequence; +using testing::Return; using testing::ReturnPointee; +using testing::ReturnRef; namespace Envoy { namespace Extensions { @@ -302,6 +308,7 @@ class LuaStreamInfoWrapperTest Filters::Common::Lua::LuaWrappersTestBase::setup(script); state_->registerType(); state_->registerType(); + state_->registerType(); } protected: @@ -468,10 +475,10 @@ TEST_F(LuaStreamInfoWrapperTest, GetDynamicMetadataBinaryData) { end )EOF"}; - ProtobufWkt::Value metadata_value; + Protobuf::Value metadata_value; constexpr uint8_t buffer[] = {'h', 'e', 0x00, 'l', 'l', 'o'}; metadata_value.set_string_value(reinterpret_cast(buffer), sizeof(buffer)); - ProtobufWkt::Struct metadata; + Protobuf::Struct metadata; metadata.mutable_fields()->insert({"bin_data", metadata_value}); setup(SCRIPT); @@ -526,18 +533,18 @@ TEST_F(LuaStreamInfoWrapperTest, SetGetComplexDynamicMetadata) { start("callMe"); EXPECT_EQ(1, stream_info.dynamicMetadata().filter_metadata_size()); - const ProtobufWkt::Struct& meta_foo = stream_info.dynamicMetadata() - .filter_metadata() - .at("envoy.lb") - .fields() - .at("foo") - .struct_value(); + const Protobuf::Struct& meta_foo = stream_info.dynamicMetadata() + .filter_metadata() + .at("envoy.lb") + .fields() + .at("foo") + .struct_value(); EXPECT_EQ(1234.0, meta_foo.fields().at("x").number_value()); EXPECT_EQ("baz", meta_foo.fields().at("y").string_value()); EXPECT_EQ(true, meta_foo.fields().at("z").bool_value()); - const ProtobufWkt::ListValue& meta_so = + const Protobuf::ListValue& meta_so = stream_info.dynamicMetadata().filter_metadata().at("envoy.lb").fields().at("so").list_value(); EXPECT_EQ(4, meta_so.values_size()); @@ -759,10 +766,10 @@ TEST_F(LuaStreamInfoWrapperTest, GetDynamicTypedMetadataBasic) { StreamInfo::FilterState::LifeSpan::FilterChain); // Create test typed metadata - ProtobufWkt::Struct test_struct; + Protobuf::Struct test_struct; (*test_struct.mutable_fields())["test_field"].set_string_value("test_value"); - ProtobufWkt::Any any_metadata; + Protobuf::Any any_metadata; any_metadata.set_type_url("type.googleapis.com/google.protobuf.Struct"); any_metadata.PackFrom(test_struct); @@ -824,10 +831,10 @@ TEST_F(LuaStreamInfoWrapperTest, GetDynamicTypedMetadataComplexStructure) { StreamInfo::FilterState::LifeSpan::FilterChain); // Create complex test metadata - ProtobufWkt::Struct complex_struct; + Protobuf::Struct complex_struct; // Add nested structure - ProtobufWkt::Struct nested_struct; + Protobuf::Struct nested_struct; (*nested_struct.mutable_fields())["inner_field"].set_string_value("inner_value"); (*complex_struct.mutable_fields())["nested"].mutable_struct_value()->CopyFrom(nested_struct); @@ -836,12 +843,12 @@ TEST_F(LuaStreamInfoWrapperTest, GetDynamicTypedMetadataComplexStructure) { (*complex_struct.mutable_fields())["number_field"].set_number_value(42.5); // Add array - ProtobufWkt::ListValue array_value; + Protobuf::ListValue array_value; array_value.add_values()->set_string_value("first"); array_value.add_values()->set_string_value("second"); (*complex_struct.mutable_fields())["array_field"].mutable_list_value()->CopyFrom(array_value); - ProtobufWkt::Any any_metadata; + Protobuf::Any any_metadata; any_metadata.set_type_url("type.googleapis.com/google.protobuf.Struct"); any_metadata.PackFrom(complex_struct); @@ -878,7 +885,7 @@ TEST_F(LuaStreamInfoWrapperTest, GetDynamicTypedMetadataInvalidTypeUrl) { StreamInfo::FilterState::LifeSpan::FilterChain); // Create metadata with invalid/unknown type URL - ProtobufWkt::Any any_metadata; + Protobuf::Any any_metadata; any_metadata.set_type_url("type.googleapis.com/invalid.unknown.Type"); any_metadata.set_value("invalid_data"); @@ -911,7 +918,7 @@ TEST_F(LuaStreamInfoWrapperTest, GetDynamicTypedMetadataUnpackFailure) { StreamInfo::FilterState::LifeSpan::FilterChain); // Create metadata with correct type URL but corrupted data - ProtobufWkt::Any any_metadata; + Protobuf::Any any_metadata; any_metadata.set_type_url("type.googleapis.com/google.protobuf.Struct"); any_metadata.set_value("corrupted_protobuf_data_that_cannot_be_unpacked"); @@ -958,17 +965,17 @@ TEST_F(LuaStreamInfoWrapperTest, IterateDynamicTypedMetadata) { StreamInfo::FilterState::LifeSpan::FilterChain); // Create first metadata entry - ProtobufWkt::Struct struct1; + Protobuf::Struct struct1; (*struct1.mutable_fields())["field_one"].set_string_value("value_one"); - ProtobufWkt::Any any1; + Protobuf::Any any1; any1.set_type_url("type.googleapis.com/google.protobuf.Struct"); any1.PackFrom(struct1); (*stream_info.metadata_.mutable_typed_filter_metadata())["envoy.metadata.one"] = any1; // Create second metadata entry - ProtobufWkt::Struct struct2; + Protobuf::Struct struct2; (*struct2.mutable_fields())["field_two"].set_string_value("value_two"); - ProtobufWkt::Any any2; + Protobuf::Any any2; any2.set_type_url("type.googleapis.com/google.protobuf.Struct"); any2.PackFrom(struct2); (*stream_info.metadata_.mutable_typed_filter_metadata())["envoy.metadata.two"] = any2; @@ -984,6 +991,717 @@ TEST_F(LuaStreamInfoWrapperTest, IterateDynamicTypedMetadata) { wrapper.reset(); } +// Test for ``filterState()`` basic functionality. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateBasic) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local filter_state_obj = object:filterState():get("test_key") + if filter_state_obj then + testPrint("found_filter_state") + testPrint(filter_state_obj) + else + testPrint("no_filter_state") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Create a simple string accessor for testing. + stream_info.filterState()->setData( + "test_key", std::make_shared("test_value"), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_filter_state")); + EXPECT_CALL(printer_, testPrint("test_value")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` with missing object. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateMissing) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local filter_state_obj = object:filterState():get("missing_key") + if filter_state_obj == nil then + testPrint("filter_state_not_found") + else + testPrint("filter_state_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("filter_state_not_found")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` with multiple objects. +TEST_F(LuaStreamInfoWrapperTest, GetMultipleFilterStateObjects) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local obj1 = object:filterState():get("key1") + local obj2 = object:filterState():get("key2") + local obj3 = object:filterState():get("nonexistent") + + if obj1 then + testPrint("found_key1") + testPrint(obj1) + end + + if obj2 then + testPrint("found_key2") + testPrint(obj2) + end + + if obj3 == nil then + testPrint("key3_not_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add multiple filter state objects. + stream_info.filterState()->setData("key1", std::make_shared("value1"), + StreamInfo::FilterState::StateType::ReadOnly, + StreamInfo::FilterState::LifeSpan::FilterChain); + + stream_info.filterState()->setData("key2", std::make_shared("value2"), + StreamInfo::FilterState::StateType::ReadOnly, + StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_key1")); + EXPECT_CALL(printer_, testPrint("value1")); + EXPECT_CALL(printer_, testPrint("found_key2")); + EXPECT_CALL(printer_, testPrint("value2")); + EXPECT_CALL(printer_, testPrint("key3_not_found")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` with numeric accessor. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateNumericAccessor) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local numeric_obj = object:filterState():get("numeric_key") + if numeric_obj then + testPrint("found_numeric") + testPrint(numeric_obj) + -- Test that it's returned as a string (new behavior) + if type(numeric_obj) == "string" then + testPrint("correct_string_type") + end + else + testPrint("numeric_not_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add numeric filter state object. + stream_info.filterState()->setData( + "numeric_key", std::make_shared(12345), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_numeric")); + EXPECT_CALL(printer_, testPrint("12345")); + EXPECT_CALL(printer_, testPrint("correct_string_type")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` with boolean accessor. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateBooleanAccessor) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local bool_obj = object:filterState():get("bool_key") + if bool_obj ~= nil then + testPrint("found_boolean") + testPrint(bool_obj) + -- Test that it's returned as a string (new behavior) + if type(bool_obj) == "string" then + testPrint("correct_string_type") + end + else + testPrint("boolean_not_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add boolean filter state object. + stream_info.filterState()->setData( + "bool_key", std::make_shared(true), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_boolean")); + EXPECT_CALL(printer_, testPrint("true")); + EXPECT_CALL(printer_, testPrint("correct_string_type")); + start("callMe"); + wrapper.reset(); +} + +// Test filter state object that supports field access. +class TestFieldSupportingFilterState : public StreamInfo::FilterState::Object { +public: + TestFieldSupportingFilterState(std::string base_value) : base_value_(base_value) {} + + absl::optional serializeAsString() const override { return base_value_; } + + bool hasFieldSupport() const override { return true; } + + FieldType getField(absl::string_view field_name) const override { + if (field_name == "string_field") { + return absl::string_view("field_string_value"); + } else if (field_name == "numeric_field") { + return int64_t(42); + } else if (field_name == "base_value") { + return absl::string_view(base_value_); + } + // Return empty variant for non-existent fields. + return {}; + } + +private: + std::string base_value_; +}; + +// Test for ``filterState()`` field access with string field. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateFieldAccessString) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local field_value = object:filterState():get("field_key", "string_field") + if field_value then + testPrint("found_string_field") + testPrint(field_value) + -- Verify it's returned as a string + if type(field_value) == "string" then + testPrint("correct_string_type") + end + else + testPrint("string_field_not_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add field-supporting filter state object. + stream_info.filterState()->setData( + "field_key", std::make_shared("base_value"), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_string_field")); + EXPECT_CALL(printer_, testPrint("field_string_value")); + EXPECT_CALL(printer_, testPrint("correct_string_type")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` field access with numeric field. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateFieldAccessNumeric) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local field_value = object:filterState():get("field_key", "numeric_field") + if field_value then + testPrint("found_numeric_field") + testPrint(field_value) + -- Verify it's returned as a number + if type(field_value) == "number" then + testPrint("correct_number_type") + end + else + testPrint("numeric_field_not_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add field-supporting filter state object. + stream_info.filterState()->setData( + "field_key", std::make_shared("base_value"), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_numeric_field")); + EXPECT_CALL(printer_, testPrint("42")); + EXPECT_CALL(printer_, testPrint("correct_number_type")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` field access with non-existent field. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateFieldAccessNonExistent) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local field_value = object:filterState():get("field_key", "nonexistent_field") + if field_value == nil then + testPrint("nonexistent_field_returned_nil") + else + testPrint("nonexistent_field_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add field-supporting filter state object. + stream_info.filterState()->setData( + "field_key", std::make_shared("base_value"), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("nonexistent_field_returned_nil")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` field access on object without field support. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateFieldAccessNoSupport) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local field_value = object:filterState():get("no_field_key", "any_field") + if field_value == nil then + testPrint("no_field_support_returned_nil") + else + testPrint("no_field_support_found") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add regular string accessor without field support. + stream_info.filterState()->setData( + "no_field_key", std::make_shared("test_value"), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("no_field_support_returned_nil")); + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` field access fallback to string serialization. +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateFieldAccessFallback) { + const std::string SCRIPT{R"EOF( + function callMe(object) + -- Test accessing the whole object without field parameter first + local whole_obj = object:filterState():get("field_key") + if whole_obj then + testPrint("found_whole_object") + testPrint(whole_obj) + end + + -- Test field access that matches the base_value + local field_value = object:filterState():get("field_key", "base_value") + if field_value then + testPrint("found_base_value_field") + testPrint(field_value) + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Add field-supporting filter state object. + stream_info.filterState()->setData( + "field_key", std::make_shared("test_base"), + StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("found_whole_object")); + EXPECT_CALL(printer_, testPrint("test_base")); // String serialization result + EXPECT_CALL(printer_, testPrint("found_base_value_field")); + EXPECT_CALL(printer_, testPrint("test_base")); // Field access result + start("callMe"); + wrapper.reset(); +} + +// Test for ``filterState()`` with null filter state object (covers lines 398-401). +TEST_F(LuaStreamInfoWrapperTest, GetFilterStateNullObject) { + const std::string SCRIPT{R"EOF( + function callMe(object) + -- Test accessing non-existent key which will return nullptr from getDataReadOnly + local null_obj = object:filterState():get("completely_nonexistent_key") + if null_obj == nil then + testPrint("null_filter_state_returned_nil") + else + testPrint("null_filter_state_found_something") + end + + -- Test field access on non-existent key + local null_field = object:filterState():get("completely_nonexistent_key", "any_field") + if null_field == nil then + testPrint("null_filter_state_field_returned_nil") + else + testPrint("null_filter_state_field_found_something") + end + end + )EOF"}; + + InSequence s; + setup(SCRIPT); + + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, test_time_.timeSystem(), nullptr, + StreamInfo::FilterState::LifeSpan::FilterChain); + + // Here we are deliberately not adding any filter state data, so ``getDataReadOnly`` + // will return nullptr. + Filters::Common::Lua::LuaDeathRef wrapper( + StreamInfoWrapper::create(coroutine_->luaState(), stream_info), true); + EXPECT_CALL(printer_, testPrint("null_filter_state_returned_nil")); + EXPECT_CALL(printer_, testPrint("null_filter_state_field_returned_nil")); + start("callMe"); + wrapper.reset(); +} + +class LuaVirtualHostWrapperTest + : public Filters::Common::Lua::LuaWrappersTestBase { +public: + void setup(const std::string& script) override { + Filters::Common::Lua::LuaWrappersTestBase::setup(script); + state_->registerType(); + state_->registerType(); + } + + const std::string NO_METADATA_FOUND_SCRIPT{R"EOF( + function callMe(object) + for _, _ in pairs(object:metadata()) do + return + end + testPrint("No metadata found") + end + )EOF"}; +}; + +// Test that VirtualHostWrapper returns metadata under the current filter configured name. +// This verifies that when virtual host has filter metadata configured under the current filter +// configured name, the wrapper can successfully retrieves and returns it. +TEST_F(LuaVirtualHostWrapperTest, GetFilterMetadataBasic) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local metadata = object:metadata() + testPrint(metadata:get("foo.bar")["name"]) + testPrint(metadata:get("foo.bar")["prop"]) + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + lua-filter-config-name: + foo.bar: + name: foo + prop: bar + )EOF"}; + + InSequence s; + setup(SCRIPT); + + // Create a mock virtual host. + auto virtual_host = std::make_shared>(); + const Router::VirtualHostConstSharedPtr virtual_host_ptr = virtual_host; + + // Load metadata into the mock virtual host. + TestUtility::loadFromYaml(METADATA, virtual_host->metadata_); + + // Set up the mock stream info to return the mock virtual host. + NiceMock stream_info; + ON_CALL(stream_info, virtualHost()).WillByDefault(ReturnRef(virtual_host_ptr)); + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + VirtualHostWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), + true); + + EXPECT_CALL(printer_, testPrint("foo")); + EXPECT_CALL(printer_, testPrint("bar")); + + start("callMe"); + wrapper.reset(); +} + +// Test that VirtualHostWrapper returns an empty metadata object when no metadata exists +// under the current filter configured name. +TEST_F(LuaVirtualHostWrapperTest, GetMetadataNoMetadataUnderFilterName) { + const std::string METADATA{R"EOF( + filter_metadata: + envoy.some_filter: + foo.bar: + name: foo + prop: bar + )EOF"}; + + InSequence s; + setup(NO_METADATA_FOUND_SCRIPT); + + // Create a mock virtual host. + auto virtual_host = std::make_shared>(); + const Router::VirtualHostConstSharedPtr virtual_host_ptr = virtual_host; + + // Load metadata into the mock virtual host. + TestUtility::loadFromYaml(METADATA, virtual_host->metadata_); + + // Set up the mock stream info to return the mock virtual host. + NiceMock stream_info; + ON_CALL(stream_info, virtualHost()).WillByDefault(ReturnRef(virtual_host_ptr)); + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + VirtualHostWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), + true); + + EXPECT_CALL(printer_, testPrint("No metadata found")); + + start("callMe"); + wrapper.reset(); +} + +// Test that VirtualHostWrapper returns an empty metadata object when no metadata is configured on +// the virtual host. This verifies that the wrapper correctly handles cases where the virtual host +// has no filter_metadata section, returning an empty metadata object without crashing. +TEST_F(LuaVirtualHostWrapperTest, GetMetadataNoMetadataAtAll) { + InSequence s; + setup(NO_METADATA_FOUND_SCRIPT); + + // Create a mock virtual host. + auto virtual_host = std::make_shared>(); + const Router::VirtualHostConstSharedPtr virtual_host_ptr = virtual_host; + + // Set up the mock stream info to return the mock virtual host. + NiceMock stream_info; + ON_CALL(stream_info, virtualHost()).WillByDefault(ReturnRef(virtual_host_ptr)); + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + VirtualHostWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), + true); + + EXPECT_CALL(printer_, testPrint("No metadata found")); + + start("callMe"); + wrapper.reset(); +} + +// Test that VirtualHostWrapper returns an empty metadata object when no virtual host matches the +// request authority. This verifies that the wrapper correctly handles cases where the stream info +// does not have a virtual host, returning an empty metadata object without crashing. +TEST_F(LuaVirtualHostWrapperTest, GetMetadataNoVirtualHost) { + InSequence s; + setup(NO_METADATA_FOUND_SCRIPT); + + // Set up the mock stream info to return the mock virtual host. + NiceMock stream_info; + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + VirtualHostWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), + true); + + EXPECT_CALL(printer_, testPrint("No metadata found")); + + start("callMe"); + wrapper.reset(); +} + +class LuaRouteWrapperTest : public Filters::Common::Lua::LuaWrappersTestBase { +public: + void setup(const std::string& script) override { + Filters::Common::Lua::LuaWrappersTestBase::setup(script); + state_->registerType(); + state_->registerType(); + } + + const std::string NO_METADATA_FOUND_SCRIPT{R"EOF( + function callMe(object) + for _, _ in pairs(object:metadata()) do + return + end + testPrint("No metadata found") + end + )EOF"}; +}; + +// Test that RouteWrapper returns metadata under the current filter configured name. +// This verifies that when route has filter metadata configured under the current filter +// configured name, the wrapper can successfully retrieves and returns it. +TEST_F(LuaRouteWrapperTest, GetFilterMetadataBasic) { + const std::string SCRIPT{R"EOF( + function callMe(object) + local metadata = object:metadata() + testPrint(metadata:get("foo.bar")["name"]) + testPrint(metadata:get("foo.bar")["prop"]) + end + )EOF"}; + + const std::string METADATA{R"EOF( + filter_metadata: + lua-filter-config-name: + foo.bar: + name: foo + prop: bar + )EOF"}; + + InSequence s; + setup(SCRIPT); + + // Create a mock route and load metadata into it. + auto route = std::make_shared>(); + TestUtility::loadFromYaml(METADATA, route->metadata_); + + // Set up the mock stream info to return the mock route. + NiceMock stream_info; + ON_CALL(stream_info, route()).WillByDefault(Return(route)); + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + RouteWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), true); + + EXPECT_CALL(printer_, testPrint("foo")); + EXPECT_CALL(printer_, testPrint("bar")); + + start("callMe"); + wrapper.reset(); +} + +// Test that RouteWrapper returns an empty metadata object when no metadata exists +// under the current filter configured name. +TEST_F(LuaRouteWrapperTest, GetMetadataNoMetadataUnderFilterName) { + const std::string METADATA{R"EOF( + filter_metadata: + envoy.some_filter: + foo.bar: + name: foo + prop: bar + )EOF"}; + + InSequence s; + setup(NO_METADATA_FOUND_SCRIPT); + + // Create a mock route and load metadata into it. + auto route = std::make_shared>(); + TestUtility::loadFromYaml(METADATA, route->metadata_); + + // Set up the mock stream info to return the mock route. + NiceMock stream_info; + ON_CALL(stream_info, route()).WillByDefault(Return(route)); + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + RouteWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), true); + + EXPECT_CALL(printer_, testPrint("No metadata found")); + + start("callMe"); + wrapper.reset(); +} + +// Test that RouteWrapper returns an empty metadata object when no metadata is configured on +// the route. This verifies that the wrapper correctly handles cases where the route +// has no filter_metadata section, returning an empty metadata object without crashing. +TEST_F(LuaRouteWrapperTest, GetMetadataNoMetadataAtAll) { + InSequence s; + setup(NO_METADATA_FOUND_SCRIPT); + + // Create a mock route but DO NOT load metadata into it. + auto route = std::make_shared>(); + + // Set up the mock stream info to return the mock route. + NiceMock stream_info; + ON_CALL(stream_info, route()).WillByDefault(Return(route)); + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + RouteWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), true); + + EXPECT_CALL(printer_, testPrint("No metadata found")); + + start("callMe"); + wrapper.reset(); +} + +// Test that RouteWrapper returns an empty metadata object when no route matches the +// request. This verifies that the wrapper correctly handles cases where the stream info +// does not have a route, returning an empty metadata object without crashing. +TEST_F(LuaRouteWrapperTest, GetMetadataNoRoute) { + InSequence s; + setup(NO_METADATA_FOUND_SCRIPT); + + // Set up the mock stream info but DO NOT config it to return a valid route. + NiceMock stream_info; + + // Set up wrapper with the mock stream info. + Filters::Common::Lua::LuaDeathRef wrapper( + RouteWrapper::create(coroutine_->luaState(), stream_info, "lua-filter-config-name"), true); + + EXPECT_CALL(printer_, testPrint("No metadata found")); + + start("callMe"); + wrapper.reset(); +} + } // namespace } // namespace Lua } // namespace HttpFilters diff --git a/test/extensions/filters/http/match_delegate/config_test.cc b/test/extensions/filters/http/match_delegate/config_test.cc index 0b63d20d77450..bb7294ed526eb 100644 --- a/test/extensions/filters/http/match_delegate/config_test.cc +++ b/test/extensions/filters/http/match_delegate/config_test.cc @@ -19,7 +19,7 @@ namespace { struct TestFactory : public Envoy::Server::Configuration::NamedHttpFilterConfigFactory { std::string name() const override { return "test"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } absl::StatusOr createFilterFactoryFromProto(const Protobuf::Message&, const std::string&, @@ -293,7 +293,7 @@ TEST(MatchWrapper, WithMatcherInvalidDataInput) { "according to allowlist"); } -struct TestAction : Matcher::ActionBase {}; +struct TestAction : Matcher::ActionBase {}; template Matcher::MatchTreeSharedPtr @@ -302,7 +302,7 @@ createMatchingTree(const std::string& name, const std::string& value) { std::make_unique(name), absl::nullopt); tree->addChild(value, Matcher::OnMatch{ - []() { return std::make_unique(); }, nullptr, false}); + std::make_shared(), nullptr, false}); return tree; } @@ -315,7 +315,7 @@ Matcher::MatchTreeSharedPtr createRequestAndRespo tree->addChild( "match", Matcher::OnMatch{ - []() { return std::make_unique(); }, + std::make_shared(), createMatchingTree( "match-header", "match"), false}); @@ -369,13 +369,11 @@ template Matcher::MatchTreeSharedPtr createMatchTreeWithOnNoMatch(const std::string& name, const std::string& value) { auto tree = *Matcher::ExactMapMatcher::create( - std::make_unique(name), - Matcher::OnMatch{ - []() { return std::make_unique(); }, nullptr, false}); + std::make_unique(name), Matcher::OnMatch{ + std::make_shared(), nullptr, false}); // No action is set on match. i.e., nullptr action factory cb. - tree->addChild(value, Matcher::OnMatch{[]() { return nullptr; }, - nullptr, false}); + tree->addChild(value, Matcher::OnMatch{nullptr, nullptr, false}); return tree; } diff --git a/test/extensions/filters/http/match_delegate/match_delegate_integration_test.cc b/test/extensions/filters/http/match_delegate/match_delegate_integration_test.cc index cc453fbd58cda..258d87facdbda 100644 --- a/test/extensions/filters/http/match_delegate/match_delegate_integration_test.cc +++ b/test/extensions/filters/http/match_delegate/match_delegate_integration_test.cc @@ -18,8 +18,8 @@ namespace MatchDelegate { namespace { using envoy::extensions::common::matching::v3::ExtensionWithMatcherPerRoute; +using Envoy::Protobuf::Any; using Envoy::Protobuf::MapPair; -using Envoy::ProtobufWkt::Any; class MatchDelegateIntegrationTest : public testing::TestWithParam, public HttpIntegrationTest { diff --git a/test/extensions/filters/http/oauth2/filter_test.cc b/test/extensions/filters/http/oauth2/filter_test.cc index 9fa62e44a06b6..1f63ff9be24b6 100644 --- a/test/extensions/filters/http/oauth2/filter_test.cc +++ b/test/extensions/filters/http/oauth2/filter_test.cc @@ -157,6 +157,7 @@ class OAuth2Test : public testing::TestWithParam { ::envoy::extensions::filters::http::oauth2::v3::CookieConfig_SameSite:: CookieConfig_SameSite_DISABLED, int csrf_token_expires_in = 0, int code_verifier_token_expires_in = 0) { + envoy::extensions::filters::http::oauth2::v3::OAuth2Config p; auto* endpoint = p.mutable_token_endpoint(); endpoint->set_cluster("auth.example.com"); @@ -387,15 +388,6 @@ name: client EXPECT_EQ(secret_reader.clientSecret(), "client_test_recheck"); EXPECT_EQ(secret_reader.hmacSecret(), "token_test"); } -// Verifies that we fail constructing the filter if the configured cluster doesn't exist. -TEST_F(OAuth2Test, InvalidCluster) { - ON_CALL(factory_context_.server_factory_context_.cluster_manager_, clusters()) - .WillByDefault(Return(Upstream::ClusterManager::ClusterInfoMaps())); - - EXPECT_THROW_WITH_MESSAGE(init(), EnvoyException, - "OAuth2 filter: unknown cluster 'auth.example.com' in config. Please " - "specify which cluster to direct OAuth requests to."); -} // Verifies that we fail constructing the filter if the authorization endpoint isn't a valid URL. TEST_F(OAuth2Test, InvalidAuthorizationEndpoint) { @@ -3737,6 +3729,100 @@ TEST_F(OAuth2Test, CookiesDecryptedBeforeForwardingWithCleanupOAuthCookiesDisabl EXPECT_EQ(cookies.at("RefreshToken"), "some-refresh-token"); } +// Verifies that requests matching the pass_through_matcher configuration are not modified by the +// filter. The request headers and cookies remain unchanged, and only the oauth_passthrough metric +// is incremented. This ensures correct behavior when the filter is configured to skip processing +// for specific requests. +TEST_F(OAuth2Test, RequestIsUnchangedWhenPassThroughMatcherMatches) { + Http::TestRequestHeaderMapImpl request_headers{ + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Path.get(), "/anypath"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Options}, + {Http::Headers::get().Cookie.get(), "OauthHMAC=some_oauth_hmac_value"}, + {Http::Headers::get().Cookie.get(), "OauthExpires=some_oauth_expires_value"}, + {Http::Headers::get().Cookie.get(), "RefreshToken=some_refresh_token_value"}, + {Http::Headers::get().Cookie.get(), "OauthNonce=some_oauth_nonce_value"}, + {Http::Headers::get().Cookie.get(), "CodeVerifier=some_code_verifier_value"}}; + + Http::TestRequestHeaderMapImpl expected_headers{ + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Path.get(), "/anypath"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Options}, + {Http::Headers::get().Cookie.get(), "OauthHMAC=some_oauth_hmac_value"}, + {Http::Headers::get().Cookie.get(), "OauthExpires=some_oauth_expires_value"}, + {Http::Headers::get().Cookie.get(), "RefreshToken=some_refresh_token_value"}, + {Http::Headers::get().Cookie.get(), "OauthNonce=some_oauth_nonce_value"}, + {Http::Headers::get().Cookie.get(), "CodeVerifier=some_code_verifier_value"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ(request_headers, expected_headers); + EXPECT_EQ(scope_.counterFromString("test.my_prefix.oauth_failure").value(), 0); + EXPECT_EQ(scope_.counterFromString("test.my_prefix.oauth_passthrough").value(), 1); + EXPECT_EQ(scope_.counterFromString("test.my_prefix.oauth_success").value(), 0); +} + +// Verify cookie prefixes "__Secure-" and "__Host-" cause addition of the "Secure" attribute at +// signout. +TEST_F(OAuth2Test, SecureAttributeAddedForSecureCookiePrefixesOnSignout) { + auto make_config = + [&](absl::string_view prefix) -> envoy::extensions::filters::http::oauth2::v3::OAuth2Config { + envoy::extensions::filters::http::oauth2::v3::OAuth2Config p; + auto* endpoint = p.mutable_token_endpoint(); + endpoint->set_cluster("auth.example.com"); + endpoint->set_uri("auth.example.com/_oauth"); + p.set_authorization_endpoint("https://auth2.example.com/oauth/authorize/"); + p.mutable_signout_path()->mutable_path()->set_exact("/_signout"); + p.mutable_redirect_path_matcher()->mutable_path()->set_exact(TEST_CALLBACK); + auto* credentials = p.mutable_credentials(); + credentials->set_client_id(TEST_CLIENT_ID); + credentials->mutable_token_secret()->set_name("secret"); + credentials->mutable_hmac_secret()->set_name("hmac"); + auto* cookie_names = credentials->mutable_cookie_names(); + cookie_names->set_oauth_hmac(absl::StrCat(prefix, "OauthHMAC")); + cookie_names->set_bearer_token(absl::StrCat(prefix, "BearerToken")); + cookie_names->set_id_token(absl::StrCat(prefix, "IdToken")); + cookie_names->set_refresh_token(absl::StrCat(prefix, "RefreshToken")); + cookie_names->set_oauth_nonce(absl::StrCat(prefix, "OauthNonce")); + cookie_names->set_code_verifier(absl::StrCat(prefix, "CodeVerifier")); + + auto* matcher = p.add_pass_through_matcher(); + matcher->set_name(":method"); + matcher->mutable_string_match()->set_exact("OPTIONS"); + + return p; + }; + + auto run_test_with_prefix = [&](absl::string_view prefix, bool expect_secure) { + auto p = make_config(prefix); + auto secret_reader = std::make_shared(); + init(std::make_shared(p, factory_context_.server_factory_context_, secret_reader, + scope_, "test.")); + + EXPECT_CALL(decoder_callbacks_, encodeHeaders_(_, true)) + .WillOnce(Invoke([&](Http::ResponseHeaderMap& passed_headers, bool) { + EXPECT_EQ(passed_headers.get(Http::Headers::get().SetCookie).size(), 6); + const auto& cookie_str = + passed_headers.get(Http::Headers::get().SetCookie)[0]->value().getStringView(); + if (expect_secure) { + EXPECT_THAT(cookie_str, testing::HasSubstr("; Secure")); + } else { + EXPECT_THAT(cookie_str, testing::Not(testing::HasSubstr("; Secure"))); + } + })); + auto request_headers = Http::TestRequestHeaderMapImpl{ + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Path.get(), "/_signout"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, + }; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); + }; + + run_test_with_prefix("__Secure-", true); + run_test_with_prefix("__Host-", true); + run_test_with_prefix("", false); +} + } // namespace Oauth2 } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/oauth2/oauth_integration_test.cc b/test/extensions/filters/http/oauth2/oauth_integration_test.cc index 5b8dd1005ba71..9d297acd32f97 100644 --- a/test/extensions/filters/http/oauth2/oauth_integration_test.cc +++ b/test/extensions/filters/http/oauth2/oauth_integration_test.cc @@ -132,7 +132,7 @@ class OauthIntegrationTest : public HttpIntegrationTest, const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); for (const auto& listener_config : listener_configs) { response.add_resources()->PackFrom(listener_config); } diff --git a/test/extensions/filters/http/oauth2/oauth_test.cc b/test/extensions/filters/http/oauth2/oauth_test.cc index 2de33bb063088..c8751d1aaeae9 100644 --- a/test/extensions/filters/http/oauth2/oauth_test.cc +++ b/test/extensions/filters/http/oauth2/oauth_test.cc @@ -29,7 +29,7 @@ using testing::Return; class MockCallbacks : public FilterCallbacks { public: - MOCK_METHOD(void, sendUnauthorizedResponse, ()); + MOCK_METHOD(void, sendUnauthorizedResponse, (const std::string& details)); MOCK_METHOD(void, onGetAccessTokenSuccess, (const std::string&, const std::string&, const std::string&, std::chrono::seconds)); MOCK_METHOD(void, onRefreshAccessTokenSuccess, @@ -135,7 +135,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenMissingExpiresIn) { client_->setCallbacks(*mock_callbacks_); client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); ASSERT_TRUE(popPendingCallback( [&](auto* callback) { callback->onSuccess(request, std::move(mock_response)); })); @@ -202,7 +202,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenIncompleteResponse) { client_->setCallbacks(*mock_callbacks_); client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); ASSERT_TRUE(popPendingCallback( [&](auto* callback) { callback->onSuccess(request, std::move(mock_response)); })); @@ -227,7 +227,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenErrorResponse) { client_->setCallbacks(*mock_callbacks_); client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); ASSERT_TRUE(popPendingCallback( [&](auto* callback) { callback->onSuccess(request, std::move(mock_response)); })); @@ -258,7 +258,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenInvalidResponse) { client_->setCallbacks(*mock_callbacks_); client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); ASSERT_TRUE(popPendingCallback( [&](auto* callback) { callback->onSuccess(request, std::move(mock_response)); })); @@ -277,7 +277,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenNetworkError) { client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(1, callbacks_.size()); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); ASSERT_TRUE(popPendingCallback([&](auto* callback) { callback->onFailure(request, Http::AsyncClient::FailureReason::Reset); @@ -301,7 +301,7 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenUnhealthyUpstream) { })); client_->setCallbacks(*mock_callbacks_); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); client_->asyncGetAccessToken("a", "b", "c", "d", "e"); } @@ -478,7 +478,7 @@ TEST_F(OAuth2ClientTest, RequestRefreshAccessTokenNetworkErrorDoubleCallStateInv TEST_F(OAuth2ClientTest, NoCluster) { ON_CALL(cm_, getThreadLocalCluster("auth")).WillByDefault(Return(nullptr)); client_->setCallbacks(*mock_callbacks_); - EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse()); + EXPECT_CALL(*mock_callbacks_, sendUnauthorizedResponse(_)); client_->asyncGetAccessToken("a", "b", "c", "d", "e"); EXPECT_EQ(0, callbacks_.size()); } diff --git a/test/extensions/filters/http/on_demand/BUILD b/test/extensions/filters/http/on_demand/BUILD index 9c6d25f2934cc..8b3de9622d14c 100644 --- a/test/extensions/filters/http/on_demand/BUILD +++ b/test/extensions/filters/http/on_demand/BUILD @@ -64,9 +64,11 @@ envoy_extension_cc_test( "//source/extensions/filters/http/on_demand:on_demand_update_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/integration:ads_integration_lib", + "//test/integration:ads_xdstp_config_sources_integration_lib", "//test/integration:fake_upstream_lib", "//test/integration:http_integration_lib", "//test/integration:scoped_rds_lib", + "//test/integration:xdstp_config_sources_integration_lib", "//test/test_common:resources_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/on_demand/odcds_integration_test.cc b/test/extensions/filters/http/on_demand/odcds_integration_test.cc index 8f37dbd8b8370..edaab40683bfc 100644 --- a/test/extensions/filters/http/on_demand/odcds_integration_test.cc +++ b/test/extensions/filters/http/on_demand/odcds_integration_test.cc @@ -11,9 +11,11 @@ #include "test/common/grpc/grpc_client_integration.h" #include "test/integration/ads_integration.h" +#include "test/integration/ads_xdstp_config_sources_integration.h" #include "test/integration/fake_upstream.h" #include "test/integration/http_integration.h" #include "test/integration/scoped_rds.h" +#include "test/integration/xdstp_config_sources_integration.h" #include "test/test_common/resources.h" #include "test/test_common/utility.h" @@ -107,32 +109,38 @@ class OdCdsIntegrationHelper { } static OnDemandCdsConfig - createOnDemandCdsConfig(envoy::config::core::v3::ConfigSource config_source, int timeout_millis) { + createOnDemandCdsConfig(absl::optional config_source, + int timeout_millis) { OnDemandCdsConfig config; - *config.mutable_source() = std::move(config_source); + if (config_source.has_value()) { + *config.mutable_source() = std::move(config_source.value()); + } *config.mutable_timeout() = ProtobufUtil::TimeUtil::MillisecondsToDuration(timeout_millis); return config; } template - static OnDemandConfigType createConfig(envoy::config::core::v3::ConfigSource config_source, - int timeout_millis) { + static OnDemandConfigType + createConfig(absl::optional config_source, + int timeout_millis) { OnDemandConfigType on_demand; *on_demand.mutable_odcds() = createOnDemandCdsConfig(std::move(config_source), timeout_millis); return on_demand; } - static OnDemandConfig createOnDemandConfig(envoy::config::core::v3::ConfigSource config_source, - int timeout_millis) { + static OnDemandConfig + createOnDemandConfig(absl::optional config_source, + int timeout_millis) { return createConfig(std::move(config_source), timeout_millis); } - static PerRouteConfig createPerRouteConfig(envoy::config::core::v3::ConfigSource config_source, - int timeout_millis) { + static PerRouteConfig + createPerRouteConfig(absl::optional config_source, + int timeout_millis) { return createConfig(std::move(config_source), timeout_millis); } - static OptRef> + static OptRef> findPerRouteConfigMap(ConfigHelper::HttpConnectionManager& hcm, absl::string_view vhost_name, absl::string_view route_name) { auto* route_config = hcm.mutable_route_config(); @@ -215,7 +223,7 @@ class OdCdsListenerBuilder { private: envoy::config::listener::v3::Listener listener_; - ProtobufWkt::Any* hcm_any_; + Protobuf::Any* hcm_any_; ConfigHelper::HttpConnectionManager hcm_; }; @@ -316,12 +324,12 @@ TEST_P(OdCdsIntegrationTest, OnDemandClusterDiscoveryWorksWithClusterHeader) { RELEASE_ASSERT(result, result.message()); odcds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); waitForNextUpstreamRequest(new_cluster_upstream_idx_); // Send response headers, and end_stream if there is no response body. @@ -359,12 +367,12 @@ TEST_P(OdCdsIntegrationTest, OnDemandClusterDiscoveryRemembersDiscoveredCluster) RELEASE_ASSERT(result, result.message()); odcds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); waitForNextUpstreamRequest(new_cluster_upstream_idx_); // Send response headers, and end_stream if there is no response body. @@ -407,7 +415,7 @@ TEST_P(OdCdsIntegrationTest, OnDemandClusterDiscoveryTimesOut) { RELEASE_ASSERT(result, result.message()); odcds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); // not sending a response to trigger the timeout @@ -441,12 +449,12 @@ TEST_P(OdCdsIntegrationTest, OnDemandClusterDiscoveryForNonexistentCluster) { RELEASE_ASSERT(result, result.message()); odcds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {}, {"new_cluster"}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {}, {"new_cluster"}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); ASSERT_TRUE(response->waitForEndStream()); verifyResponse(std::move(response), "503", {}, {}); @@ -510,10 +518,33 @@ TEST_P(OdCdsIntegrationTest, DisablingOdCdsAtVirtualHostLevelWorks) { cleanupUpstreamAndDownstream(); } -class OdCdsAdsIntegrationTest : public AdsIntegrationTest { +class OdCdsAdsIntegrationTest + : public AdsIntegrationTestBase, + public testing::TestWithParam< + std::tuple> { public: + OdCdsAdsIntegrationTest() : AdsIntegrationTestBase(ipVersion(), sotwOrDelta()) {} + + void TearDown() override { cleanUpXdsConnection(); } + + static std::string protocolTestParamsToString( + const ::testing::TestParamInfo< + std::tuple>& p) { + return fmt::format( + "{}_{}_{}_{}", TestUtility::ipVersionToString(std::get<0>(p.param)), + std::get<1>(p.param) == Grpc::ClientType::GoogleGrpc ? "GoogleGrpc" : "EnvoyGrpc", + std::get<2>(p.param) == Grpc::SotwOrDelta::Delta ? "Delta" : "StateOfTheWorld", + std::get<3>(p.param) ? "WithOdcdsOverAdsFix" : "WithoutOdcdsOverAdsFix"); + } + Network::Address::IpVersion ipVersion() const override { return std::get<0>(GetParam()); } + Grpc::ClientType clientType() const override { return std::get<1>(GetParam()); } + Grpc::SotwOrDelta sotwOrDelta() const { return std::get<2>(GetParam()); } + bool odcds_over_ads_fix_enabled() const { return std::get<3>(GetParam()); } + void initialize() override { - AdsIntegrationTest::initialize(); + config_helper_.addRuntimeOverride("envoy.reloadable_features.odcds_over_ads_fix", + odcds_over_ads_fix_enabled() ? "true" : "false"); + AdsIntegrationTestBase::initialize(); test_server_->waitUntilListenersReady(); new_cluster_upstream_idx_ = fake_upstreams_.size(); @@ -533,6 +564,57 @@ class OdCdsAdsIntegrationTest : public AdsIntegrationTest { return builder.listener(); } + envoy::config::listener::v3::Listener buildListenerWithMultiRoute() { + OdCdsListenerBuilder builder(Network::Test::getLoopbackAddressString(ipVersion())); + auto ads_config_source = OdCdsIntegrationHelper::createAdsOdCdsConfigSource(); + auto& hcm = builder.hcm(); + // Set the ODCDS filter on the HCM to use ADS, and a long timeout. + auto odcds_config = + OdCdsIntegrationHelper::createOnDemandConfig(std::move(ads_config_source), 10000); + hcm.mutable_http_filters(0)->mutable_typed_config()->PackFrom(std::move(odcds_config)); + // The clusters are on-demand - no need to validate them. + hcm.mutable_route_config()->mutable_validate_clusters()->set_value(false); + // Update the route to match "/" to cluster: "new_cluster1". + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->clear_cluster_header(); + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->set_cluster("new_cluster1"); + // Duplicate the route for the virtual-host (make 2 new routes). + hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes()->Add()->CopyFrom( + hcm.route_config().virtual_hosts(0).routes(0)); + hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes()->Add()->CopyFrom( + hcm.route_config().virtual_hosts(0).routes(0)); + // Change the first route to match "/match2" to a cluster: "new_cluster2". + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->set_cluster("new_cluster2"); + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_match() + ->set_prefix("/match2"); + // Change the first route to match "/match3" to a cluster: "new_cluster3". + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(1) + ->mutable_route() + ->set_cluster("new_cluster3"); + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(1) + ->mutable_match() + ->set_prefix("/match3"); + return builder.listener(); + } + bool compareRequest(const std::string& type_url, const std::vector& expected_resource_subscriptions, const std::vector& expected_resource_unsubscriptions, @@ -544,19 +626,19 @@ class OdCdsAdsIntegrationTest : public AdsIntegrationTest { void doInitialCommunications() { // initial cluster query - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {}, {}, true)); - sendDeltaDiscoveryResponse(Config::TypeUrl::get().Cluster, - {}, {}, "1"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {}, true)); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {}, "1"); // initial listener query - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Listener, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); auto odcds_listener = buildListener(); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Listener, {odcds_listener}, {}, "2"); + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); // acks - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {}, {})); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Listener, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); // listener got acked, so register the http port now. test_server_->waitUntilListenersReady(); @@ -572,7 +654,11 @@ INSTANTIATE_TEST_SUITE_P( testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), testing::ValuesIn(TestEnvironment::getsGrpcVersionsForTest()), // Only delta xDS is supported for on-demand CDS. - testing::Values(Grpc::SotwOrDelta::Delta, Grpc::SotwOrDelta::UnifiedDelta))); + testing::Values(Grpc::SotwOrDelta::Delta, Grpc::SotwOrDelta::UnifiedDelta), + // Whether to use the new/old OdCdsApiImpl (will be removed once + // "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions" + // is deprecated). + testing::Values(true, false))); // tests a scenario when: // - making a request to an unknown cluster @@ -590,10 +676,10 @@ TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterDiscoveryWorksWithClusterHeader) {"Pick-This-Cluster", "new_cluster"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {})); - sendDeltaDiscoveryResponse(Config::TypeUrl::get().Cluster, - {new_cluster_}, {}, "3"); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {})); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); waitForNextUpstreamRequest(new_cluster_upstream_idx_); // Send response headers, and end_stream if there is no response body. @@ -623,10 +709,10 @@ TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterDiscoveryRemembersDiscoveredClust {"Pick-This-Cluster", "new_cluster"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {})); - sendDeltaDiscoveryResponse(Config::TypeUrl::get().Cluster, - {new_cluster_}, {}, "3"); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {})); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); waitForNextUpstreamRequest(new_cluster_upstream_idx_); // Send response headers, and end_stream if there is no response body. @@ -661,7 +747,7 @@ TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterDiscoveryTimesOut) { {"Pick-This-Cluster", "new_cluster"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {})); // not sending a response ASSERT_TRUE(response->waitForEndStream()); @@ -686,10 +772,946 @@ TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterDiscoveryAsksForNonexistentCluste {"Pick-This-Cluster", "new_cluster"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {})); - sendDeltaDiscoveryResponse(Config::TypeUrl::get().Cluster, - {}, {"new_cluster"}, "3"); - EXPECT_TRUE(compareRequest(Config::TypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {})); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {"new_cluster"}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "503", {}, {}); + + cleanupUpstreamAndDownstream(); +} + +// tests a scenario where: +// - 2 listeners each with its HCM configured with OdCds. +// - making 2 concurrent downstream requests one to listener1, and a short while after a second to +// listener2. +// - Observing that a single CDS request is sent to the ADS server. +// - sending a single CDS response back to the Envoy containing the cluster. +// - both requests are resumed. +TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterMultipleListenersSameClusters) { + initialize(); + // Initial cluster query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {}, true)); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {}, "1"); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListener(); + odcds_listener.set_name("listener_0"); + auto odcds_listener2 = buildListener(); + odcds_listener2.set_name("listener_1"); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener, odcds_listener2}, {}, "2"); + + // Acks. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http", "http_1"}); + + // Send first downstream request. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", "new_cluster"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {})); + + // Send second downstream request. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http_1"))); + Http::TestRequestHeaderMapImpl request_headers2{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", "new_cluster"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Send the CDS response with the cluster, and expect an Ack after that (if + // there were repeated requests, there won't be an ack next). + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for one of the requests to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + // Wait for the other request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + + // Cleanup. + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } +} + +// tests a scenario where: +// - a single listener with its HCM configured with 2 routes for ODCDS. +// - making a downstream request to route1. +// - Observing that a CDS request to new_cluster1 is sent to the ADS server. +// - Sending the CDS response for new_cluster1. +// - making a downstream request to route2. +// - Observing that a CDS request to new_cluster2 is sent to the ADS server (without removing +// new_cluster1). +// - Sending the CDS response for new_cluster2. +// - both requests are resumed. +TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterDiscoveryMultipleClustersSequentially) { + initialize(); + // Create 2 clusters (that have to the same endpoint). + envoy::config::cluster::v3::Cluster new_cluster1 = ConfigHelper::buildStaticCluster( + "new_cluster1", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster2 = ConfigHelper::buildStaticCluster( + "new_cluster2", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + + // Initial cluster query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {}, true)); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {}, "1"); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListenerWithMultiRoute(); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); + + // Acks. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http"}); + + // Send first downstream request to the route of new_cluster1. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster1 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster1"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster1}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + cleanupUpstreamAndDownstream(); + + // Send a second downstream request but to the route of new_cluster2. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers2{ + {":method", "GET"}, {":path", "/match2"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Expect a new_cluster2 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster2"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster2}, {}, "4"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request2 = std::move(upstream_request_); + upstream_request2->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } +} + +// tests a scenario where: +// - cds_config is not used. +// - a single listener with its HCM configured with 2 routes for ODCDS. +// - making a downstream request to route1. +// - Observing that a CDS request to new_cluster1 is sent to the ADS server. +// - Sending the CDS response for new_cluster1. +// - making a downstream request to route2. +// - Observing that a CDS request to new_cluster2 is sent to the ADS server (without removing +// new_cluster1). +// - Sending the CDS response for new_cluster2. +// - both requests are resumed. +TEST_P(OdCdsAdsIntegrationTest, NoCdsConfigOnDemandClusterMultipleClustersSequentially) { + // This test does not work with the previous OdCdsApi implementation (OdCdsApiImpl), + // but works with the new one (XdstpOdCdsApiImpl). + // Once envoy.reloadable_features.odcds_over_ads_fix is removed, this test + // will only execute the fixed component. + if (!odcds_over_ads_fix_enabled()) { + GTEST_SKIP() << "This test only passes with the new XdstpOdCdsApiImpl implementation"; + } + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + bootstrap.mutable_dynamic_resources()->clear_cds_config(); + }); + initialize(); + // Create 2 clusters (that have to the same endpoint). + envoy::config::cluster::v3::Cluster new_cluster1 = ConfigHelper::buildStaticCluster( + "new_cluster1", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster2 = ConfigHelper::buildStaticCluster( + "new_cluster2", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListenerWithMultiRoute(); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); + + // Acks. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http"}); + + // Send first downstream request to the route of new_cluster1. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster1 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster1"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster1}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + cleanupUpstreamAndDownstream(); + + // Send a second downstream request but to the route of new_cluster2. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers2{ + {":method", "GET"}, {":path", "/match2"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Expect a new_cluster2 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster2"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster2}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request2 = std::move(upstream_request_); + upstream_request2->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } +} + +// tests a scenario where: +// - a single listener with its HCM configured with 3 routes for ODCDS. +// - making a downstream request to route1. +// - Observing that a CDS request to new_cluster1 is sent to the ADS server. +// - Sending the CDS response for new_cluster1. +// - making a downstream request to route2. +// - Observing that a CDS request to new_cluster2 is sent to the ADS server (without removing +// new_cluster1). +// - making a downstream request to route3. +// - Observing that a CDS request to new_cluster2 and new_cluster3 is sent to the ADS server +// (without removing new_cluster1). +// - Sending the CDS response for new_cluster2, new_cluster3. +// - both requests are resumed. +TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterTwoClustersBeforeResponseAfterInitialCluster) { + initialize(); + // Create 3 clusters (that have to the same endpoint). + envoy::config::cluster::v3::Cluster new_cluster1 = ConfigHelper::buildStaticCluster( + "new_cluster1", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster2 = ConfigHelper::buildStaticCluster( + "new_cluster2", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster3 = ConfigHelper::buildStaticCluster( + "new_cluster3", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + + // Initial cluster query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {}, true)); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {}, "1"); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListenerWithMultiRoute(); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); + + // Acks. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http"}); + + // Send first downstream request to the route of new_cluster1. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster1 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster1"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster1}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + cleanupUpstreamAndDownstream(); + + // Send a second downstream request to the route of new_cluster2. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers2{ + {":method", "GET"}, {":path", "/match2"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Expect a new_cluster2 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster2"}, {})); + + // Send a third downstream request to the route of new_cluster3. + IntegrationCodecClientPtr codec_client3 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers3{ + {":method", "GET"}, {":path", "/match3"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response3 = codec_client3->makeHeaderOnlyRequest(request_headers3); + + // Expect a new_cluster3 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster3"}, {})); + // Send a CDS response with both clusters, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster2, new_cluster3}, {}, "4"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for one of the requests to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request2 = std::move(upstream_request_); + upstream_request2->encodeHeaders(default_response_headers_, true); + cleanupUpstreamAndDownstream(); + + // Wait for the second request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request3 = std::move(upstream_request_); + upstream_request3->encodeHeaders(default_response_headers_, true); + + // Ensure that all response arrived with 200. + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + ASSERT_TRUE(response3->waitForEndStream()); + verifyResponse(std::move(response3), "200", {}, {}); + + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } + if (codec_client3) { + codec_client3->close(); + } +} + +// tests a scenario where: +// - cds_config is not used. +// - a single listener with its HCM configured with 3 routes for ODCDS. +// - making a downstream request to route1. +// - Observing that a CDS request to new_cluster1 is sent to the ADS server. +// - Sending the CDS response for new_cluster1. +// - making a downstream request to route2. +// - Observing that a CDS request to new_cluster2 is sent to the ADS server (without removing +// new_cluster1). +// - making a downstream request to route3. +// - Observing that a CDS request to new_cluster2 and new_cluster3 is sent to the ADS server +// (without removing new_cluster1). +// - Sending the CDS response for new_cluster2, new_cluster3. +// - both requests are resumed. +TEST_P(OdCdsAdsIntegrationTest, + NoCdsConfigOnDemandDiscoveryTwoClustersBeforeResponseAfterInitialCluster) { + // This test does not work with the previous OdCdsApi implementation (OdCdsApiImpl), + // but works with the new one (XdstpOdCdsApiImpl). + // Once envoy.reloadable_features.odcds_over_ads_fix is removed, this test + // will only execute the fixed component. + if (!odcds_over_ads_fix_enabled()) { + GTEST_SKIP() << "This test only passes with the new XdstpOdCdsApiImpl implementation"; + } + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + bootstrap.mutable_dynamic_resources()->clear_cds_config(); + }); + initialize(); + // Create 3 clusters (that have to the same endpoint). + envoy::config::cluster::v3::Cluster new_cluster1 = ConfigHelper::buildStaticCluster( + "new_cluster1", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster2 = ConfigHelper::buildStaticCluster( + "new_cluster2", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster3 = ConfigHelper::buildStaticCluster( + "new_cluster3", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListenerWithMultiRoute(); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); + + // Ack. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http"}); + + // Send first downstream request to the route of new_cluster1. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster1 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster1"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster1}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + cleanupUpstreamAndDownstream(); + + // Send a second downstream request to the route of new_cluster2. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers2{ + {":method", "GET"}, {":path", "/match2"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Expect a new_cluster2 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster2"}, {})); + + // Send a third downstream request to the route of new_cluster3. + IntegrationCodecClientPtr codec_client3 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers3{ + {":method", "GET"}, {":path", "/match3"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response3 = codec_client3->makeHeaderOnlyRequest(request_headers3); + + // Expect a new_cluster3 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster3"}, {})); + // Send a CDS response with both clusters, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster2, new_cluster3}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for one of the requests to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request2 = std::move(upstream_request_); + upstream_request2->encodeHeaders(default_response_headers_, true); + cleanupUpstreamAndDownstream(); + + // Wait for the second request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request3 = std::move(upstream_request_); + upstream_request3->encodeHeaders(default_response_headers_, true); + + // Ensure that all response arrived with 200. + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + ASSERT_TRUE(response3->waitForEndStream()); + verifyResponse(std::move(response3), "200", {}, {}); + + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } + if (codec_client3) { + codec_client3->close(); + } +} + +/*****************/ + +// tests a scenario where: +// - a single listener with its HCM configured with 3 routes for ODCDS. +// - making a downstream request to route1. +// - Observing that a CDS request to new_cluster1 is sent to the ADS server. +// - Sending the CDS response for new_cluster1. +// - making a downstream request to route2. +// - Observing that a CDS request to new_cluster2 is sent to the ADS server (without removing +// new_cluster1). +// - making a downstream request to route3. +// - Observing that a CDS request to new_cluster2 and new_cluster3 is sent to the ADS server +// (without removing new_cluster1). +// - Sending the CDS response for new_cluster3. +// - request is resumed. +// - Sending the CDS response for new_cluster2. +// - final request is resumed. +TEST_P(OdCdsAdsIntegrationTest, + OnDemandClusterTwoClustersReceivingSecondFirstBeforeResponseAfterInitialCluster) { + initialize(); + // Create 3 clusters (that have to the same endpoint). + envoy::config::cluster::v3::Cluster new_cluster1 = ConfigHelper::buildStaticCluster( + "new_cluster1", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster2 = ConfigHelper::buildStaticCluster( + "new_cluster2", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster3 = ConfigHelper::buildStaticCluster( + "new_cluster3", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + + // Initial cluster query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {}, true)); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {}, "1"); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListenerWithMultiRoute(); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); + + // Acks. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http"}); + + // Send first downstream request to the route of new_cluster1. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster1 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster1"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster1}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + cleanupUpstreamAndDownstream(); + + // Send a second downstream request to the route of new_cluster2. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers2{ + {":method", "GET"}, {":path", "/match2"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Expect a new_cluster2 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster2"}, {})); + + // Send a third downstream request to the route of new_cluster3. + IntegrationCodecClientPtr codec_client3 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers3{ + {":method", "GET"}, {":path", "/match3"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response3 = codec_client3->makeHeaderOnlyRequest(request_headers3); + + // Expect a new_cluster3 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster3"}, {})); + + // Send a CDS response with only new_cluster3, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster3}, {}, "4"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the third request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request3 = std::move(upstream_request_); + upstream_request3->encodeHeaders(default_response_headers_, true); + cleanupUpstreamAndDownstream(); + // Ensure that the third request got a 200 response. + ASSERT_TRUE(response3->waitForEndStream()); + verifyResponse(std::move(response3), "200", {}, {}); + + // Send a CDS response with only new_cluster2, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster2}, {}, "5"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the second request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request2 = std::move(upstream_request_); + upstream_request2->encodeHeaders(default_response_headers_, true); + // Ensure that the second request got a 200 response. + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } + if (codec_client3) { + codec_client3->close(); + } +} + +// tests a scenario where: +// - a single listener with its HCM configured with 3 routes for ODCDS. +// - making a downstream request to route1. +// - Observing that a CDS request to new_cluster1 is sent to the ADS server. +// - Sending the CDS response for new_cluster1. +// - making a downstream request to route2. +// - Observing that a CDS request to new_cluster2 is sent to the ADS server (without removing +// new_cluster1). +// - making a downstream request to route3. +// - Observing that a CDS request to new_cluster2 and new_cluster3 is sent to the ADS server +// (without removing new_cluster1). +// - Disconnect the ADS stream, and ensure correct reconnection. +// - Sending the CDS response for new_cluster2, new_cluster3. +// - both requests are resumed. +TEST_P(OdCdsAdsIntegrationTest, + OnDemandClusterTwoClustersBeforeResponseAndDisconnectAfterInitialCluster) { + initialize(); + // Create 3 clusters (that have to the same endpoint). + envoy::config::cluster::v3::Cluster new_cluster1 = ConfigHelper::buildStaticCluster( + "new_cluster1", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster2 = ConfigHelper::buildStaticCluster( + "new_cluster2", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + envoy::config::cluster::v3::Cluster new_cluster3 = ConfigHelper::buildStaticCluster( + "new_cluster3", fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + + // Initial cluster query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {}, true)); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {}, {}, "1"); + + // Initial listener query. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + auto odcds_listener = buildListenerWithMultiRoute(); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {odcds_listener}, {}, "2"); + + // Acks. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Listener is acked, register the http port now. + test_server_->waitUntilListenersReady(); + registerTestServerPorts({"http"}); + + // Send first downstream request to the route of new_cluster1. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Expect a new_cluster1 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster1"}, {})); + // Send the CDS response with the cluster, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster1}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for the request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + cleanupUpstreamAndDownstream(); + + // Send a second downstream request to the route of new_cluster2. + IntegrationCodecClientPtr codec_client2 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers2{ + {":method", "GET"}, {":path", "/match2"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response2 = codec_client2->makeHeaderOnlyRequest(request_headers2); + + // Expect a new_cluster2 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster2"}, {})); + + // Send a third downstream request to the route of new_cluster3. + IntegrationCodecClientPtr codec_client3 = + makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers3{ + {":method", "GET"}, {":path", "/match3"}, {":scheme", "http"}, {":authority", "vhost.first"}}; + IntegrationStreamDecoderPtr response3 = codec_client3->makeHeaderOnlyRequest(request_headers3); + + // Expect a new_cluster3 CDS on-demand request. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster3"}, {})); + + // Disconnect the xDS stream. + xds_stream_->finishGrpcStream(Grpc::Status::Internal); + // Allow reconnection to the xDS-stream. + AssertionResult result = xds_connection_->waitForNewStream(*dispatcher_, xds_stream_); + RELEASE_ASSERT(result, result.message()); + xds_stream_->startGrpcStream(); + + // A CDS request for {"*", "new_cluster1", "new_cluster2", "new_cluster3"} + // should be received. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, + {"*", "new_cluster1", "new_cluster2", "new_cluster3"}, {}, true)); + // The listeners should already include odcds_listener, nothing to remove. + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Listener, {}, {})); + + // Send a CDS response with both clusters, and expect an Ack after that. + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster2, new_cluster3}, {}, "3"); + EXPECT_TRUE(compareRequest(Config::TestTypeUrl::get().Cluster, {}, {})); + + // Wait for one of the requests to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request2 = std::move(upstream_request_); + upstream_request2->encodeHeaders(default_response_headers_, true); + cleanupUpstreamAndDownstream(); + + // Wait for the second request to arrive at the upstream, and send a reply. + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + FakeStreamPtr upstream_request3 = std::move(upstream_request_); + upstream_request3->encodeHeaders(default_response_headers_, true); + + // Ensure that all response arrived with 200. + ASSERT_TRUE(response2->waitForEndStream()); + verifyResponse(std::move(response2), "200", {}, {}); + ASSERT_TRUE(response3->waitForEndStream()); + verifyResponse(std::move(response3), "200", {}, {}); + + cleanupUpstreamAndDownstream(); + if (codec_client2) { + codec_client2->close(); + } + if (codec_client3) { + codec_client3->close(); + } +} + +class OdCdsXdstpIntegrationTest : public XdsTpConfigsIntegration { +public: + void initialize() override { + // Skipping port usage validation because this tests will create new clusters + // that will be sent to the OD-CDS subscriptions. + config_helper_.skipPortUsageValidation(); + + // Set up the listener and add the PerRouteConfig in it that will have the + // ODCDS filter. + config_helper_.addConfigModifier( + [&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* static_resources = bootstrap.mutable_static_resources(); + // Replace the listener. + *static_resources->mutable_listeners(0) = buildListener(); + }); + + // Envoy will only connect to the xDS-TP servers that are defined in the + // bootstrap, but won't issue a subscription yet. + on_server_init_function_ = [this]() { + connectAuthority1(); + connectDefaultAuthority(); + }; + XdsTpConfigsIntegration::initialize(); + + test_server_->waitUntilListenersReady(); + // Add a fake cluster server that will be returned for the OD-CDS request. + new_cluster_upstream_idx_ = fake_upstreams_.size(); + addFakeUpstream(Http::CodecType::HTTP2); + new_cluster_ = ConfigHelper::buildStaticCluster( + "xdstp://authority1.com/envoy.config.cluster.v3.Cluster/on_demand_clusters/new_cluster", + fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + registerTestServerPorts({"http"}); + } + + envoy::config::listener::v3::Listener buildListener() { + OdCdsListenerBuilder builder(Network::Test::getLoopbackAddressString(ipVersion())); + auto per_route_config = OdCdsIntegrationHelper::createPerRouteConfig(absl::nullopt, 2500); + OdCdsIntegrationHelper::addPerRouteConfig(builder.hcm(), std::move(per_route_config), + "integration", {}); + return builder.listener(); + } + + envoy::config::endpoint::v3::ClusterLoadAssignment + buildClusterLoadAssignment(const std::string& name, size_t upstream_idx) { + return ConfigHelper::buildClusterLoadAssignment( + name, Network::Test::getLoopbackAddressString(ipVersion()), + fake_upstreams_[upstream_idx]->localAddress()->ip()->port()); + } + + bool compareRequest(const std::string& type_url, + const std::vector& expected_resource_subscriptions, + const std::vector& expected_resource_unsubscriptions, + bool expect_node = false) { + return compareDeltaDiscoveryRequest(type_url, expected_resource_subscriptions, + expected_resource_unsubscriptions, + Grpc::Status::WellKnownGrpcStatus::Ok, "", expect_node); + }; + + std::size_t new_cluster_upstream_idx_; + envoy::config::cluster::v3::Cluster new_cluster_; +}; + +INSTANTIATE_TEST_SUITE_P( + IpVersionsClientTypeDeltaWildcard, OdCdsXdstpIntegrationTest, + testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + testing::ValuesIn(TestEnvironment::getsGrpcVersionsForTest()), + // TODO(adisuissa): add SotW validation - this should work + // as long as there isn't both empty wildcard and on-demand + // on the same xds-tp gRPC-mux (which is not supported at + // the moment). + // Only delta xDS is supported for on-demand CDS. + testing::Values(Grpc::SotwOrDelta::Delta, Grpc::SotwOrDelta::UnifiedDelta))); + +// tests a scenario when: +// - making a request to an unknown cluster +// - odcds initiates a connection with a request for the cluster +// - a response contains the cluster +// - request is resumed +TEST_P(OdCdsXdstpIntegrationTest, OnDemandClusterDiscoveryWorksWithClusterHeader) { + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + const std::string& cluster_name = new_cluster_.name(); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", cluster_name}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Authority1 should receive the ODCDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().Cluster, "", {cluster_name}, {cluster_name}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {new_cluster_}, {new_cluster_}, {}, + "1", {}, authority1_xds_stream_.get()); + // Expect a CDS ACK from authority1. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {cluster_name}, {}, + {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", + authority1_xds_stream_.get())); + + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + // Send response headers, and end_stream if there is no response body. + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + cleanupUpstreamAndDownstream(); +} + +// tests a scenario when: +// - making a request to an unknown cluster +// - odcds initiates a connection with a request for the cluster +// - a response contains the cluster +// - request is resumed +// - another request is sent to the same cluster +// - no odcds happens, because the cluster is known +TEST_P(OdCdsXdstpIntegrationTest, OnDemandClusterDiscoveryRemembersDiscoveredCluster) { + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + const std::string& cluster_name = new_cluster_.name(); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", cluster_name}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Authority1 should receive the ODCDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().Cluster, "", {cluster_name}, {cluster_name}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {new_cluster_}, {new_cluster_}, {}, + "1", {}, authority1_xds_stream_.get()); + // Expect a CDS ACK from authority1. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {cluster_name}, {}, + {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", + authority1_xds_stream_.get())); + + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + // Send response headers, and end_stream if there is no response body. + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + // Next request should be handled right away (no xDS subscription). + response = codec_client_->makeHeaderOnlyRequest(request_headers); + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + cleanupUpstreamAndDownstream(); +} + +// tests a scenario when: +// - making a request to an unknown cluster +// - odcds initiates a connection with a request for the cluster +// - waiting for response times out +// - request is resumed +TEST_P(OdCdsXdstpIntegrationTest, OnDemandClusterDiscoveryTimesOut) { + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + const std::string& cluster_name = new_cluster_.name(); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", cluster_name}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Authority1 should receive the ODCDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().Cluster, "", {cluster_name}, {cluster_name}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + // not sending a response ASSERT_TRUE(response->waitForEndStream()); verifyResponse(std::move(response), "503", {}, {}); @@ -697,6 +1719,288 @@ TEST_P(OdCdsAdsIntegrationTest, OnDemandClusterDiscoveryAsksForNonexistentCluste cleanupUpstreamAndDownstream(); } +// tests a scenario when: +// - making a request to an unknown cluster +// - odcds initiates a connection with a request for the cluster +// - a response says that there is no such cluster +// - request is resumed +TEST_P(OdCdsXdstpIntegrationTest, OnDemandClusterDiscoveryAsksForNonexistentCluster) { + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + const std::string& cluster_name = new_cluster_.name(); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", cluster_name}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Authority1 should receive the ODCDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().Cluster, "", {cluster_name}, {cluster_name}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + // Send a response to remove the requested cluster (not found). + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {cluster_name}, "1", {}, + authority1_xds_stream_.get()); + // Expect a CDS ACK from authority1. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {cluster_name}, {}, + {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", + authority1_xds_stream_.get())); + + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "503", {}, {}); + + cleanupUpstreamAndDownstream(); +} + +// tests a scenario when: +// - making a request to an unknown cluster +// - odcds initiates a connection with a request for the cluster +// - a response contains an EDS cluster +// - an EDS request is sent to the same authority +// - an EDS response is received +// - request is resumed +TEST_P(OdCdsXdstpIntegrationTest, OnDemandCdsWithEds) { + initialize(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + const std::string cds_cluster_name = + "xdstp://authority1.com/envoy.config.cluster.v3.Cluster/on_demand_clusters/" + "new_cluster_with_eds"; + const std::string eds_service_name = + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/on_demand_clusters/" + "new_cluster_with_eds"; + + envoy::config::cluster::v3::Cluster new_cluster_with_eds; + new_cluster_with_eds.set_name(cds_cluster_name); + new_cluster_with_eds.set_type(envoy::config::cluster::v3::Cluster::EDS); + auto* eds_cluster_config = new_cluster_with_eds.mutable_eds_cluster_config(); + eds_cluster_config->set_service_name(eds_service_name); + ConfigHelper::setHttp2(new_cluster_with_eds); + + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", cds_cluster_name}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Authority1 should receive the ODCDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().Cluster, "", {cds_cluster_name}, {cds_cluster_name}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {new_cluster_with_eds}, {new_cluster_with_eds}, {}, "1", + {}, authority1_xds_stream_.get()); + // After the CDS response, Envoy will send an EDS request for the new cluster. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {eds_service_name}, {eds_service_name}, + {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse( + Config::TestTypeUrl::get().ClusterLoadAssignment, + {buildClusterLoadAssignment(eds_service_name, new_cluster_upstream_idx_)}, + {buildClusterLoadAssignment(eds_service_name, new_cluster_upstream_idx_)}, {}, "2", {}, + authority1_xds_stream_.get()); + // Now, Envoy should ACK the original CDS response. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {cds_cluster_name}, + {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", + authority1_xds_stream_.get())); + // And finally, Envoy should ACK the EDS response. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {eds_service_name}, {}, {}, false, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + // Send response headers, and end_stream if there is no response body. + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + cleanupUpstreamAndDownstream(); +} + +/** + * Tests a use-case where OD-CDS is using xDS-TP based config source, and an + * (old) ADS source updates the wildcard clusters subscriptions. + */ +class OdCdsXdstpAdsIntegrationTest : public AdsXdsTpConfigsIntegrationTest { +public: + OdCdsXdstpAdsIntegrationTest() : AdsXdsTpConfigsIntegrationTest() { + // Override the sotw_or_delta_ settings to only use SotW-ADS. + // Note that in the future this can be modified to support other types as + // well, but currently not needed. + ads_config_type_override_ = envoy::config::core::v3::ApiConfigSource::GRPC; + } + + void initialize() override { + // Skipping port usage validation because this tests will create new clusters + // that will be sent to the OD-CDS subscriptions. + config_helper_.skipPortUsageValidation(); + AdsXdsTpConfigsIntegrationTest::initialize(); + + // Add a fake cluster server that will be returned for the OD-CDS request. + new_cluster_upstream_idx_ = fake_upstreams_.size(); + addFakeUpstream(Http::CodecType::HTTP2); + new_cluster_ = ConfigHelper::buildStaticCluster( + "xdstp://authority1.com/envoy.config.cluster.v3.Cluster/on_demand_clusters/new_cluster", + fake_upstreams_[new_cluster_upstream_idx_]->localAddress()->ip()->port(), + Network::Test::getLoopbackAddressString(ipVersion())); + } + + envoy::config::listener::v3::Listener buildListener() { + OdCdsListenerBuilder builder(Network::Test::getLoopbackAddressString(ipVersion())); + auto per_route_config = OdCdsIntegrationHelper::createPerRouteConfig(absl::nullopt, 2500); + OdCdsIntegrationHelper::addPerRouteConfig(builder.hcm(), std::move(per_route_config), + "integration", {}); + return builder.listener(); + } + + envoy::config::endpoint::v3::ClusterLoadAssignment + buildClusterLoadAssignment(const std::string& name, size_t upstream_idx) { + return ConfigHelper::buildClusterLoadAssignment( + name, Network::Test::getLoopbackAddressString(ipVersion()), + fake_upstreams_[upstream_idx]->localAddress()->ip()->port()); + } + + bool compareRequest(const std::string& type_url, + const std::vector& expected_resource_subscriptions, + const std::vector& expected_resource_unsubscriptions, + bool expect_node = false) { + return compareDeltaDiscoveryRequest(type_url, expected_resource_subscriptions, + expected_resource_unsubscriptions, + Grpc::Status::WellKnownGrpcStatus::Ok, "", expect_node); + }; + + // Compares a discovery request from the (old) ADS stream. This only supports + // SotW at the moment. + AssertionResult compareAdsDiscoveryRequest( + const std::string& expected_type_url, const std::string& expected_version, + const std::vector& expected_resource_names, bool expect_node = false, + const Protobuf::int32 expected_error_code = Grpc::Status::WellKnownGrpcStatus::Ok, + const std::string& expected_error_substring = "") { + return compareSotwDiscoveryRequest(expected_type_url, expected_version, expected_resource_names, + expect_node, expected_error_code, expected_error_substring); + } + + // Sends a discovery response using the (old) ADS stream. This only supports + // SotW at the moment. + template + void sendAdsDiscoveryResponse(const std::string& type_url, + const std::vector& state_of_the_world, + const std::string& version) { + sendSotwDiscoveryResponse(type_url, state_of_the_world, version); + } + + std::size_t new_cluster_upstream_idx_; + envoy::config::cluster::v3::Cluster new_cluster_; +}; + +INSTANTIATE_TEST_SUITE_P( + IpVersionsClientTypeDeltaWildcard, OdCdsXdstpAdsIntegrationTest, + testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + testing::ValuesIn(TestEnvironment::getsGrpcVersionsForTest()), + // TODO(adisuissa): add SotW validation - this should work + // as long as there isn't both empty wildcard and on-demand + // on the same xds-tp gRPC-mux (which is not supported at + // the moment). + // Only delta xDS is supported for on-demand CDS. + testing::Values(Grpc::SotwOrDelta::Delta, Grpc::SotwOrDelta::UnifiedDelta))); + +// tests a scenario when: +// - Envoy receives a CDS over SotW-ADS update, and receives 1 cluster +// - downstream client makes a request to an unknown cluster +// - odcds initiates a connection with a request for the cluster +// - a response contains the cluster +// - request is resumed +// - Envoy receives an update to the CDS over SotW-ADS +// - another request is sent to the same on-demand cluster +// - no odcds happens, because the cluster is known, and the request is successful +TEST_P(OdCdsXdstpAdsIntegrationTest, OnDemandClusterDiscoveryWithSotwAds) { + // Sets the cds_config (lds is needed to allow proper integration test suite initialization). + setupClustersFromOldAds(); + setupListenersFromOldAds(); + initialize(); + + // Handle the CDS request - send a single cluster. + // Wait for ADS clusters request and send a cluster that points to load + // assignment in authority1.com. + EXPECT_TRUE(compareAdsDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, true)); + envoy::config::cluster::v3::Cluster sotw_cluster = ConfigHelper::buildStaticCluster( + "sotw_cluster", 1234, Network::Test::getLoopbackAddressString(ipVersion())); + sendAdsDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {sotw_cluster}, "1"); + + // Send the Listener (with the OD-CDS filter) using the old ADS. + EXPECT_TRUE(compareAdsDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {})); + const envoy::config::listener::v3::Listener listener = buildListener(); + sendAdsDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {listener}, "1"); + + // Old ADS receives a CDS and a LDS ACK. + EXPECT_TRUE(compareAdsDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {})); + EXPECT_TRUE(compareAdsDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {})); + // Expected 5 clusters: dummy, authority1_cluster, default_authority_cluster, + // ads_cluster and sotw_cluster. + EXPECT_EQ(5, test_server_->gauge("cluster_manager.active_clusters")->value()); + + // Envoy should now complete initialization. + test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); + registerTestServerPorts({"http"}); + + // Send the first request. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + const std::string& cluster_name = new_cluster_.name(); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "vhost.first"}, + {"Pick-This-Cluster", cluster_name}}; + IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); + + // Authority1 should receive the ODCDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().Cluster, "", {cluster_name}, {cluster_name}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {new_cluster_}, {new_cluster_}, {}, + "1", {}, authority1_xds_stream_.get()); + // Expect a CDS ACK from authority1. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {cluster_name}, {}, + {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", + authority1_xds_stream_.get())); + + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + // Send response headers, and end_stream if there is no response body. + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + // Expected 6 clusters: dummy, authority1_cluster, default_authority_cluster, + // ads_cluster, sotw_cluster, and the OD-CDS-cluster. + EXPECT_EQ(6, test_server_->gauge("cluster_manager.active_clusters")->value()); + + // Update the SotW cluster, and send it. + sotw_cluster.mutable_connect_timeout()->set_seconds(5); + sendAdsDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {sotw_cluster}, "2"); + // Old ADS receives a CDS ACK. + EXPECT_TRUE(compareAdsDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {})); + // Expected 6 clusters: dummy, authority1_cluster, default_authority_cluster, + // ads_cluster, sotw_cluster, and the OD-CDS-cluster. + EXPECT_EQ(6, test_server_->gauge("cluster_manager.active_clusters")->value()); + + // Next request should be handled right away (no xDS subscription). + response = codec_client_->makeHeaderOnlyRequest(request_headers); + waitForNextUpstreamRequest(new_cluster_upstream_idx_); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + verifyResponse(std::move(response), "200", {}, {}); + + cleanupUpstreamAndDownstream(); +} + class OdCdsScopedRdsIntegrationTestBase : public ScopedRdsIntegrationTest { public: void addOnDemandConfig(OdCdsIntegrationHelper::OnDemandConfig config) { @@ -864,12 +2168,12 @@ on_demand: true RELEASE_ASSERT(result, result.message()); odcds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, - odcds_stream_.get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, + {}, odcds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); waitForNextUpstreamRequest(new_cluster_upstream_idx_); // Send response headers, and end_stream if there is no response body. @@ -1012,6 +2316,5 @@ TEST_P(OdCdsScopedRdsIntegrationTest, OnDemandUpdateFailsBecauseOdCdsIsDisabledI cleanupUpstreamAndDownstream(); } - } // namespace } // namespace Envoy diff --git a/test/extensions/filters/http/on_demand/on_demand_integration_test.cc b/test/extensions/filters/http/on_demand/on_demand_integration_test.cc index bdaa4d013cbae..a64e69cfc7bdc 100644 --- a/test/extensions/filters/http/on_demand/on_demand_integration_test.cc +++ b/test/extensions/filters/http/on_demand/on_demand_integration_test.cc @@ -362,9 +362,9 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsVirtualHostAddUpdateRemove) { // A spontaneous VHDS DiscoveryResponse adds two virtual hosts sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, buildVirtualHost1(), {}, "2", vhds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + Config::TestTypeUrl::get().VirtualHost, buildVirtualHost1(), {}, "2", vhds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); testRouterHeaderOnlyRequestAndResponse(nullptr, 1, "/one", "vhost.first"); cleanupUpstreamAndDownstream(); @@ -375,10 +375,10 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsVirtualHostAddUpdateRemove) { // A spontaneous VHDS DiscoveryResponse removes newly added virtual hosts sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, {}, {"my_route/vhost_1", "my_route/vhost_2"}, "3", + Config::TestTypeUrl::get().VirtualHost, {}, {"my_route/vhost_1", "my_route/vhost_2"}, "3", vhds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); // an upstream request to an (now) unknown domain codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); @@ -388,11 +388,11 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsVirtualHostAddUpdateRemove) { {":authority", "vhost.first"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost.first")}, {}, vhds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, {buildVirtualHost2()}, {}, "4", vhds_stream_.get(), + Config::TestTypeUrl::get().VirtualHost, {buildVirtualHost2()}, {}, "4", vhds_stream_.get(), {"my_route/vhost.first"}); waitForNextUpstreamRequest(1); @@ -422,9 +422,9 @@ TEST_P(OnDemandVhdsIntegrationTest, RdsWithVirtualHostsVhdsVirtualHostAddUpdateR // A spontaneous VHDS DiscoveryResponse adds two virtual hosts sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, buildVirtualHost1(), {}, "2", vhds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + Config::TestTypeUrl::get().VirtualHost, buildVirtualHost1(), {}, "2", vhds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); // verify that rds-based virtual host can be resolved testRouterHeaderOnlyRequestAndResponse(nullptr, 1, "/rdsone", "vhost.rds.first"); @@ -439,10 +439,10 @@ TEST_P(OnDemandVhdsIntegrationTest, RdsWithVirtualHostsVhdsVirtualHostAddUpdateR // A spontaneous VHDS DiscoveryResponse removes virtual hosts added via vhds sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, {}, {"my_route/vhost_1", "my_route/vhost_2"}, "3", + Config::TestTypeUrl::get().VirtualHost, {}, {"my_route/vhost_1", "my_route/vhost_2"}, "3", vhds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); // verify rds-based virtual host is still present testRouterHeaderOnlyRequestAndResponse(nullptr, 1, "/rdsone", "vhost.rds.first"); @@ -456,11 +456,11 @@ TEST_P(OnDemandVhdsIntegrationTest, RdsWithVirtualHostsVhdsVirtualHostAddUpdateR {":authority", "vhost.first"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost.first")}, {}, vhds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, {buildVirtualHost2()}, {}, "4", vhds_stream_.get(), + Config::TestTypeUrl::get().VirtualHost, {buildVirtualHost2()}, {}, "4", vhds_stream_.get(), {"my_route/vhost.first"}); waitForNextUpstreamRequest(1); @@ -497,7 +497,7 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsOnDemandUpdateWithResourceNameAsAlias) { {":authority", "vhost_1"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost_1")}, {}, vhds_stream_.get())); @@ -543,7 +543,7 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsOnDemandUpdateFailToResolveTheAlias) { {":authority", "vhost.third"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost.third")}, {}, vhds_stream_.get())); // Send an empty response back (the management server isn't aware of vhost.third) @@ -582,7 +582,7 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsOnDemandUpdateFailToResolveOneAliasOutOf {":authority", "vhost.third"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost.third")}, {}, vhds_stream_.get())); // Send an empty response back (the management server isn't aware of vhost.third) @@ -615,7 +615,7 @@ TEST_P(OnDemandVhdsIntegrationTest, VhdsOnDemandUpdateHttpConnectionCloses) { auto encoder_decoder = codec_client_->startRequest(request_headers); Http::RequestEncoder& encoder = encoder_decoder.first; IntegrationStreamDecoderPtr response = std::move(encoder_decoder.second); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost_1")}, {}, vhds_stream_.get())); @@ -654,13 +654,13 @@ TEST_P(OnDemandVhdsIntegrationTest, MultipleUpdates) { {":authority", "vhost.first"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost.first")}, {}, vhds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, {buildVirtualHost2()}, {}, "4", vhds_stream_.get(), + Config::TestTypeUrl::get().VirtualHost, {buildVirtualHost2()}, {}, "4", vhds_stream_.get(), {"my_route/vhost.first"}); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); waitForNextUpstreamRequest(1); @@ -682,15 +682,15 @@ TEST_P(OnDemandVhdsIntegrationTest, MultipleUpdates) { {":authority", "vhost.second"}, {"x-lyft-user-id", "123"}}; IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(request_headers); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {vhdsRequestResourceName("vhost.second")}, {}, vhds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, + Config::TestTypeUrl::get().VirtualHost, {TestUtility::parseYaml( virtualHostYaml("my_route/vhost_2", "vhost.second"))}, {}, "4", vhds_stream_.get(), {"my_route/vhost.second"}); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); waitForNextUpstreamRequest(1); @@ -706,13 +706,13 @@ TEST_P(OnDemandVhdsIntegrationTest, MultipleUpdates) { { // Attempt to push updates for both vhost.first and vhost.second sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, + Config::TestTypeUrl::get().VirtualHost, {TestUtility::parseYaml( fmt::format(VhostTemplateAfterUpdate, "my_route/vhost_1", "vhost.first")), TestUtility::parseYaml( fmt::format(VhostTemplateAfterUpdate, "my_route/vhost_2", "vhost.second"))}, {}, "5", vhds_stream_.get()); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); // verify that both virtual hosts have been updated @@ -739,24 +739,24 @@ TEST_P(OnDemandVhdsIntegrationTest, AttemptAddingDuplicateDomainNames) { // A spontaneous VHDS DiscoveryResponse with duplicate domains that results in an error in the // ack. sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, + Config::TestTypeUrl::get().VirtualHost, {TestUtility::parseYaml( virtualHostYaml("my_route/vhost_1", "vhost.duplicate")), TestUtility::parseYaml( virtualHostYaml("my_route/vhost_2", "vhost.duplicate"))}, {}, "2", vhds_stream_.get()); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get(), 13, "Only unique values for domains are permitted")); // Another update, this time valid, should result in no errors sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, + Config::TestTypeUrl::get().VirtualHost, {TestUtility::parseYaml( virtualHostYaml("my_route/vhost_3", "vhost.third"))}, {}, "2", vhds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); cleanupUpstreamAndDownstream(); } diff --git a/test/extensions/filters/http/proto_api_scrubber/filter_test.cc b/test/extensions/filters/http/proto_api_scrubber/filter_test.cc index 363025289dff2..743ad98525f53 100644 --- a/test/extensions/filters/http/proto_api_scrubber/filter_test.cc +++ b/test/extensions/filters/http/proto_api_scrubber/filter_test.cc @@ -31,7 +31,7 @@ using ::Envoy::Http::MockStreamDecoderFilterCallbacks; using ::Envoy::Http::MockStreamEncoderFilterCallbacks; using ::Envoy::Http::TestRequestHeaderMapImpl; using ::Envoy::Http::TestResponseHeaderMapImpl; -using ::Envoy::ProtobufWkt::Struct; +using ::Envoy::Protobuf::Struct; using ::testing::Eq; inline constexpr const char kApiKeysDescriptorRelativePath[] = "test/proto/apikeys.descriptor"; diff --git a/test/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util_test.cc b/test/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util_test.cc index e572ecb2d56e9..c48324ca69e41 100644 --- a/test/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util_test.cc +++ b/test/extensions/filters/http/proto_message_extraction/extraction_util/extraction_util_test.cc @@ -38,9 +38,9 @@ namespace { using ::Envoy::Protobuf::Field; using ::Envoy::Protobuf::FieldMask; using Envoy::Protobuf::FileDescriptorSet; +using ::Envoy::Protobuf::Struct; using ::Envoy::Protobuf::Type; using ::Envoy::Protobuf::field_extraction::CordMessageData; -using ::Envoy::ProtobufWkt::Struct; using ::Envoy::StatusHelpers::IsOkAndHolds; using ::Envoy::StatusHelpers::StatusIs; using ::extraction::TestRequest; @@ -224,15 +224,15 @@ class ExtractionUtilTest : public ::testing::Test { }; TEST_F(ExtractionUtilTest, IsEmptyStruct_EmptyStruct) { - ProtobufWkt::Struct message_struct; - message_struct.mutable_fields()->insert({kTypeProperty, ProtobufWkt::Value()}); + Protobuf::Struct message_struct; + message_struct.mutable_fields()->insert({kTypeProperty, Protobuf::Value()}); EXPECT_TRUE(IsEmptyStruct(message_struct)); } TEST_F(ExtractionUtilTest, IsEmptyStruct_NonEmptyStruct) { - ProtobufWkt::Struct message_struct; - message_struct.mutable_fields()->insert({kTypeProperty, ProtobufWkt::Value()}); - message_struct.mutable_fields()->insert({"another_field", ProtobufWkt::Value()}); + Protobuf::Struct message_struct; + message_struct.mutable_fields()->insert({kTypeProperty, Protobuf::Value()}); + message_struct.mutable_fields()->insert({"another_field", Protobuf::Value()}); EXPECT_FALSE(IsEmptyStruct(message_struct)); } diff --git a/test/extensions/filters/http/proto_message_extraction/filter_test.cc b/test/extensions/filters/http/proto_message_extraction/filter_test.cc index 6d8a1698e9d45..8b549d763e2dc 100644 --- a/test/extensions/filters/http/proto_message_extraction/filter_test.cc +++ b/test/extensions/filters/http/proto_message_extraction/filter_test.cc @@ -32,7 +32,7 @@ using ::Envoy::Http::MockStreamDecoderFilterCallbacks; using ::Envoy::Http::MockStreamEncoderFilterCallbacks; using ::Envoy::Http::TestRequestHeaderMapImpl; using ::Envoy::Http::TestResponseHeaderMapImpl; -using ::Envoy::ProtobufWkt::Struct; +using ::Envoy::Protobuf::Struct; using ::testing::Eq; constexpr absl::string_view kFilterName = "envoy.filters.http.proto_message_extraction"; @@ -95,8 +95,8 @@ fields { } )pb"; -void checkProtoStruct(Envoy::ProtobufWkt::Struct got, absl::string_view expected_in_pbtext) { - Envoy::ProtobufWkt::Struct expected; +void checkProtoStruct(Envoy::Protobuf::Struct got, absl::string_view expected_in_pbtext) { + Envoy::Protobuf::Struct expected; ASSERT_THAT(Envoy::Protobuf::TextFormat::ParseFromString(expected_in_pbtext, &expected), true); EXPECT_THAT(Envoy::TestUtility::protoEqual(got, expected, true), true) << "got:\n" @@ -217,7 +217,7 @@ TEST_F(FilterTestExtractOk, UnarySingleBuffer) { EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedRequestExtractedResult); })); @@ -240,7 +240,7 @@ TEST_F(FilterTestExtractOk, UnarySingleBuffer) { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedResponseExtractedResult); })); @@ -278,7 +278,7 @@ TEST_F(FilterTestExtractOk, UnarySingleBufferWithMultipleFields) { EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -347,7 +347,7 @@ fields { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -420,7 +420,7 @@ TEST_F(FilterTestExtractOk, UnarySingleBufferWithMultipleFieldsforResponseOnly) EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -464,7 +464,7 @@ fields { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -569,7 +569,7 @@ fields { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -670,7 +670,7 @@ TEST_F(FilterTestExtractOk, UnaryMultipeBuffers) { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedResponseExtractedResult); })); @@ -737,7 +737,7 @@ extraction_by_method: { )pb"); Envoy::Buffer::InstancePtr request_data4 = Envoy::Grpc::Common::serializeToGrpcFrame(request4); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.proto_message_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -854,7 +854,7 @@ fields { )pb"); Envoy::Buffer::InstancePtr response_data4 = Envoy::Grpc::Common::serializeToGrpcFrame(response4); EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.proto_message_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -930,7 +930,7 @@ TEST_F(FilterTestExtractOk, RequestResponseWithTrailers) { EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -988,7 +988,7 @@ fields { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1123,7 +1123,7 @@ supported_types: { )pb"); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.proto_message_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1250,7 +1250,7 @@ fields { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1394,7 +1394,7 @@ repeated_supported_types: { )pb"); Envoy::Buffer::InstancePtr request_data = Envoy::Grpc::Common::serializeToGrpcFrame(request); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.proto_message_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1628,7 +1628,7 @@ TEST_F(FilterTestWithExtractRedacted, UnarySingleBuffer) { EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1692,7 +1692,7 @@ fields { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1793,7 +1793,7 @@ extraction_by_method: { )pb"); Envoy::Buffer::InstancePtr request_data4 = Envoy::Grpc::Common::serializeToGrpcFrame(request4); EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.proto_message_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -1912,7 +1912,7 @@ fields { )pb"); Envoy::Buffer::InstancePtr response_data4 = Envoy::Grpc::Common::serializeToGrpcFrame(response4); EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([](const std::string& ns, const ProtobufWkt::Struct& new_dynamic_metadata) { + .WillOnce(Invoke([](const std::string& ns, const Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.proto_message_extraction"); checkProtoStruct(new_dynamic_metadata, R"pb( fields { @@ -2168,7 +2168,7 @@ TEST_F(FilterTestWithExtractModeUnspecified, ModeUnspecified) { EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedRequestExtractedResult); })); @@ -2191,7 +2191,7 @@ TEST_F(FilterTestWithExtractModeUnspecified, ModeUnspecified) { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedResponseExtractedResult); })); @@ -2228,7 +2228,7 @@ TEST_F(FilterTestWithExtractDirectiveUnspecified, HappyPath) { EXPECT_CALL(mock_decoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedRequestExtractedResult); })); @@ -2251,7 +2251,7 @@ TEST_F(FilterTestWithExtractDirectiveUnspecified, HappyPath) { EXPECT_CALL(mock_encoder_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce( - Invoke([](const std::string& ns, const Envoy::ProtobufWkt::Struct& new_dynamic_metadata) { + Invoke([](const std::string& ns, const Envoy::Protobuf::Struct& new_dynamic_metadata) { EXPECT_EQ(ns, kFilterName); checkProtoStruct(new_dynamic_metadata, kExpectedResponseExtractedResult); })); diff --git a/test/extensions/filters/http/proto_message_extraction/integration_test.cc b/test/extensions/filters/http/proto_message_extraction/integration_test.cc index 5414d49bd0e14..2b4e003587979 100644 --- a/test/extensions/filters/http/proto_message_extraction/integration_test.cc +++ b/test/extensions/filters/http/proto_message_extraction/integration_test.cc @@ -20,7 +20,7 @@ using ::apikeys::CreateApiKeyRequest; using ::Envoy::Extensions::HttpFilters::GrpcFieldExtraction::checkSerializedData; void compareJson(const std::string& actual, const std::string& expected) { - ProtobufWkt::Value expected_value, actual_value; + Protobuf::Value expected_value, actual_value; TestUtility::loadFromJson(expected, expected_value); TestUtility::loadFromJson(actual, actual_value); EXPECT_TRUE(TestUtility::protoEqual(expected_value, actual_value)) diff --git a/test/extensions/filters/http/rate_limit_quota/BUILD b/test/extensions/filters/http/rate_limit_quota/BUILD index b620048c90af2..65cf6d47e7877 100644 --- a/test/extensions/filters/http/rate_limit_quota/BUILD +++ b/test/extensions/filters/http/rate_limit_quota/BUILD @@ -127,3 +127,31 @@ envoy_extension_cc_test( "@envoy_api//envoy/type/v3:pkg_cc_proto", ], ) + +envoy_extension_cc_test( + name = "filter_persistence_test", + size = "large", + srcs = ["filter_persistence_test.cc"], + extension_names = ["envoy.filters.http.rate_limit_quota"], + shard_count = 4, + tags = [ + "cpu:3", + "skip_on_windows", + ], + deps = [ + ":test_utils", + "//source/common/http:message_lib", + "//source/extensions/filters/http/rate_limit_quota", + "//source/extensions/filters/http/rate_limit_quota:config", + "//test/common/http:common_lib", + "//test/integration:http_integration_lib", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:status_utility_lib", + "//test/test_common:test_runtime_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/rate_limit_quota/v3:pkg_cc_proto", + "@envoy_api//envoy/service/rate_limit_quota/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/rate_limit_quota/client_test.cc b/test/extensions/filters/http/rate_limit_quota/client_test.cc index 8f01704f73733..0d3d7031c496e 100644 --- a/test/extensions/filters/http/rate_limit_quota/client_test.cc +++ b/test/extensions/filters/http/rate_limit_quota/client_test.cc @@ -108,8 +108,8 @@ class GlobalClientTest : public ::testing::Test { std::make_shared(std::make_shared()); buckets_tls_->set([initial_tl_buckets_cache](Unused) { return initial_tl_buckets_cache; }); - mock_stream_client->expectClientCreation(); - global_client_ = std::make_shared( + mock_stream_client->expectClientCreationWithFactory(); + global_client_ = std::make_unique( mock_stream_client->config_with_hash_key_, mock_stream_client->context_, mock_domain_, reporting_interval_, *buckets_tls_, *mock_stream_client->dispatcher_); // Set callbacks to handle asynchronous timing. @@ -120,8 +120,13 @@ class GlobalClientTest : public ::testing::Test { unordered_differencer_.set_repeated_field_comparison(MessageDifferencer::AS_SET); } + void TearDown() override { + // Normally called by TlsStore destructor as part of filter factory cb deletion. + mock_stream_client->dispatcher_->deferredDelete(std::move(global_client_)); + } + std::unique_ptr mock_stream_client = nullptr; - std::shared_ptr global_client_ = nullptr; + std::unique_ptr global_client_ = nullptr; ThreadLocal::TypedSlotPtr buckets_tls_ = nullptr; GlobalClientCallbacks* cb_ptr_ = nullptr; @@ -1252,28 +1257,68 @@ TEST_F(GlobalClientTest, TestAbandonAction) { ASSERT_FALSE(bucket_after); } +TEST_F(GlobalClientTest, TestResponseBucketMissingId) { + mock_stream_client->expectStreamCreation(1); + // Expect expiration timers to start for each of the response's assignments & + // a reset of the TokenBucket assignment's expiration timer (even when not + // resetting the TokenBucket itself). + mock_stream_client->expectTimerCreations(reporting_interval_, action_ttl, 1); + + // Expect initial bucket creations to each trigger immediate bucket-specific + // reports. + RateLimitQuotaUsageReports initial_report = buildReports( + std::vector{{/*allowed=*/1, /*denied=*/0, /*bucket_id=*/sample_bucket_id_}}); + EXPECT_CALL( + mock_stream_client->stream_, + sendMessageRaw_(Grpc::ProtoBufferEqIgnoreRepeatedFieldOrdering(initial_report), false)); + + cb_ptr_->expectBuckets({sample_id_hash_}); + global_client_->createBucket(sample_bucket_id_, sample_id_hash_, default_allow_action, nullptr, + std::chrono::milliseconds::zero(), true); + cb_ptr_->waitForExpectedBuckets(); + + setAtomic(1, getQuotaUsage(*buckets_tls_, sample_id_hash_)->num_requests_allowed); + + RateLimitQuotaUsageReports expected_reports = buildReports( + std::vector{{/*allowed=*/1, /*denied=*/0, /*bucket_id=*/sample_bucket_id_}}); + EXPECT_CALL( + mock_stream_client->stream_, + sendMessageRaw_(Grpc::ProtoBufferEqIgnoreRepeatedFieldOrdering(expected_reports), false)); + + mock_stream_client->timer_->invokeCallback(); + waitForNotification(cb_ptr_->report_sent); + + // Test that an invalid bucket id in a response is ignored but doesn't disrupt processing of other + // buckets. + auto empty_id_allow_action = buildBlanketAction(BucketId(), false); + auto deny_action = buildBlanketAction(sample_bucket_id_, true); + std::unique_ptr response = std::make_unique(); + response->add_bucket_action()->CopyFrom(empty_id_allow_action); + response->add_bucket_action()->CopyFrom(deny_action); + + // Mimic sending the response across the stream. + WAIT_FOR_LOG_CONTAINS("error", "Received an RLQS response, but a bucket is missing its id.", { + global_client_->onReceiveMessage(std::move(response)); + waitForNotification(cb_ptr_->response_processed); + }); + + // Expect the deny-all bucket to have made it into TLS. + std::shared_ptr deny_all_bucket = getBucket(*buckets_tls_, sample_id_hash_); + ASSERT_TRUE(deny_all_bucket->cached_action); + EXPECT_TRUE(unordered_differencer_.Equals(*deny_all_bucket->cached_action, deny_action)); +} + class LocalClientTest : public GlobalClientTest { protected: LocalClientTest() : GlobalClientTest() {} void SetUp() override { GlobalClientTest::SetUp(); - // Initialize the TLS slot. - client_tls_ = std::make_unique>( - mock_stream_client->context_.server_factory_context_.thread_local_); - // Create a ThreadLocal wrapper for the global client initialized in the - // GlobalClientTest. - auto tl_global_client = std::make_shared(global_client_); - // Set the TLS slot to return copies of the shared_ptr holding that - // ThreadLocal object. - client_tls_->set([tl_global_client](Unused) { return tl_global_client; }); - // Create the local client for testing. - local_client_ = std::make_unique(*client_tls_, *buckets_tls_); + local_client_ = std::make_unique(global_client_.get(), *buckets_tls_); } std::unique_ptr local_client_ = nullptr; - ThreadLocal::TypedSlotPtr client_tls_ = nullptr; }; TEST_F(LocalClientTest, TestLocalClient) { diff --git a/test/extensions/filters/http/rate_limit_quota/client_test_utils.h b/test/extensions/filters/http/rate_limit_quota/client_test_utils.h index c03c7c340e3aa..5743f2f3d965b 100644 --- a/test/extensions/filters/http/rate_limit_quota/client_test_utils.h +++ b/test/extensions/filters/http/rate_limit_quota/client_test_utils.h @@ -28,6 +28,14 @@ using testing::Invoke; using testing::Return; using testing::Unused; +// Async RPC clients include resetting all active streams on destruction. This +// mock extends the base mock to include mocking the reset. +class MockAsyncClientWithReset : public Grpc::MockAsyncClient { +public: + MOCK_METHOD(void, resetActiveStreams, ()); + ~MockAsyncClientWithReset() override { resetActiveStreams(); } +}; + // Used to mock a local rate limit client entirely. class MockRateLimitClient : public RateLimitClient { public: @@ -42,6 +50,20 @@ class MockRateLimitClient : public RateLimitClient { MOCK_METHOD(std::shared_ptr, getBucket, (size_t id), (override)); }; +// Support the other method of creating a RLQS streaming client. +class FakeClientFactory : public Grpc::AsyncClientFactory { +public: + FakeClientFactory(Grpc::RawAsyncClientPtr async_client) + : async_client_(std::move(async_client)) {} + ~FakeClientFactory() override = default; + absl::StatusOr createUncachedRawAsyncClient() override { + return std::move(async_client_); + } + +private: + Grpc::RawAsyncClientPtr async_client_ = nullptr; +}; + // Used when creating a "real" global rate limit client with mocked, underlying // interfaces. class RateLimitTestClient { @@ -51,10 +73,40 @@ class RateLimitTestClient { config_with_hash_key_ = Grpc::GrpcServiceConfigWithHashKey(grpc_service_); } + void expectClientReset() { + EXPECT_CALL(*async_client_, resetActiveStreams()).WillOnce([&]() { + if (stream_callbacks_ != nullptr) { + stream_.resetStream(); + } + }); + } + void expectClientCreation() { EXPECT_CALL(context_.server_factory_context_.cluster_manager_.async_client_manager_, getOrCreateRawAsyncClientWithHashKey(_, _, _)) - .WillOnce(Invoke(this, &RateLimitTestClient::mockCreateAsyncClient)); + .Times(testing::AtLeast(1)) + .WillRepeatedly(Invoke(this, &RateLimitTestClient::mockCreateAsyncClient)); + } + + void expectClientCreationWithFactory() { + EXPECT_CALL(context_.server_factory_context_.cluster_manager_.async_client_manager_, + factoryForGrpcService(_, _, _)) + .Times(testing::AtLeast(1)) + .WillRepeatedly(Invoke(this, &RateLimitTestClient::mockCreateAsyncClientFactory)); + } + + void failClientCreation() { + EXPECT_CALL(context_.server_factory_context_.cluster_manager_.async_client_manager_, + factoryForGrpcService(_, _, _)) + .Times(testing::AtLeast(1)) + .WillRepeatedly([]() { return absl::InternalError("Mock client creation failure"); }); + } + + void failClientCreationWithFactory() { + EXPECT_CALL(context_.server_factory_context_.cluster_manager_.async_client_manager_, + factoryForGrpcService(_, _, _)) + .Times(testing::AtLeast(1)) + .WillRepeatedly([]() { return absl::InternalError("Mock client creation failure"); }); } void expectStreamCreation(int times) { @@ -75,6 +127,8 @@ class RateLimitTestClient { .Times(times) .WillRepeatedly(Invoke(this, &RateLimitTestClient::mockStartRaw)); } + // The stream object is only directly reset when undergoing filter shutdown. + EXPECT_CALL(stream_, resetStream()); } // We don't know the eventual intent of each timer at creation time. Expect @@ -126,10 +180,22 @@ class RateLimitTestClient { void expectTimeSource() {} + Grpc::AsyncClientFactoryPtr mockCreateAsyncClientFactory(Unused, Unused, Unused) { + std::unique_ptr async_client = + std::make_unique(); + async_client_ = async_client.get(); + expectClientReset(); + return std::make_unique(std::move(async_client)); + } + Grpc::RawAsyncClientSharedPtr mockCreateAsyncClient(Unused, Unused, Unused) { - auto client = std::make_shared(); - async_client_ = client.get(); - return client; + if (owned_async_client_ != nullptr) { + return owned_async_client_; + } + owned_async_client_ = std::make_shared(); + async_client_ = owned_async_client_.get(); + expectClientReset(); + return owned_async_client_; } void setStreamStartToFail(int fail_starts) { fail_starts_ = fail_starts; } @@ -150,7 +216,8 @@ class RateLimitTestClient { Grpc::GrpcServiceConfigWithHashKey config_with_hash_key_; envoy::config::core::v3::GrpcService grpc_service_; - Grpc::MockAsyncClient* async_client_ = nullptr; + std::shared_ptr owned_async_client_ = nullptr; + MockAsyncClientWithReset* async_client_ = nullptr; Grpc::MockAsyncStream stream_; NiceMock stream_info_; Grpc::RawAsyncStreamCallbacks* stream_callbacks_; diff --git a/test/extensions/filters/http/rate_limit_quota/config_test.cc b/test/extensions/filters/http/rate_limit_quota/config_test.cc index 11c5f878d90fe..d2599a8cc27d4 100644 --- a/test/extensions/filters/http/rate_limit_quota/config_test.cc +++ b/test/extensions/filters/http/rate_limit_quota/config_test.cc @@ -5,6 +5,7 @@ #include "envoy/http/filter_factory.h" #include "source/extensions/filters/http/rate_limit_quota/config.h" +#include "source/extensions/filters/http/rate_limit_quota/filter_persistence.h" #include "test/extensions/filters/http/rate_limit_quota/client_test_utils.h" #include "test/mocks/http/mocks.h" @@ -56,13 +57,101 @@ TEST(RateLimitQuotaFilterConfigTest, RateLimitQuotaFilterWithCorrectProto) { // Handle the global client's creation by expecting the underlying async grpc // client creation. getOrThrow fails otherwise. auto mock_stream_client = std::make_unique(); - mock_stream_client->expectClientCreation(); + mock_stream_client->expectClientCreationWithFactory(); RateLimitQuotaFilterFactory factory; std::string stats_prefix = "test"; Http::FilterFactoryCb cb = factory.createFilterFactoryFromProtoTyped( filter_config, stats_prefix, mock_stream_client->context_); cb(filter_callback); + + GlobalTlsStores::clear(); +} + +TEST(RateLimitQuotaFilterConfigTest, RateLimitQuotaFilterWithInvalidMatcher) { + std::string filter_config_yaml = R"EOF( + rlqs_server: + envoy_grpc: + cluster_name: "rate_limit_quota_server" + domain: test + bucket_matchers: + matcher_list: + matchers: + # Assign requests with header['env'] set to 'staging' to the bucket { name: 'staging' } + predicate: + single_predicate: + input: + name: input_not_found + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value_match: + exact: 3.14 + on_match: + action: + name: rate_limit_quota + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings + bucket_id_builder: + bucket_id_builder: + "name": + string_value: "prod" + reporting_interval: 60s + )EOF"; + envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaFilterConfig filter_config; + TestUtility::loadFromYaml(filter_config_yaml, filter_config); + + // Handle the global client's creation by expecting the underlying async grpc + // client creation. getOrThrow fails otherwise. + auto mock_stream_client = std::make_unique(); + + RateLimitQuotaFilterFactory factory; + std::string stats_prefix = "test"; + EXPECT_THROW_WITH_REGEX(factory.createFilterFactoryFromProtoTyped(filter_config, stats_prefix, + mock_stream_client->context_), + EnvoyException, + "Didn't find a registered implementation.*'input_not_found'"); +} + +TEST(RateLimitQuotaFilterConfigTest, RateLimitQuotaFilterWithInvalidGrpcClient) { + std::string filter_config_yaml = R"EOF( + rlqs_server: + envoy_grpc: + cluster_name: "rate_limit_quota_server" + domain: test + bucket_matchers: + matcher_list: + matchers: + # Assign requests with header['env'] set to 'staging' to the bucket { name: 'staging' } + predicate: + single_predicate: + input: + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: environment + value_match: + exact: staging + on_match: + action: + name: rate_limit_quota + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings + bucket_id_builder: + bucket_id_builder: + "name": + string_value: "prod" + reporting_interval: 60s + )EOF"; + envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaFilterConfig filter_config; + TestUtility::loadFromYaml(filter_config_yaml, filter_config); + + auto mock_stream_client = std::make_unique(); + mock_stream_client->failClientCreationWithFactory(); + + RateLimitQuotaFilterFactory factory; + std::string stats_prefix = "test"; + EXPECT_THROW_WITH_REGEX(factory.createFilterFactoryFromProtoTyped(filter_config, stats_prefix, + mock_stream_client->context_), + EnvoyException, "Mock client creation failure"); } } // namespace diff --git a/test/extensions/filters/http/rate_limit_quota/filter_persistence_test.cc b/test/extensions/filters/http/rate_limit_quota/filter_persistence_test.cc new file mode 100644 index 0000000000000..238f0be7b55cc --- /dev/null +++ b/test/extensions/filters/http/rate_limit_quota/filter_persistence_test.cc @@ -0,0 +1,527 @@ +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/config_source.pb.h" +#include "envoy/extensions/filters/http/rate_limit_quota/v3/rate_limit_quota.pb.h" +#include "envoy/network/connection.h" +#include "envoy/service/rate_limit_quota/v3/rlqs.pb.h" + +#include "source/extensions/filters/http/rate_limit_quota/filter_persistence.h" + +#include "test/common/http/common.h" +#include "test/integration/autonomous_upstream.h" +#include "test/integration/fake_upstream.h" +#include "test/integration/http_integration.h" +#include "test/integration/integration_stream_decoder.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_cat.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RateLimitQuota { +namespace { + +using Envoy::ProtoEq; +using envoy::config::cluster::v3::Cluster; +using envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaBucketSettings; +using envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaFilterConfig; +using envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager; +using envoy::service::rate_limit_quota::v3::BucketId; +using envoy::service::rate_limit_quota::v3::RateLimitQuotaResponse; +using envoy::service::rate_limit_quota::v3::RateLimitQuotaUsageReports; +using Protobuf::util::MessageDifferencer; + +MATCHER_P2(ProtoEqIgnoringFieldAndOrdering, expected, + /* const FieldDescriptor* */ ignored_field, "") { + MessageDifferencer differencer; + ASSERT(ignored_field != nullptr, "Field to ignore not found."); + differencer.IgnoreField(ignored_field); + differencer.set_repeated_field_comparison(MessageDifferencer::AS_SET); + + if (differencer.Compare(arg, expected)) { + return true; + } + *result_listener << "Expected:\n" << expected.DebugString() << "\nActual:\n" << arg.DebugString(); + return false; +} + +static constexpr char kDefaultRateLimitQuotaFilter[] = R"EOF( +name: "envoy.filters.http.rate_limit_quota" +typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig" + rlqs_server: + envoy_grpc: + cluster_name: "rlqs_upstream_0" + domain: "test_domain" + bucket_matchers: + matcher_list: + matchers: + predicate: + single_predicate: + input: + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: environment + name: "HttpRequestHeaderMatchInput" + value_match: + exact: staging + on_match: + action: + name: rate_limit_quota + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings + bucket_id_builder: + bucket_id_builder: + "test_key_1": + string_value: "test_value_1" + "test_key_2": + string_value: "test_value_2" + no_assignment_behavior: + fallback_rate_limit: + blanket_rule: ALLOW_ALL + reporting_interval: 5s + on_no_match: + action: + name: rate_limit_quota + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings" + bucket_id_builder: + bucket_id_builder: + "on_no_match_key": + string_value: "on_no_match_value" + no_assignment_behavior: + fallback_rate_limit: + blanket_rule: ALLOW_ALL + reporting_interval: 5s +)EOF"; + +class FilterPersistenceTest : public Event::TestUsingSimulatedTime, + public HttpIntegrationTest, + public Grpc::GrpcClientIntegrationParamTest { +protected: + FilterPersistenceTest() : HttpIntegrationTest(Http::CodecType::HTTP2, ipVersion()) { + setUpstreamProtocol(Http::CodecType::HTTP2); + setUpIntegrationTest(); + } + + void setUpIntegrationTest() { + config_helper_.addConfigModifier( + [&]([[maybe_unused]] envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // Start the server with a default RLQS filter that will then be + // updated by LDS / xDS in the test cases. + config_helper_.prependFilter(kDefaultRateLimitQuotaFilter); + }); + setUpstreamProtocol(Http::CodecType::HTTP2); + setDownstreamProtocol(Http::CodecType::HTTP2); + HttpIntegrationTest::initialize(); + } + + // The RLQS upstream shouldn't be autonomous as it will handle the long-lived + // RLQS stream. + void createUpstreams() override { + setUpstreamCount(3); + + autonomous_upstream_ = true; + traffic_endpoint_ = upstream_address_fn_(0); + createUpstream(traffic_endpoint_, upstreamConfig()); + traffic_upstream_ = dynamic_cast(fake_upstreams_[0].get()); + + autonomous_upstream_ = false; + // Testing requires multiple RLQS upstream targets. + for (int i = 0; i < 2; ++i) { + FakeRlqsUpstreamRefs rlqs_upstream{}; + rlqs_upstream.rlqs_endpoint_ = upstream_address_fn_(i + 1); + createUpstream(rlqs_upstream.rlqs_endpoint_, upstreamConfig()); + rlqs_upstream.rlqs_upstream_ = fake_upstreams_[i + 1].get(); + + rlqs_upstreams_.push_back(std::move(rlqs_upstream)); + } + + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + traffic_cluster_ = bootstrap.mutable_static_resources()->mutable_clusters(0); + for (size_t i = 0; i < rlqs_upstreams_.size(); ++i) { + FakeRlqsUpstreamRefs& rlqs_upstream_refs = rlqs_upstreams_.at(i); + rlqs_upstream_refs.rlqs_cluster_ = bootstrap.mutable_static_resources()->add_clusters(); + + rlqs_upstream_refs.rlqs_cluster_->MergeFrom(bootstrap.static_resources().clusters(0)); + + rlqs_upstream_refs.rlqs_cluster_->set_name(absl::StrCat("rlqs_upstream_", i)); + } + }); + } + + void updateConfigInPlace(std::function modifier) { + // update_success starts at 1 after the initial server configuration. + test_server_->waitForCounterEq("listener_manager.lds.update_success", config_updates_ + 1); + test_server_->waitForCounterEq("listener_manager.listener_modified", config_updates_); + test_server_->waitForCounterEq("listener_manager.listener_in_place_updated", config_updates_); + + ConfigHelper new_config_helper(version_, config_helper_.bootstrap()); + new_config_helper.addConfigModifier(modifier); + new_config_helper.setLds(absl::StrCat(config_updates_ + 1)); + + test_server_->waitForCounterEq("listener_manager.lds.update_success", config_updates_ + 2); + test_server_->waitForCounterEq("listener_manager.listener_modified", config_updates_ + 1); + test_server_->waitForCounterEq("listener_manager.listener_in_place_updated", + config_updates_ + 1); + test_server_->waitForGaugeEq("listener_manager.total_filter_chains_draining", 0); + config_updates_++; + } + + void wipeFilters() { + updateConfigInPlace([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + auto* hcm_filter = listener->mutable_filter_chains(0)->mutable_filters(0); + HttpConnectionManager hcm_config; + hcm_filter->mutable_typed_config()->UnpackTo(&hcm_config); + hcm_config.clear_http_filters(); + hcm_filter->mutable_typed_config()->PackFrom(hcm_config); + }); + absl::SleepFor(absl::Seconds(1)); + EXPECT_EQ(GlobalTlsStores::size(), 0); + } + + void cleanUp() { + wipeFilters(); + for (auto& rlqs_upstream : rlqs_upstreams_) { + if (rlqs_upstream.rlqs_connection_ != nullptr) { + ASSERT_TRUE(rlqs_upstream.rlqs_connection_->close()); + ASSERT_TRUE(rlqs_upstream.rlqs_connection_->waitForDisconnect()); + } + } + cleanupUpstreamAndDownstream(); + } + + void TearDown() override { cleanUp(); } + + // Send a request through the envoy & possibly to traffic_upstream_. Returns + // the response's status code. + std::string + sendRequest(const absl::flat_hash_map* custom_headers = nullptr) { + auto codec_client = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + if (custom_headers != nullptr) { + for (auto const& pair : *custom_headers) { + headers.addCopy(pair.first, pair.second); + } + } + // Trigger responses from the autonomous upstream. + headers.addCopy(AutonomousStream::RESPOND_AFTER_REQUEST_HEADERS, "yes"); + + IntegrationStreamDecoderPtr response = codec_client->makeHeaderOnlyRequest(headers); + bool stream_ended = response->waitForEndStream(); + + codec_client->close(); + if (!stream_ended) { + return ""; + } + EXPECT_TRUE(response->complete()); + return std::string(response->headers().getStatusValue()); + } + + void expectRlqsUsageReports(int upstream_index, + const RateLimitQuotaUsageReports& expected_reports, + bool expect_new_stream = false) { + FakeRlqsUpstreamRefs& rlqs_refs = rlqs_upstreams_.at(upstream_index); + if (expect_new_stream) { + ASSERT_TRUE(rlqs_refs.rlqs_upstream_->waitForHttpConnection(*dispatcher_, + rlqs_refs.rlqs_connection_)); + ASSERT_TRUE( + rlqs_refs.rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_refs.rlqs_stream_)); + rlqs_refs.rlqs_stream_->startGrpcStream(); + } + + RateLimitQuotaUsageReports reports; + ASSERT_TRUE(rlqs_refs.rlqs_stream_->waitForGrpcMessage(*dispatcher_, reports)); + + // Ignore time_elapsed as it is often not deterministic. + const Protobuf::FieldDescriptor* time_elapsed_desc = + RateLimitQuotaUsageReports::BucketQuotaUsage::GetDescriptor()->FindFieldByName( + "time_elapsed"); + ASSERT_THAT(reports, ProtoEqIgnoringFieldAndOrdering(expected_reports, time_elapsed_desc)); + } + + // Can only be called after the RLQS stream has been established with a RLQS + // usage report. + void sendRlqsResponse(int upstream_index, const RateLimitQuotaResponse& rlqs_response) { + rlqs_upstreams_.at(upstream_index).rlqs_stream_->sendGrpcMessage(rlqs_response); + } + + void cleanupRlqsStream(int upstream_index) { + FakeRlqsUpstreamRefs& rlqs_refs = rlqs_upstreams_.at(upstream_index); + if (rlqs_refs.rlqs_connection_ != nullptr) { + ASSERT_TRUE(rlqs_refs.rlqs_connection_->close()); + ASSERT_TRUE(rlqs_refs.rlqs_connection_->waitForDisconnect()); + rlqs_refs.rlqs_connection_ = nullptr; + rlqs_refs.rlqs_stream_ = nullptr; + } + absl::SleepFor(absl::Seconds(1)); + } + + envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaFilterConfig + rlqs_filter_config_{}; + + struct FakeRlqsUpstreamRefs { + Network::Address::InstanceConstSharedPtr rlqs_endpoint_ = nullptr; + FakeUpstream* rlqs_upstream_ = nullptr; + // Each FakeUpstream can only handle 1 active stream at a time, so we just + // keep track of the most recent connection & stream. + FakeHttpConnectionPtr rlqs_connection_ = nullptr; + FakeStreamPtr rlqs_stream_ = nullptr; + Cluster* rlqs_cluster_ = nullptr; + }; + std::vector rlqs_upstreams_{}; + + Network::Address::InstanceConstSharedPtr traffic_endpoint_ = nullptr; + AutonomousUpstream* traffic_upstream_ = nullptr; + Cluster* traffic_cluster_ = nullptr; + + int config_updates_ = 0; +}; +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeferredProcessing, FilterPersistenceTest, + GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::GrpcClientIntegrationParamTest::protocolTestParamsToString); + +TEST_P(FilterPersistenceTest, TestPersistenceWithLdsUpdates) { + RateLimitQuotaUsageReports expected_reports; + TestUtility::loadFromYaml(R"EOF( +domain: "test_domain" +bucket_quota_usages: + bucket_id: + bucket: + "test_key_1": + "test_value_1" + "test_key_2": + "test_value_2" + num_requests_allowed: 1 +)EOF", + expected_reports); + // The first request should trigger an immediate usage report. The + // no-assignment behavior is ALLOW_ALL so the first request should be allowed. + absl::flat_hash_map headers = {{"environment", "staging"}}; + ASSERT_EQ(sendRequest(&headers), "200"); + expectRlqsUsageReports(0, expected_reports, true); + + RateLimitQuotaResponse rlqs_response; + TestUtility::loadFromYaml(R"EOF( +bucket_action: + bucket_id: + bucket: + "test_key_1": + "test_value_1" + "test_key_2": + "test_value_2" + quota_assignment_action: + assignment_time_to_live: + seconds: 120 + rate_limit_strategy: + blanket_rule: DENY_ALL +)EOF", + rlqs_response); + // RLQS response explicitly sets the cache to DENY_ALL. + sendRlqsResponse(0, rlqs_response); + absl::SleepFor(absl::Seconds(0.5)); + ASSERT_EQ(sendRequest(&headers), "429"); + + // Send an LDS update and make sure that the cache persisted. The cached + // DENY_ALL assignment should be hit, and the filter's new + // deny_response_settings should set the response code to 403. + updateConfigInPlace([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + + auto* hcm_filter = listener->mutable_filter_chains(0)->mutable_filters(0); + HttpConnectionManager hcm_config; + hcm_filter->mutable_typed_config()->UnpackTo(&hcm_config); + + auto* rlqs_filter = hcm_config.mutable_http_filters(0); + RateLimitQuotaFilterConfig rlqs_filter_config; + rlqs_filter->mutable_typed_config()->UnpackTo(&rlqs_filter_config); + + // Change the deny_response_settings to send 403 status codes. + auto* on_match_action = rlqs_filter_config.mutable_bucket_matchers() + ->mutable_matcher_list() + ->mutable_matchers(0) + ->mutable_on_match() + ->mutable_action(); + RateLimitQuotaBucketSettings on_match_settings; + on_match_action->mutable_typed_config()->UnpackTo(&on_match_settings); + on_match_settings.mutable_deny_response_settings()->mutable_http_status()->set_code( + envoy::type::v3::StatusCode::Forbidden); + + // Re-pack in reverse order. + on_match_action->mutable_typed_config()->PackFrom(on_match_settings); + rlqs_filter->mutable_typed_config()->PackFrom(rlqs_filter_config); + hcm_filter->mutable_typed_config()->PackFrom(hcm_config); + }); + ASSERT_EQ(sendRequest(&headers), "403"); +} + +TEST_P(FilterPersistenceTest, TestPersistenceWithLdsUpdateToNewDomain) { + RateLimitQuotaUsageReports expected_reports; + TestUtility::loadFromYaml(R"EOF( +domain: "test_domain" +bucket_quota_usages: + bucket_id: + bucket: + "test_key_1": + "test_value_1" + "test_key_2": + "test_value_2" + num_requests_allowed: 1 +)EOF", + expected_reports); + // The first request should trigger an immediate usage report. The + // no-assignment behavior is ALLOW_ALL so the first request should be allowed. + absl::flat_hash_map headers = {{"environment", "staging"}}; + ASSERT_EQ(sendRequest(&headers), "200"); + expectRlqsUsageReports(0, expected_reports, true); + + RateLimitQuotaResponse rlqs_response; + TestUtility::loadFromYaml(R"EOF( +bucket_action: + bucket_id: + bucket: + "test_key_1": + "test_value_1" + "test_key_2": + "test_value_2" + quota_assignment_action: + assignment_time_to_live: + seconds: 120 + rate_limit_strategy: + blanket_rule: DENY_ALL +)EOF", + rlqs_response); + // RLQS response explicitly sets the cache to DENY_ALL. + sendRlqsResponse(0, rlqs_response); + absl::SleepFor(absl::Seconds(0.5)); + ASSERT_EQ(sendRequest(&headers), "429"); + + // Send an LDS update with a new filter domain. A different domain or RLQS + // server target should not match to the same persistent cache, so this should + // trigger creation of a new filter factory that will create a new, global + // RLQS client, quota assignment cache, etc. + updateConfigInPlace([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + + auto* hcm_filter = listener->mutable_filter_chains(0)->mutable_filters(0); + HttpConnectionManager hcm_config; + hcm_filter->mutable_typed_config()->UnpackTo(&hcm_config); + + auto* rlqs_filter = hcm_config.mutable_http_filters(0); + RateLimitQuotaFilterConfig rlqs_filter_config; + rlqs_filter->mutable_typed_config()->UnpackTo(&rlqs_filter_config); + + // Change the filter's top-level domain. + rlqs_filter_config.set_domain("new_domain"); + + // Re-pack in reverse order. + rlqs_filter->mutable_typed_config()->PackFrom(rlqs_filter_config); + hcm_filter->mutable_typed_config()->PackFrom(hcm_config); + }); + + // With an entirely fresh quota cache state & RLQS stream again, the next + // request should be allowed & trigger an initial usage report. + // Cleanup of the previous RLQS stream is needed as the same FakeUpstream can + // only handle 1 active stream at a time. + cleanupRlqsStream(0); + ASSERT_EQ(sendRequest(&headers), "200"); + + RateLimitQuotaUsageReports expected_reports_2 = expected_reports; + expected_reports_2.set_domain("new_domain"); + expectRlqsUsageReports(0, expected_reports_2, true); + + // RLQS response explicitly sets the new cache to DENY_ALL. + sendRlqsResponse(0, rlqs_response); + absl::SleepFor(absl::Seconds(0.5)); + ASSERT_EQ(sendRequest(&headers), "429"); +} + +TEST_P(FilterPersistenceTest, TestPersistenceWithLdsUpdateToNewRlqsServer) { + RateLimitQuotaUsageReports expected_reports; + TestUtility::loadFromYaml(R"EOF( +domain: "test_domain" +bucket_quota_usages: + bucket_id: + bucket: + "test_key_1": + "test_value_1" + "test_key_2": + "test_value_2" + num_requests_allowed: 1 +)EOF", + expected_reports); + // The first request should trigger an immediate usage report. The + // no-assignment behavior is ALLOW_ALL so the first request should be allowed. + absl::flat_hash_map headers = {{"environment", "staging"}}; + ASSERT_EQ(sendRequest(&headers), "200"); + expectRlqsUsageReports(0, expected_reports, true); + + RateLimitQuotaResponse rlqs_response; + TestUtility::loadFromYaml(R"EOF( +bucket_action: + bucket_id: + bucket: + "test_key_1": + "test_value_1" + "test_key_2": + "test_value_2" + quota_assignment_action: + assignment_time_to_live: + seconds: 120 + rate_limit_strategy: + blanket_rule: DENY_ALL +)EOF", + rlqs_response); + // RLQS response explicitly sets the cache to DENY_ALL. + sendRlqsResponse(0, rlqs_response); + absl::SleepFor(absl::Seconds(0.5)); + ASSERT_EQ(sendRequest(&headers), "429"); + + // Send an LDS update with a new filter domain. A different domain or RLQS + // server target should not match to the same persistent cache, so this should + // trigger creation of a new filter factory that will create a new, global + // RLQS client, quota assignment cache, etc. + updateConfigInPlace([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + + auto* hcm_filter = listener->mutable_filter_chains(0)->mutable_filters(0); + HttpConnectionManager hcm_config; + hcm_filter->mutable_typed_config()->UnpackTo(&hcm_config); + + auto* rlqs_filter = hcm_config.mutable_http_filters(0); + RateLimitQuotaFilterConfig rlqs_filter_config; + rlqs_filter->mutable_typed_config()->UnpackTo(&rlqs_filter_config); + + // Change the filter's top-level RLQS server target. + rlqs_filter_config.mutable_rlqs_server()->mutable_envoy_grpc()->set_cluster_name( + "rlqs_upstream_1"); + + // Re-pack in reverse order. + rlqs_filter->mutable_typed_config()->PackFrom(rlqs_filter_config); + hcm_filter->mutable_typed_config()->PackFrom(hcm_config); + }); + + ASSERT_EQ(sendRequest(&headers), "200"); + + // Expect a duplicate, initial report on the new stream. + expectRlqsUsageReports(1, expected_reports, true); + + // RLQS response explicitly sets the new cache to DENY_ALL. + sendRlqsResponse(1, rlqs_response); + absl::SleepFor(absl::Seconds(0.5)); + ASSERT_EQ(sendRequest(&headers), "429"); +} + +} // namespace +} // namespace RateLimitQuota +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/rate_limit_quota/filter_test.cc b/test/extensions/filters/http/rate_limit_quota/filter_test.cc index 96cdd58b18195..50cbfb708c776 100644 --- a/test/extensions/filters/http/rate_limit_quota/filter_test.cc +++ b/test/extensions/filters/http/rate_limit_quota/filter_test.cc @@ -68,6 +68,7 @@ class FilterTest : public testing::Test { void addMatcherConfig(xds::type::matcher::v3::Matcher& matcher) { config_.mutable_bucket_matchers()->MergeFrom(matcher); match_tree_ = matcher_factory_.create(matcher)(); + EXPECT_TRUE(visitor_.errors().empty()); } void addMatcherConfig(MatcherConfigType config_type) { @@ -150,7 +151,7 @@ class FilterTest : public testing::Test { ASSERT_TRUE(match_result.ok()); // Retrieve the matched action. const RateLimitOnMatchAction* match_action = - dynamic_cast(match_result.value().get()); + dynamic_cast(match_result.value().get()); RateLimitQuotaValidationVisitor visitor = {}; // Generate the bucket ids. @@ -277,7 +278,7 @@ TEST_F(FilterTest, RequestMatchingWithInvalidOnNoMatch) { ASSERT_TRUE(match_result.ok()); // Retrieve the matched action. const RateLimitOnMatchAction* match_action = - dynamic_cast(match_result.value().get()); + dynamic_cast(match_result.value().get()); RateLimitQuotaValidationVisitor visitor = {}; // Generate the bucket ids. diff --git a/test/extensions/filters/http/rate_limit_quota/integration_test.cc b/test/extensions/filters/http/rate_limit_quota/integration_test.cc index d5585a693eeca..29bdfeca4b0eb 100644 --- a/test/extensions/filters/http/rate_limit_quota/integration_test.cc +++ b/test/extensions/filters/http/rate_limit_quota/integration_test.cc @@ -18,6 +18,7 @@ #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" #include "source/common/runtime/runtime_features.h" +#include "source/extensions/filters/http/rate_limit_quota/filter_persistence.h" #include "test/common/grpc/grpc_client_integration.h" #include "test/common/http/common.h" @@ -42,6 +43,7 @@ namespace RateLimitQuota { namespace { using Envoy::ProtoEq; +using envoy::config::cluster::v3::Cluster; using envoy::service::rate_limit_quota::v3::BucketId; using envoy::service::rate_limit_quota::v3::RateLimitQuotaResponse; using envoy::service::rate_limit_quota::v3::RateLimitQuotaUsageReports; @@ -88,43 +90,43 @@ class RateLimitQuotaIntegrationTest : public Event::TestUsingSimulatedTime, } void createUpstreams() override { - HttpIntegrationTest::createUpstreams(); - - // Create separate side stream for rate limit quota server - for (int i = 0; i < 2; ++i) { - grpc_upstreams_.push_back(&addFakeUpstream(Http::CodecType::HTTP2)); - } + setUpstreamCount(2); + + autonomous_upstream_ = true; + traffic_endpoint_ = upstream_address_fn_(0); + createUpstream(traffic_endpoint_, upstreamConfig()); + traffic_upstream_ = dynamic_cast(fake_upstreams_[0].get()); + + autonomous_upstream_ = false; + // Testing requires multiple RLQS upstream targets. + rlqs_endpoint_ = upstream_address_fn_(1); + createUpstream(rlqs_endpoint_, upstreamConfig()); + rlqs_upstream_ = fake_upstreams_[1].get(); } void initializeConfig(ConfigOption config_option = {}, const std::string& log_format = "") { config_helper_.addConfigModifier([this, config_option, log_format]( envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - // Ensure "HTTP2 with no prior knowledge." Necessary for gRPC and for - // headers - ConfigHelper::setHttp2( - *(bootstrap.mutable_static_resources()->mutable_clusters()->Mutable(0))); - // Enable access logging for testing dynamic metadata. if (!log_format.empty()) { HttpIntegrationTest::useAccessLog(log_format); } - // Clusters for ExtProc gRPC servers, starting by copying an existing - // cluster - for (size_t i = 0; i < grpc_upstreams_.size(); ++i) { - auto* server_cluster = bootstrap.mutable_static_resources()->add_clusters(); - server_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - std::string cluster_name = absl::StrCat("rlqs_server_", i); - server_cluster->set_name(cluster_name); - server_cluster->mutable_load_assignment()->set_cluster_name(cluster_name); - } + traffic_cluster_ = bootstrap.mutable_static_resources()->mutable_clusters(0); + // Ensure "HTTP2 with no prior knowledge." Necessary for gRPC and for + // headers + ConfigHelper::setHttp2(*traffic_cluster_); + + rlqs_cluster_ = bootstrap.mutable_static_resources()->add_clusters(); + rlqs_cluster_->MergeFrom(bootstrap.static_resources().clusters(0)); + rlqs_cluster_->set_name("rlqs_server"); if (config_option.valid_rlqs_server) { // Load configuration of the server from YAML and use a helper to // add a grpc_service stanza pointing to the cluster that we just // made - setGrpcService(*proto_config_.mutable_rlqs_server(), "rlqs_server_0", - grpc_upstreams_[0]->localAddress()); + setGrpcService(*proto_config_.mutable_rlqs_server(), "rlqs_server", + rlqs_upstream_->localAddress()); } else { // Set up the gRPC service with wrong cluster name and address. setGrpcService(*proto_config_.mutable_rlqs_server(), "rlqs_wrong_server", @@ -192,8 +194,8 @@ class RateLimitQuotaIntegrationTest : public Event::TestUsingSimulatedTime, // Send downstream client request. void sendClientRequest(const absl::flat_hash_map* custom_headers = nullptr) { - auto conn = makeClientConnection(lookupPort("http")); - codec_client_ = makeHttpConnection(std::move(conn)); + auto codec_client = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl headers; HttpTestUtility::addDefaultHeaders(headers); if (custom_headers != nullptr) { @@ -201,7 +203,14 @@ class RateLimitQuotaIntegrationTest : public Event::TestUsingSimulatedTime, headers.addCopy(pair.first, pair.second); } } - response_ = codec_client_->makeHeaderOnlyRequest(headers); + // Trigger responses from the autonomous upstream. + headers.addCopy(AutonomousStream::RESPOND_AFTER_REQUEST_HEADERS, "yes"); + + response_ = codec_client->makeHeaderOnlyRequest(headers); + EXPECT_TRUE(response_->waitForEndStream()); + + codec_client->close(); + EXPECT_TRUE(response_->complete()); } void cleanUp() { @@ -217,9 +226,8 @@ class RateLimitQuotaIntegrationTest : public Event::TestUsingSimulatedTime, bool expectDeniedRequest(int expected_status_code, std::vector> expected_headers = {}, std::string expected_body = "") { - if (!response_->waitForEndStream()) + if (!response_->complete()) return false; - EXPECT_TRUE(response_->complete()); EXPECT_EQ(response_->headers().getStatusValue(), absl::StrCat(expected_status_code)); // Check for expected headers & body if set. @@ -234,37 +242,28 @@ class RateLimitQuotaIntegrationTest : public Event::TestUsingSimulatedTime, if (!expected_body.empty()) { EXPECT_THAT(response_->body(), testing::StrEq(expected_body)); } - - cleanupUpstreamAndDownstream(); return true; } bool expectAllowedRequest() { - // Handle the request received by upstream. - if (!fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)) - return false; - if (!fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)) - return false; - if (!upstream_request_->waitForEndStream(*dispatcher_)) - return false; - upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); - upstream_request_->encodeData(100, true); - // Verify the response to downstream. - if (!response_->waitForEndStream()) + if (!response_->complete()) return false; - EXPECT_TRUE(response_->complete()); EXPECT_EQ(response_->headers().getStatusValue(), "200"); - - cleanupUpstreamAndDownstream(); return true; } envoy::extensions::filters::http::rate_limit_quota::v3::RateLimitQuotaFilterConfig proto_config_{}; - std::vector grpc_upstreams_; + Network::Address::InstanceConstSharedPtr traffic_endpoint_; + AutonomousUpstream* traffic_upstream_; + Cluster* traffic_cluster_; + + Network::Address::InstanceConstSharedPtr rlqs_endpoint_; + FakeUpstream* rlqs_upstream_; FakeHttpConnectionPtr rlqs_connection_; FakeStreamPtr rlqs_stream_; + Cluster* rlqs_cluster_; IntegrationStreamDecoderPtr response_; // TODO(bsurber): Implement report timing & usage aggregation based on each // bucket's reporting_interval field. Currently this is not supported and all @@ -272,6 +271,9 @@ class RateLimitQuotaIntegrationTest : public Event::TestUsingSimulatedTime, int report_interval_sec_ = 5; RateLimitStrategy deny_all_strategy; RateLimitStrategy allow_all_strategy; + + // Access to static state, needed to reset between tests. + GlobalTlsStores global_tls_stores_; }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeferredProcessing, RateLimitQuotaIntegrationTest, @@ -287,8 +289,8 @@ TEST_P(RateLimitQuotaIntegrationTest, StartFailed) { absl::flat_hash_map custom_headers = {{"environment", "staging"}, {"group", "envoy"}}; sendClientRequest(&custom_headers); - EXPECT_FALSE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_, - std::chrono::seconds(1))); + EXPECT_FALSE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_, + std::chrono::seconds(1))); } TEST_P(RateLimitQuotaIntegrationTest, BasicFlowEmptyResponse) { @@ -300,7 +302,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowEmptyResponse) { sendClientRequest(&custom_headers); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -308,23 +310,8 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowEmptyResponse) { RateLimitQuotaUsageReports reports; ASSERT_TRUE(rlqs_stream_->waitForGrpcMessage(*dispatcher_, reports)); - // Send the response from RLQS server. - RateLimitQuotaResponse rlqs_response; - // Response with empty bucket action. - rlqs_response.add_bucket_action(); - rlqs_stream_->sendGrpcMessage(rlqs_response); - - // Handle the request received by upstream. - ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); - ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); - ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); - upstream_request_->encodeData(100, true); - // Verify the response to downstream. - ASSERT_TRUE(response_->waitForEndStream()); - EXPECT_TRUE(response_->complete()); - EXPECT_EQ(response_->headers().getStatusValue(), "200"); + ASSERT_TRUE(expectAllowedRequest()); } TEST_P(RateLimitQuotaIntegrationTest, BasicFlowResponseNotMatched) { @@ -336,7 +323,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowResponseNotMatched) { sendClientRequest(&custom_headers); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -353,16 +340,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowResponseNotMatched) { rlqs_stream_->sendGrpcMessage(rlqs_response); // Handle the request received by upstream. - ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); - ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); - ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); - upstream_request_->encodeData(100, true); - - // Verify the response to downstream. - ASSERT_TRUE(response_->waitForEndStream()); - EXPECT_TRUE(response_->complete()); - EXPECT_EQ(response_->headers().getStatusValue(), "200"); + expectAllowedRequest(); } TEST_P(RateLimitQuotaIntegrationTest, BasicFlowResponseMatched) { @@ -374,7 +352,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowResponseMatched) { sendClientRequest(&custom_headers); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); // Handle the request received by upstream. @@ -408,7 +386,7 @@ TEST_P(RateLimitQuotaIntegrationTest, TestBasicMetadataLogging) { sendClientRequest(&custom_headers); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Wait for the first usage reports. @@ -428,16 +406,7 @@ TEST_P(RateLimitQuotaIntegrationTest, TestBasicMetadataLogging) { rlqs_stream_->sendGrpcMessage(rlqs_response); // Handle the request received by upstream. - ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); - ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); - ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); - upstream_request_->encodeData(100, true); - - // Verify the response to downstream. - ASSERT_TRUE(response_->waitForEndStream()); - EXPECT_TRUE(response_->complete()); - EXPECT_EQ(response_->headers().getStatusValue(), "200"); + ASSERT_TRUE(expectAllowedRequest()); std::string log_output0 = HttpIntegrationTest::waitForAccessLog(HttpIntegrationTest::access_log_name_, 0, true); @@ -464,7 +433,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowMultiSameRequest) { // the cache. if (i == 0) { // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -489,17 +458,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowMultiSameRequest) { // Send the response from RLQS server. rlqs_stream_->sendGrpcMessage(rlqs_response); } - // Handle the request received by upstream. - ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); - ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); - ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); - upstream_request_->encodeData(100, true); - - // Verify the response to downstream. - ASSERT_TRUE(response_->waitForEndStream()); - EXPECT_TRUE(response_->complete()); - EXPECT_EQ(response_->headers().getStatusValue(), "200"); + ASSERT_TRUE(expectAllowedRequest()); cleanUp(); } @@ -525,7 +484,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowMultiDifferentRequest) { // Expect a stream to open to the RLQS server with the first filter hit. if (i == 0) { // Start the gRPC stream to RLQS server on the first request. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); } @@ -587,7 +546,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestNoAssignmentDenyAll) { if (i == 0) { // Start the gRPC stream to RLQS server on the first request. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -642,7 +601,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestNoAssignmentDenyAllWithSet if (i == 0) { // Start the gRPC stream to RLQS server on the first request. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -697,7 +656,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestNoAssignmentDenyAllWithEmp if (i == 0) { // Start the gRPC stream to RLQS server on the first request. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -767,7 +726,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiDifferentRequestNoAssignementAllowAll if (i == 0) { // Start the gRPC stream to RLQS server on the first reports. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); } @@ -834,7 +793,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiDifferentRequestNoAssignementDenyAll) if (i == 0) { // Start the gRPC stream to RLQS server on the first reports. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); } @@ -873,7 +832,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowPeriodicalReport) { sendClientRequest(&custom_headers); // Expect the RLQS stream to start. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // No-assignment behavior dictates that initial traffic should be allowed. @@ -957,7 +916,7 @@ TEST_P(RateLimitQuotaIntegrationTest, BasicFlowPeriodicalReportWithStreamClosed) WAIT_FOR_LOG_CONTAINS("debug", "RLQS buckets cache written to TLS.", { sendClientRequest(&custom_headers); }); - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when traffic first hits the RLQS bucket. @@ -1067,7 +1026,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiRequestWithTokenBucketThrottling) { // server as the subsequent requests will find the entry in the cache. if (i == 0) { // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); ASSERT_TRUE(expectAllowedRequest()); @@ -1135,7 +1094,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiRequestWithTokenBucketExpiration) { sendClientRequest(&custom_headers); // Start the gRPC stream to RLQS server on the first request. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); rlqs_stream_->startGrpcStream(); @@ -1219,7 +1178,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiRequestWithTokenBucketReplacement) { sendClientRequest(&custom_headers); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1320,7 +1279,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiRequestWithUnsupportedStrategy) { // server as the subsequent requests will find the entry in the cache. if (i == 0) { // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1370,7 +1329,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiRequestWithUnsetStrategy) { // server as the subsequent requests will find the entry in the cache. if (i == 0) { // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1412,7 +1371,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiRequestWithUnsupportedDefaultAction) EXPECT_TRUE(expectAllowedRequest()); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1447,7 +1406,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestWithExpiredAssignmentDeny) { sendClientRequest(&custom_headers); }); // Start the gRPC stream to RLQS server. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1509,7 +1468,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestWithExpiredAssignmentAllow // 1st request will start the gRPC stream. if (i == 0) { // Start the gRPC stream to RLQS server on the first request. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1575,7 +1534,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestWithExpirationToDefaultDen expectAllowedRequest(); if (i == 0) { // Expect a gRPC stream to the RLQS server opened with the first bucket. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1634,7 +1593,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestWithExpirationWithoutFallb if (i == 0) { // Start the gRPC stream to RLQS server & send the initial report. - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. @@ -1681,7 +1640,7 @@ TEST_P(RateLimitQuotaIntegrationTest, MultiSameRequestWithAbandonAction) { // Send first request & expect a new RLQS stream. sendClientRequest(&custom_headers); - ASSERT_TRUE(grpc_upstreams_[0]->waitForHttpConnection(*dispatcher_, rlqs_connection_)); + ASSERT_TRUE(rlqs_upstream_->waitForHttpConnection(*dispatcher_, rlqs_connection_)); ASSERT_TRUE(rlqs_connection_->waitForNewStream(*dispatcher_, rlqs_stream_)); // Expect an initial report when the RLQS bucket is first hit. diff --git a/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc b/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc index 5b84f13822385..41e303ae23f08 100644 --- a/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc +++ b/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc @@ -286,8 +286,8 @@ TEST_P(RatelimitIntegrationTest, OverLimit) { waitForFailedUpstreamResponse(429, 0); EXPECT_THAT(responses_[0].get()->headers(), - Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, - Http::Headers::get().EnvoyRateLimitedValues.True)); + ContainsHeader(Http::Headers::get().EnvoyRateLimited, + Http::Headers::get().EnvoyRateLimitedValues.True)); cleanup(); @@ -313,8 +313,8 @@ TEST_P(RatelimitIntegrationTest, OverLimitWithHeaders) { }); EXPECT_THAT(responses_[0].get()->headers(), - Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, - Http::Headers::get().EnvoyRateLimitedValues.True)); + ContainsHeader(Http::Headers::get().EnvoyRateLimited, + Http::Headers::get().EnvoyRateLimitedValues.True)); cleanup(); @@ -412,17 +412,17 @@ TEST_P(RatelimitFilterHeadersEnabledIntegrationTest, OkWithFilterHeaders) { EXPECT_THAT( responses_[0].get()->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitLimit, "1, 1;w=60;name=\"first\", 4;w=3600;name=\"second\"")); EXPECT_THAT( responses_[0].get()->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitRemaining, "2")); EXPECT_THAT( responses_[0].get()->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitReset, "3")); @@ -449,17 +449,17 @@ TEST_P(RatelimitFilterHeadersEnabledIntegrationTest, OverLimitWithFilterHeaders) EXPECT_THAT( responses_[0].get()->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitLimit, "1, 1;w=60;name=\"first\", 4;w=3600;name=\"second\"")); EXPECT_THAT( responses_[0].get()->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitRemaining, "2")); EXPECT_THAT( responses_[0].get()->headers(), - Http::HeaderValueOf( + ContainsHeader( Extensions::HttpFilters::Common::RateLimit::XRateLimitHeaders::get().XRateLimitReset, "3")); @@ -479,7 +479,7 @@ TEST_P(RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest, waitForFailedUpstreamResponse(429, 0); EXPECT_THAT(responses_[0].get()->headers(), - ::testing::Not(Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, _))); + ::testing::Not(ContainsHeader(Http::Headers::get().EnvoyRateLimited, _))); cleanup(); @@ -526,10 +526,10 @@ TEST_P(RatelimitIntegrationTest, OverLimitResponseHeadersToAdd) { waitForFailedUpstreamResponse(429, 0); EXPECT_THAT(responses_[0].get()->headers(), - Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, - Http::Headers::get().EnvoyRateLimitedValues.True)); + ContainsHeader(Http::Headers::get().EnvoyRateLimited, + Http::Headers::get().EnvoyRateLimitedValues.True)); EXPECT_THAT(responses_[0].get()->headers(), - Http::HeaderValueOf("x-global-ratelimit-service", "rate_limit_service")); + ContainsHeader("x-global-ratelimit-service", "rate_limit_service")); cleanup(); EXPECT_EQ(nullptr, test_server_->counter("cluster.cluster_0.ratelimit.ok")); diff --git a/test/extensions/filters/http/ratelimit/ratelimit_test.cc b/test/extensions/filters/http/ratelimit/ratelimit_test.cc index 53bdc75f661e4..513b2c1f8c8f9 100644 --- a/test/extensions/filters/http/ratelimit/ratelimit_test.cc +++ b/test/extensions/filters/http/ratelimit/ratelimit_test.cc @@ -62,7 +62,7 @@ class HttpRateLimitFilterTest : public testing::Test { auto status = absl::OkStatus(); config_ = std::make_shared( proto_config, factory_context_.local_info_, *factory_context_.store_.rootScope(), - factory_context_.runtime_loader_, factory_context_.http_context_, status); + factory_context_.runtime_loader_, factory_context_, status); EXPECT_TRUE(status.ok()); client_ = new Filters::Common::RateLimit::MockClient(); @@ -177,6 +177,19 @@ class HttpRateLimitFilterTest : public testing::Test { denominator: HUNDRED )EOF"; + const std::string inlined_rate_limit_actions_config_ = R"EOF( + domain: "bar" + rate_limits: + - actions: + - request_headers: + header_name: "x-header-name" + descriptor_key: "header-name" + - actions: + - generic_key: + descriptor_value: "generic-key" + apply_on_stream_done: true + )EOF"; + Filters::Common::RateLimit::MockClient* client_; NiceMock filter_callbacks_; Stats::StatNamePool pool_{filter_callbacks_.clusterInfo()->statsScope().symbolTable()}; @@ -410,6 +423,7 @@ TEST_F(HttpRateLimitFilterTest, OkResponseWithAdditionalHitsAddend) { WithArgs<0>(Invoke([&](Filters::Common::RateLimit::RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + EXPECT_CALL(*client_, detach()); filter_->onDestroy(); request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OK, nullptr, nullptr, nullptr, "", nullptr); @@ -758,13 +772,13 @@ TEST_F(HttpRateLimitFilterTest, LimitResponseWithDynamicMetadata) { filter_->decodeHeaders(request_headers_, false)); Filters::Common::RateLimit::DynamicMetadataPtr dynamic_metadata = - std::make_unique(); + std::make_unique(); auto* fields = dynamic_metadata->mutable_fields(); (*fields)["name"] = ValueUtil::stringValue("my-limit"); (*fields)["x"] = ValueUtil::numberValue(3); EXPECT_CALL(filter_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce(Invoke([&dynamic_metadata](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.http.ratelimit"); EXPECT_TRUE(TestUtility::protoEqual(returned_dynamic_metadata, *dynamic_metadata)); })); @@ -2025,6 +2039,7 @@ TEST_F(HttpRateLimitFilterTest, PerRouteRateLimitsAndOnStreamDone) { EXPECT_EQ("header-value", descriptors[0].entries_[0].value_); EXPECT_EQ(789, descriptors[0].hits_addend_.value()); })); + EXPECT_CALL(*client_, detach()); filter_->onDestroy(); request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OK, nullptr, std::make_unique(), nullptr, "", @@ -2110,6 +2125,162 @@ TEST_F(HttpRateLimitFilterTest, FailureModeHundredPercentFailsClose) { .value()); } +TEST_F(HttpRateLimitFilterTest, InlinedRateLimitAction) { + setUpTest(inlined_rate_limit_actions_config_); + request_headers_.addCopy("x-header-name", "header-value"); + + EXPECT_CALL(*client_, limit(_, _, _, _, _, 0)) + .WillOnce(Invoke( + [this](Filters::Common::RateLimit::RequestCallbacks& callbacks, const std::string& domain, + const std::vector& descriptors, Tracing::Span&, + OptRef, uint32_t) -> void { + request_callbacks_ = &callbacks; + EXPECT_EQ("bar", domain); + EXPECT_EQ(1, descriptors.size()); + EXPECT_EQ("header-name", descriptors[0].entries_[0].key_); + EXPECT_EQ("header-value", descriptors[0].entries_[0].value_); + })); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_CALL(filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::CoreResponseFlag::RateLimited)); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "429"}, + {"x-envoy-ratelimited", Http::Headers::get().EnvoyRateLimitedValues.True}}; + EXPECT_CALL(filter_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0); + + request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OverLimit, nullptr, + std::make_unique(), nullptr, "", + nullptr); + + EXPECT_EQ(1U, filter_callbacks_.clusterInfo() + ->statsScope() + .counterFromStatName(ratelimit_over_limit_) + .value()); + + EXPECT_EQ( + 1U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_4xx_).value()); + EXPECT_EQ( + 1U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value()); + EXPECT_EQ("request_rate_limited", filter_callbacks_.details()); +} + +TEST_F(HttpRateLimitFilterTest, PerRouteOverridesInlinedRateLimit) { + const std::string route_config_yaml = R"EOF( + domain: "foo" + rate_limits: + - actions: + - request_headers: + header_name: "x-header-name-route" + descriptor_key: "header-name-route" + )EOF"; + setUpTest(inlined_rate_limit_actions_config_, route_config_yaml); + request_headers_.addCopy("x-header-name-route", "header-value"); + + EXPECT_CALL(*client_, limit(_, _, _, _, _, 0)) + .WillOnce(Invoke( + [this](Filters::Common::RateLimit::RequestCallbacks& callbacks, const std::string& domain, + const std::vector& descriptors, Tracing::Span&, + OptRef, uint32_t) -> void { + request_callbacks_ = &callbacks; + EXPECT_EQ("foo", domain); + EXPECT_EQ(1, descriptors.size()); + EXPECT_EQ("header-name-route", descriptors[0].entries_[0].key_); + EXPECT_EQ("header-value", descriptors[0].entries_[0].value_); + })); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_CALL(filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::CoreResponseFlag::RateLimited)); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "429"}, + {"x-envoy-ratelimited", Http::Headers::get().EnvoyRateLimitedValues.True}}; + EXPECT_CALL(filter_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0); + + request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OverLimit, nullptr, + std::make_unique(), nullptr, "", + nullptr); + + EXPECT_EQ(1U, filter_callbacks_.clusterInfo() + ->statsScope() + .counterFromStatName(ratelimit_over_limit_) + .value()); + + EXPECT_EQ( + 1U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_4xx_).value()); + EXPECT_EQ( + 1U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value()); + EXPECT_EQ("request_rate_limited", filter_callbacks_.details()); +} + +TEST_F(HttpRateLimitFilterTest, InlinedRateLimitActionOnStreamDone) { + setUpTest(inlined_rate_limit_actions_config_); + request_headers_.addCopy("x-header-name", "header-value"); + + EXPECT_CALL(*client_, limit(_, _, _, _, _, 0)) + .WillOnce(Invoke( + [this](Filters::Common::RateLimit::RequestCallbacks& callbacks, const std::string& domain, + const std::vector& descriptors, Tracing::Span&, + OptRef, uint32_t) -> void { + request_callbacks_ = &callbacks; + EXPECT_EQ("bar", domain); + EXPECT_EQ(1, descriptors.size()); + EXPECT_EQ("header-name", descriptors[0].entries_[0].key_); + EXPECT_EQ("header-value", descriptors[0].entries_[0].value_); + })); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_CALL(filter_callbacks_, continueDecoding()); + + request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OK, nullptr, + std::make_unique(), nullptr, "", + nullptr); + + EXPECT_CALL(*client_, limit(_, _, _, _, _, 0)) + .WillOnce(Invoke( + [this](Filters::Common::RateLimit::RequestCallbacks& callbacks, const std::string& domain, + const std::vector& descriptors, Tracing::Span&, + OptRef, uint32_t) -> void { + request_callbacks_ = &callbacks; + EXPECT_EQ("bar", domain); + EXPECT_EQ(1, descriptors.size()); + EXPECT_EQ("generic_key", descriptors[0].entries_[0].key_); + EXPECT_EQ("generic-key", descriptors[0].entries_[0].value_); + })); + EXPECT_CALL(*client_, detach()); + filter_->onDestroy(); + + request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OK, nullptr, + std::make_unique(), nullptr, "", + nullptr); + + EXPECT_EQ(0U, filter_callbacks_.clusterInfo() + ->statsScope() + .counterFromStatName(ratelimit_over_limit_) + .value()); + + EXPECT_EQ( + 0U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_4xx_).value()); + EXPECT_EQ( + 0U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value()); +} + } // namespace } // namespace RateLimitFilter } // namespace HttpFilters diff --git a/test/extensions/filters/http/rbac/BUILD b/test/extensions/filters/http/rbac/BUILD index 1936c757eaf73..b83daa7ba2a3a 100644 --- a/test/extensions/filters/http/rbac/BUILD +++ b/test/extensions/filters/http/rbac/BUILD @@ -36,8 +36,8 @@ envoy_extension_cc_test( rbe_pool = "6gig", tags = ["skip_on_windows"], deps = [ + "//source/extensions/common/matcher:ip_range_matcher_lib", "//source/extensions/common/matcher:matcher_lib", - "//source/extensions/common/matcher:trie_matcher_lib", "//source/extensions/filters/common/rbac:utility_lib", "//source/extensions/filters/http/rbac:rbac_filter_lib", "//source/extensions/matching/http/cel_input:cel_input_lib", @@ -67,8 +67,8 @@ envoy_extension_cc_test( tags = ["skip_on_windows"], deps = [ "//source/extensions/clusters/dynamic_forward_proxy:cluster", + "//source/extensions/common/matcher:ip_range_matcher_lib", "//source/extensions/common/matcher:matcher_lib", - "//source/extensions/common/matcher:trie_matcher_lib", "//source/extensions/filters/http/dynamic_forward_proxy:config", "//source/extensions/filters/http/header_to_metadata:config", "//source/extensions/filters/http/rbac:config", @@ -78,6 +78,7 @@ envoy_extension_cc_test( "//source/extensions/matching/input_matchers/cel_matcher:config", "//source/extensions/matching/input_matchers/ip:config", "//source/extensions/matching/network/common:inputs_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/config:utility_lib", "//test/integration:http_protocol_integration_lib", "@envoy_api//envoy/extensions/filters/http/rbac/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc b/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc index 10829ce779f47..9cf8df7c3dfc4 100644 --- a/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc +++ b/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc @@ -2,6 +2,7 @@ #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "source/common/protobuf/utility.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/integration/http_protocol_integration.h" @@ -747,12 +748,12 @@ TEST_P(RBACIntegrationTest, RouteMetadataMatcherAllow) { baz: bat )EOF"; - ProtobufWkt::Struct value; + Protobuf::Struct value; TestUtility::loadFromYaml(yaml, value); auto default_route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); default_route->mutable_metadata()->mutable_filter_metadata()->insert( - Protobuf::MapPair(key, value)); + Protobuf::MapPair(key, value)); }); initialize(); @@ -791,12 +792,12 @@ TEST_P(RBACIntegrationTest, RouteMetadataMatcherDeny) { foo: baz )EOF"; - ProtobufWkt::Struct value; + Protobuf::Struct value; TestUtility::loadFromYaml(yaml, value); auto default_route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); default_route->mutable_metadata()->mutable_filter_metadata()->insert( - Protobuf::MapPair(key, value)); + Protobuf::MapPair(key, value)); }); initialize(); @@ -833,12 +834,12 @@ TEST_P(RBACIntegrationTest, DEPRECATED_FEATURE_TEST(DynamicMetadataMatcherAllow) baz: bat )EOF"; - ProtobufWkt::Struct value; + Protobuf::Struct value; TestUtility::loadFromYaml(yaml, value); auto default_route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); default_route->mutable_metadata()->mutable_filter_metadata()->insert( - Protobuf::MapPair(key, value)); + Protobuf::MapPair(key, value)); }); initialize(); @@ -877,12 +878,12 @@ TEST_P(RBACIntegrationTest, DynamicMetadataMatcherDeny) { foo: baz )EOF"; - ProtobufWkt::Struct value; + Protobuf::Struct value; TestUtility::loadFromYaml(yaml, value); auto default_route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); default_route->mutable_metadata()->mutable_filter_metadata()->insert( - Protobuf::MapPair(key, value)); + Protobuf::MapPair(key, value)); }); initialize(); @@ -1594,6 +1595,10 @@ name: dynamic_forward_proxy dns_cache_config: name: foo dns_lookup_family: {} + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig )EOF", save_upstream_config, Network::Test::ipVersionToDnsFamily(GetParam())); @@ -1636,6 +1641,10 @@ name: envoy.clusters.dynamic_forward_proxy dns_cache_config: name: foo dns_lookup_family: {} + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig )EOF", Network::Test::ipVersionToDnsFamily(GetParam())); diff --git a/test/extensions/filters/http/rbac/rbac_filter_test.cc b/test/extensions/filters/http/rbac/rbac_filter_test.cc index 4668f7394725d..cb1a1c28a1e3a 100644 --- a/test/extensions/filters/http/rbac/rbac_filter_test.cc +++ b/test/extensions/filters/http/rbac/rbac_filter_test.cc @@ -296,17 +296,17 @@ class RoleBasedAccessControlFilterTest : public testing::Test { void setMetadata() { ON_CALL(req_info_, setDynamicMetadata("envoy.filters.http.rbac", _)) - .WillByDefault(Invoke([this](const std::string&, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([this](const std::string&, const Protobuf::Struct& obj) { req_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair("envoy.filters.http.rbac", obj)); + Protobuf::MapPair("envoy.filters.http.rbac", obj)); })); ON_CALL(req_info_, setDynamicMetadata( Filters::Common::RBAC::DynamicMetadataKeysSingleton::get().CommonNamespace, _)) - .WillByDefault(Invoke([this](const std::string&, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([this](const std::string&, const Protobuf::Struct& obj) { req_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair( + Protobuf::MapPair( Filters::Common::RBAC::DynamicMetadataKeysSingleton::get().CommonNamespace, obj)); })); } diff --git a/test/extensions/filters/http/reverse_conn/BUILD b/test/extensions/filters/http/reverse_conn/BUILD new file mode 100644 index 0000000000000..47fdc349e5408 --- /dev/null +++ b/test/extensions/filters/http/reverse_conn/BUILD @@ -0,0 +1,32 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "reverse_conn_filter_test", + size = "medium", + srcs = ["reverse_conn_filter_test.cc"], + deps = [ + "//envoy/network:connection_interface", + "//source/common/buffer:buffer_lib", + "//source/common/http:message_lib", + "//source/common/network:address_lib", + "//source/common/network:connection_lib", + "//source/common/network:socket_interface_lib", + "//source/common/thread_local:thread_local_lib", + "//source/extensions/filters/http/reverse_conn:reverse_conn_filter_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/reverse_conn/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/reverse_conn/reverse_conn_filter_test.cc b/test/extensions/filters/http/reverse_conn/reverse_conn_filter_test.cc new file mode 100644 index 0000000000000..2313c1290d7b6 --- /dev/null +++ b/test/extensions/filters/http/reverse_conn/reverse_conn_filter_test.cc @@ -0,0 +1,1366 @@ +#include "envoy/common/optref.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/filters/http/reverse_conn/v3/reverse_conn.pb.h" +#include "envoy/network/connection.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/utility.h" +#include "source/common/http/message_impl.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/connection_impl.h" +#include "source/common/network/socket_interface.h" +#include "source/common/network/socket_interface_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_connection_handshake.pb.h" +#include "source/extensions/filters/http/reverse_conn/reverse_conn_filter.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/ssl/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/logging.h" +#include "test/test_common/test_runtime.h" + +// Include reverse connection components for testing +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator.h" +#include "source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/reverse_tunnel_initiator_extension.h" +#include "source/common/thread_local/thread_local_impl.h" + +// Add namespace alias for convenience +namespace ReverseConnection = Envoy::Extensions::Bootstrap::ReverseConnection; + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::ByMove; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ReverseConn { + +class ReverseConnFilterTest : public testing::Test { +protected: + void SetUp() override { + // Initialize stats scope + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + + // Set up the mock context + EXPECT_CALL(context_, threadLocal()).WillRepeatedly(ReturnRef(thread_local_)); + EXPECT_CALL(context_, scope()).WillRepeatedly(ReturnRef(*stats_scope_)); + EXPECT_CALL(context_, clusterManager()).WillRepeatedly(ReturnRef(cluster_manager_)); + + // Set up the mock callbacks + EXPECT_CALL(callbacks_, connection()) + .WillRepeatedly(Return(OptRef{connection_})); + EXPECT_CALL(callbacks_, streamInfo()).WillRepeatedly(ReturnRef(stream_info_)); + EXPECT_CALL(stream_info_, dynamicMetadata()).WillRepeatedly(ReturnRef(metadata_)); + + // // Create the configs + // upstream_config_.set_stat_prefix("test_prefix"); + // downstream_config_.set_stat_prefix("test_prefix"); + } + + // Helper method to set up upstream extension only + void setupUpstreamExtension() { + // Create the upstream socket interface and extension + upstream_socket_interface_ = + std::make_unique(context_); + upstream_extension_ = std::make_unique( + *upstream_socket_interface_, context_, upstream_config_); + + // Set up the extension in the global socket interface registry + auto* registered_upstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (registered_upstream_interface) { + auto* registered_acceptor = dynamic_cast( + const_cast(registered_upstream_interface)); + if (registered_acceptor) { + // Set up the extension for the registered upstream socket interface + registered_acceptor->extension_ = upstream_extension_.get(); + } + } + } + + // Helper method to set up downstream extension only + void setupDownstreamExtension() { + // Create the downstream socket interface and extension + downstream_socket_interface_ = + std::make_unique(context_); + downstream_extension_ = std::make_unique( + context_, downstream_config_); + + // Set up the extension in the global socket interface registry + auto* registered_downstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.downstream_socket_interface"); + if (registered_downstream_interface) { + auto* registered_initiator = dynamic_cast( + const_cast(registered_downstream_interface)); + if (registered_initiator) { + // Set up the extension for the registered downstream socket interface + registered_initiator->extension_ = downstream_extension_.get(); + } + } + } + + // Helper method to set up both upstream and downstream extensions + void setupExtensions() { + setupUpstreamExtension(); + setupDownstreamExtension(); + } + + // Helper function to create a filter with default config + std::unique_ptr createFilter() { + envoy::extensions::filters::http::reverse_conn::v3::ReverseConn config; + config.mutable_ping_interval()->set_value(5); // 5 seconds + auto filter_config = std::make_shared(config); + auto filter = std::make_unique(filter_config); + filter->setDecoderFilterCallbacks(callbacks_); + return filter; + } + + // Helper function to create a filter with custom config + std::unique_ptr createFilterWithConfig(uint32_t ping_interval) { + envoy::extensions::filters::http::reverse_conn::v3::ReverseConn config; + config.mutable_ping_interval()->set_value(ping_interval); + auto filter_config = std::make_shared(config); + auto filter = std::make_unique(filter_config); + filter->setDecoderFilterCallbacks(callbacks_); + return filter; + } + + // Helper function to create HTTP headers + Http::TestRequestHeaderMapImpl createHeaders(const std::string& method, const std::string& path) { + Http::TestRequestHeaderMapImpl headers; + headers.setMethod(method); + headers.setPath(path); + headers.setHost("example.com"); + return headers; + } + + // Helper function to create reverse connection request headers + Http::TestRequestHeaderMapImpl + createReverseConnectionRequestHeaders(uint32_t content_length = 100) { + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength(content_length); + return headers; + } + + // Helper function to create reverse connection info request headers + Http::TestRequestHeaderMapImpl createReverseConnectionInfoHeaders(const std::string& role = "") { + auto headers = createHeaders("GET", "/reverse_connections"); + if (!role.empty()) { + headers.addCopy(Http::LowerCaseString("role"), role); + } + return headers; + } + + // Helper function to test the private matchRequestPath method + bool testMatchRequestPath(ReverseConnFilter* filter, const std::string& request_path, + const std::string& api_path) { + // Use the friend class access to call the private method + return filter->matchRequestPath(request_path, api_path); + } + + // Helper functions to call private methods in ReverseConnFilter + ReverseConnection::UpstreamSocketManager* + testGetUpstreamSocketManager(ReverseConnFilter* filter) { + return filter->getUpstreamSocketManager(); + } + + const ReverseConnection::ReverseTunnelInitiator* + testGetDownstreamSocketInterface(ReverseConnFilter* filter) { + return filter->getDownstreamSocketInterface(); + } + + ReverseConnection::ReverseTunnelAcceptorExtension* + testGetUpstreamSocketInterfaceExtension(ReverseConnFilter* filter) { + return filter->getUpstreamSocketInterfaceExtension(); + } + + ReverseConnection::ReverseTunnelInitiatorExtension* + testGetDownstreamSocketInterfaceExtension(ReverseConnFilter* filter) { + return filter->getDownstreamSocketInterfaceExtension(); + } + + // Helper function to call the private saveDownstreamConnection method + void testSaveDownstreamConnection(ReverseConnFilter* filter, Network::Connection& connection, + const std::string& node_id, const std::string& cluster_id) { + filter->saveDownstreamConnection(connection, node_id, cluster_id); + } + + // Helper function to test the private getQueryParam method + std::string testGetQueryParam(ReverseConnFilter* filter, const std::string& key) { + // Call the private method using friend class access + return filter->getQueryParam(key); + } + + // Helper function to test the private determineRole method + std::string testDetermineRole(ReverseConnFilter* filter) { return filter->determineRole(); } + + // Helper function to create a protobuf handshake argument + std::string createHandshakeArg(const std::string& tenant_uuid, const std::string& cluster_uuid, + const std::string& node_uuid) { + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface:: + ReverseConnHandshakeArg arg; + arg.set_tenant_uuid(tenant_uuid); + arg.set_cluster_uuid(cluster_uuid); + arg.set_node_uuid(node_uuid); + return arg.SerializeAsString(); + } + + // Helper method to set up upstream thread local slot for testing + void setupUpstreamThreadLocalSlot() { + // Call onServerInitialized to set up the extension references properly + upstream_extension_->onServerInitialized(); + + // Create a thread local registry for upstream with the properly initialized extension + upstream_thread_local_registry_ = + std::make_shared(dispatcher_, + upstream_extension_.get()); + + upstream_tls_slot_ = + ThreadLocal::TypedSlot::makeUnique( + thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the upstream slot to return our registry + upstream_tls_slot_->set( + [registry = upstream_thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Override the TLS slot with our test version + upstream_extension_->setTestOnlyTLSRegistry(std::move(upstream_tls_slot_)); + } + + // Helper method to set up downstream thread local slot for testing + void setupDownstreamThreadLocalSlot() { + // Call onServerInitialized to set up the extension references properly + downstream_extension_->onServerInitialized(); + + // Create a thread local registry for downstream with the dispatcher + downstream_thread_local_registry_ = + std::make_shared(dispatcher_, + *stats_scope_); + + downstream_tls_slot_ = + ThreadLocal::TypedSlot::makeUnique( + thread_local_); + + // Set up the downstream slot to return our registry + downstream_tls_slot_->set( + [registry = downstream_thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Override the TLS slot with our test version + downstream_extension_->setTestOnlyTLSRegistry(std::move(downstream_tls_slot_)); + } + + // Helper method to set up thread local slot for testing + void setupThreadLocalSlot() { + setupUpstreamThreadLocalSlot(); + setupDownstreamThreadLocalSlot(); + } + + NiceMock context_; + NiceMock thread_local_; + NiceMock cluster_manager_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock callbacks_; + NiceMock connection_; + NiceMock socket_; + NiceMock io_handle_; + NiceMock stream_info_; + envoy::config::core::v3::Metadata metadata_; + NiceMock dispatcher_{"worker_0"}; + + // Mock socket for testing + std::unique_ptr mock_socket_; + std::unique_ptr> mock_io_handle_; + + // Helper method to set up socket mock with proper expectations for tests + void setupSocketMock(bool expect_duplicate = true) { + // Create a mock socket that inherits from ConnectionSocket + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle_ = std::make_unique>(); + + // Set up IO handle expectations + EXPECT_CALL(*mock_io_handle_, fdDoNotUse()).WillRepeatedly(Return(123)); + EXPECT_CALL(*mock_io_handle_, isOpen()).WillRepeatedly(Return(true)); + + // Only expect duplicate() if the socket will actually be used + if (expect_duplicate) { + EXPECT_CALL(*mock_io_handle_, duplicate()).WillOnce(Invoke([&]() { + auto duplicated_handle = std::make_unique>(); + EXPECT_CALL(*duplicated_handle, fdDoNotUse()).WillRepeatedly(Return(124)); + EXPECT_CALL(*duplicated_handle, isOpen()).WillRepeatedly(Return(true)); + EXPECT_CALL(*duplicated_handle, resetFileEvents()); + return duplicated_handle; + })); + } + + // Set up socket expectations + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(true)); + + // Store the mock_io_handle in the socket + mock_socket_ptr->io_handle_ = std::move(mock_io_handle_); + + // Cast the mock to the base ConnectionSocket type and store it + mock_socket_ = std::unique_ptr(mock_socket_ptr.release()); + + // Set up connection to return the socket + EXPECT_CALL(connection_, getSocket()).WillRepeatedly(ReturnRef(mock_socket_)); + } + + // Thread local components for testing upstream socket manager + std::unique_ptr> + upstream_tls_slot_; + std::shared_ptr upstream_thread_local_registry_; + std::unique_ptr upstream_socket_interface_; + std::unique_ptr upstream_extension_; + + std::unique_ptr> + downstream_tls_slot_; + std::shared_ptr downstream_thread_local_registry_; + std::unique_ptr downstream_socket_interface_; + std::unique_ptr downstream_extension_; + + // Config for reverse connection socket interface + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface upstream_config_; + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface downstream_config_; + + // Set debug logging for this test + LogLevelSetter log_level_setter_{ENVOY_SPDLOG_LEVEL(debug)}; + + void TearDown() override { + // Clean up thread local components + upstream_tls_slot_.reset(); + upstream_thread_local_registry_.reset(); + upstream_extension_.reset(); + upstream_socket_interface_.reset(); + + downstream_tls_slot_.reset(); + downstream_thread_local_registry_.reset(); + downstream_extension_.reset(); + downstream_socket_interface_.reset(); + } + + // Helper method to create an initiated connection for testing + void createInitiatedConnection(const std::string& node_id, const std::string& cluster_id) { + // Manually set the gauge values to simulate initiated connections + setInitiatedConnectionStats(node_id, cluster_id, 1); + } + + // Helper method to manually set gauge values for testing initiated connections + void setInitiatedConnectionStats(const std::string& node_id, const std::string& cluster_id, + uint64_t count = 1) { + // Set cross-worker stats (these are the ones used by getCrossWorkerStatMap) + auto& stats_store = downstream_extension_->getStatsScope(); + + // Set host connection stat - use the pattern expected by getCrossWorkerStatMap + std::string host_stat_name = fmt::format("reverse_connections.host.{}.connected", node_id); + auto& host_gauge = + stats_store.gaugeFromString(host_stat_name, Stats::Gauge::ImportMode::Accumulate); + host_gauge.set(count); + + // Set cluster connection stat - use the pattern expected by getCrossWorkerStatMap + std::string cluster_stat_name = + fmt::format("reverse_connections.cluster.{}.connected", cluster_id); + auto& cluster_gauge = + stats_store.gaugeFromString(cluster_stat_name, Stats::Gauge::ImportMode::Accumulate); + cluster_gauge.set(count); + } +}; + +// Test basic filter construction and configuration +TEST_F(ReverseConnFilterTest, BasicConstruction) { + auto filter = createFilter(); + EXPECT_NE(filter, nullptr); +} + +// Test filter construction with default config +TEST_F(ReverseConnFilterTest, DefaultConfig) { + envoy::extensions::filters::http::reverse_conn::v3::ReverseConn config; + // Don't set ping_interval, should use default + auto filter_config = std::make_shared(config); + auto filter = std::make_unique(filter_config); + filter->setDecoderFilterCallbacks(callbacks_); + + EXPECT_NE(filter, nullptr); + EXPECT_EQ(filter_config->pingInterval().count(), 2); // Default is 2 seconds +} + +// Test filter construction with custom ping interval +TEST_F(ReverseConnFilterTest, CustomPingInterval) { + auto filter = createFilterWithConfig(10); + EXPECT_NE(filter, nullptr); +} + +// Test filter destruction +TEST_F(ReverseConnFilterTest, FilterDestruction) { + auto filter = createFilter(); + EXPECT_NE(filter, nullptr); + + // Should not crash on destruction + filter.reset(); + EXPECT_EQ(filter, nullptr); +} + +// Test onDestroy method +TEST_F(ReverseConnFilterTest, OnDestroy) { + auto filter = createFilter(); + EXPECT_NE(filter, nullptr); + + // Should not crash when onDestroy is called + filter->onDestroy(); +} + +// Test helper functions for socket interface access - Extension not created +TEST_F(ReverseConnFilterTest, SocketInterfaceHelpersNoExtensions) { + auto filter = createFilter(); + + // Test all four helper functions when no extensions are created + auto* upstream_manager = testGetUpstreamSocketManager(filter.get()); + auto* upstream_extension = testGetUpstreamSocketInterfaceExtension(filter.get()); + auto* downstream_extension = testGetDownstreamSocketInterfaceExtension(filter.get()); + + EXPECT_EQ(upstream_manager, nullptr); // No TLS registry set up + EXPECT_EQ(upstream_extension, nullptr); // No extension set up + EXPECT_EQ(downstream_extension, nullptr); // No extension set up +} + +// Test helper functions for socket interface access - Extensions created but no TLS slots +TEST_F(ReverseConnFilterTest, SocketInterfaceHelpersExtensionsNoSlots) { + auto filter = createFilter(); + + // Set up extensions but don't set up TLS slots + setupExtensions(); + + // Test all four helper functions when extensions are created but TLS slots are not set up + auto* upstream_manager = testGetUpstreamSocketManager(filter.get()); + auto* upstream_extension = testGetUpstreamSocketInterfaceExtension(filter.get()); + auto* downstream_extension = testGetDownstreamSocketInterfaceExtension(filter.get()); + + // Upstream manager should be nullptr because TLS registry is not set up + EXPECT_EQ(upstream_manager, nullptr); + + // Extensions should be found since we created them, but TLS slots are not set up + EXPECT_NE(upstream_extension, nullptr); + EXPECT_EQ(upstream_extension, upstream_extension_.get()); + EXPECT_NE(downstream_extension, nullptr); + EXPECT_EQ(downstream_extension, downstream_extension_.get()); +} + +// Test helper functions for socket interface access - Extensions and TLS slots set up +TEST_F(ReverseConnFilterTest, SocketInterfaceHelpersExtensionsAndSlots) { + auto filter = createFilter(); + + // Set up extensions and TLS slots + setupExtensions(); + setupThreadLocalSlot(); + + // Test all four helper functions when everything is properly set up + auto* upstream_manager = testGetUpstreamSocketManager(filter.get()); + auto* downstream_interface = testGetDownstreamSocketInterface(filter.get()); + auto* upstream_extension = testGetUpstreamSocketInterfaceExtension(filter.get()); + auto* downstream_extension = testGetDownstreamSocketInterfaceExtension(filter.get()); + + // All should be non-null when properly set up + EXPECT_NE(upstream_manager, nullptr); + EXPECT_EQ(upstream_manager, upstream_thread_local_registry_->socketManager()); + + EXPECT_NE(downstream_interface, nullptr); + + EXPECT_NE(upstream_extension, nullptr); + EXPECT_EQ(upstream_extension, upstream_extension_.get()); + + EXPECT_NE(downstream_extension, nullptr); + EXPECT_EQ(downstream_extension, downstream_extension_.get()); +} + +// Test decodeHeaders with non-reverse connection path (should continue) +TEST_F(ReverseConnFilterTest, DecodeHeadersNonReverseConnectionPath) { + auto filter = createFilter(); + auto headers = createHeaders("GET", "/some/other/path"); + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, false); + EXPECT_EQ(status, Http::FilterHeadersStatus::Continue); +} + +// Test decodeHeaders with reverse connection path but wrong method +TEST_F(ReverseConnFilterTest, DecodeHeadersReverseConnectionPathWrongMethod) { + auto filter = createFilter(); + auto headers = createHeaders("PUT", "/reverse_connections"); + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, false); + EXPECT_EQ(status, Http::FilterHeadersStatus::Continue); +} + +// Test config validation with valid ping interval +TEST_F(ReverseConnFilterTest, ConfigValidationValidPingInterval) { + envoy::extensions::filters::http::reverse_conn::v3::ReverseConn config; + config.mutable_ping_interval()->set_value(1); // Valid: 1 second + + auto filter_config = std::make_shared(config); + EXPECT_EQ(filter_config->pingInterval().count(), 1); +} + +// Test config validation with zero ping interval +TEST_F(ReverseConnFilterTest, ConfigValidationZeroPingInterval) { + envoy::extensions::filters::http::reverse_conn::v3::ReverseConn config; + config.mutable_ping_interval()->set_value(0); // Zero should use default + + auto filter_config = std::make_shared(config); + EXPECT_EQ(filter_config->pingInterval().count(), 2); // Default is 2 seconds +} + +// Test config validation with large ping interval +TEST_F(ReverseConnFilterTest, ConfigValidationLargePingInterval) { + envoy::extensions::filters::http::reverse_conn::v3::ReverseConn config; + config.mutable_ping_interval()->set_value(3600); // 1 hour + + auto filter_config = std::make_shared(config); + EXPECT_EQ(filter_config->pingInterval().count(), 3600); +} + +// Test matchRequestPath helper function +TEST_F(ReverseConnFilterTest, MatchRequestPath) { + auto filter = createFilter(); + + // Test exact match + EXPECT_TRUE(testMatchRequestPath(filter.get(), "/reverse_connections", "/reverse_connections")); + + // Test prefix match + EXPECT_TRUE( + testMatchRequestPath(filter.get(), "/reverse_connections/request", "/reverse_connections")); + + // Test no match + EXPECT_FALSE(testMatchRequestPath(filter.get(), "/some/other/path", "/reverse_connections")); + + // Test empty path + EXPECT_FALSE(testMatchRequestPath(filter.get(), "", "/reverse_connections")); + + // Test shorter path + EXPECT_FALSE(testMatchRequestPath(filter.get(), "/reverse", "/reverse_connections")); +} + +// Test decodeHeaders with POST method for reverse connection accept request +TEST_F(ReverseConnFilterTest, DecodeHeadersPostReverseConnectionAccept) { + auto filter = createFilter(); + + // Create headers for POST request to /reverse_connections/request + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength("100"); // Set content length for protobuf + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, false); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +// Test decodeHeaders with POST method for non-accept reverse connection path +TEST_F(ReverseConnFilterTest, DecodeHeadersPostNonAcceptPath) { + auto filter = createFilter(); + + // Create headers for POST request to /reverse_connections (not /request) + auto headers = createHeaders("POST", "/reverse_connections"); + headers.setContentLength("100"); + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, false); + EXPECT_EQ(status, Http::FilterHeadersStatus::Continue); +} + +// acceptReverseConnection Tests + +// Test acceptReverseConnection with valid protobuf data +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionValidProtobuf) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Create valid protobuf handshake argument + std::string handshake_arg = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + + // Set up headers for reverse connection request + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock with proper expectations + setupSocketMock(true); // Expect duplicate() for valid protobuf + + // Process headers first + Http::FilterHeadersStatus header_status = filter->decodeHeaders(headers, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with protobuf data + Buffer::OwnedImpl data(handshake_arg); + + // Process data - this should call acceptReverseConnection + Http::FilterDataStatus data_status = filter->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Verify that the socket was added to the upstream socket manager + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + // Try to get the socket for the node - should be available + auto retrieved_socket = socket_manager->getConnectionSocket("node-789"); + EXPECT_NE(retrieved_socket, nullptr); + + // Verify stats were updated + auto* extension = upstream_extension_.get(); + ASSERT_NE(extension, nullptr); + + // Get per-worker stats to verify the connection was counted + auto stat_map = extension->getPerWorkerStatMap(); + + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node-789"], 1); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster-456"], 1); +} + +// Test acceptReverseConnection with incomplete protobuf data +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionProtobufIncomplete) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Set up headers for reverse connection request with large content length + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength("100"); // Expect 100 bytes but only send 10 + + // Set up socket mock - expect no duplicate() since the socket won't be used + setupSocketMock(false); + + // Process headers first + Http::FilterHeadersStatus header_status = filter->decodeHeaders(headers, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with incomplete protobuf data (less than expected size) + Buffer::OwnedImpl data("incomplete"); + + // Process data - this should return StopIterationAndBuffer waiting for more data + Http::FilterDataStatus data_status = filter->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationAndBuffer); + + // Verify that no socket was added to the upstream socket manager + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + auto retrieved_socket = socket_manager->getConnectionSocket("node-789"); + EXPECT_EQ(retrieved_socket, nullptr); +} + +// Test acceptReverseConnection with invalid protobuf data +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionInvalidProtobufParseFailure) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Set up headers for reverse connection request + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength("43"); // Match the actual data size we'll send + + // Set up socket mock - saveDownstreamConnection is not called since after + // protobuf unmarshalling since the node_uuid is empty + setupSocketMock(false); + + // Expect sendLocalReply to be called with BadGateway status + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::BadGateway, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + // Verify the HTTP status code + EXPECT_EQ(code, Http::Code::BadGateway); + + // Deserialize the protobuf response to check the actual message + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface:: + ReverseConnHandshakeRet ret; + EXPECT_TRUE(ret.ParseFromString(std::string(body))); + EXPECT_EQ(ret.status(), envoy::extensions::bootstrap::reverse_tunnel:: + downstream_socket_interface::ReverseConnHandshakeRet::REJECTED); + EXPECT_EQ(ret.status_message(), + "Failed to parse request message or required fields missing"); + })); + + // Process headers first + Http::FilterHeadersStatus header_status = filter->decodeHeaders(headers, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with invalid protobuf data that can't be parsed + // Send exactly 43 bytes to match the content length + Buffer::OwnedImpl data("invalid protobuf data that cannot be parsed"); + + // Process data - this should call acceptReverseConnection and fail parsing + // The filter should return StopIterationNoBuffer and send a local reply + Http::FilterDataStatus data_status = filter->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Verify that no socket was added to the upstream socket manager + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + auto retrieved_socket = socket_manager->getConnectionSocket("node-789"); + EXPECT_EQ(retrieved_socket, nullptr); +} + +// Test acceptReverseConnection with empty node_uuid in protobuf +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionEmptyNodeUuid) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Create protobuf with empty node_uuid + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::ReverseConnHandshakeArg + arg; + arg.set_tenant_uuid("tenant-123"); + arg.set_cluster_uuid("cluster-456"); + arg.set_node_uuid(""); // Empty node_uuid + std::string handshake_arg = arg.SerializeAsString(); + + // Set up headers for reverse connection request + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock - since the node_uuid is empty, the socket is not saved + setupSocketMock(false); + + // Expect sendLocalReply to be called with BadGateway status for empty node_uuid + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::BadGateway, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + // Verify the HTTP status code + EXPECT_EQ(code, Http::Code::BadGateway); + + // Deserialize the protobuf response to check the actual message + envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface:: + ReverseConnHandshakeRet ret; + EXPECT_TRUE(ret.ParseFromString(std::string(body))); + EXPECT_EQ(ret.status(), envoy::extensions::bootstrap::reverse_tunnel:: + downstream_socket_interface::ReverseConnHandshakeRet::REJECTED); + EXPECT_EQ(ret.status_message(), + "Failed to parse request message or required fields missing"); + })); + + // Process headers first + Http::FilterHeadersStatus header_status = filter->decodeHeaders(headers, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with protobuf data + Buffer::OwnedImpl data(handshake_arg); + + // Process data - this should call acceptReverseConnection and reject + Http::FilterDataStatus data_status = filter->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Check that no stats were recorded for the cluster + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + auto* extension = socket_manager->getUpstreamExtension(); + ASSERT_NE(extension, nullptr); + + // Get cross-worker stats to verify no connection was counted + auto cross_worker_stat_map = upstream_extension_->getCrossWorkerStatMap(); + EXPECT_EQ(cross_worker_stat_map["test_scope.reverse_connections.clusters.cluster-456"], 0); +} + +// Test acceptReverseConnection with SSL certificate information +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionWithSSLCertificate) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Create valid protobuf handshake argument + std::string handshake_arg = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + + // Set up headers for reverse connection request + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength(std::to_string(handshake_arg.length())); + + // Mock SSL connection with certificate + auto mock_ssl = std::make_shared>(); + std::vector dns_sans = {"tenantId=ssl-tenant", "clusterId=ssl-cluster"}; + EXPECT_CALL(*mock_ssl, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mock_ssl, dnsSansPeerCertificate()).WillRepeatedly(Return(dns_sans)); + + // Set up connection with SSL + EXPECT_CALL(connection_, ssl()).WillRepeatedly(Return(mock_ssl)); + + // Set up socket mock + setupSocketMock(true); // Expect duplicate() for SSL test + + // Process headers first + Http::FilterHeadersStatus header_status = filter->decodeHeaders(headers, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with protobuf data + Buffer::OwnedImpl data(handshake_arg); + + // Process data - this should call acceptReverseConnection + Http::FilterDataStatus data_status = filter->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Verify that the socket was added to the upstream socket manager + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + // Try to get the socket for the node - should be available + auto retrieved_socket = socket_manager->getConnectionSocket("node-789"); + EXPECT_NE(retrieved_socket, nullptr); +} + +// Test acceptReverseConnection with multiple sockets for same node +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionMultipleSockets) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Create valid protobuf handshake argument + std::string handshake_arg = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + + // Set up headers for reverse connection request + auto headers = createHeaders("POST", "/reverse_connections/request"); + headers.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock for first connection + setupSocketMock(true); + + // Process headers first + Http::FilterHeadersStatus header_status = filter->decodeHeaders(headers, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with protobuf data + Buffer::OwnedImpl data1(handshake_arg); + + // Process first data - this should call acceptReverseConnection + Http::FilterDataStatus data_status1 = filter->decodeData(data1, false); + EXPECT_EQ(data_status1, Http::FilterDataStatus::StopIterationNoBuffer); + + // Create second filter instance for second connection + auto filter2 = createFilter(); + + // Set up headers for second reverse connection request + auto headers2 = createHeaders("POST", "/reverse_connections/request"); + headers2.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock for second connection + setupSocketMock(true); + + // Process headers for second connection + Http::FilterHeadersStatus header_status2 = filter2->decodeHeaders(headers2, false); + EXPECT_EQ(header_status2, Http::FilterHeadersStatus::StopIteration); + + // Create buffer with protobuf data for second connection + Buffer::OwnedImpl data2(handshake_arg); + + // Process second data - this should call acceptReverseConnection + Http::FilterDataStatus data_status2 = filter2->decodeData(data2, true); + EXPECT_EQ(data_status2, Http::FilterDataStatus::StopIterationNoBuffer); + + // Verify that both sockets were added to the upstream socket manager + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + // Try to get the first socket for the node + auto retrieved_socket1 = socket_manager->getConnectionSocket("node-789"); + EXPECT_NE(retrieved_socket1, nullptr); + + // Try to get the second socket for the node + auto retrieved_socket2 = socket_manager->getConnectionSocket("node-789"); + EXPECT_NE(retrieved_socket2, nullptr); + + // Verify stats were updated correctly for multiple connections + auto* extension = upstream_extension_.get(); + ASSERT_NE(extension, nullptr); + + // Get per-worker stats to verify the connections were counted + auto stat_map = extension->getPerWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node-789"], 2); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster-456"], 2); +} + +// Test acceptReverseConnection with multiple nodes and clusters for cross-worker stats +TEST_F(ReverseConnFilterTest, AcceptReverseConnectionMultipleNodesCrossWorkerStats) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + // Create first filter and connection + auto filter1 = createFilter(); + std::string handshake_arg1 = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + auto headers1 = createHeaders("POST", "/reverse_connections/request"); + headers1.setContentLength(std::to_string(handshake_arg1.length())); + + // Set up socket mock for first connection + setupSocketMock(true); + + Http::FilterHeadersStatus header_status1 = filter1->decodeHeaders(headers1, false); + EXPECT_EQ(header_status1, Http::FilterHeadersStatus::StopIteration); + + Buffer::OwnedImpl data1(handshake_arg1); + Http::FilterDataStatus data_status1 = filter1->decodeData(data1, true); + EXPECT_EQ(data_status1, Http::FilterDataStatus::StopIterationNoBuffer); + + // Create second filter and connection with different node/cluster + auto filter2 = createFilter(); + std::string handshake_arg2 = createHandshakeArg("tenant-456", "cluster-789", "node-123"); + auto headers2 = createHeaders("POST", "/reverse_connections/request"); + headers2.setContentLength(std::to_string(handshake_arg2.length())); + + // Set up socket mock for second connection + setupSocketMock(true); + + Http::FilterHeadersStatus header_status2 = filter2->decodeHeaders(headers2, false); + EXPECT_EQ(header_status2, Http::FilterHeadersStatus::StopIteration); + + Buffer::OwnedImpl data2(handshake_arg2); + Http::FilterDataStatus data_status2 = filter2->decodeData(data2, true); + EXPECT_EQ(data_status2, Http::FilterDataStatus::StopIterationNoBuffer); + + // Verify that both sockets were added to the upstream socket manager + auto* socket_manager = upstream_thread_local_registry_->socketManager(); + ASSERT_NE(socket_manager, nullptr); + + // Try to get both sockets + auto retrieved_socket1 = socket_manager->getConnectionSocket("node-789"); + EXPECT_NE(retrieved_socket1, nullptr); + + auto retrieved_socket2 = socket_manager->getConnectionSocket("node-123"); + EXPECT_NE(retrieved_socket2, nullptr); + + // Verify cross-worker stats were updated correctly for both connections + auto* extension = upstream_extension_.get(); + ASSERT_NE(extension, nullptr); + + // Get cross-worker stats to verify both connections were counted + auto cross_worker_stat_map = extension->getCrossWorkerStatMap(); + EXPECT_EQ(cross_worker_stat_map["test_scope.reverse_connections.nodes.node-789"], 1); + EXPECT_EQ(cross_worker_stat_map["test_scope.reverse_connections.clusters.cluster-456"], 1); + EXPECT_EQ(cross_worker_stat_map["test_scope.reverse_connections.nodes.node-123"], 1); + EXPECT_EQ(cross_worker_stat_map["test_scope.reverse_connections.clusters.cluster-789"], 1); +} + +// Test saveDownstreamConnection without socket manager initialized +TEST_F(ReverseConnFilterTest, SaveDownstreamConnectionNoSocketManager) { + // Set up extensions but not thread local slot - socket manager will not be initialized + setupExtensions(); + auto filter = createFilter(); + + // Set up socket mock + setupSocketMock(false); + + // Call saveDownstreamConnection - should fail since socket manager is not initialized + testSaveDownstreamConnection(filter.get(), connection_, "node-789", "cluster-456"); + + // Check that no stats were recorded since the socket manager is not available + auto* extension = upstream_extension_.get(); + ASSERT_NE(extension, nullptr); + + // Get per-worker stats to verify no connection was counted + auto stat_map = extension->getPerWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node-789"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster-456"], 0); +} + +// Test saveDownstreamConnection with original socket closure +TEST_F(ReverseConnFilterTest, SaveDownstreamConnectionOriginalSocketClosed) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Set up socket mock with closed socket + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle_ = std::make_unique>(); + + // Set up IO handle expectations for closed socket + EXPECT_CALL(*mock_io_handle_, fdDoNotUse()).WillRepeatedly(Return(123)); + EXPECT_CALL(*mock_io_handle_, isOpen()).WillRepeatedly(Return(false)); // Socket is closed + + // Don't expect duplicate() since socket is closed + EXPECT_CALL(*mock_io_handle_, duplicate()).Times(0); + + // Set up socket expectations + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(false)); // Socket is closed + + // Store the mock_io_handle in the socket + mock_socket_ptr->io_handle_ = std::move(mock_io_handle_); + + // Cast the mock to the base ConnectionSocket type and store it + mock_socket_ = std::unique_ptr(mock_socket_ptr.release()); + + // Set up connection to return the socket + EXPECT_CALL(connection_, getSocket()).WillRepeatedly(ReturnRef(mock_socket_)); + + // Call saveDownstreamConnection directly - should fail since socket is closed + testSaveDownstreamConnection(filter.get(), connection_, "node-789", "cluster-456"); + + // Check that no stats were recorded since the socket was closed + auto* extension = upstream_extension_.get(); + ASSERT_NE(extension, nullptr); + + // Get per-worker stats to verify no connection was counted + auto stat_map = extension->getPerWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node-789"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster-456"], 0); +} + +// Test saveDownstreamConnection with duplicate failure +TEST_F(ReverseConnFilterTest, SaveDownstreamConnectionDuplicateFailure) { + // Set up extensions and thread local slot for upstream socket manager + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Set up socket mock with duplicate failure + auto mock_socket_ptr = std::make_unique>(); + auto mock_io_handle_ = std::make_unique>(); + + // Set up IO handle expectations + EXPECT_CALL(*mock_io_handle_, fdDoNotUse()).WillRepeatedly(Return(123)); + EXPECT_CALL(*mock_io_handle_, isOpen()).WillRepeatedly(Return(true)); + + // Expect duplicate() to fail (return nullptr) + EXPECT_CALL(*mock_io_handle_, duplicate()).WillOnce(Return(nullptr)); + + // Set up socket expectations + EXPECT_CALL(*mock_socket_ptr, ioHandle()).WillRepeatedly(ReturnRef(*mock_io_handle_)); + EXPECT_CALL(*mock_socket_ptr, isOpen()).WillRepeatedly(Return(true)); + + // Store the mock_io_handle in the socket + mock_socket_ptr->io_handle_ = std::move(mock_io_handle_); + + // Cast the mock to the base ConnectionSocket type and store it + mock_socket_ = std::unique_ptr(mock_socket_ptr.release()); + + // Set up connection to return the socket + EXPECT_CALL(connection_, getSocket()).WillRepeatedly(ReturnRef(mock_socket_)); + + // Call saveDownstreamConnection directly - should fail since duplicate() returns nullptr + testSaveDownstreamConnection(filter.get(), connection_, "node-789", "cluster-456"); + + // Check that no stats were recorded since the duplicate operation failed + auto* extension = upstream_extension_.get(); + ASSERT_NE(extension, nullptr); + + // Get per-worker stats to verify no connection was counted + auto stat_map = extension->getPerWorkerStatMap(); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.node.node-789"], 0); + EXPECT_EQ(stat_map["test_scope.reverse_connections.worker_0.cluster.cluster-456"], 0); +} + +// Test getQueryParam +TEST_F(ReverseConnFilterTest, GetQueryParamAllCases) { + // Set up extensions and thread local slots to avoid crashes + setupExtensions(); + setupThreadLocalSlot(); + + auto filter = createFilter(); + + // Test with existing query parameters - use a reverse-connection path but with GET method to + // avoid triggering the full logic + auto headers = createHeaders( + "GET", "/reverse_connections?node_id=test-node&cluster_id=test-cluster&role=initiator"); + // Call decodeHeaders to properly set up the request headers + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); + + EXPECT_EQ(testGetQueryParam(filter.get(), "node_id"), "test-node"); + EXPECT_EQ(testGetQueryParam(filter.get(), "cluster_id"), "test-cluster"); + EXPECT_EQ(testGetQueryParam(filter.get(), "role"), "initiator"); + + // Test with non-existent query parameter + EXPECT_EQ(testGetQueryParam(filter.get(), "non_existent"), ""); + + // Test with empty query string + auto headers_empty = createHeaders("GET", "/reverse_connections"); + auto filter_empty = createFilter(); + Http::FilterHeadersStatus status_empty = filter_empty->decodeHeaders(headers_empty, true); + EXPECT_EQ(status_empty, Http::FilterHeadersStatus::StopIteration); + + EXPECT_EQ(testGetQueryParam(filter_empty.get(), "node_id"), ""); + EXPECT_EQ(testGetQueryParam(filter_empty.get(), "cluster_id"), ""); + EXPECT_EQ(testGetQueryParam(filter_empty.get(), "role"), ""); +} + +// Test determineRole with different interface registration scenarios +TEST_F(ReverseConnFilterTest, DetermineRoleDifferentInterfaceRegistration) { + // Test with only downstream extension enabled - should return "initiator" + auto filter_downstream_only = createFilter(); + setupDownstreamExtension(); + setupDownstreamThreadLocalSlot(); + EXPECT_EQ(testDetermineRole(filter_downstream_only.get()), "initiator"); + + // Test with both extensions enabled - should return "both" + auto filter_both = createFilter(); + setupExtensions(); + setupThreadLocalSlot(); + EXPECT_EQ(testDetermineRole(filter_both.get()), "both"); +} + +// Test GET request with initiator role - with remote node +TEST_F(ReverseConnFilterTest, GetRequestInitiatorRoleWithRemoteNode) { + // Set up both extensions + setupExtensions(); + setupThreadLocalSlot(); + + // Create an initiated connection by setting stats + createInitiatedConnection("test-node", "test-cluster"); + + // Now test the GET request + auto filter = createFilter(); + + // Create GET request with initiator role and remote node + auto headers = createHeaders("GET", "/reverse_connections?role=initiator&node_id=test-node"); + + // Expect sendLocalReply to be called with OK status and node-specific stats + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::OK, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + EXPECT_EQ(code, Http::Code::OK); + // Should return JSON with available_connections for the specific node + EXPECT_TRUE(body.find("available_connections") != absl::string_view::npos); + // Should return count of 1 since we manually set the stats + EXPECT_TRUE(body.find("\"available_connections\":1") != absl::string_view::npos); + })); + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +// Test GET request with initiator role - with remote cluster +TEST_F(ReverseConnFilterTest, GetRequestInitiatorRoleWithRemoteCluster) { + // Set up both extensions + setupExtensions(); + setupThreadLocalSlot(); + + // Create an initiated connection by setting stats + createInitiatedConnection("test-node", "test-cluster"); + + // Now test the GET request + auto filter = createFilter(); + + // Create GET request with initiator role and remote cluster + auto headers = + createHeaders("GET", "/reverse_connections?role=initiator&cluster_id=test-cluster"); + + // Expect sendLocalReply to be called with OK status and cluster-specific stats + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::OK, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + EXPECT_EQ(code, Http::Code::OK); + // Should return JSON with available_connections for the specific cluster + EXPECT_TRUE(body.find("available_connections") != absl::string_view::npos); + // Should return count of 1 since we manually set the stats + EXPECT_TRUE(body.find("\"available_connections\":1") != absl::string_view::npos); + })); + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +// Test GET request with initiator role - no node/cluster (aggregated stats) +TEST_F(ReverseConnFilterTest, GetRequestInitiatorRoleAggregatedStats) { + // Set up both extensions + setupExtensions(); + setupThreadLocalSlot(); + + // Create an initiated connection by setting stats + createInitiatedConnection("test-node", "test-cluster"); + + // Now test the GET request + auto filter = createFilter(); + + // Create GET request with initiator role but no node_id or cluster_id + auto headers = createHeaders("GET", "/reverse_connections?role=initiator"); + + // Expect sendLocalReply to be called with OK status and aggregated stats + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::OK, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + EXPECT_EQ(code, Http::Code::OK); + // Should return JSON with aggregated stats (accepted and connected arrays) + EXPECT_TRUE(body.find("accepted") != absl::string_view::npos); + EXPECT_TRUE(body.find("connected") != absl::string_view::npos); + // Should show test-cluster in the connected array since we set the stats + EXPECT_TRUE(body.find("test-cluster") != absl::string_view::npos); + })); + + Http::FilterHeadersStatus status = filter->decodeHeaders(headers, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +// Test GET request with responder role - upstream extension present, with remote node +TEST_F(ReverseConnFilterTest, GetRequestResponderRoleWithRemoteNode) { + // Set up both extensions + setupExtensions(); + setupThreadLocalSlot(); + + // Create an accepted connection by sending a reverse connection request + auto filter1 = createFilter(); + std::string handshake_arg = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + auto headers1 = createHeaders("POST", "/reverse_connections/request"); + headers1.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock for the connection + setupSocketMock(true); + + // Process the reverse connection request to create an accepted connection + Http::FilterHeadersStatus header_status = filter1->decodeHeaders(headers1, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + Buffer::OwnedImpl data(handshake_arg); + Http::FilterDataStatus data_status = filter1->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Now test the GET request + auto filter2 = createFilter(); + + // Create GET request with responder role and remote node + auto headers2 = createHeaders("GET", "/reverse_connections?role=responder&node_id=node-789"); + + // Expect sendLocalReply to be called with OK status and node-specific stats + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::OK, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + EXPECT_EQ(code, Http::Code::OK); + // Should return JSON with available_connections for the specific node + EXPECT_TRUE(body.find("available_connections") != absl::string_view::npos); + // Should return count of 1 since we created an actual connection + EXPECT_TRUE(body.find("\"available_connections\":1") != absl::string_view::npos); + })); + + Http::FilterHeadersStatus status = filter2->decodeHeaders(headers2, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +// Test GET request with responder role - upstream extension present, with remote cluster +TEST_F(ReverseConnFilterTest, GetRequestResponderRoleWithRemoteCluster) { + // Set up both extensions + setupExtensions(); + setupThreadLocalSlot(); + + // Create an accepted connection by sending a reverse connection request + auto filter1 = createFilter(); + std::string handshake_arg = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + auto headers1 = createHeaders("POST", "/reverse_connections/request"); + headers1.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock for the connection + setupSocketMock(true); + + // Process the reverse connection request to create an accepted connection + Http::FilterHeadersStatus header_status = filter1->decodeHeaders(headers1, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + Buffer::OwnedImpl data(handshake_arg); + Http::FilterDataStatus data_status = filter1->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Now test the GET request + auto filter2 = createFilter(); + + // Create GET request with responder role and remote cluster + auto headers2 = + createHeaders("GET", "/reverse_connections?role=responder&cluster_id=cluster-456"); + + // Expect sendLocalReply to be called with OK status and cluster-specific stats + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::OK, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + EXPECT_EQ(code, Http::Code::OK); + // Should return JSON with available_connections for the specific cluster + EXPECT_TRUE(body.find("available_connections") != absl::string_view::npos); + // Should return count of 1 since we created an actual connection + EXPECT_TRUE(body.find("\"available_connections\":1") != absl::string_view::npos); + })); + + Http::FilterHeadersStatus status = filter2->decodeHeaders(headers2, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +// Test GET request with responder role - upstream extension present, no node/cluster (aggregated +// stats) +TEST_F(ReverseConnFilterTest, GetRequestResponderRoleAggregatedStats) { + // Set up both extensions + setupExtensions(); + setupThreadLocalSlot(); + + // Create an accepted connection by sending a reverse connection request + auto filter1 = createFilter(); + std::string handshake_arg = createHandshakeArg("tenant-123", "cluster-456", "node-789"); + auto headers1 = createHeaders("POST", "/reverse_connections/request"); + headers1.setContentLength(std::to_string(handshake_arg.length())); + + // Set up socket mock for the connection + setupSocketMock(true); + + // Process the reverse connection request to create an accepted connection + Http::FilterHeadersStatus header_status = filter1->decodeHeaders(headers1, false); + EXPECT_EQ(header_status, Http::FilterHeadersStatus::StopIteration); + + Buffer::OwnedImpl data(handshake_arg); + Http::FilterDataStatus data_status = filter1->decodeData(data, true); + EXPECT_EQ(data_status, Http::FilterDataStatus::StopIterationNoBuffer); + + // Now test the GET request + auto filter2 = createFilter(); + + // Create GET request with responder role but no node_id or cluster_id + auto headers2 = createHeaders("GET", "/reverse_connections?role=responder"); + + // Expect sendLocalReply to be called with OK status and aggregated stats + EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::OK, _, _, _, _)) + .WillOnce(Invoke([](Http::Code code, absl::string_view body, + std::function, + const absl::optional&, absl::string_view) { + EXPECT_EQ(code, Http::Code::OK); + // Should return JSON with aggregated stats (accepted and connected arrays) + EXPECT_TRUE(body.find("accepted") != absl::string_view::npos); + EXPECT_TRUE(body.find("connected") != absl::string_view::npos); + // Should show cluster-456 in the accepted array since we created an actual connection + EXPECT_TRUE(body.find("cluster-456") != absl::string_view::npos); + // Should show node-789 in the connected array since we created an actual connection + EXPECT_TRUE(body.find("node-789") != absl::string_view::npos); + })); + + Http::FilterHeadersStatus status = filter2->decodeHeaders(headers2, true); + EXPECT_EQ(status, Http::FilterHeadersStatus::StopIteration); +} + +} // namespace ReverseConn +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/set_metadata/set_metadata_filter_test.cc b/test/extensions/filters/http/set_metadata/set_metadata_filter_test.cc index 5ada203e0d78d..11b89d1df4b41 100644 --- a/test/extensions/filters/http/set_metadata/set_metadata_filter_test.cc +++ b/test/extensions/filters/http/set_metadata/set_metadata_filter_test.cc @@ -45,12 +45,12 @@ class SetMetadataFilterTest : public testing::Test { filter_->onDestroy(); } - void checkKeyInt(const ProtobufWkt::Struct& s, std::string key, int val) { + void checkKeyInt(const Protobuf::Struct& s, std::string key, int val) { const auto& fields = s.fields(); const auto it = fields.find(key); ASSERT_NE(it, fields.end()); const auto& pbval = it->second; - ASSERT_EQ(pbval.kind_case(), ProtobufWkt::Value::kNumberValue); + ASSERT_EQ(pbval.kind_case(), Protobuf::Value::kNumberValue); EXPECT_EQ(pbval.number_value(), val); } @@ -80,7 +80,7 @@ TEST_F(SetMetadataFilterTest, DeprecatedSimple) { const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); checkKeyInt(tags.struct_value(), "mytag0", 1); } @@ -125,18 +125,18 @@ TEST_F(SetMetadataFilterTest, DeprecatedWithMerge) { const auto it_mylist = fields.find("mylist"); ASSERT_NE(it_mylist, fields.end()); const auto& mylist = it_mylist->second; - ASSERT_EQ(mylist.kind_case(), ProtobufWkt::Value::kListValue); + ASSERT_EQ(mylist.kind_case(), Protobuf::Value::kListValue); const auto& vals = mylist.list_value().values(); ASSERT_EQ(vals.size(), 2); - ASSERT_EQ(vals[0].kind_case(), ProtobufWkt::Value::kStringValue); + ASSERT_EQ(vals[0].kind_case(), Protobuf::Value::kStringValue); EXPECT_EQ(vals[0].string_value(), "a"); - ASSERT_EQ(vals[1].kind_case(), ProtobufWkt::Value::kStringValue); + ASSERT_EQ(vals[1].kind_case(), Protobuf::Value::kStringValue); EXPECT_EQ(vals[1].string_value(), "b"); const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); const auto& tags_struct = tags.struct_value(); checkKeyInt(tags_struct, "mytag0", 1); @@ -163,7 +163,7 @@ TEST_F(SetMetadataFilterTest, UntypedSimple) { const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); checkKeyInt(tags.struct_value(), "mytag0", 1); } @@ -228,18 +228,18 @@ TEST_F(SetMetadataFilterTest, UntypedWithAllowOverwrite) { const auto it_mylist = fields.find("mylist"); ASSERT_NE(it_mylist, fields.end()); const auto& mylist = it_mylist->second; - ASSERT_EQ(mylist.kind_case(), ProtobufWkt::Value::kListValue); + ASSERT_EQ(mylist.kind_case(), Protobuf::Value::kListValue); const auto& vals = mylist.list_value().values(); ASSERT_EQ(vals.size(), 2); - ASSERT_EQ(vals[0].kind_case(), ProtobufWkt::Value::kStringValue); + ASSERT_EQ(vals[0].kind_case(), Protobuf::Value::kStringValue); EXPECT_EQ(vals[0].string_value(), "a"); - ASSERT_EQ(vals[1].kind_case(), ProtobufWkt::Value::kStringValue); + ASSERT_EQ(vals[1].kind_case(), Protobuf::Value::kStringValue); EXPECT_EQ(vals[1].string_value(), "b"); const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); const auto& tags_struct = tags.struct_value(); checkKeyInt(tags_struct, "mytag0", 1); @@ -280,16 +280,16 @@ TEST_F(SetMetadataFilterTest, UntypedWithNoAllowOverwrite) { const auto it_mylist = fields.find("mylist"); ASSERT_NE(it_mylist, fields.end()); const auto& mylist = it_mylist->second; - ASSERT_EQ(mylist.kind_case(), ProtobufWkt::Value::kListValue); + ASSERT_EQ(mylist.kind_case(), Protobuf::Value::kListValue); const auto& vals = mylist.list_value().values(); ASSERT_EQ(vals.size(), 1); - ASSERT_EQ(vals[0].kind_case(), ProtobufWkt::Value::kStringValue); + ASSERT_EQ(vals[0].kind_case(), Protobuf::Value::kStringValue); EXPECT_EQ(vals[0].string_value(), "a"); const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); const auto& tags_struct = tags.struct_value(); checkKeyInt(tags_struct, "mytag0", 1); @@ -392,7 +392,7 @@ TEST_F(SetMetadataFilterTest, UntypedWithDeprecated) { const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); checkKeyInt(tags.struct_value(), "mytag0", 1); } @@ -422,7 +422,7 @@ TEST_F(SetMetadataFilterTest, TypedWithDeprecated) { const auto it_tags = fields.find("tags"); ASSERT_NE(it_tags, fields.end()); const auto& tags = it_tags->second; - ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + ASSERT_EQ(tags.kind_case(), Protobuf::Value::kStructValue); checkKeyInt(tags.struct_value(), "mytag0", 0); // Verify that `metadata` contains our typed Config. diff --git a/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc b/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc index aa64b31cf7c16..2460f1559dc09 100644 --- a/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc +++ b/test/extensions/filters/http/stateful_session/stateful_session_integration_test.cc @@ -73,7 +73,7 @@ class StatefulSessionIntegrationTest : public Envoy::HttpIntegrationTest, public // Update per route config of default route. if (!per_route_config_yaml.empty()) { auto* route = virtual_host.mutable_routes(0); - ProtobufWkt::Any per_route_config; + Protobuf::Any per_route_config; TestUtility::loadFromYaml(per_route_config_yaml, per_route_config); route->mutable_typed_per_filter_config()->insert( diff --git a/test/extensions/filters/http/tap/BUILD b/test/extensions/filters/http/tap/BUILD index e692767264af1..ba500cd660673 100644 --- a/test/extensions/filters/http/tap/BUILD +++ b/test/extensions/filters/http/tap/BUILD @@ -47,6 +47,7 @@ envoy_extension_cc_test( "//source/extensions/filters/http/tap:tap_config_impl", "//test/extensions/common/tap:common", "//test/mocks:common_lib", + "//test/mocks/http:http_mocks", "//test/mocks/network:network_mocks", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", diff --git a/test/extensions/filters/http/tap/common.h b/test/extensions/filters/http/tap/common.h index dc8876c148137..70f10c0b35c2e 100644 --- a/test/extensions/filters/http/tap/common.h +++ b/test/extensions/filters/http/tap/common.h @@ -13,9 +13,8 @@ class MockHttpTapConfig : public HttpTapConfig { public: HttpPerRequestTapperPtr createPerRequestTapper(const envoy::extensions::filters::http::tap::v3::Tap& tap_config, - uint64_t stream_id, - OptRef connection) override { - return HttpPerRequestTapperPtr{createPerRequestTapper_(tap_config, stream_id, connection)}; + Http::StreamDecoderFilterCallbacks& decoder_callbacks) override { + return HttpPerRequestTapperPtr{createPerRequestTapper_(tap_config, decoder_callbacks)}; } Extensions::Common::Tap::PerTapSinkHandleManagerPtr @@ -25,8 +24,8 @@ class MockHttpTapConfig : public HttpTapConfig { } MOCK_METHOD(HttpPerRequestTapper*, createPerRequestTapper_, - (const envoy::extensions::filters::http::tap::v3::Tap& tap_config, uint64_t stream_id, - OptRef)); + (const envoy::extensions::filters::http::tap::v3::Tap& tap_config, + Http::StreamDecoderFilterCallbacks& decoder_callbacks)); MOCK_METHOD(Extensions::Common::Tap::PerTapSinkHandleManager*, createPerTapSinkHandleManager_, (uint64_t trace_id)); MOCK_METHOD(uint32_t, maxBufferedRxBytes, (), (const)); diff --git a/test/extensions/filters/http/tap/tap_config_impl_test.cc b/test/extensions/filters/http/tap/tap_config_impl_test.cc index f413af5ca371b..1fbe3f252ed6f 100644 --- a/test/extensions/filters/http/tap/tap_config_impl_test.cc +++ b/test/extensions/filters/http/tap/tap_config_impl_test.cc @@ -6,6 +6,7 @@ #include "test/extensions/common/tap/common.h" #include "test/extensions/filters/http/tap/common.h" #include "test/mocks/common.h" +#include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" @@ -28,6 +29,7 @@ namespace TapCommon = Extensions::Common::Tap; class HttpPerRequestTapperImplTest : public testing::Test { public: HttpPerRequestTapperImplTest() { + EXPECT_CALL(callbacks_, streamId()).WillRepeatedly(Return(1)); EXPECT_CALL(*config_, createPerTapSinkHandleManager_(1)).WillOnce(Return(sink_manager_)); EXPECT_CALL(*config_, createMatchStatusVector()) .WillOnce(Return(ByMove(TapCommon::Matcher::MatchStatusVector(1)))); @@ -35,8 +37,7 @@ class HttpPerRequestTapperImplTest : public testing::Test { EXPECT_CALL(*config_, timeSource()).WillRepeatedly(ReturnRef(time_system_)); time_system_.setSystemTime(std::chrono::seconds(0)); EXPECT_CALL(matcher_, onNewStream(_)).WillOnce(SaveArgAddress(&statuses_)); - tapper_ = std::make_unique(config_, tap_config_, 1, - OptRef{}); + tapper_ = std::make_unique(config_, tap_config_, callbacks_); } std::shared_ptr config_{std::make_shared()}; @@ -52,6 +53,7 @@ class HttpPerRequestTapperImplTest : public testing::Test { const Http::TestRequestTrailerMapImpl request_trailers_{{"c", "d"}}; const Http::TestResponseHeaderMapImpl response_headers_{{"e", "f"}}; const Http::TestResponseTrailerMapImpl response_trailers_{{"g", "h"}}; + NiceMock callbacks_; Event::SimulatedTimeSystem time_system_; }; @@ -321,26 +323,61 @@ TEST_F(HttpPerRequestTapperImplTest, StreamedMatchResponseTrailers) { EXPECT_TRUE(tapper_->onDestroyLog()); } +// Request headers are not guaranteed to be present during +// response reply. +// One known scenario is - request headers are too large. In this +// case processing of the request will be terminated with 431 +// status before request headers are parsed. +TEST_F(HttpPerRequestTapperImplTest, StreamNoRequestHeader) { + EXPECT_CALL(*config_, streaming()).WillRepeatedly(Return(true)); + EXPECT_CALL(*config_, maxBufferedRxBytes()).WillRepeatedly(Return(1024)); + EXPECT_CALL(*config_, maxBufferedTxBytes()).WillRepeatedly(Return(1024)); + + InSequence s; + EXPECT_CALL(matcher_, onHttpResponseHeaders(_, _)) + .WillOnce(Assign(&(*statuses_)[0].matches_, true)); + EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( + R"EOF( +http_streamed_trace_segment: + trace_id: 1 +)EOF"))); + EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( + R"EOF( +http_streamed_trace_segment: + trace_id: 1 + response_headers: + headers: + - key: e + value: f +)EOF"))); + // onResponseHeaders called without onRequestHeaders prior + tapper_->onResponseHeaders(response_headers_); +} + class HttpPerRequestTapperImplForSpecificConfigTest : public testing::Test { public: HttpPerRequestTapperImplForSpecificConfigTest() { + EXPECT_CALL(callbacks_, streamId()).WillRepeatedly(Return(1)); EXPECT_CALL(*config_, createPerTapSinkHandleManager_(1)).WillOnce(Return(sink_manager_)); EXPECT_CALL(*config_, createMatchStatusVector()) .WillOnce(Return(ByMove(TapCommon::Matcher::MatchStatusVector(1)))); EXPECT_CALL(*config_, rootMatcher()).WillRepeatedly(ReturnRef(matcher_)); EXPECT_CALL(*config_, timeSource()).WillRepeatedly(ReturnRef(time_system_)); - time_system_.setSystemTime(std::chrono::seconds(0)); + time_system_.setSystemTime(std::chrono::seconds(9)); EXPECT_CALL(matcher_, onNewStream(_)).WillOnce(SaveArgAddress(&statuses_)); tap_config_.set_record_headers_received_time(true); tap_config_.set_record_downstream_connection(true); + tap_config_.set_record_upstream_connection(true); connection_.stream_info_.downstream_connection_info_provider_->setLocalAddress( std::make_shared("127.0.0.1", 1234)); connection_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( std::make_shared("127.0.0.1", 4321)); - tapper_ = std::make_unique(config_, tap_config_, 1, connection_); + EXPECT_CALL(callbacks_, connection()) + .WillRepeatedly(Return(OptRef(connection_))); + tapper_ = std::make_unique(config_, tap_config_, callbacks_); Network::ConnectionInfoProviderSharedPtr local_connection_info_provider = std::make_shared( @@ -363,6 +400,7 @@ class HttpPerRequestTapperImplForSpecificConfigTest : public testing::Test { const Http::TestResponseTrailerMapImpl response_trailers_{{"g", "h"}}; Event::SimulatedTimeSystem time_system_; NiceMock connection_; + NiceMock callbacks_; }; // Buffered tap with a match and with record_headers_received_time is true. @@ -399,7 +437,7 @@ TEST_F(HttpPerRequestTapperImplForSpecificConfigTest, BufferedFlowTapWithSpecifi trailers: - key: c value: d - headers_received_time: 1970-01-01T00:00:00Z + headers_received_time: 1970-01-01T00:00:09Z response: headers: - key: e @@ -409,7 +447,7 @@ TEST_F(HttpPerRequestTapperImplForSpecificConfigTest, BufferedFlowTapWithSpecifi trailers: - key: g value: h - headers_received_time: 1970-01-01T00:00:00Z + headers_received_time: 1970-01-01T00:00:09Z downstream_connection: local_address: socket_address: @@ -419,6 +457,15 @@ TEST_F(HttpPerRequestTapperImplForSpecificConfigTest, BufferedFlowTapWithSpecifi socket_address: address: 127.0.0.1 port_value: 4321 + upstream_connection: + local_address: + socket_address: + address: 127.1.2.3 + port_value: 58443 + remote_address: + socket_address: + address: 10.0.0.1 + port_value: 443 )EOF"))); EXPECT_TRUE(tapper_->onDestroyLog()); } diff --git a/test/extensions/filters/http/tap/tap_filter_integration_test.cc b/test/extensions/filters/http/tap/tap_filter_integration_test.cc index 4dbb77b196892..d809ba2a05d62 100644 --- a/test/extensions/filters/http/tap/tap_filter_integration_test.cc +++ b/test/extensions/filters/http/tap/tap_filter_integration_test.cc @@ -1206,6 +1206,76 @@ name: tap EXPECT_EQ(1UL, test_server_->counter("http.config_test.tap.rq_tapped")->value()); } +// Verify option record_upstream_connection +// when a request header is matched in a static configuration. +TEST_P(TapIntegrationTest, StaticFilePerHttpBufferTraceTapUpstreamConnection) { + constexpr absl::string_view filter_config = + R"EOF( +name: tap +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap + common_config: + static_config: + match: + http_request_headers_match: + headers: + - name: foo + string_match: + exact: bar + output_config: + sinks: + - format: PROTO_BINARY_LENGTH_DELIMITED + file_per_tap: + path_prefix: {} + record_upstream_connection: true +)EOF"; + + const std::string path_prefix = getTempPathPrefix(); + initializeFilter(fmt::format(filter_config, path_prefix)); + + // Initial request/response with tap. + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + makeRequest(request_headers_tap_, {"hello"}, &request_trailers_, response_headers_no_tap_, + {"world"}, &response_trailers_); + codec_client_->close(); + test_server_->waitForCounterGe("http.config_test.downstream_cx_destroy", 1); + + std::vector traces = + Extensions::Common::Tap::readTracesFromPath(path_prefix); + ASSERT_EQ(1, traces.size()); + EXPECT_TRUE(traces[0].has_http_buffered_trace()); + EXPECT_TRUE(traces[0].http_buffered_trace().has_upstream_connection()); + using ::testing::AnyOf; + using ::testing::StrEq; + std::string upstream_local_address = traces[0] + .http_buffered_trace() + .upstream_connection() + .local_address() + .socket_address() + .address(); + EXPECT_THAT(upstream_local_address, AnyOf(StrEq("127.0.0.1"), StrEq("::1"))); + EXPECT_TRUE(traces[0] + .http_buffered_trace() + .upstream_connection() + .local_address() + .socket_address() + .has_port_value()); + std::string upstream_remote_address = traces[0] + .http_buffered_trace() + .upstream_connection() + .remote_address() + .socket_address() + .address(); + EXPECT_THAT(upstream_remote_address, AnyOf(StrEq("127.0.0.1"), StrEq("::1"))); + EXPECT_TRUE(traces[0] + .http_buffered_trace() + .upstream_connection() + .remote_address() + .socket_address() + .has_port_value()); + EXPECT_EQ(1UL, test_server_->counter("http.config_test.tap.rq_tapped")->value()); +} + // Verify that body matching works. TEST_P(TapIntegrationTest, AdminBodyMatching) { initializeFilter(admin_filter_config_); diff --git a/test/extensions/filters/http/tap/tap_filter_test.cc b/test/extensions/filters/http/tap/tap_filter_test.cc index 893d26281de7d..0d14b6f59852f 100644 --- a/test/extensions/filters/http/tap/tap_filter_test.cc +++ b/test/extensions/filters/http/tap/tap_filter_test.cc @@ -54,10 +54,8 @@ class TapFilterTest : public testing::Test { filter_ = std::make_unique(filter_config_); if (has_config) { - EXPECT_CALL(callbacks_, streamId()); - EXPECT_CALL(callbacks_, connection()); http_per_request_tapper_ = new MockHttpPerRequestTapper(); - EXPECT_CALL(*http_tap_config_, createPerRequestTapper_(_, _, _)) + EXPECT_CALL(*http_tap_config_, createPerRequestTapper_(_, _)) .WillOnce(Return(http_per_request_tapper_)); } diff --git a/test/extensions/filters/http/thrift_to_metadata/filter_test.cc b/test/extensions/filters/http/thrift_to_metadata/filter_test.cc index c24be79929476..154f3b47f0816 100644 --- a/test/extensions/filters/http/thrift_to_metadata/filter_test.cc +++ b/test/extensions/filters/http/thrift_to_metadata/filter_test.cc @@ -23,7 +23,7 @@ namespace HttpFilters { namespace ThriftToMetadata { MATCHER_P(MapEq, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); @@ -32,7 +32,7 @@ MATCHER_P(MapEq, rhs, "") { } MATCHER_P(MapNumEq, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).number_value(), entry.second); diff --git a/test/extensions/filters/http/wasm/BUILD b/test/extensions/filters/http/wasm/BUILD index 79553cc3ae90f..aeec85283107c 100644 --- a/test/extensions/filters/http/wasm/BUILD +++ b/test/extensions/filters/http/wasm/BUILD @@ -18,6 +18,7 @@ envoy_package() envoy_extension_cc_test( name = "wasm_filter_test", + size = "enormous", srcs = ["wasm_filter_test.cc"], data = envoy_select_wasm_cpp_tests([ "//test/extensions/filters/http/wasm/test_data:test_cpp.wasm", @@ -55,13 +56,13 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "config_test", - size = "large", + size = "enormous", srcs = ["config_test.cc"], data = envoy_select_wasm_cpp_tests([ "//test/extensions/filters/http/wasm/test_data:test_cpp.wasm", ]), extension_names = ["envoy.filters.http.wasm"], - rbe_pool = "2core", + rbe_pool = "4core", shard_count = 16, tags = ["skip_on_windows"], deps = [ @@ -81,6 +82,7 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "wasm_filter_integration_test", + size = "enormous", srcs = ["wasm_filter_integration_test.cc"], data = envoy_select_wasm_cpp_tests([ "//test/extensions/filters/http/wasm/test_data:test_cpp.wasm", diff --git a/test/extensions/filters/http/wasm/test_data/BUILD b/test/extensions/filters/http/wasm/test_data/BUILD index dc89c15852d4e..1ded204f9bc2c 100644 --- a/test/extensions/filters/http/wasm/test_data/BUILD +++ b/test/extensions/filters/http/wasm/test_data/BUILD @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load( "//bazel:envoy_build_system.bzl", "envoy_cc_test_library", diff --git a/test/extensions/filters/http/wasm/wasm_filter_integration_test.cc b/test/extensions/filters/http/wasm/wasm_filter_integration_test.cc index 844068cca9e4c..d9d69b6f4ad63 100644 --- a/test/extensions/filters/http/wasm/wasm_filter_integration_test.cc +++ b/test/extensions/filters/http/wasm/wasm_filter_integration_test.cc @@ -28,7 +28,7 @@ class WasmFilterIntegrationTest } // Wasm filters are expensive to setup and sometime default is not enough, // It needs to increase timeout to avoid flaky tests - setListenersBoundTimeout(10 * TestUtility::DefaultTimeout); + setListenersBoundTimeout(30 * TestUtility::DefaultTimeout); } void TearDown() override { fake_upstream_connection_.reset(); } diff --git a/test/extensions/filters/http/wasm/wasm_filter_test.cc b/test/extensions/filters/http/wasm/wasm_filter_test.cc index 7eb4d3f33ccc5..8a587aa8ad8e7 100644 --- a/test/extensions/filters/http/wasm/wasm_filter_test.cc +++ b/test/extensions/filters/http/wasm/wasm_filter_test.cc @@ -16,7 +16,7 @@ using testing::Return; using testing::ReturnRef; MATCHER_P(MapEq, rhs, "") { - const Envoy::ProtobufWkt::Struct& obj = arg; + const Envoy::Protobuf::Struct& obj = arg; EXPECT_TRUE(rhs.size() > 0); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); @@ -1075,7 +1075,7 @@ TEST_P(WasmHttpFilterTest, GrpcCall) { const Http::AsyncClient::RequestOptions& options) -> Grpc::AsyncRequest* { EXPECT_EQ(service_full_name, "service"); EXPECT_EQ(method_name, "method"); - ProtobufWkt::Value value; + Protobuf::Value value; EXPECT_TRUE( value.ParseFromArray(message->linearize(message->length()), message->length())); EXPECT_EQ(value.string_value(), "request"); @@ -1102,7 +1102,7 @@ TEST_P(WasmHttpFilterTest, GrpcCall) { EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter().decodeHeaders(request_headers, false)); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1198,7 +1198,7 @@ TEST_P(WasmHttpFilterTest, GrpcCallFailure) { const Http::AsyncClient::RequestOptions& options) -> Grpc::AsyncRequest* { EXPECT_EQ(service_full_name, "service"); EXPECT_EQ(method_name, "method"); - ProtobufWkt::Value value; + Protobuf::Value value; EXPECT_TRUE( value.ParseFromArray(message->linearize(message->length()), message->length())); EXPECT_EQ(value.string_value(), "request"); @@ -1240,7 +1240,7 @@ TEST_P(WasmHttpFilterTest, GrpcCallFailure) { EXPECT_EQ(filter().grpcCancel(0xFF02), proxy_wasm::WasmResult::NotFound); EXPECT_EQ(filter().grpcClose(0xFF02), proxy_wasm::WasmResult::NotFound); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1289,7 +1289,7 @@ TEST_P(WasmHttpFilterTest, GrpcCallCancel) { const Http::AsyncClient::RequestOptions& options) -> Grpc::AsyncRequest* { EXPECT_EQ(service_full_name, "service"); EXPECT_EQ(method_name, "method"); - ProtobufWkt::Value value; + Protobuf::Value value; EXPECT_TRUE( value.ParseFromArray(message->linearize(message->length()), message->length())); EXPECT_EQ(value.string_value(), "request"); @@ -1349,7 +1349,7 @@ TEST_P(WasmHttpFilterTest, GrpcCallClose) { const Http::AsyncClient::RequestOptions& options) -> Grpc::AsyncRequest* { EXPECT_EQ(service_full_name, "service"); EXPECT_EQ(method_name, "method"); - ProtobufWkt::Value value; + Protobuf::Value value; EXPECT_TRUE( value.ParseFromArray(message->linearize(message->length()), message->length())); EXPECT_EQ(value.string_value(), "request"); @@ -1409,7 +1409,7 @@ TEST_P(WasmHttpFilterTest, GrpcCallAfterDestroyed) { const Http::AsyncClient::RequestOptions& options) -> Grpc::AsyncRequest* { EXPECT_EQ(service_full_name, "service"); EXPECT_EQ(method_name, "method"); - ProtobufWkt::Value value; + Protobuf::Value value; EXPECT_TRUE( value.ParseFromArray(message->linearize(message->length()), message->length())); EXPECT_EQ(value.string_value(), "request"); @@ -1448,7 +1448,7 @@ TEST_P(WasmHttpFilterTest, GrpcCallAfterDestroyed) { wasm_.reset(); } - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1538,7 +1538,7 @@ TEST_P(WasmHttpFilterTest, GrpcStream) { EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter().decodeHeaders(request_headers, false)); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1599,7 +1599,7 @@ TEST_P(WasmHttpFilterTest, GrpcStreamCloseLocal) { EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter().decodeHeaders(request_headers, false)); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("close"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1659,7 +1659,7 @@ TEST_P(WasmHttpFilterTest, GrpcStreamCloseRemote) { EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter().decodeHeaders(request_headers, false)); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1709,7 +1709,7 @@ TEST_P(WasmHttpFilterTest, GrpcStreamCancel) { EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter().decodeHeaders(request_headers, false)); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1766,7 +1766,7 @@ TEST_P(WasmHttpFilterTest, GrpcStreamOpenAtShutdown) { EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter().decodeHeaders(request_headers, false)); - ProtobufWkt::Value value; + Protobuf::Value value; value.set_string_value("response"); std::string response_string; EXPECT_TRUE(value.SerializeToString(&response_string)); @@ -1800,7 +1800,7 @@ TEST_P(WasmHttpFilterTest, Metadata) { setupTest("", "metadata"); setupFilter(); envoy::config::core::v3::Node node_data; - ProtobufWkt::Value node_val; + Protobuf::Value node_val; node_val.set_string_value("wasm_node_get_value"); (*node_data.mutable_metadata()->mutable_fields())["wasm_node_get_key"] = node_val; (*node_data.mutable_metadata()->mutable_fields())["wasm_node_list_key"] = @@ -1821,7 +1821,7 @@ TEST_P(WasmHttpFilterTest, Metadata) { } request_stream_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair( + Protobuf::MapPair( "envoy.filters.http.wasm", MessageUtil::keyValueStruct("wasm_request_get_key", "wasm_request_get_value"))); @@ -1857,14 +1857,14 @@ TEST_P(WasmHttpFilterTest, Property) { return; } envoy::config::core::v3::Node node_data; - ProtobufWkt::Value node_val; + Protobuf::Value node_val; node_val.set_string_value("sample_data"); (*node_data.mutable_metadata()->mutable_fields())["istio.io/metadata"] = node_val; EXPECT_CALL(local_info_, node()).WillRepeatedly(ReturnRef(node_data)); setupTest("", "property"); setupFilter(); request_stream_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair( + Protobuf::MapPair( "envoy.filters.http.wasm", MessageUtil::keyValueStruct("wasm_request_get_key", "wasm_request_get_value"))); EXPECT_CALL(request_stream_info_, responseCode()).WillRepeatedly(Return(403)); diff --git a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_fuzz_test.cc b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_fuzz_test.cc index f8400374608b4..193abf44c5c11 100644 --- a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_fuzz_test.cc +++ b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_fuzz_test.cc @@ -11,15 +11,17 @@ namespace ProxyProtocol { DEFINE_PROTO_FUZZER( const test::extensions::filters::listener::proxy_protocol::ProxyProtocolTestCase& input) { + Stats::IsolatedStoreImpl store; + ConfigSharedPtr cfg; try { TestUtility::validate(input); + // Config constructor can throw as it validates proto config. + cfg = std::make_shared(*store.rootScope(), input.config()); } catch (const ProtoValidationException& e) { ENVOY_LOG_MISC(debug, "ProtoValidationException: {}", e.what()); return; } - Stats::IsolatedStoreImpl store; - ConfigSharedPtr cfg = std::make_shared(*store.rootScope(), input.config()); auto filter = std::make_unique(std::move(cfg)); ListenerFilterWithDataFuzzer fuzzer; diff --git a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc index 89a39b1e9e65c..75e87a6f4354a 100644 --- a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc +++ b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc @@ -1535,51 +1535,6 @@ TEST_P(ProxyProtocolTest, V2ParseExtensionsLargeThanInitMaxReadBytes) { EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } -TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterest) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({ - {"envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener", "false"}, - }); - // A well-formed ipv4/tcp with a pair of TLV extensions is accepted - constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, - 0x54, 0x0a, 0x21, 0x11, 0x00, 0x1a, 0x01, 0x02, 0x03, 0x04, - 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; - constexpr uint8_t tlv1[] = {0x0, 0x0, 0x1, 0xff}; - constexpr uint8_t tlv_type_authority[] = {0x02, 0x00, 0x07, 0x66, 0x6f, - 0x6f, 0x2e, 0x63, 0x6f, 0x6d}; - constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; - - envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; - auto rule = proto_config.add_rules(); - rule->set_tlv_type(0x02); - rule->mutable_on_tlv_present()->set_key("PP2 type authority"); - - connect(true, &proto_config); - write(buffer, sizeof(buffer)); - dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - - write(tlv1, sizeof(tlv1)); - write(tlv_type_authority, sizeof(tlv_type_authority)); - write(data, sizeof(data)); - expectData("DATA"); - - EXPECT_EQ(1, server_connection_->streamInfo().dynamicMetadata().filter_metadata_size()); - EXPECT_EQ(0, server_connection_->streamInfo().dynamicMetadata().typed_filter_metadata_size()); - - auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); - EXPECT_EQ(1, metadata.size()); - EXPECT_EQ(1, metadata.count(ProxyProtocol)); - - auto fields = metadata.at(ProxyProtocol).fields(); - EXPECT_EQ(1, fields.size()); - EXPECT_EQ(1, fields.count("PP2 type authority")); - - auto value_s = fields.at("PP2 type authority").string_value(); - ASSERT_THAT(value_s, ElementsAre(0x66, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d)); - disconnect(); - EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); -} - TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterestAndEmitWithSpecifiedMetadataNamespace) { // A well-formed ipv4/tcp with a pair of TLV extensions is accepted constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, @@ -1621,76 +1576,7 @@ TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterestAndEmitWithSpecifiedMetadataName EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } -TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterest) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({ - {"envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener", "false"}, - }); - // A well-formed ipv4/tcp with a pair of TLV extensions is accepted - constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, - 0x54, 0x0a, 0x21, 0x11, 0x00, 0x39, 0x01, 0x02, 0x03, 0x04, - 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; - // a TLV of type 0x00 with size of 4 (1 byte is value) - constexpr uint8_t tlv1[] = {0x00, 0x00, 0x01, 0xff}; - // a TLV of type 0x02 with size of 10 bytes (7 bytes are value) - constexpr uint8_t tlv_type_authority[] = {0x02, 0x00, 0x07, 0x66, 0x6f, - 0x6f, 0x2e, 0x63, 0x6f, 0x6d}; - // a TLV of type 0x0f with size of 6 bytes (3 bytes are value) - constexpr uint8_t tlv3[] = {0x0f, 0x00, 0x03, 0xf0, 0x00, 0x0f}; - // a TLV of type 0xea with size of 25 bytes (22 bytes are value) - constexpr uint8_t tlv_vpc_id[] = {0xea, 0x00, 0x16, 0x01, 0x76, 0x70, 0x63, 0x2d, 0x30, - 0x32, 0x35, 0x74, 0x65, 0x73, 0x74, 0x32, 0x66, 0x61, - 0x36, 0x63, 0x36, 0x33, 0x68, 0x61, 0x37}; - constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; - - envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; - auto rule_type_authority = proto_config.add_rules(); - rule_type_authority->set_tlv_type(0x02); - rule_type_authority->mutable_on_tlv_present()->set_key("PP2 type authority"); - - auto rule_vpc_id = proto_config.add_rules(); - rule_vpc_id->set_tlv_type(0xea); - rule_vpc_id->mutable_on_tlv_present()->set_key("PP2 vpc id"); - - connect(true, &proto_config); - write(buffer, sizeof(buffer)); - dispatcher_->run(Event::Dispatcher::RunType::NonBlock); - - write(tlv1, sizeof(tlv1)); - write(tlv_type_authority, sizeof(tlv_type_authority)); - write(tlv3, sizeof(tlv3)); - write(tlv_vpc_id, sizeof(tlv_vpc_id)); - write(data, sizeof(data)); - expectData("DATA"); - - EXPECT_EQ(1, server_connection_->streamInfo().dynamicMetadata().filter_metadata_size()); - EXPECT_EQ(0, server_connection_->streamInfo().dynamicMetadata().typed_filter_metadata_size()); - - auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); - EXPECT_EQ(1, metadata.size()); - EXPECT_EQ(1, metadata.count(ProxyProtocol)); - - auto fields = metadata.at(ProxyProtocol).fields(); - EXPECT_EQ(2, fields.size()); - EXPECT_EQ(1, fields.count("PP2 type authority")); - EXPECT_EQ(1, fields.count("PP2 vpc id")); - - auto value_type_authority = fields.at("PP2 type authority").string_value(); - ASSERT_THAT(value_type_authority, ElementsAre(0x66, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d)); - - auto value_vpc_id = fields.at("PP2 vpc id").string_value(); - ASSERT_THAT(value_vpc_id, - ElementsAre(0x01, 0x76, 0x70, 0x63, 0x2d, 0x30, 0x32, 0x35, 0x74, 0x65, 0x73, 0x74, - 0x32, 0x66, 0x61, 0x36, 0x63, 0x36, 0x33, 0x68, 0x61, 0x37)); - disconnect(); - EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); -} - TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndSanitiseNonUtf8) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({ - {"envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener", "false"}, - }); // A well-formed ipv4/tcp with a pair of TLV extensions is accepted. constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a, 0x21, 0x11, 0x00, 0x39, 0x01, 0x02, 0x03, 0x04, @@ -1731,7 +1617,7 @@ TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndSanitiseNonUtf8) { expectData("DATA"); EXPECT_EQ(1, server_connection_->streamInfo().dynamicMetadata().filter_metadata_size()); - EXPECT_EQ(0, server_connection_->streamInfo().dynamicMetadata().typed_filter_metadata_size()); + EXPECT_EQ(1, server_connection_->streamInfo().dynamicMetadata().typed_filter_metadata_size()); auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); EXPECT_EQ(1, metadata.size()); diff --git a/test/extensions/filters/listener/tls_inspector/tls_inspector_integration_test.cc b/test/extensions/filters/listener/tls_inspector/tls_inspector_integration_test.cc index de46a2fc68346..f846b5378ce74 100644 --- a/test/extensions/filters/listener/tls_inspector/tls_inspector_integration_test.cc +++ b/test/extensions/filters/listener/tls_inspector/tls_inspector_integration_test.cc @@ -22,6 +22,53 @@ namespace Envoy { namespace { +class LargeBufferListenerFilter : public Network::ListenerFilter { +public: + static constexpr int BUFFER_SIZE = 512; + // Network::ListenerFilter + Network::FilterStatus onAccept(Network::ListenerFilterCallbacks&) override { + ENVOY_LOG_MISC(debug, "LargeBufferListenerFilter::onAccept"); + return Network::FilterStatus::StopIteration; + } + + // this needs to be smaller than the client hello, but larger than tls inspector's initial read + // buffer size. + size_t maxReadBytes() const override { return BUFFER_SIZE; } + + Network::FilterStatus onData(Network::ListenerFilterBuffer& buffer) override { + auto raw_slice = buffer.rawSlice(); + ENVOY_LOG_MISC(debug, "LargeBufferListenerFilter::onData: recv: {}", raw_slice.len_); + return Network::FilterStatus::Continue; + } +}; + +class LargeBufferListenerFilterConfigFactory + : public Server::Configuration::NamedListenerFilterConfigFactory { +public: + // NamedListenerFilterConfigFactory + Network::ListenerFilterFactoryCb createListenerFilterFactoryFromProto( + const Protobuf::Message&, + const Network::ListenerFilterMatcherSharedPtr& listener_filter_matcher, + Server::Configuration::ListenerFactoryContext&) override { + return [listener_filter_matcher](Network::ListenerFilterManager& filter_manager) -> void { + filter_manager.addAcceptFilter(listener_filter_matcher, + std::make_unique()); + }; + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; + } + + std::string name() const override { + // This fake original_dest should be used only in integration test! + return "envoy.filters.listener.large_buffer"; + } +}; +static Registry::RegisterFactory + register_; + class TlsInspectorIntegrationTest : public testing::TestWithParam, public BaseIntegrationTest { public: @@ -96,6 +143,38 @@ class TlsInspectorIntegrationTest : public testing::TestWithParammutable_listeners(0) + ->mutable_listener_filters_timeout(); + timeout->MergeFrom(ProtobufUtil::TimeUtil::MillisecondsToDuration(1000)); + bootstrap.mutable_static_resources() + ->mutable_listeners(0) + ->set_continue_on_listener_filters_timeout(true); + }); + BaseIntegrationTest::initialize(); + + context_manager_ = std::make_unique( + server_factory_context_); + } + void setupConnections(bool listener_filter_disabled, bool expect_connection_open, bool ssl_client, const std::string& log_format = "%RESPONSE_CODE_DETAILS%", const Ssl::ClientSslTransportOptions& ssl_options = {}, @@ -171,9 +250,8 @@ TEST_P(TlsInspectorIntegrationTest, DisabledTlsInspectorFailsFilterChainFind) { TEST_P(TlsInspectorIntegrationTest, ContinueOnListenerTimeout) { setupConnections(/*listener_filter_disabled=*/false, /*expect_connection_open=*/true, /*ssl_client=*/false); - // The length of tls hello message is defined as `TLS_MAX_CLIENT_HELLO = 64 * 1024` - // if tls inspect filter doesn't read the max length of hello message data, it - // will continue wait. Then the listener filter timeout timer will be triggered. + // The listener filter will not process the following data but will only wait for 1 second + // to timeout and then fall over to another listener filter chain. Buffer::OwnedImpl buffer("fake data"); client_->write(buffer, false); // The timeout is set as one seconds, advance 2 seconds to trigger the timeout. @@ -182,6 +260,36 @@ TEST_P(TlsInspectorIntegrationTest, ContinueOnListenerTimeout) { EXPECT_THAT(waitForAccessLog(listener_access_log_name_), testing::Eq("-")); } +TEST_P(TlsInspectorIntegrationTest, TlsInspectorMetadataPopulatedInAccessLog) { + initializeWithTlsInspector( + /*ssl_client=*/false, + /*log_format=*/"%DYNAMIC_METADATA(envoy.filters.listener.tls_inspector:failure_reason)%", + false, false, false); + Network::Address::InstanceConstSharedPtr address = + Ssl::getSslAddress(version_, lookupPort("echo")); + context_ = + Ssl::createClientSslTransportSocketFactory(/*ssl_options=*/{}, *context_manager_, *api_); + auto transport_socket_factory = std::make_unique(); + Network::TransportSocketPtr transport_socket = + transport_socket_factory->createTransportSocket(nullptr, nullptr); + client_ = dispatcher_->createClientConnection(address, Network::Address::InstanceConstSharedPtr(), + std::move(transport_socket), nullptr, nullptr); + std::shared_ptr payload_reader = + std::make_shared(*dispatcher_); + client_->addReadFilter(payload_reader); + client_->addConnectionCallbacks(connect_callbacks_); + client_->connect(); + Buffer::OwnedImpl buffer("fake data"); + client_->write(buffer, false); + while (!connect_callbacks_.connected() && !connect_callbacks_.closed()) { + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + // The timeout is set as one seconds, advance 2 seconds to trigger the timeout. + timeSystem().advanceTimeWaitImpl(std::chrono::milliseconds(2000)); + client_->close(Network::ConnectionCloseType::NoFlush); + EXPECT_THAT(waitForAccessLog(listener_access_log_name_), testing::Eq("ClientHelloNotDetected")); +} + // The `JA3` fingerprint is correct in the access log. TEST_P(TlsInspectorIntegrationTest, JA3FingerprintIsSet) { // These TLS options will create a client hello message with @@ -284,6 +392,47 @@ TEST_P(TlsInspectorIntegrationTest, RequestedBufferSizeCanGrow) { 515); } +TEST_P(TlsInspectorIntegrationTest, RequestedBufferSizeCanStartBig) { + initializeWithTlsInspectorWithLargeBufferFilter(); + + Network::Address::InstanceConstSharedPtr address = + Ssl::getSslAddress(version_, lookupPort("echo")); + + Ssl::ClientSslTransportOptions ssl_options; + ssl_options.setCipherSuites({"ECDHE-RSA-AES128-GCM-SHA256"}); + ssl_options.setTlsVersion(envoy::extensions::transport_sockets::tls::v3::TlsParameters::TLSv1_2); + const std::string really_long_sni(absl::StrCat(std::string(240, 'a'), ".foo.com")); + ssl_options.setSni(really_long_sni); + context_ = Ssl::createClientSslTransportSocketFactory(ssl_options, *context_manager_, *api_); + Network::TransportSocketPtr transport_socket = context_->createTransportSocket( + std::make_shared( + absl::string_view(""), std::vector(), std::vector{}), + nullptr); + + client_ = dispatcher_->createClientConnection(address, Network::Address::InstanceConstSharedPtr(), + std::move(transport_socket), nullptr, nullptr); + client_->addConnectionCallbacks(connect_callbacks_); + client_->connect(); + + while (!connect_callbacks_.connected() && !connect_callbacks_.closed()) { + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + + client_->close(Network::ConnectionCloseType::NoFlush); + + test_server_->waitUntilHistogramHasSamples("tls_inspector.bytes_processed"); + auto bytes_processed_histogram = test_server_->histogram("tls_inspector.bytes_processed"); + EXPECT_EQ( + TestUtility::readSampleCount(test_server_->server().dispatcher(), *bytes_processed_histogram), + 1); + auto bytes_processed = static_cast( + TestUtility::readSampleSum(test_server_->server().dispatcher(), *bytes_processed_histogram)); + EXPECT_EQ(bytes_processed, 515); + // Double check that the test is effective by ensuring that the + // LargeBufferListenerFilter::BUFFER_SIZE is smaller than the client hello. + EXPECT_GT(bytes_processed, LargeBufferListenerFilter::BUFFER_SIZE); +} + // This test verifies that `JA4` fingerprinting works with a malformed ClientHello that // should still be valid enough to extract a `JA4` hash TEST_P(TlsInspectorIntegrationTest, JA4FingerprintWithMalformedClientHello) { diff --git a/test/extensions/filters/listener/tls_inspector/tls_inspector_test.cc b/test/extensions/filters/listener/tls_inspector/tls_inspector_test.cc index 6580927c1f456..70be65038c9e9 100644 --- a/test/extensions/filters/listener/tls_inspector/tls_inspector_test.cc +++ b/test/extensions/filters/listener/tls_inspector/tls_inspector_test.cc @@ -103,7 +103,7 @@ class TlsInspectorTest : public testing::TestWithParam filter_; - Network::MockListenerFilterCallbacks cb_; + NiceMock cb_; Network::MockConnectionSocket socket_; NiceMock dispatcher_; Event::FileReadyCb file_event_callback_; @@ -281,6 +281,12 @@ TEST_P(TlsInspectorTest, ClientHelloTooBig) { mockSysCallForPeek(client_hello, true); EXPECT_CALL(socket_, detectedTransportProtocol()).Times(::testing::AnyNumber()); EXPECT_TRUE(file_event_callback_(Event::FileReadyType::Read).ok()); + + Protobuf::Struct expected_metadata; + auto& fields = *expected_metadata.mutable_fields(); + fields[Filter::failureReasonKey()].set_string_value(Filter::failureReasonClientHelloTooLarge()); + EXPECT_CALL(cb_, setDynamicMetadata(Filter::dynamicMetadataKey(), ProtoEq(expected_metadata))); + auto state = filter_->onData(*buffer_); EXPECT_EQ(Network::FilterStatus::StopIteration, state); EXPECT_EQ(1, cfg_->stats().client_hello_too_large_.value()); @@ -409,6 +415,13 @@ TEST_P(TlsInspectorTest, NotSsl) { mockSysCallForPeek(data); // trigger the event to copy the client hello message into buffer:q EXPECT_TRUE(file_event_callback_(Event::FileReadyType::Read).ok()); + + Protobuf::Struct expected_metadata; + auto& fields = *expected_metadata.mutable_fields(); + fields[Filter::failureReasonKey()].set_string_value( + Filter::failureReasonClientHelloNotDetected()); + EXPECT_CALL(cb_, setDynamicMetadata(Filter::dynamicMetadataKey(), ProtoEq(expected_metadata))); + auto state = filter_->onData(*buffer_); EXPECT_EQ(Network::FilterStatus::Continue, state); EXPECT_EQ(1, cfg_->stats().tls_not_found_.value()); diff --git a/test/extensions/filters/network/common/fuzz/validated_input_generator_any_map_extensions.cc b/test/extensions/filters/network/common/fuzz/validated_input_generator_any_map_extensions.cc index 8a5f234df6273..8b45226e90334 100644 --- a/test/extensions/filters/network/common/fuzz/validated_input_generator_any_map_extensions.cc +++ b/test/extensions/filters/network/common/fuzz/validated_input_generator_any_map_extensions.cc @@ -19,7 +19,7 @@ namespace ProtobufMessage { ValidatedInputGenerator::AnyMap composeFiltersAnyMap() { static const auto dummy_proto_msg = []() -> std::unique_ptr { - return std::make_unique(); + return std::make_unique(); }; static ValidatedInputGenerator::AnyMap any_map; diff --git a/test/extensions/filters/network/dubbo_proxy/mocks.cc b/test/extensions/filters/network/dubbo_proxy/mocks.cc index f42a21a3ee129..1c74494ed30dd 100644 --- a/test/extensions/filters/network/dubbo_proxy/mocks.cc +++ b/test/extensions/filters/network/dubbo_proxy/mocks.cc @@ -107,7 +107,7 @@ MockFilterConfigFactory::MockFilterConfigFactory() MockFilterConfigFactory::~MockFilterConfigFactory() = default; FilterFactoryCb -MockFilterConfigFactory::createFilterFactoryFromProtoTyped(const ProtobufWkt::Struct& proto_config, +MockFilterConfigFactory::createFilterFactoryFromProtoTyped(const Protobuf::Struct& proto_config, const std::string& stat_prefix, Server::Configuration::FactoryContext&) { config_struct_ = proto_config; diff --git a/test/extensions/filters/network/dubbo_proxy/mocks.h b/test/extensions/filters/network/dubbo_proxy/mocks.h index cee1ae8aa0bbe..616fa3417b6d5 100644 --- a/test/extensions/filters/network/dubbo_proxy/mocks.h +++ b/test/extensions/filters/network/dubbo_proxy/mocks.h @@ -300,18 +300,18 @@ template class MockFactoryBase : public NamedDubboFilterConf const std::string name_; }; -class MockFilterConfigFactory : public MockFactoryBase { +class MockFilterConfigFactory : public MockFactoryBase { public: MockFilterConfigFactory(); ~MockFilterConfigFactory() override; DubboFilters::FilterFactoryCb - createFilterFactoryFromProtoTyped(const ProtobufWkt::Struct& proto_config, + createFilterFactoryFromProtoTyped(const Protobuf::Struct& proto_config, const std::string& stat_prefix, Server::Configuration::FactoryContext& context) override; std::shared_ptr mock_filter_; - ProtobufWkt::Struct config_struct_; + Protobuf::Struct config_struct_; std::string config_stat_prefix_; }; diff --git a/test/extensions/filters/network/dubbo_proxy/route_matcher_test.cc b/test/extensions/filters/network/dubbo_proxy/route_matcher_test.cc index f9c6460fa6db7..649fa639fb8b8 100644 --- a/test/extensions/filters/network/dubbo_proxy/route_matcher_test.cc +++ b/test/extensions/filters/network/dubbo_proxy/route_matcher_test.cc @@ -959,7 +959,7 @@ interface: org.apache.dubbo.demo.DemoService criteria->metadataMatchCriteria(); EXPECT_EQ(2, mmc.size()); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -1035,7 +1035,7 @@ interface: org.apache.dubbo.demo.DemoService NiceMock context; SingleRouteMatcherImpl matcher(config, context); - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("v1"); v2.set_string_value("v2"); v3.set_string_value("v3"); @@ -1132,7 +1132,7 @@ interface: org.apache.dubbo.demo.DemoService NiceMock context; SingleRouteMatcherImpl matcher(config, context); - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("v1"); v2.set_string_value("v2"); v3.set_string_value("v3"); diff --git a/test/extensions/filters/network/dubbo_proxy/router_test.cc b/test/extensions/filters/network/dubbo_proxy/router_test.cc index 8f26e582a172e..1d12f85448b20 100644 --- a/test/extensions/filters/network/dubbo_proxy/router_test.cc +++ b/test/extensions/filters/network/dubbo_proxy/router_test.cc @@ -98,8 +98,8 @@ class DubboRouterTestBase { } void verifyMetadataMatchCriteriaFromRequest(bool route_entry_has_match) { - ProtobufWkt::Struct request_struct; - ProtobufWkt::Value val; + Protobuf::Struct request_struct; + Protobuf::Value val; // Populate metadata like StreamInfo.setDynamicMetadata() would. auto& fields_map = *request_struct.mutable_fields(); @@ -150,8 +150,8 @@ class DubboRouterTestBase { } void verifyMetadataMatchCriteriaFromRoute(bool route_entry_has_match) { - ProtobufWkt::Struct route_struct; - ProtobufWkt::Value val; + Protobuf::Struct route_struct; + Protobuf::Value val; // Populate metadata like StreamInfo.setDynamicMetadata() would. auto& fields_map = *route_struct.mutable_fields(); @@ -193,8 +193,8 @@ class DubboRouterTestBase { } void verifyMetadataMatchCriteriaFromPreviousCompute() { - ProtobufWkt::Struct request_struct; - ProtobufWkt::Value val; + Protobuf::Struct request_struct; + Protobuf::Value val; // Populate metadata like StreamInfo.setDynamicMetadata() would. auto& fields_map = *request_struct.mutable_fields(); diff --git a/test/extensions/filters/network/ext_authz/ext_authz_test.cc b/test/extensions/filters/network/ext_authz/ext_authz_test.cc index 2a4370303ab44..131a72d78ab6e 100644 --- a/test/extensions/filters/network/ext_authz/ext_authz_test.cc +++ b/test/extensions/filters/network/ext_authz/ext_authz_test.cc @@ -104,16 +104,16 @@ class ExtAuthzFilterTest : public testing::Test { (*fields)["ext_authz_duration"] = ValueUtil::numberValue(10); EXPECT_CALL(filter_callbacks_.connection_.stream_info_, setDynamicMetadata(_, _)) - .WillOnce(Invoke([&response](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { - EXPECT_EQ(ns, NetworkFilterNames::get().ExtAuthorization); - EXPECT_TRUE( - returned_dynamic_metadata.fields().at("ext_authz_duration").has_number_value()); - EXPECT_TRUE( - TestUtility::protoEqual(returned_dynamic_metadata, response.dynamic_metadata)); - EXPECT_EQ(response.dynamic_metadata.fields().at("ext_authz_duration").number_value(), - returned_dynamic_metadata.fields().at("ext_authz_duration").number_value()); - })); + .WillOnce(Invoke( + [&response](const std::string& ns, const Protobuf::Struct& returned_dynamic_metadata) { + EXPECT_EQ(ns, NetworkFilterNames::get().ExtAuthorization); + EXPECT_TRUE( + returned_dynamic_metadata.fields().at("ext_authz_duration").has_number_value()); + EXPECT_TRUE( + TestUtility::protoEqual(returned_dynamic_metadata, response.dynamic_metadata)); + EXPECT_EQ(response.dynamic_metadata.fields().at("ext_authz_duration").number_value(), + returned_dynamic_metadata.fields().at("ext_authz_duration").number_value()); + })); EXPECT_CALL(filter_callbacks_, continueReading()); request_callbacks_->onComplete(std::make_unique(response)); @@ -385,7 +385,7 @@ TEST_F(ExtAuthzFilterTest, ImmediateOK) { addr_); filter_callbacks_.connection_.dispatcher_.globalTimeSystem().advanceTimeWait( std::chrono::milliseconds(5)); - ProtobufWkt::Struct dynamic_metadata; + Protobuf::Struct dynamic_metadata; (*dynamic_metadata.mutable_fields())["baz"] = ValueUtil::stringValue("hello-ok"); (*dynamic_metadata.mutable_fields())["x"] = ValueUtil::numberValue(12); // Since this is a stack response, duration should be 0; @@ -404,7 +404,7 @@ TEST_F(ExtAuthzFilterTest, ImmediateOK) { EXPECT_CALL(filter_callbacks_.connection_.stream_info_, setDynamicMetadata(_, _)) .WillOnce(Invoke([&dynamic_metadata](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_TRUE(returned_dynamic_metadata.fields().contains("ext_authz_duration")); EXPECT_TRUE(dynamic_metadata.fields().contains("ext_authz_duration")); EXPECT_EQ(ns, NetworkFilterNames::get().ExtAuthorization); @@ -440,7 +440,7 @@ TEST_F(ExtAuthzFilterTest, ImmediateNOK) { addr_); filter_callbacks_.connection_.stream_info_.downstream_connection_info_provider_->setLocalAddress( addr_); - ProtobufWkt::Struct dynamic_metadata; + Protobuf::Struct dynamic_metadata; (*dynamic_metadata.mutable_fields())["baz"] = ValueUtil::stringValue("hello-nok"); (*dynamic_metadata.mutable_fields())["x"] = ValueUtil::numberValue(15); EXPECT_CALL(filter_callbacks_, continueReading()).Times(0); @@ -454,7 +454,7 @@ TEST_F(ExtAuthzFilterTest, ImmediateNOK) { }))); EXPECT_CALL(filter_callbacks_.connection_.stream_info_, setDynamicMetadata(_, _)) .WillOnce(Invoke([&dynamic_metadata](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_EQ(ns, NetworkFilterNames::get().ExtAuthorization); EXPECT_FALSE(returned_dynamic_metadata.fields().contains("ext_authz_duration")); EXPECT_FALSE(dynamic_metadata.fields().contains("ext_authz_duration")); diff --git a/test/extensions/filters/network/ext_proc/ext_proc_integration_test.cc b/test/extensions/filters/network/ext_proc/ext_proc_integration_test.cc index 12fb32b7c8f61..e339bd9fa6fb8 100644 --- a/test/extensions/filters/network/ext_proc/ext_proc_integration_test.cc +++ b/test/extensions/filters/network/ext_proc/ext_proc_integration_test.cc @@ -23,7 +23,7 @@ using envoy::service::network_ext_proc::v3::ProcessingResponse; // Test-only filter that sets both typed and untyped connection metadata based on filter config class MetadataSetterFilter : public Network::ReadFilter { public: - MetadataSetterFilter(const ProtobufWkt::Struct& filter_config) : filter_config_(filter_config) {} + MetadataSetterFilter(const Protobuf::Struct& filter_config) : filter_config_(filter_config) {} Network::FilterStatus onNewConnection() override { // Set untyped metadata from config @@ -50,11 +50,11 @@ class MetadataSetterFilter : public Network::ReadFilter { for (const auto& [namespace_name, string_value] : typed_namespaces.fields()) { if (string_value.has_string_value()) { // Create a StringValue - ProtobufWkt::StringValue string_proto; + Protobuf::StringValue string_proto; string_proto.set_value(string_value.string_value()); // Serialize to an Any - ProtobufWkt::Any typed_value; + Protobuf::Any typed_value; typed_value.PackFrom(string_proto); // Use the appropriate way to add typed metadata @@ -78,7 +78,7 @@ class MetadataSetterFilter : public Network::ReadFilter { private: Network::ReadFilterCallbacks* callbacks_{nullptr}; - const ProtobufWkt::Struct& filter_config_; + const Protobuf::Struct& filter_config_; }; class MetadataSetterFilterFactory : public Server::Configuration::NamedNetworkFilterConfigFactory { @@ -86,14 +86,14 @@ class MetadataSetterFilterFactory : public Server::Configuration::NamedNetworkFi absl::StatusOr createFilterFactoryFromProto(const Protobuf::Message& proto_config, Server::Configuration::FactoryContext&) override { - const auto& struct_config = dynamic_cast(proto_config); + const auto& struct_config = dynamic_cast(proto_config); return [struct_config](Network::FilterManager& filter_manager) -> void { filter_manager.addReadFilter(std::make_shared(struct_config)); }; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "test.metadata_setter"; } @@ -222,25 +222,25 @@ class NetworkExtProcFilterIntegrationTest for (int i = 0; i < filters->size(); i++) { if ((*filters)[i].name() == "test.metadata_setter") { - ProtobufWkt::Struct existing_config; + Protobuf::Struct existing_config; if ((*filters)[i].has_typed_config()) { (*filters)[i].typed_config().UnpackTo(&existing_config); } // Set untyped metadata if (!untyped_values.empty()) { - ProtobufWkt::Struct metadata_struct; + Protobuf::Struct metadata_struct; auto* fields = metadata_struct.mutable_fields(); for (const auto& [key, value] : untyped_values) { (*fields)[key].set_string_value(value); } - ProtobufWkt::Value namespace_value; + Protobuf::Value namespace_value; *namespace_value.mutable_struct_value() = metadata_struct; if (!existing_config.fields().contains("untyped_metadata")) { - ProtobufWkt::Value untyped_value; + Protobuf::Value untyped_value; existing_config.mutable_fields()->insert({"untyped_metadata", untyped_value}); } @@ -252,13 +252,13 @@ class NetworkExtProcFilterIntegrationTest // Set typed metadata if (typed_value.has_value()) { if (!existing_config.fields().contains("typed_metadata")) { - ProtobufWkt::Value typed_value; + Protobuf::Value typed_value; existing_config.mutable_fields()->insert({"typed_metadata", typed_value}); } auto* typed_metadata = existing_config.mutable_fields()->at("typed_metadata").mutable_struct_value(); - typed_metadata->mutable_fields()->insert({namespace_name, ProtobufWkt::Value()}); + typed_metadata->mutable_fields()->insert({namespace_name, Protobuf::Value()}); typed_metadata->mutable_fields() ->at(namespace_name) .set_string_value(typed_value.value()); @@ -486,13 +486,41 @@ TEST_P(NetworkExtProcFilterIntegrationTest, TcpProxyUpstreamHalfCloseBothWays) { ProcessingRequest write_request; ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, write_request)); - EXPECT_EQ(write_request.has_write_data(), true); - EXPECT_EQ(write_request.write_data().data(), "server_response"); - EXPECT_EQ(write_request.write_data().end_of_stream(), true); - sendWriteGrpcMessage("server_data_inspected", true); + if (!write_request.write_data().end_of_stream()) { + size_t total_upstream_data = 0; + // We got partial data without end_of_stream + std::string partial_data = write_request.write_data().data(); + std::string partial_response = partial_data + "_inspected"; + sendWriteGrpcMessage(partial_response, false); - tcp_client->waitForData("server_data_inspected"); + // Wait for client to receive the partial data + total_upstream_data += partial_response.length(); + ASSERT_TRUE(tcp_client->waitForData(total_upstream_data)); + + // Wait for the final message with end_of_stream + ProcessingRequest final_request; + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, final_request)); + EXPECT_EQ(final_request.has_write_data(), true); + EXPECT_EQ(final_request.write_data().end_of_stream(), true); + + // Respond to the final message + std::string final_data = final_request.write_data().data(); + std::string final_response = final_data.empty() ? "" : final_data + "_inspected"; + sendReadGrpcMessage(final_response, true); + + // Wait for the final data if non-empty + if (!final_response.empty()) { + total_upstream_data += final_response.length(); + ASSERT_TRUE(tcp_client->waitForData(total_upstream_data)); + } + } else { + // We got the complete data with end_of_stream in one message + EXPECT_EQ(write_request.write_data().data(), "server_response"); + EXPECT_EQ(write_request.write_data().end_of_stream(), true); + sendWriteGrpcMessage("server_data_inspected", true); + tcp_client->waitForData("server_data_inspected"); + } // Close everything ASSERT_TRUE(fake_upstream_connection->close()); @@ -933,7 +961,7 @@ TEST_P(NetworkExtProcFilterIntegrationTest, TypedMetadataForwarding) { EXPECT_EQ(typed_metadata.type_url(), "type.googleapis.com/google.protobuf.StringValue"); // Deserialize the StringValue to verify the content - ProtobufWkt::StringValue string_value; + Protobuf::StringValue string_value; EXPECT_TRUE(string_value.ParseFromString(typed_metadata.value())); EXPECT_EQ(string_value.value(), "hello-world"); @@ -979,7 +1007,7 @@ TEST_P(NetworkExtProcFilterIntegrationTest, BothTypedAndUntypedMetadataForwardin EXPECT_EQ(typed_metadata.type_url(), "type.googleapis.com/google.protobuf.StringValue"); // Deserialize the StringValue - ProtobufWkt::StringValue string_value; + Protobuf::StringValue string_value; EXPECT_TRUE(string_value.ParseFromString(typed_metadata.value())); EXPECT_EQ(string_value.value(), "typed-test-value"); diff --git a/test/extensions/filters/network/ext_proc/ext_proc_test.cc b/test/extensions/filters/network/ext_proc/ext_proc_test.cc index 8f9173a4f595e..a9584057ec4c4 100644 --- a/test/extensions/filters/network/ext_proc/ext_proc_test.cc +++ b/test/extensions/filters/network/ext_proc/ext_proc_test.cc @@ -132,15 +132,14 @@ class NetworkExtProcFilterTest : public testing::Test { void addDynamicMetadata(const std::string& namespace_key, const std::string& key, const std::string& value) { auto& metadata = *stream_info_.metadata_.mutable_filter_metadata(); - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; auto& fields = *struct_obj.mutable_fields(); fields[key].set_string_value(value); metadata[namespace_key] = struct_obj; } // Add typed dynamic metadata to the stream info - void addTypedDynamicMetadata(const std::string& namespace_key, - const ProtobufWkt::Any& typed_value) { + void addTypedDynamicMetadata(const std::string& namespace_key, const Protobuf::Any& typed_value) { stream_info_.metadata_.mutable_typed_filter_metadata()->insert({namespace_key, typed_value}); } @@ -719,7 +718,7 @@ TEST_F(NetworkExtProcFilterTest, TypedMetadataForwarding) { recreateFilterWithMetadataOptions({}, {"typed-namespace"}); // Create a typed metadata value - ProtobufWkt::Any typed_value; + Protobuf::Any typed_value; typed_value.set_type_url("type.googleapis.com/envoy.test.TestMessage"); typed_value.set_value("test-value"); @@ -727,7 +726,7 @@ TEST_F(NetworkExtProcFilterTest, TypedMetadataForwarding) { addTypedDynamicMetadata("typed-namespace", typed_value); // Create another typed value that shouldn't be forwarded - ProtobufWkt::Any other_typed_value; + Protobuf::Any other_typed_value; other_typed_value.set_type_url("type.googleapis.com/envoy.test.OtherMessage"); other_typed_value.set_value("other-value"); addTypedDynamicMetadata("other-namespace", other_typed_value); @@ -776,7 +775,7 @@ TEST_F(NetworkExtProcFilterTest, BothTypedAndUntypedMetadataForwarding) { addDynamicMetadata("untyped-ns", "key1", "value1"); // Add typed metadata - ProtobufWkt::Any typed_value; + Protobuf::Any typed_value; typed_value.set_type_url("type.googleapis.com/envoy.test.TestMessage"); typed_value.set_value("test-value"); addTypedDynamicMetadata("typed-ns", typed_value); diff --git a/test/extensions/filters/network/generic_proxy/codecs/dubbo/config_test.cc b/test/extensions/filters/network/generic_proxy/codecs/dubbo/config_test.cc index e2b1aff1f40da..e2ac0257081e7 100644 --- a/test/extensions/filters/network/generic_proxy/codecs/dubbo/config_test.cc +++ b/test/extensions/filters/network/generic_proxy/codecs/dubbo/config_test.cc @@ -230,19 +230,19 @@ TEST(DubboResponseTest, DubboResponseTest) { } TEST(DubboServerCodecTest, DubboServerCodecTest) { - auto codec = std::make_unique(); - codec->initilize(std::make_unique()); + NiceMock callbacks; + NiceMock mock_connection_; - MockServerCodecCallbacks callbacks; - DubboServerCodec server_codec(std::move(codec)); - server_codec.setCodecCallbacks(callbacks); - - auto raw_serializer = const_cast( - dynamic_cast(server_codec.codec_->serializer().get())); + ON_CALL(callbacks, connection()) + .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Decode failure. { - server_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); Buffer::OwnedImpl buffer; buffer.writeBEInt(0); buffer.writeBEInt(0); @@ -253,7 +253,10 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { // Waiting for header. { - server_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\xc2', 0x00})); @@ -264,7 +267,10 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { // Waiting for data. { - server_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\xc2', 0x00})); @@ -277,7 +283,13 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { // Decode request. { - server_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + + auto raw_serializer = const_cast( + dynamic_cast(server_codec.codec_->serializer().get())); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\xc2', 0x00})); @@ -294,7 +306,13 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { // Decode heartbeat request. { - server_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + + auto raw_serializer = const_cast( + dynamic_cast(server_codec.codec_->serializer().get())); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\xe2', 00})); @@ -309,7 +327,13 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { // Encode response. { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + auto raw_serializer = const_cast( + dynamic_cast(server_codec.codec_->serializer().get())); MockEncodingContext encoding_context; DubboRequest request(createDubboRequst(false)); DubboResponse response( @@ -322,6 +346,11 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { } { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + Status status = absl::OkStatus(); DubboRequest request(createDubboRequst(false)); @@ -336,6 +365,11 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { } { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + Status status(StatusCode::kInvalidArgument, "test_message"); DubboRequest request(createDubboRequst(false)); @@ -351,6 +385,11 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { } { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + Status status(StatusCode::kAborted, "test_message2"); DubboRequest request(createDubboRequst(false)); @@ -364,22 +403,42 @@ TEST(DubboServerCodecTest, DubboServerCodecTest) { EXPECT_EQ("anything", typed_inner_response.content().result()->toString().value().get()); EXPECT_EQ("test_message2", typed_inner_response.content().attachments().at("reason")); } + + // Decode buffer limit. + { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboServerCodec server_codec(std::move(codec)); + server_codec.setCodecCallbacks(callbacks); + + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(4)); + + Buffer::OwnedImpl buffer; + buffer.add(std::string({'\xda', '\xbb', '\xc2', 0x00})); + buffer.writeBEInt(1); + buffer.writeBEInt(8); + buffer.add("anything"); + + EXPECT_CALL(callbacks, onDecodingFailure(_)); + server_codec.decode(buffer, false); + } } TEST(DubboClientCodecTest, DubboClientCodecTest) { - auto codec = std::make_unique(); - codec->initilize(std::make_unique()); - MockClientCodecCallbacks callbacks; - DubboClientCodec client_codec(std::move(codec)); - client_codec.setCodecCallbacks(callbacks); + NiceMock callbacks; + NiceMock mock_connection_; - auto raw_serializer = const_cast( - dynamic_cast(client_codec.codec_->serializer().get())); + ON_CALL(callbacks, connection()) + .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Decode failure. { - client_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); Buffer::OwnedImpl buffer; buffer.writeBEInt(0); @@ -391,7 +450,10 @@ TEST(DubboClientCodecTest, DubboClientCodecTest) { // Waiting for header. { - client_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\x02', 20})); @@ -402,7 +464,10 @@ TEST(DubboClientCodecTest, DubboClientCodecTest) { // Waiting for data. { - client_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\x02', 20})); @@ -415,7 +480,13 @@ TEST(DubboClientCodecTest, DubboClientCodecTest) { // Decode response. { - client_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); + + auto raw_serializer = const_cast( + dynamic_cast(client_codec.codec_->serializer().get())); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\x02', 20})); @@ -435,7 +506,13 @@ TEST(DubboClientCodecTest, DubboClientCodecTest) { // Decode heartbeat request. { - client_codec.metadata_.reset(); + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); + + auto raw_serializer = const_cast( + dynamic_cast(client_codec.codec_->serializer().get())); Buffer::OwnedImpl buffer; buffer.add(std::string({'\xda', '\xbb', '\xe2', 00})); @@ -450,6 +527,14 @@ TEST(DubboClientCodecTest, DubboClientCodecTest) { // Encode normal request. { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); + + auto raw_serializer = const_cast( + dynamic_cast(client_codec.codec_->serializer().get())); + MockEncodingContext encoding_context; DubboRequest request(createDubboRequst(false)); @@ -462,6 +547,14 @@ TEST(DubboClientCodecTest, DubboClientCodecTest) { // Encode one-way request. { + auto codec = std::make_unique(); + codec->initilize(std::make_unique()); + DubboClientCodec client_codec(std::move(codec)); + client_codec.setCodecCallbacks(callbacks); + + auto raw_serializer = const_cast( + dynamic_cast(client_codec.codec_->serializer().get())); + MockEncodingContext encoding_context; DubboRequest request(createDubboRequst(true)); diff --git a/test/extensions/filters/network/generic_proxy/codecs/http1/config_test.cc b/test/extensions/filters/network/generic_proxy/codecs/http1/config_test.cc index ba774324ca363..b445334b652ac 100644 --- a/test/extensions/filters/network/generic_proxy/codecs/http1/config_test.cc +++ b/test/extensions/filters/network/generic_proxy/codecs/http1/config_test.cc @@ -260,6 +260,7 @@ class Http1ServerCodecTest : public testing::Test { TEST_F(Http1ServerCodecTest, DecodeHeaderOnlyRequest) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Empty methods. Call these methods to increase coverage. codec_->onStatusImpl("", 0); @@ -295,6 +296,7 @@ TEST_F(Http1ServerCodecTest, DecodeHeaderOnlyRequest) { TEST_F(Http1ServerCodecTest, DecodeRequestWithBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -334,6 +336,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestWithBody) { TEST_F(Http1ServerCodecTest, DecodeRequestAndCloseConnectionAfterHeader) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -355,6 +358,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestAndCloseConnectionAfterHeader) { TEST_F(Http1ServerCodecTest, DecodeRequestAndCloseConnectionAfterBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -377,6 +381,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestAndCloseConnectionAfterBody) { TEST_F(Http1ServerCodecTest, DecodeRequestWithChunkedBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -425,6 +430,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestWithChunkedBody) { TEST_F(Http1ServerCodecTest, DecodeRequestWithChunkedBodyWithMultipleFrames) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -480,6 +486,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestWithChunkedBodyWithMultipleFrames) { TEST_F(Http1ServerCodecTest, DecodeUnexpectedRequest) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Connect. { @@ -787,7 +794,7 @@ TEST_F(Http1ServerCodecTest, EncodeResponseWithChunkedBodyButNotSetChunkHeader) TEST_F(Http1ServerCodecTest, DecodeRequestAndEncodeResponse) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); - + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Do repeated request and response. for (size_t i = 0; i < 100; i++) { Buffer::OwnedImpl buffer; @@ -830,6 +837,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestAndEncodeResponse) { TEST_F(Http1ServerCodecTest, DecodeExpectRequestAndItWillBeRepliedDirectly) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -871,6 +879,7 @@ TEST_F(Http1ServerCodecTest, DecodeExpectRequestAndItWillBeRepliedDirectly) { TEST_F(Http1ServerCodecTest, ResponseCompleteBeforeRequestComplete) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -915,6 +924,7 @@ TEST_F(Http1ServerCodecTest, ResponseCompleteBeforeRequestComplete) { TEST_F(Http1ServerCodecTest, NewRequestBeforeFirstRequestComplete) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -947,6 +957,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestInSingleFrameMode) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -984,6 +995,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestInSingleFrameModeButBodyTooLarge1) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -1003,6 +1015,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestInSingleFrameModeButBodyTooLarge2) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -1022,6 +1035,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestWithChunkedBodyInSingleFrameMode) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -1063,6 +1077,7 @@ TEST_F(Http1ServerCodecTest, DecodeRequestWithChunkedBodyWithMultipleFramesInSin ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -1135,6 +1150,24 @@ TEST_F(Http1ServerCodecTest, EncodeResponseInSingleFrameMode) { } } +TEST_F(Http1ServerCodecTest, AboveDecodeBufferLimit) { + ON_CALL(codec_callbacks_, connection()) + .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(4)); + + Buffer::OwnedImpl buffer; + + buffer.add("GET / HTTP/1.1\r\n" + "Host: host\r\n" + "Content-Length: 4\r\n" + "custom: value\r\n" + "\r\n" + "body"); + + EXPECT_CALL(codec_callbacks_, onDecodingFailure(_)); + codec_->decode(buffer, false); +} + class Http1ClientCodecTest : public testing::Test { public: Http1ClientCodecTest() { initializeCodec(); } @@ -1223,6 +1256,7 @@ class Http1ClientCodecTest : public testing::Test { TEST_F(Http1ClientCodecTest, DecodeHeaderOnlyResponse) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Empty methods. Call these methods to increase coverage. codec_->onUrlImpl("", 0); @@ -1251,6 +1285,7 @@ TEST_F(Http1ClientCodecTest, DecodeHeaderOnlyResponse) { TEST_F(Http1ClientCodecTest, ResponseComesBeforeRequest) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -1265,6 +1300,7 @@ TEST_F(Http1ClientCodecTest, ResponseComesBeforeRequest) { TEST_F(Http1ClientCodecTest, DecodeHTTP10Response) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); Buffer::OwnedImpl buffer; @@ -1279,6 +1315,7 @@ TEST_F(Http1ClientCodecTest, DecodeHTTP10Response) { TEST_F(Http1ClientCodecTest, DecodeResponseForHeadRequest) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingHeadRequest(); @@ -1303,6 +1340,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseForHeadRequest) { TEST_F(Http1ClientCodecTest, DecodeResponseShouldNotHasBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1327,6 +1365,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseShouldNotHasBody) { TEST_F(Http1ClientCodecTest, Decode1xxResponseAndItWillBeIgnored) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1341,6 +1380,7 @@ TEST_F(Http1ClientCodecTest, Decode1xxResponseAndItWillBeIgnored) { TEST_F(Http1ClientCodecTest, DecodeResponseWithBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1376,6 +1416,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseWithBody) { TEST_F(Http1ClientCodecTest, DecodeResponseAndCloseConnectionAfterHeader) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1398,6 +1439,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseAndCloseConnectionAfterHeader) { TEST_F(Http1ClientCodecTest, DecodeResponseAndCloseConnectionAfterBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1421,6 +1463,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseAndCloseConnectionAfterBody) { TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1462,6 +1505,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBody) { TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBodyWithMultipleFrames) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); encodingGetRequest(); @@ -1511,6 +1555,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBodyWithMultipleFrames) { TEST_F(Http1ClientCodecTest, DecodeUnexpectedResponse) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Transfer-Encoding and Content-Length are set at same time. { @@ -1681,6 +1726,7 @@ TEST_F(Http1ClientCodecTest, DecodeUnexpectedResponse) { TEST_F(Http1ClientCodecTest, EncodeHeaderOnlyRequest) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Create a request. auto headers = Http::RequestHeaderMapImpl::create(); @@ -1711,6 +1757,7 @@ TEST_F(Http1ClientCodecTest, EncodeHeaderOnlyRequest) { TEST_F(Http1ClientCodecTest, EncodeRequestMissRequiredHeaders) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Create a request without method. auto headers = Http::RequestHeaderMapImpl::create(); @@ -1733,6 +1780,7 @@ TEST_F(Http1ClientCodecTest, EncodeRequestMissRequiredHeaders) { TEST_F(Http1ClientCodecTest, EncodeRequestWithBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Create a request. auto headers = Http::RequestHeaderMapImpl::create(); @@ -1774,6 +1822,7 @@ TEST_F(Http1ClientCodecTest, EncodeRequestWithBody) { TEST_F(Http1ClientCodecTest, EncodeRequestWithChunkdBody) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Create a request. auto headers = Http::RequestHeaderMapImpl::create(); @@ -1819,6 +1868,7 @@ TEST_F(Http1ClientCodecTest, EncodeRequestWithChunkdBody) { TEST_F(Http1ClientCodecTest, EncodeRequestAndDecodeResponse) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Do repeated request and response. for (size_t i = 0; i < 100; i++) { @@ -1864,6 +1914,7 @@ TEST_F(Http1ClientCodecTest, EncodeRequestAndDecodeResponse) { TEST_F(Http1ClientCodecTest, ResponseCompleteBeforeRequestComplete) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); // Create a request. auto headers = Http::RequestHeaderMapImpl::create(); @@ -1905,6 +1956,7 @@ TEST_F(Http1ClientCodecTest, ResponseCompleteBeforeRequestComplete) { TEST_F(Http1ClientCodecTest, EncodeRequestInSingleFrameMode) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); initializeCodec(true, 8 * 1024 * 1024); @@ -1938,6 +1990,7 @@ TEST_F(Http1ClientCodecTest, EncodeRequestInSingleFrameMode) { TEST_F(Http1ClientCodecTest, DecodeResponseInSingleFrameMode) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); initializeCodec(true, 8 * 1024 * 1024); @@ -1970,6 +2023,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseInSingleFrameMode) { TEST_F(Http1ClientCodecTest, DecodeResponseInSingleFrameModeButBodyIsTooLarge1) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); initializeCodec(true, 4); @@ -1990,6 +2044,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseInSingleFrameModeButBodyIsTooLarge1) TEST_F(Http1ClientCodecTest, DecodeResponseInSingleFrameModeButBodyIsTooLarge2) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); initializeCodec(true, 4); @@ -2010,6 +2065,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseInSingleFrameModeButBodyIsTooLarge2) TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBodyInSingleFrameMode) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); initializeCodec(true, 8 * 1024 * 1024); @@ -2046,6 +2102,7 @@ TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBodyInSingleFrameMode) { TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBodyWithMultipleFramesInSingleFrameMode) { ON_CALL(codec_callbacks_, connection()) .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(1024 * 1024)); initializeCodec(true, 8 * 1024 * 1024); @@ -2084,6 +2141,23 @@ TEST_F(Http1ClientCodecTest, DecodeResponseWithChunkedBodyWithMultipleFramesInSi codec_->decode(buffer, false); } +TEST_F(Http1ClientCodecTest, AboveDecodeBufferLimit) { + ON_CALL(codec_callbacks_, connection()) + .WillByDefault(testing::Return(makeOptRef(mock_connection_))); + ON_CALL(mock_connection_, bufferLimit()).WillByDefault(testing::Return(4)); + + Buffer::OwnedImpl buffer; + + buffer.add("HTTP/1.1 200 OK\r\n" + "Content-Length: 4\r\n" + "custom: value\r\n" + "\r\n" + "body"); + + EXPECT_CALL(codec_callbacks_, onDecodingFailure(_)); + codec_->decode(buffer, false); +} + TEST(Http1CodecFactoryTest, Http1CodecFactoryTest) { NiceMock context; ProtoConfig proto_config; diff --git a/test/extensions/filters/network/generic_proxy/config_test.cc b/test/extensions/filters/network/generic_proxy/config_test.cc index d21737c819798..d86da067b5599 100644 --- a/test/extensions/filters/network/generic_proxy/config_test.cc +++ b/test/extensions/filters/network/generic_proxy/config_test.cc @@ -273,7 +273,7 @@ TEST(BasicFilterConfigTest, CreatingCodecFactory) { TEST(BasicFilterConfigTest, CreatingFilterFactories) { NiceMock factory_context; - ProtobufWkt::RepeatedPtrField filters_proto_config; + Protobuf::RepeatedPtrField filters_proto_config; envoy::config::core::v3::TypedExtensionConfig codec_config; const std::string yaml_config_0 = R"EOF( diff --git a/test/extensions/filters/network/generic_proxy/fake_codec.h b/test/extensions/filters/network/generic_proxy/fake_codec.h index 1f9cd32f56378..a5b2c6389a899 100644 --- a/test/extensions/filters/network/generic_proxy/fake_codec.h +++ b/test/extensions/filters/network/generic_proxy/fake_codec.h @@ -64,6 +64,8 @@ class FakeStreamCodecFactory : public CodecFactory { class FakeResponse : public FakeStreamBase { public: + FakeResponse() = default; + FakeResponse(int code, bool ok) : status_(code, ok) {} absl::string_view protocol() const override { return protocol_; } StreamStatus status() const override { return status_; } @@ -163,6 +165,11 @@ class FakeStreamCodecFactory : public CodecFactory { } void setCodecCallbacks(ServerCodecCallbacks& callback) override { callback_ = &callback; } + void onConnected() override { + ASSERT(callback_->connection().has_value()); + ASSERT(callback_->connection()->state() == Network::Connection::State::Open); + ASSERT(!callback_->connection()->connecting()); + } void decode(Buffer::Instance& buffer, bool) override { ENVOY_LOG(debug, "FakeServerCodec::decode: {}", buffer.toString()); @@ -417,7 +424,7 @@ class FakeStreamCodecFactoryConfig : public CodecFactoryConfig { createCodecFactory(const Protobuf::Message& config, Envoy::Server::Configuration::ServerFactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::set configTypes() override { return {"envoy.generic_proxy.codecs.fake.type"}; } std::string name() const override { return "envoy.generic_proxy.codecs.fake"; } @@ -433,7 +440,7 @@ class FakeAccessLogExtensionFilterFactory : public AccessLog::ExtensionFilterFac public: // AccessLogFilterFactory AccessLog::FilterPtr createFilter(const envoy::config::accesslog::v3::ExtensionFilter&, - Server::Configuration::FactoryContext&) override { + Server::Configuration::GenericFactoryContext&) override { return std::make_unique(); } @@ -442,7 +449,7 @@ class FakeAccessLogExtensionFilterFactory : public AccessLog::ExtensionFilterFac } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.generic_proxy.access_log.fake"; } }; diff --git a/test/extensions/filters/network/generic_proxy/integration_test.cc b/test/extensions/filters/network/generic_proxy/integration_test.cc index 73111aa899e69..94c8bda2ea709 100644 --- a/test/extensions/filters/network/generic_proxy/integration_test.cc +++ b/test/extensions/filters/network/generic_proxy/integration_test.cc @@ -793,6 +793,55 @@ TEST_P(IntegrationTest, MultipleRequestsWithMultipleFrames) { cleanup(); } +TEST_P(IntegrationTest, UpstreamEndStreamFrameThenDisconnect) { + FakeStreamCodecFactoryConfig codec_factory_config; + Registry::InjectFactory registration(codec_factory_config); + + initialize(defaultConfig(true), std::make_unique()); + + EXPECT_TRUE(makeClientConnectionForTest()); + + FakeStreamCodecFactory::FakeRequest request; + request.host_ = "service_name_0"; + request.method_ = "hello"; + request.path_ = "/path_or_anything"; + request.protocol_ = "fake_fake_fake"; + request.data_ = {{"version", "v1"}}; + + sendRequestForTest(request); + + waitForUpstreamConnectionForTest(); + const std::function data_validator = + [](const std::string& data) -> bool { return data.find("v1") != std::string::npos; }; + waitForUpstreamRequestForTest(data_validator); + + FakeStreamCodecFactory::FakeResponse response; + response.protocol_ = "fake_fake_fake"; + response.status_ = StreamStatus(0, true); + response.data_["zzzz"] = "OK"; + response.stream_frame_flags_ = FrameFlags(1, 0); + sendResponseForTest(response); + + FakeStreamCodecFactory::FakeCommonFrame error; + error.data_["zzzz"] = "OK"; + error.stream_frame_flags_ = + FrameFlags(1, FrameFlags::FLAG_END_STREAM | FrameFlags::FLAG_DRAIN_CLOSE); + sendResponseForTest(response); + + // Partial cleanup (upstream only) + AssertionResult result = upstream_connection_->close(); + RELEASE_ASSERT(result, result.message()); + result = upstream_connection_->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + upstream_connection_.reset(); + + // Run the event loop after the upstream connection is closed and reset, but before closing the + // client connection. + integration_->dispatcher_->run(Envoy::Event::Dispatcher::RunType::Block); + + client_connection_->close(Envoy::Network::ConnectionCloseType::NoFlush); +} + } // namespace } // namespace GenericProxy } // namespace NetworkFilters diff --git a/test/extensions/filters/network/generic_proxy/mocks/codec.h b/test/extensions/filters/network/generic_proxy/mocks/codec.h index e1c7ff8c167e4..8a89e5cc3a079 100644 --- a/test/extensions/filters/network/generic_proxy/mocks/codec.h +++ b/test/extensions/filters/network/generic_proxy/mocks/codec.h @@ -55,6 +55,7 @@ class MockServerCodec : public ServerCodec { } MOCK_METHOD(void, setCodecCallbacks, (ServerCodecCallbacks & callbacks)); + MOCK_METHOD(void, onConnected, ()); MOCK_METHOD(void, decode, (Buffer::Instance & buffer, bool end_stream)); MOCK_METHOD(EncodingResult, encode, (const StreamFrame&, EncodingContext& ctx)); MOCK_METHOD(ResponseHeaderFramePtr, respond, @@ -100,7 +101,7 @@ class MockStreamCodecFactoryConfig : public CodecFactoryConfig { (const Protobuf::Message&, Server::Configuration::ServerFactoryContext&)); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::set configTypes() override { return {"envoy.generic_proxy.codecs.mock.type"}; } std::string name() const override { return "envoy.generic_proxy.codecs.mock"; } diff --git a/test/extensions/filters/network/generic_proxy/mocks/filter.cc b/test/extensions/filters/network/generic_proxy/mocks/filter.cc index 4e5e3cf487263..8c4f8ab5394b6 100644 --- a/test/extensions/filters/network/generic_proxy/mocks/filter.cc +++ b/test/extensions/filters/network/generic_proxy/mocks/filter.cc @@ -17,7 +17,7 @@ MockRequestFramesHandler::MockRequestFramesHandler() = default; MockStreamFilterConfig::MockStreamFilterConfig() { ON_CALL(*this, createEmptyConfigProto()).WillByDefault(Invoke([]() { - return std::make_unique(); + return std::make_unique(); })); ON_CALL(*this, createFilterFactoryFromProto(_, _, _)) .WillByDefault(Return([](FilterChainFactoryCallbacks&) {})); diff --git a/test/extensions/filters/network/generic_proxy/proxy_test.cc b/test/extensions/filters/network/generic_proxy/proxy_test.cc index 06cfe59f53140..73ccb1556bad9 100644 --- a/test/extensions/filters/network/generic_proxy/proxy_test.cc +++ b/test/extensions/filters/network/generic_proxy/proxy_test.cc @@ -62,7 +62,7 @@ class FilterConfigTest : public testing::Test { TestUtility::loadFromYaml(tracing_config_yaml, tracing_config); - tracing_config_ = std::make_unique( + tracing_config_ = std::make_unique( envoy::config::core::v3::TrafficDirection::OUTBOUND, tracing_config); } @@ -232,6 +232,12 @@ class FilterTest : public FilterConfigTest { TEST_F(FilterTest, SimpleOnNewConnection) { initializeFilter(); + + EXPECT_CALL(*server_codec_, onConnected()).WillOnce(Invoke([this] { + ASSERT_NE(server_codec_callbacks_, nullptr); + ASSERT_EQ(&filter_callbacks_.connection_, server_codec_callbacks_->connection().ptr()); + })); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); } diff --git a/test/extensions/filters/network/generic_proxy/route_test.cc b/test/extensions/filters/network/generic_proxy/route_test.cc index 56aa14d4e114b..3eb597570f12b 100644 --- a/test/extensions/filters/network/generic_proxy/route_test.cc +++ b/test/extensions/filters/network/generic_proxy/route_test.cc @@ -85,7 +85,7 @@ class BazFactory : public RouteTypedMetadataFactory { std::string name() const override { return "baz"; } // Returns nullptr (conversion failure) if d is empty. std::unique_ptr - parse(const ProtobufWkt::Struct& d) const override { + parse(const Protobuf::Struct& d) const override { if (d.fields().find("name") != d.fields().end()) { return std::make_unique(d.fields().at("name").string_value()); } @@ -93,7 +93,7 @@ class BazFactory : public RouteTypedMetadataFactory { } std::unique_ptr - parse(const ProtobufWkt::Any&) const override { + parse(const Protobuf::Any&) const override { return nullptr; } }; @@ -130,7 +130,7 @@ TEST_F(RouteEntryImplTest, RouteTypedMetadata) { */ TEST_F(RouteEntryImplTest, RoutePerFilterConfig) { ON_CALL(filter_config_, createEmptyRouteConfigProto()).WillByDefault(Invoke([]() { - return std::make_unique(); + return std::make_unique(); })); Registry::InjectFactory registration(filter_config_); @@ -175,7 +175,7 @@ TEST_F(RouteEntryImplTest, RouteTimeout) { */ TEST_F(RouteEntryImplTest, RoutePerFilterConfigWithUnknownType) { ON_CALL(filter_config_, createEmptyRouteConfigProto()).WillByDefault(Invoke([]() { - return std::make_unique(); + return std::make_unique(); })); Registry::InjectFactory registration(filter_config_); @@ -206,7 +206,7 @@ TEST_F(RouteEntryImplTest, RoutePerFilterConfigWithUnknownTypeButEnableExtension scoped_runtime.mergeValues({{"envoy.reloadable_features.no_extension_lookup_by_name", "false"}}); ON_CALL(filter_config_, createEmptyRouteConfigProto()).WillByDefault(Invoke([]() { - return std::make_unique(); + return std::make_unique(); })); Registry::InjectFactory registration(filter_config_); @@ -264,7 +264,7 @@ TEST_F(RouteEntryImplTest, NullRouteEmptyProto) { TEST_F(RouteEntryImplTest, NullRouteSpecificConfig) { Registry::InjectFactory registration(filter_config_); ON_CALL(filter_config_, createEmptyRouteConfigProto()).WillByDefault(Invoke([]() { - return std::make_unique(); + return std::make_unique(); })); const std::string yaml_config = R"EOF( @@ -279,16 +279,6 @@ TEST_F(RouteEntryImplTest, NullRouteSpecificConfig) { EXPECT_EQ(route_->perFilterConfig("envoy.filters.generic.mock_filter"), nullptr); }; -/** - * Test the simple route action wrapper. - */ -TEST(RouteMatchActionTest, SimpleRouteMatchActionTest) { - auto entry = std::make_shared>(); - RouteMatchAction action(entry); - - EXPECT_EQ(action.route().get(), entry.get()); -} - /** * Test the simple data input validator. */ @@ -321,13 +311,11 @@ TEST(RouteMatchActionFactoryTest, SimpleRouteMatchActionFactoryTest) { TestUtility::loadFromYaml(yaml_config, proto_config); RouteActionContext context{server_context}; - auto factory_cb = factory.createActionFactoryCb(proto_config, context, - server_context.messageValidationVisitor()); - - EXPECT_EQ(factory_cb()->getTyped().route().get(), - factory_cb()->getTyped().route().get()); + auto action = + factory.createAction(proto_config, context, server_context.messageValidationVisitor()); - EXPECT_EQ(factory_cb()->getTyped().route()->clusterName(), "cluster_0"); + EXPECT_NE(action, nullptr); + EXPECT_EQ(action->getTyped().clusterName(), "cluster_0"); } class RouteMatcherImplTest : public testing::Test { diff --git a/test/extensions/filters/network/generic_proxy/router/router_test.cc b/test/extensions/filters/network/generic_proxy/router/router_test.cc index 8c2fd694efbc8..b06084849b6f8 100644 --- a/test/extensions/filters/network/generic_proxy/router/router_test.cc +++ b/test/extensions/filters/network/generic_proxy/router/router_test.cc @@ -196,8 +196,8 @@ class RouterFilterTest : public testing::Test { } void verifyMetadataMatchCriteria() { - ProtobufWkt::Struct request_struct; - ProtobufWkt::Value val; + Protobuf::Struct request_struct; + Protobuf::Value val; // Populate metadata like StreamInfo.setDynamicMetadata() would. auto& fields_map = *request_struct.mutable_fields(); @@ -961,6 +961,35 @@ TEST_F(RouterFilterTest, UpstreamRequestPoolReadyAndRequestEncodingFailure) { mock_downstream_connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); } +TEST_F(RouterFilterTest, UpstreamRequestPoolReadyAndResponseStatusError) { + setup(); + kickOffNewUpstreamRequest(true); + + EXPECT_CALL(mock_generic_upstream_->mock_client_codec_, encode(_, _)) + .WillOnce(Invoke([this](const StreamFrame&, EncodingContext& ctx) -> EncodingResult { + EXPECT_EQ(ctx.routeEntry().ptr(), &mock_route_entry_); + return 0; + })); + + expectInjectContextToUpstreamRequest(); + + notifyUpstreamSuccess(); + + EXPECT_CALL(mock_filter_callback_, onResponseHeaderFrame(_)).WillOnce(Invoke([this](ResponsePtr) { + // When the response is sent to callback, the upstream request should be removed. + EXPECT_EQ(0, filter_->upstreamRequestsSize()); + })); + EXPECT_CALL(*mock_generic_upstream_, removeUpstreamRequest(_)); + EXPECT_CALL(*mock_generic_upstream_, cleanUp(false)); + expectFinalizeUpstreamSpanWithError(); + + auto response = std::make_unique(0, false); + notifyDecodingSuccess(std::move(response), {}); + + // Mock downstream closing. + mock_downstream_connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); +} + TEST_F(RouterFilterTest, LoadBalancerContextDownstreamConnection) { setup(); EXPECT_CALL(mock_filter_callback_, connection()); diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index f471b6664c9de..3ebc18497a5ac 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -154,6 +154,21 @@ stat_prefix: router "chain."); } +TEST_F(HttpConnectionManagerConfigTest, NonXdsTpRouteWithoutConfigSource) { + const std::string yaml_string = R"EOF( +codec_type: http1 +stat_prefix: router +rds: + route_config_name: route1 +http_filters: +- name: foo + )EOF"; + + EXPECT_THROW_WITH_REGEX( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "An RDS config must have either a 'config_source' or an xDS-TP based 'route_config_name'"); +} + TEST_F(HttpConnectionManagerConfigTest, MiscConfig) { const std::string yaml_string = R"EOF( codec_type: http1 @@ -727,28 +742,6 @@ TEST_F(HttpConnectionManagerConfigTest, DefaultInternalAddress) { EXPECT_FALSE(config.internalAddressConfig().isInternalAddress(default_ip_address)); } -TEST_F(HttpConnectionManagerConfigTest, LegacyDefaultInternalAddress) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues( - {{"envoy.reloadable_features.explicit_internal_address_config", "false"}}); - const std::string yaml_string = R"EOF( - stat_prefix: ingress_http - route_config: - name: local_route - http_filters: - - name: envoy.filters.http.router - )EOF"; - - HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, - date_provider_, route_config_provider_manager_, - &scoped_routes_config_provider_manager_, tracer_manager_, - filter_config_provider_manager_, creation_status_); - ASSERT_TRUE(creation_status_.ok()); - // Previously, Envoy considered RFC1918 IP addresses to be internal, by default. - Network::Address::Ipv4Instance default_ip_address{"10.48.179.130", 0, nullptr}; - EXPECT_TRUE(config.internalAddressConfig().isInternalAddress(default_ip_address)); -} - TEST_F(HttpConnectionManagerConfigTest, CidrRangeBasedInternalAddress) { const std::string yaml_string = R"EOF( stat_prefix: ingress_http @@ -913,6 +906,119 @@ TEST_F(HttpConnectionManagerConfigTest, DisabledStreamIdleTimeout) { EXPECT_EQ(0, config.streamIdleTimeout().count()); } +TEST_F(HttpConnectionManagerConfigTest, StreamIdleTimeoutDefault) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + ASSERT_TRUE(creation_status_.ok()); + // 5 minutes -> ms. + EXPECT_EQ(5 * 60 * 1000, config.streamIdleTimeout().count()); +} + +// Tracks stream_idle_timeout. If neither stream_idle_timeout nor stream_flush_timeout are set, +// stream_flush_timeout should default to stream_idle_timeout's default. +TEST_F(HttpConnectionManagerConfigTest, StreamFlushTimeoutDefault) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + ASSERT_TRUE(creation_status_.ok()); + ASSERT_TRUE(config.streamFlushTimeout().has_value()); + // 5 minutes. + EXPECT_EQ(5 * 60 * 1000, config.streamFlushTimeout().value().count()); +} + +// If stream_idle_timeout is set and stream_flush_timeout is not set, stream_flush_timeout should +// default to stream_idle_timeout. +TEST_F(HttpConnectionManagerConfigTest, StreamFlushTimeoutDefaultStreamIdleTimeoutSet) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + stream_idle_timeout: 10s + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + ASSERT_TRUE(creation_status_.ok()); + ASSERT_TRUE(config.streamFlushTimeout().has_value()); + // 10 seconds. + EXPECT_EQ(10 * 1000, config.streamFlushTimeout().value().count()); +} + +// Validate that an explicit zero stream flush timeout disables it. +TEST_F(HttpConnectionManagerConfigTest, DisabledStreamFlushTimeout) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + stream_flush_timeout: 0s + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + ASSERT_TRUE(creation_status_.ok()); + ASSERT_TRUE(config.streamFlushTimeout().has_value()); + EXPECT_EQ(0, config.streamFlushTimeout().value().count()); +} + +// Validate that the flush timeout and idle timeout can be set independently. +TEST_F(HttpConnectionManagerConfigTest, StreamFlushTimeoutAndStreamIdleTimeoutSet) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + stream_idle_timeout: 10s + stream_flush_timeout: 20s + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + ASSERT_TRUE(creation_status_.ok()); + EXPECT_EQ(10 * 1000, config.streamIdleTimeout().count()); + ASSERT_TRUE(config.streamFlushTimeout().has_value()); + EXPECT_EQ(20 * 1000, config.streamFlushTimeout().value().count()); +} + // Validate that idle_timeout set in common_http_protocol_options is used. TEST_F(HttpConnectionManagerConfigTest, CommonHttpProtocolIdleTimeout) { const std::string yaml_string = R"EOF( @@ -2615,7 +2721,7 @@ class OriginalIPDetectionExtensionNotCreatedFactory : public Http::OriginalIPDet } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { @@ -2631,7 +2737,7 @@ class EarlyHeaderMutationExtensionNotCreatedFactory : public Http::EarlyHeaderMu } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { @@ -3241,7 +3347,7 @@ class DefaultHeaderValidatorFactoryConfigOverride : public Http::HeaderValidator createFromProto(const Protobuf::Message& message, Server::Configuration::ServerFactoryContext& server_context) override { auto mptr = ::Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(message), server_context.messageValidationVisitor(), + dynamic_cast(message), server_context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidate(); + return std::make_unique(); } absl::StatusOr @@ -243,7 +243,7 @@ createMatchingTree(const std::string& name, const std::string& value) { std::make_unique(name), absl::nullopt); tree->addChild(value, Matcher::OnMatch{ - []() { return std::make_unique(); }, nullptr, false}); + std::make_shared(), nullptr, false}); return tree; } @@ -376,7 +376,7 @@ TEST(DelegatingNetworkFilterManager, RemoveReadFilterAndInitializeReadFilters) { } // Custom action type for testing non-skip action -class TestAction : public Matcher::ActionBase { +class TestAction : public Matcher::ActionBase { public: explicit TestAction(const std::string& value = "test_value") : value_(value) {} @@ -394,7 +394,7 @@ createMatchingTreeWithTestAction(const std::string& name, const std::string& val std::make_unique(name), absl::nullopt); tree->addChild(value, Matcher::OnMatch{ - []() { return std::make_unique(); }, nullptr, false}); + std::make_shared(), nullptr, false}); return tree; } diff --git a/test/extensions/filters/network/match_delegate/match_delegate_integration_test.cc b/test/extensions/filters/network/match_delegate/match_delegate_integration_test.cc index b8d612b81f9d1..49cb75b094aaa 100644 --- a/test/extensions/filters/network/match_delegate/match_delegate_integration_test.cc +++ b/test/extensions/filters/network/match_delegate/match_delegate_integration_test.cc @@ -18,21 +18,21 @@ namespace MatchDelegate { namespace { using envoy::extensions::common::matching::v3::ExtensionWithMatcher; -using Envoy::ProtobufWkt::StringValue; -using Envoy::ProtobufWkt::UInt32Value; +using Envoy::Protobuf::StringValue; +using Envoy::Protobuf::UInt32Value; // A simple network filter that counts connections and data. class CountingFilter : public Network::Filter { public: // Read filter methods Network::FilterStatus onData(Buffer::Instance& data, bool) override { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); data_bytes_ += data.length(); return Network::FilterStatus::Continue; } Network::FilterStatus onNewConnection() override { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); connection_count_++; return Network::FilterStatus::Continue; } @@ -43,7 +43,7 @@ class CountingFilter : public Network::Filter { // Write filter methods Network::FilterStatus onWrite(Buffer::Instance& data, bool) override { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); write_bytes_ += data.length(); return Network::FilterStatus::Continue; } @@ -54,23 +54,23 @@ class CountingFilter : public Network::Filter { // Thread-safe getters for counter values static uint32_t getConnectionCount() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return connection_count_; } static uint64_t getDataBytes() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return data_bytes_; } static uint64_t getWriteBytes() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); return write_bytes_; } // Reset all counters static void resetCounters() { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); connection_count_ = 0; data_bytes_ = 0; write_bytes_ = 0; diff --git a/test/extensions/filters/network/ratelimit/ratelimit_test.cc b/test/extensions/filters/network/ratelimit/ratelimit_test.cc index 8c51eea83bf7a..f183d6eeedff7 100644 --- a/test/extensions/filters/network/ratelimit/ratelimit_test.cc +++ b/test/extensions/filters/network/ratelimit/ratelimit_test.cc @@ -291,7 +291,7 @@ TEST_F(RateLimitFilterTest, OverLimitWithDynamicMetadata) { EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data, false)); Filters::Common::RateLimit::DynamicMetadataPtr dynamic_metadata = - std::make_unique(); + std::make_unique(); auto* fields = dynamic_metadata->mutable_fields(); (*fields)["name"] = ValueUtil::stringValue("my-limit"); (*fields)["x"] = ValueUtil::numberValue(3); @@ -299,7 +299,7 @@ TEST_F(RateLimitFilterTest, OverLimitWithDynamicMetadata) { EXPECT_CALL(filter_callbacks_.connection_, streamInfo()).WillOnce(ReturnRef(stream_info)); EXPECT_CALL(stream_info, setDynamicMetadata(_, _)) .WillOnce(Invoke([&dynamic_metadata](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_EQ(ns, NetworkFilterNames::get().RateLimit); EXPECT_TRUE(TestUtility::protoEqual(returned_dynamic_metadata, *dynamic_metadata)); })); diff --git a/test/extensions/filters/network/rbac/filter_test.cc b/test/extensions/filters/network/rbac/filter_test.cc index 46af305b79fd7..4229b293b5cbb 100644 --- a/test/extensions/filters/network/rbac/filter_test.cc +++ b/test/extensions/filters/network/rbac/filter_test.cc @@ -207,6 +207,16 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test { .WillByDefault(ReturnPointee(stream_info_.downstream_connection_info_provider_)); } + void setLocalAddressWithNetworkNamespace(const std::string& network_namespace_path, + uint16_t port = 123) { + address_ = std::make_shared( + "127.0.0.1", port, nullptr, absl::make_optional(std::string(network_namespace_path))); + + stream_info_.downstream_connection_info_provider_->setLocalAddress(address_); + ON_CALL(callbacks_.connection_.stream_info_, downstreamAddressProvider()) + .WillByDefault(ReturnPointee(stream_info_.downstream_connection_info_provider_)); + } + void checkAccessLogMetadata(bool expected) { auto filter_meta = stream_info_.dynamicMetadata().filter_metadata().at( Filters::Common::RBAC::DynamicMetadataKeysSingleton::get().CommonNamespace); @@ -218,18 +228,18 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test { void setMetadata() { ON_CALL(stream_info_, setDynamicMetadata(NetworkFilterNames::get().Rbac, _)) - .WillByDefault(Invoke([this](const std::string&, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([this](const std::string&, const Protobuf::Struct& obj) { stream_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair(NetworkFilterNames::get().Rbac, - obj)); + Protobuf::MapPair(NetworkFilterNames::get().Rbac, + obj)); })); ON_CALL(stream_info_, setDynamicMetadata( Filters::Common::RBAC::DynamicMetadataKeysSingleton::get().CommonNamespace, _)) - .WillByDefault(Invoke([this](const std::string&, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([this](const std::string&, const Protobuf::Struct& obj) { stream_info_.metadata_.mutable_filter_metadata()->insert( - Protobuf::MapPair( + Protobuf::MapPair( Filters::Common::RBAC::DynamicMetadataKeysSingleton::get().CommonNamespace, obj)); })); } @@ -494,6 +504,106 @@ TEST_F(RoleBasedAccessControlNetworkFilterTest, MatcherDenied) { filter_meta.fields().at("shadow_rules_prefix_shadow_engine_result").string_value()); } +TEST_F(RoleBasedAccessControlNetworkFilterTest, MatcherNetworkNamespaceAllowed) { + envoy::extensions::filters::network::rbac::v3::RBAC config; + config.set_stat_prefix("tcp."); + config.set_shadow_rules_stat_prefix("shadow_rules_prefix_"); + + const std::string matcher_yaml = R"EOF( +matcher_list: + matchers: + - predicate: + single_predicate: + input: + name: envoy.matching.inputs.network_namespace + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.NetworkNamespaceInput + value_match: + exact: "/var/run/netns/ns1" + on_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/envoy.config.rbac.v3.Action + name: allow_ns + action: ALLOW +on_no_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/envoy.config.rbac.v3.Action + name: deny_all + action: DENY +)EOF"; + + xds::type::matcher::v3::Matcher matcher; + TestUtility::loadFromYaml(matcher_yaml, matcher); + *config.mutable_matcher() = matcher; + + config_ = std::make_shared( + config, *store_.rootScope(), context_, ProtobufMessage::getStrictValidationVisitor()); + initFilter(); + + setLocalAddressWithNetworkNamespace("/var/run/netns/ns1", 123); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(1U, config_->stats().allowed_.value()); + EXPECT_EQ(0U, config_->stats().denied_.value()); +} + +TEST_F(RoleBasedAccessControlNetworkFilterTest, MatcherNetworkNamespaceDenied) { + envoy::extensions::filters::network::rbac::v3::RBAC config; + config.set_stat_prefix("tcp."); + config.set_shadow_rules_stat_prefix("shadow_rules_prefix_"); + + const std::string matcher_yaml = R"EOF( +matcher_list: + matchers: + - predicate: + single_predicate: + input: + name: envoy.matching.inputs.network_namespace + typed_config: + "@type": type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.NetworkNamespaceInput + value_match: + exact: "/var/run/netns/ns1" + on_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/envoy.config.rbac.v3.Action + name: allow_ns + action: ALLOW +on_no_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/envoy.config.rbac.v3.Action + name: deny_all + action: DENY +)EOF"; + + xds::type::matcher::v3::Matcher matcher; + TestUtility::loadFromYaml(matcher_yaml, matcher); + *config.mutable_matcher() = matcher; + + config_ = std::make_shared( + config, *store_.rootScope(), context_, ProtobufMessage::getStrictValidationVisitor()); + initFilter(); + + setLocalAddressWithNetworkNamespace("/var/run/netns/other", 123); + + EXPECT_CALL(callbacks_.connection_, close(Network::ConnectionCloseType::NoFlush, _)).Times(2); + + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data_, false)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data_, false)); + EXPECT_EQ(0U, config_->stats().allowed_.value()); + EXPECT_EQ(1U, config_->stats().denied_.value()); +} + // Log Tests TEST_F(RoleBasedAccessControlNetworkFilterTest, ShouldLog) { setupPolicy(true, false, envoy::config::rbac::v3::RBAC::LOG); diff --git a/test/extensions/filters/network/redis_proxy/BUILD b/test/extensions/filters/network/redis_proxy/BUILD index 7de9d7778560d..6da2d7901c583 100644 --- a/test/extensions/filters/network/redis_proxy/BUILD +++ b/test/extensions/filters/network/redis_proxy/BUILD @@ -209,6 +209,7 @@ envoy_extension_cc_test( deps = [ "//source/extensions/filters/network/common/redis:fault_lib", "//source/extensions/filters/network/redis_proxy:config", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:integration_lib", "@envoy_api//envoy/service/redis_auth/v3:pkg_cc_proto", ], diff --git a/test/extensions/filters/network/redis_proxy/command_splitter_impl_test.cc b/test/extensions/filters/network/redis_proxy/command_splitter_impl_test.cc index 907545706bf31..a30ad3706e32d 100644 --- a/test/extensions/filters/network/redis_proxy/command_splitter_impl_test.cc +++ b/test/extensions/filters/network/redis_proxy/command_splitter_impl_test.cc @@ -407,7 +407,7 @@ TEST_P(RedisSingleServerRequestTest, NoUpstream) { }; INSTANTIATE_TEST_SUITE_P(RedisSingleServerRequestTest, RedisSingleServerRequestTest, - testing::ValuesIn(Common::Redis::SupportedCommands::simpleCommands())); + testing::Values("get", "set", "incr", "zadd")); INSTANTIATE_TEST_SUITE_P(RedisSimpleRequestCommandHandlerMixedCaseTests, RedisSingleServerRequestTest, testing::Values("INCR", "inCrBY")); @@ -1338,7 +1338,7 @@ TEST_P(RedisSingleServerRequestWithLatencyMicrosTest, Success) { INSTANTIATE_TEST_SUITE_P(RedisSingleServerRequestWithLatencyMicrosTest, RedisSingleServerRequestWithLatencyMicrosTest, - testing::ValuesIn(Common::Redis::SupportedCommands::simpleCommands())); + testing::Values("get", "set", "incr", "zadd")); // In subclasses of fault test, we mock the expected faults in the constructor, as the // fault manager is owned by the splitter, which is also generated later in construction @@ -1393,7 +1393,7 @@ class RedisSingleServerRequestWithErrorWithDelayFaultTest INSTANTIATE_TEST_SUITE_P(RedisSingleServerRequestWithErrorFaultTest, RedisSingleServerRequestWithErrorFaultTest, - testing::ValuesIn(Common::Redis::SupportedCommands::simpleCommands())); + testing::Values("get", "set", "incr", "zadd")); TEST_P(RedisSingleServerRequestWithErrorWithDelayFaultTest, Fault) { InSequence s; @@ -1427,7 +1427,7 @@ TEST_P(RedisSingleServerRequestWithErrorWithDelayFaultTest, Fault) { INSTANTIATE_TEST_SUITE_P(RedisSingleServerRequestWithErrorWithDelayFaultTest, RedisSingleServerRequestWithErrorWithDelayFaultTest, - testing::ValuesIn(Common::Redis::SupportedCommands::simpleCommands())); + testing::Values("get", "set", "incr", "zadd")); class RedisSingleServerRequestWithDelayFaultTest : public RedisSingleServerRequestWithFaultTest { public: @@ -1479,7 +1479,7 @@ TEST_P(RedisSingleServerRequestWithDelayFaultTest, Fault) { INSTANTIATE_TEST_SUITE_P(RedisSingleServerRequestWithDelayFaultTest, RedisSingleServerRequestWithDelayFaultTest, - testing::ValuesIn(Common::Redis::SupportedCommands::simpleCommands())); + testing::Values("get", "set", "incr", "zadd")); class ScanHandlerTest : public FragmentedRequestCommandHandlerTest, public testing::WithParamInterface { @@ -1959,6 +1959,312 @@ TEST_P(SelectHandlerTest, SingleShardErrorResponse) { } INSTANTIATE_TEST_SUITE_P(SelectHandlerTest, SelectHandlerTest, testing::Values("select")); + +class RoleHandlerTest : public FragmentedRequestCommandHandlerTest, + public testing::WithParamInterface { +public: + void setup(uint16_t shard_size, const std::list& null_handle_indexes, + bool mirrored = false) { + std::vector request_strings = {"role"}; + makeRequestToShard(shard_size, request_strings, null_handle_indexes, mirrored); + } + + Common::Redis::RespValuePtr masterResponse() { + Common::Redis::RespValuePtr response = std::make_unique(); + response->type(Common::Redis::RespType::Array); + std::vector elements(3); + elements[0].type(Common::Redis::RespType::BulkString); + elements[0].asString() = "master"; + elements[1].type(Common::Redis::RespType::Integer); + elements[1].asInteger() = 0; + elements[2].type(Common::Redis::RespType::Array); + response->asArray().swap(elements); + return response; + } + + Common::Redis::RespValuePtr slaveResponse() { + Common::Redis::RespValuePtr response = std::make_unique(); + response->type(Common::Redis::RespType::Array); + std::vector elements(5); + elements[0].type(Common::Redis::RespType::BulkString); + elements[0].asString() = "slave"; + elements[1].type(Common::Redis::RespType::BulkString); + elements[1].asString() = "127.0.0.1"; + elements[2].type(Common::Redis::RespType::Integer); + elements[2].asInteger() = 6379; + elements[3].type(Common::Redis::RespType::BulkString); + elements[3].asString() = "connected"; + elements[4].type(Common::Redis::RespType::Integer); + elements[4].asInteger() = 0; + response->asArray().swap(elements); + return response; + } + + Common::Redis::RespValuePtr bulkStringResponse() { + Common::Redis::RespValuePtr response = std::make_unique(); + response->type(Common::Redis::RespType::BulkString); + response->asString() = "master"; + return response; + } +}; + +TEST_P(RoleHandlerTest, Normal) { + InSequence s; + + setup(2, {}); + EXPECT_NE(nullptr, handle_); + + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Array); + std::vector elements(2); + // elements[0] corresponds to pool_callbacks_[0] (master) + elements[0].type(Common::Redis::RespType::Array); + std::vector master_elements(3); + master_elements[0].type(Common::Redis::RespType::BulkString); + master_elements[0].asString() = "master"; + master_elements[1].type(Common::Redis::RespType::Integer); + master_elements[1].asInteger() = 0; + master_elements[2].type(Common::Redis::RespType::Array); + elements[0].asArray().swap(master_elements); + // elements[1] corresponds to pool_callbacks_[1] (slave) + elements[1].type(Common::Redis::RespType::Array); + std::vector slave_elements(5); + slave_elements[0].type(Common::Redis::RespType::BulkString); + slave_elements[0].asString() = "slave"; + slave_elements[1].type(Common::Redis::RespType::BulkString); + slave_elements[1].asString() = "127.0.0.1"; + slave_elements[2].type(Common::Redis::RespType::Integer); + slave_elements[2].asInteger() = 6379; + slave_elements[3].type(Common::Redis::RespType::BulkString); + slave_elements[3].asString() = "connected"; + slave_elements[4].type(Common::Redis::RespType::Integer); + slave_elements[4].asInteger() = 0; + elements[1].asArray().swap(slave_elements); + expected_response.asArray().swap(elements); + + pool_callbacks_[0]->onResponse(masterResponse()); + time_system_.setMonotonicTime(std::chrono::milliseconds(10)); + EXPECT_CALL( + store_, + deliverHistogramToSinks( + Property(&Stats::Metric::name, "redis.foo.command." + GetParam() + ".latency"), 10)); + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + pool_callbacks_[1]->onResponse(slaveResponse()); + + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".success").value()); +} + +TEST_P(RoleHandlerTest, Mirrored) { + InSequence s; + + setupMirrorPolicy(); + setup(2, {}, true); + EXPECT_NE(nullptr, handle_); + + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Array); + std::vector elements(2); + elements[0].type(Common::Redis::RespType::Array); + std::vector master_elements(3); + master_elements[0].type(Common::Redis::RespType::BulkString); + master_elements[0].asString() = "master"; + master_elements[1].type(Common::Redis::RespType::Integer); + master_elements[1].asInteger() = 0; + master_elements[2].type(Common::Redis::RespType::Array); + elements[0].asArray().swap(master_elements); + elements[1].type(Common::Redis::RespType::Array); + std::vector master_elements2(3); + master_elements2[0].type(Common::Redis::RespType::BulkString); + master_elements2[0].asString() = "master"; + master_elements2[1].type(Common::Redis::RespType::Integer); + master_elements2[1].asInteger() = 0; + master_elements2[2].type(Common::Redis::RespType::Array); + elements[1].asArray().swap(master_elements2); + expected_response.asArray().swap(elements); + + pool_callbacks_[0]->onResponse(masterResponse()); + mirror_pool_callbacks_[0]->onResponse(masterResponse()); + + time_system_.setMonotonicTime(std::chrono::milliseconds(10)); + EXPECT_CALL( + store_, + deliverHistogramToSinks( + Property(&Stats::Metric::name, "redis.foo.command." + GetParam() + ".latency"), 10)); + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + pool_callbacks_[1]->onResponse(masterResponse()); + mirror_pool_callbacks_[1]->onResponse(masterResponse()); + + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".success").value()); +} + +TEST_P(RoleHandlerTest, BulkStringResponse) { + InSequence s; + + setup(1, {}); + EXPECT_NE(nullptr, handle_); + + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Array); + std::vector elements(1); + elements[0].type(Common::Redis::RespType::BulkString); + elements[0].asString() = "master"; + expected_response.asArray().swap(elements); + + time_system_.setMonotonicTime(std::chrono::milliseconds(5)); + EXPECT_CALL( + store_, + deliverHistogramToSinks( + Property(&Stats::Metric::name, "redis.foo.command." + GetParam() + ".latency"), 5)); + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + pool_callbacks_[0]->onResponse(bulkStringResponse()); + + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".success").value()); +} + +TEST_P(RoleHandlerTest, NoUpstreamHostForAll) { + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Error); + expected_response.asString() = "no upstream host"; + + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + setup(0, {}); + EXPECT_EQ(nullptr, handle_); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".error").value()); +} + +TEST_P(RoleHandlerTest, NoUpstreamHostForOne) { + InSequence s; + + setup(2, {0}); + EXPECT_NE(nullptr, handle_); + + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Error); + expected_response.asString() = "finished with 1 error(s)"; + + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + pool_callbacks_[1]->onResponse(masterResponse()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".error").value()); +} + +TEST_P(RoleHandlerTest, UpstreamFailure) { + InSequence s; + + setup(2, {}); + EXPECT_NE(nullptr, handle_); + + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Error); + expected_response.asString() = "finished with 1 error(s)"; + + pool_callbacks_[1]->onFailure(); + + time_system_.setMonotonicTime(std::chrono::milliseconds(5)); + EXPECT_CALL( + store_, + deliverHistogramToSinks( + Property(&Stats::Metric::name, "redis.foo.command." + GetParam() + ".latency"), 5)); + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + pool_callbacks_[0]->onResponse(masterResponse()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".error").value()); +} + +TEST_P(RoleHandlerTest, InvalidUpstreamResponse) { + InSequence s; + + setup(2, {}); + EXPECT_NE(nullptr, handle_); + + Common::Redis::RespValue expected_response; + expected_response.type(Common::Redis::RespType::Error); + expected_response.asString() = "finished with 1 error(s)"; + + pool_callbacks_[1]->onResponse(masterResponse()); + + Common::Redis::RespValuePtr invalid_response = std::make_unique(); + invalid_response->type(Common::Redis::RespType::Integer); + invalid_response->asInteger() = 123; + + time_system_.setMonotonicTime(std::chrono::milliseconds(10)); + EXPECT_CALL( + store_, + deliverHistogramToSinks( + Property(&Stats::Metric::name, "redis.foo.command." + GetParam() + ".latency"), 10)); + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&expected_response))); + pool_callbacks_[0]->onResponse(std::move(invalid_response)); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".total").value()); + EXPECT_EQ(1UL, store_.counter("redis.foo.command." + GetParam() + ".error").value()); +} + +TEST_P(RoleHandlerTest, Cancel) { + InSequence s; + + setup(2, {}); + EXPECT_NE(nullptr, handle_); + + EXPECT_CALL(pool_requests_[0], cancel()); + EXPECT_CALL(pool_requests_[1], cancel()); + handle_->cancel(); +} + +TEST_F(RoleHandlerTest, RoleWithMultipleArguments) { + InSequence s; + + Common::Redis::RespValuePtr request{new Common::Redis::RespValue()}; + makeBulkStringArray(*request, {"role", "1", "2"}); + + Common::Redis::RespValue response; + response.type(Common::Redis::RespType::Error); + response.asString() = "ERR wrong number of arguments for 'role' command"; + + EXPECT_CALL(callbacks_, connectionAllowed()).WillOnce(Return(true)); + EXPECT_CALL(callbacks_, onResponse_(PointeesEq(&response))); + handle_ = splitter_.makeRequest(std::move(request), callbacks_, dispatcher_, stream_info_); + EXPECT_EQ(nullptr, handle_); +} + +TEST_F(RoleHandlerTest, RoleNoArgs) { + InSequence s; + + // Test the special handling path for ROLE command in makeRequest + Common::Redis::RespValuePtr request{new Common::Redis::RespValue()}; + makeBulkStringArray(*request, {"role"}); + + // Set up the mock calls for the ROLE special handling path + EXPECT_CALL(callbacks_, connectionAllowed()).WillOnce(Return(true)); + EXPECT_CALL(*conn_pool_, shardSize_()).WillOnce(Return(1)); + + ConnPool::PoolCallbacks* pool_callback; + Common::Redis::Client::MockPoolRequest pool_request; + + EXPECT_CALL(*conn_pool_, makeRequestToShard_(0, _, _)) + .WillOnce(DoAll(WithArg<2>(SaveArgAddress(&pool_callback)), Return(&pool_request))); + + handle_ = splitter_.makeRequest(std::move(request), callbacks_, dispatcher_, stream_info_); + EXPECT_NE(nullptr, handle_); + + // Verify that the total counter is incremented in the special handling path + EXPECT_EQ(1UL, store_.counter("redis.foo.command.role.total").value()); + + // Complete the request successfully + Common::Redis::RespValuePtr role_response = std::make_unique(); + role_response->type(Common::Redis::RespType::Array); + std::vector elements(1); + elements[0].type(Common::Redis::RespType::BulkString); + elements[0].asString() = "master"; + role_response->asArray().swap(elements); + + EXPECT_CALL(callbacks_, onResponse_(_)); + pool_callback->onResponse(std::move(role_response)); +} + +INSTANTIATE_TEST_SUITE_P(RoleHandlerTest, RoleHandlerTest, testing::Values("role")); } // namespace CommandSplitter } // namespace RedisProxy } // namespace NetworkFilters diff --git a/test/extensions/filters/network/redis_proxy/redis_proxy_integration_test.cc b/test/extensions/filters/network/redis_proxy/redis_proxy_integration_test.cc index 1f391a18ec948..412a05812cf9f 100644 --- a/test/extensions/filters/network/redis_proxy/redis_proxy_integration_test.cc +++ b/test/extensions/filters/network/redis_proxy/redis_proxy_integration_test.cc @@ -6,6 +6,7 @@ #include "source/common/common/fmt.h" #include "source/extensions/filters/network/common/redis/fault_impl.h" #include "source/extensions/filters/network/redis_proxy/command_splitter_impl.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/common/grpc/grpc_client_integration.h" #include "test/integration/integration.h" @@ -88,6 +89,10 @@ constexpr absl::string_view CONFIG_WITH_REDIRECTION_DNS = R"EOF({} name: foo dns_lookup_family: {} max_hosts: 100 + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig )EOF"; // This is a configuration with batching enabled. diff --git a/test/extensions/filters/network/reverse_tunnel/BUILD b/test/extensions/filters/network/reverse_tunnel/BUILD new file mode 100644 index 0000000000000..99f041894b867 --- /dev/null +++ b/test/extensions/filters/network/reverse_tunnel/BUILD @@ -0,0 +1,78 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_names = ["envoy.filters.network.reverse_tunnel"], + rbe_pool = "6gig", + deps = [ + "//source/extensions/filters/network/reverse_tunnel:config", + "//test/mocks/server:factory_context_mocks", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "filter_unit_test", + srcs = ["filter_unit_test.cc"], + extension_names = ["envoy.filters.network.reverse_tunnel"], + rbe_pool = "6gig", + deps = [ + "//source/common/stats:isolated_store_lib", + "//source/common/stream_info:uint64_accessor_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:upstream_socket_manager_lib", + "//source/extensions/filters/network/reverse_tunnel:reverse_tunnel_filter_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/server:overload_manager_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/test_common:logging_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "integration_test", + size = "large", + srcs = ["integration_test.cc"], + extension_names = [ + "envoy.filters.network.reverse_tunnel", + "envoy.bootstrap.reverse_tunnel.upstream_socket_interface", + "envoy.bootstrap.reverse_tunnel.downstream_socket_interface", + "envoy.bootstrap.internal_listener", + "envoy.resolvers.reverse_connection", + "envoy.filters.network.echo", + ], + rbe_pool = "6gig", + deps = [ + "//source/extensions/bootstrap/internal_listener:config", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_resolver_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//source/extensions/filters/network/echo:config", + "//source/extensions/filters/network/reverse_tunnel:config", + "//source/extensions/filters/network/set_filter_state:config", + "//source/extensions/transport_sockets/internal_upstream:config", + "//test/integration:integration_lib", + "//test/test_common:logging_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/internal_upstream/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/network/reverse_tunnel/config_test.cc b/test/extensions/filters/network/reverse_tunnel/config_test.cc new file mode 100644 index 0000000000000..0e048c762d15e --- /dev/null +++ b/test/extensions/filters/network/reverse_tunnel/config_test.cc @@ -0,0 +1,162 @@ +#include "envoy/config/core/v3/base.pb.h" + +#include "source/extensions/filters/network/reverse_tunnel/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { +namespace { + +TEST(ReverseTunnelFilterConfigFactoryTest, ValidConfiguration) { + ReverseTunnelFilterConfigFactory factory; + + const std::string yaml_string = R"EOF( +ping_interval: + seconds: 5 +auto_close_connections: false +request_path: "/custom/reverse" +request_method: PUT +)EOF"; + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + + NiceMock context; + auto result = factory.createFilterFactoryFromProto(proto_config, context); + ASSERT_TRUE(result.ok()); + Network::FilterFactoryCb cb = result.value(); + + EXPECT_TRUE(cb != nullptr); + + Network::MockFilterManager filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +TEST(ReverseTunnelFilterConfigFactoryTest, DefaultConfiguration) { + ReverseTunnelFilterConfigFactory factory; + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + // Set minimum required fields for configuration. + proto_config.set_request_path("/reverse_connections/request"); + proto_config.set_request_method(envoy::config::core::v3::POST); + + NiceMock context; + auto result = factory.createFilterFactoryFromProto(proto_config, context); + ASSERT_TRUE(result.ok()); + Network::FilterFactoryCb cb = result.value(); + + EXPECT_TRUE(cb != nullptr); + + Network::MockFilterManager filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +TEST(ReverseTunnelFilterConfigFactoryTest, ConfigProperties) { + ReverseTunnelFilterConfigFactory factory; + + EXPECT_EQ("envoy.filters.network.reverse_tunnel", factory.name()); + + ProtobufTypes::MessagePtr empty_config = factory.createEmptyConfigProto(); + EXPECT_TRUE(empty_config != nullptr); + EXPECT_EQ("envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel", + empty_config->GetTypeName()); +} + +TEST(ReverseTunnelFilterConfigFactoryTest, ConfigurationNoValidation) { + ReverseTunnelFilterConfigFactory factory; + + const std::string yaml_string = R"EOF( +ping_interval: + seconds: 1 + nanos: 500000000 +auto_close_connections: true +request_path: "/test/path" +request_method: POST +)EOF"; + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + + NiceMock context; + auto result = factory.createFilterFactoryFromProto(proto_config, context); + ASSERT_TRUE(result.ok()); + Network::FilterFactoryCb cb = result.value(); + + EXPECT_TRUE(cb != nullptr); + + Network::MockFilterManager filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +TEST(ReverseTunnelFilterConfigFactoryTest, MinimalConfigurationYaml) { + ReverseTunnelFilterConfigFactory factory; + + const std::string yaml_string = R"EOF( +request_path: "/minimal" +request_method: POST +)EOF"; + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + + NiceMock context; + auto result = factory.createFilterFactoryFromProto(proto_config, context); + ASSERT_TRUE(result.ok()); + Network::FilterFactoryCb cb = result.value(); + + EXPECT_TRUE(cb != nullptr); + + Network::MockFilterManager filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +TEST(ReverseTunnelFilterConfigFactoryTest, FactoryType) { + ReverseTunnelFilterConfigFactory factory; + + // Test that the factory name matches expected. + EXPECT_EQ("envoy.filters.network.reverse_tunnel", factory.name()); +} + +TEST(ReverseTunnelFilterConfigFactoryTest, CreateFilterFactoryFromProtoTyped) { + ReverseTunnelFilterConfigFactory factory; + + const std::string yaml_string = R"EOF( +ping_interval: + seconds: 3 +auto_close_connections: true +request_path: "/factory/test" +request_method: PUT +)EOF"; + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + + NiceMock context; + auto result = factory.createFilterFactoryFromProto(proto_config, context); + ASSERT_TRUE(result.ok()); + Network::FilterFactoryCb cb = result.value(); + + EXPECT_TRUE(cb != nullptr); + + // Test the factory callback creates the filter properly. + Network::MockFilterManager filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +} // namespace +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/reverse_tunnel/filter_unit_test.cc b/test/extensions/filters/network/reverse_tunnel/filter_unit_test.cc new file mode 100644 index 0000000000000..875b351b1df29 --- /dev/null +++ b/test/extensions/filters/network/reverse_tunnel/filter_unit_test.cc @@ -0,0 +1,1621 @@ +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.pb.h" +#include "envoy/server/factory_context.h" +#include "envoy/thread_local/thread_local.h" + +#include "source/common/router/string_accessor_impl.h" +#include "source/common/stats/isolated_store_impl.h" +#include "source/common/stream_info/uint64_accessor_impl.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/reverse_tunnel_acceptor_extension.h" +#include "source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/upstream_socket_manager.h" +#include "source/extensions/filters/network/reverse_tunnel/reverse_tunnel_filter.h" + +namespace ReverseConnection = Envoy::Extensions::Bootstrap::ReverseConnection; + +#include "test/mocks/event/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/server/overload_manager.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/logging.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { +namespace { + +// Helper to create invalid HTTP that will trigger codec dispatch errors +class HttpErrorHelper { +public: + static std::vector getHttpErrorPatterns() { + return { + // Trigger codec dispatch with various malformed patterns + "GET /path HTTP/1.1\r\nInvalid-Header\r\n\r\n", // Header without colon + "POST /path HTTP/1.1\r\nContent-Length: abc\r\n\r\n", // Non-numeric content length + "INVALID_METHOD /path HTTP/1.1\r\nHost: test\r\n\r\n", // Invalid method + std::string("\xFF\xFE\xFD\xFC", 4), // Binary junk + "GET /path HTTP/999.999\r\n\r\n", // Invalid HTTP version + "GET\r\n\r\n", // Incomplete request line + "GET /path\r\n\r\n", // Missing HTTP version + "GET /path HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: invalid\r\n\r\n" // Invalid encoding + }; + } +}; + +class ReverseTunnelFilterUnitTest : public testing::Test { +protected: + void SetUp() override { + // Initialize stats scope + stats_scope_ = Stats::ScopeSharedPtr(stats_store_.createScope("test_scope.")); + } + +public: + ReverseTunnelFilterUnitTest() : stats_store_(), overload_manager_() { + // Prepare proto config with defaults. + proto_config_.set_request_path("/reverse_connections/request"); + proto_config_.set_request_method(envoy::config::core::v3::GET); + config_ = std::make_shared(proto_config_, factory_context_); + filter_ = std::make_unique(config_, *stats_store_.rootScope(), + overload_manager_); + + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + // Provide a default socket for getSocket(). + auto socket = std::make_unique(); + auto* socket_raw = socket.get(); + // Store unique_ptr inside a shared location to return const ref each time. + static Network::ConnectionSocketPtr stored_socket; + stored_socket = std::move(socket); + EXPECT_CALL(callbacks_.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_socket)); + EXPECT_CALL(*socket_raw, isOpen()).WillRepeatedly(testing::Return(true)); + // Stub required methods used by processAcceptedConnection(). + EXPECT_CALL(*socket_raw, ioHandle()) + .WillRepeatedly(testing::ReturnRef(*callbacks_.socket_.io_handle_)); + + filter_->initializeReadFilterCallbacks(callbacks_); + } + + // Helper method to set up upstream extension. + void setupUpstreamExtension() { + // Create the upstream socket interface and extension. + upstream_socket_interface_ = + std::make_unique(context_); + upstream_extension_ = std::make_unique( + *upstream_socket_interface_, context_, upstream_config_); + + // Set up the extension in the global socket interface registry. + auto* registered_upstream_interface = + Network::socketInterface("envoy.bootstrap.reverse_tunnel.upstream_socket_interface"); + if (registered_upstream_interface) { + auto* registered_acceptor = dynamic_cast( + const_cast(registered_upstream_interface)); + if (registered_acceptor) { + // Set up the extension for the registered upstream socket interface. + registered_acceptor->extension_ = upstream_extension_.get(); + } + } + } + + // Helper method to set up upstream thread local slot for testing. + void setupUpstreamThreadLocalSlot() { + // Call onServerInitialized to set up the extension references properly. + upstream_extension_->onServerInitialized(); + + // Create a thread local registry for upstream with the dispatcher. + upstream_thread_local_registry_ = + std::make_shared(dispatcher_, + upstream_extension_.get()); + + upstream_tls_slot_ = + ThreadLocal::TypedSlot::makeUnique( + thread_local_); + thread_local_.setDispatcher(&dispatcher_); + + // Set up the upstream slot to return our registry. + upstream_tls_slot_->set( + [registry = upstream_thread_local_registry_](Event::Dispatcher&) { return registry; }); + + // Override the TLS slot with our test version. + upstream_extension_->setTestOnlyTLSRegistry(std::move(upstream_tls_slot_)); + } + + // Helper to craft raw HTTP/1.1 request string. + std::string makeHttpRequest(const std::string& method, const std::string& path, + const std::string& body = "") { + std::string req = fmt::format("{} {} HTTP/1.1\r\n", method, path); + req += "Host: localhost\r\n"; + req += fmt::format("Content-Length: {}\r\n\r\n", body.size()); + req += body; + return req; + } + + // Helper to build reverse tunnel headers block. + std::string makeRtHeaders(const std::string& node, const std::string& cluster, + const std::string& tenant) { + std::string headers; + headers += "x-envoy-reverse-tunnel-node-id: " + node + "\r\n"; + headers += "x-envoy-reverse-tunnel-cluster-id: " + cluster + "\r\n"; + headers += "x-envoy-reverse-tunnel-tenant-id: " + tenant + "\r\n"; + return headers; + } + + // Helper to craft HTTP request with reverse tunnel headers and optional body. + std::string makeHttpRequestWithRtHeaders(const std::string& method, const std::string& path, + const std::string& node, const std::string& cluster, + const std::string& tenant, + const std::string& body = "") { + std::string req = fmt::format("{} {} HTTP/1.1\r\n", method, path); + req += "Host: localhost\r\n"; + req += makeRtHeaders(node, cluster, tenant); + req += fmt::format("Content-Length: {}\r\n\r\n", body.size()); + req += body; + return req; + } + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config_; + ReverseTunnelFilterConfigSharedPtr config_; + std::unique_ptr filter_; + Stats::IsolatedStoreImpl stats_store_; + NiceMock overload_manager_; + NiceMock callbacks_; + + // Thread local slot setup for downstream socket interface. + NiceMock context_; + NiceMock factory_context_; + NiceMock thread_local_; + NiceMock cluster_manager_; + Stats::ScopeSharedPtr stats_scope_; + NiceMock dispatcher_{"worker_0"}; + // Config for reverse connection socket interface. + envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface upstream_config_; + // Thread local components for testing upstream socket interface. + std::unique_ptr> + upstream_tls_slot_; + std::shared_ptr upstream_thread_local_registry_; + std::unique_ptr upstream_socket_interface_; + std::unique_ptr upstream_extension_; + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::debug); + + void TearDown() override { + // Clean up thread local components to avoid issues during destruction. + upstream_tls_slot_.reset(); + upstream_thread_local_registry_.reset(); + upstream_extension_.reset(); + upstream_socket_interface_.reset(); + } +}; + +TEST_F(ReverseTunnelFilterUnitTest, NewConnectionContinues) { + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); +} + +TEST_F(ReverseTunnelFilterUnitTest, HttpDispatchErrorStopsIteration) { + // Simulate invalid HTTP by feeding raw bytes; dispatch will attempt and return error. + Buffer::OwnedImpl data("INVALID"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data, false)); +} + +TEST_F(ReverseTunnelFilterUnitTest, FullFlowAccepts) { + + // Configure reverse tunnel filter. + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + // Filter state does not affect acceptance. + + // Capture writes to connection. + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); + // Stats: accepted should increment. + auto accepted = TestUtility::findCounter(stats_store_, "reverse_tunnel.handshake.accepted"); + ASSERT_NE(nullptr, accepted); + EXPECT_EQ(1, accepted->value()); +} + +TEST_F(ReverseTunnelFilterUnitTest, FullFlowMissingHeadersIsBadRequest) { + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + // Missing required headers should cause 400. + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + Buffer::OwnedImpl request(makeHttpRequest("GET", "/reverse_connections/request", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); +} + +TEST_F(ReverseTunnelFilterUnitTest, FullFlowParseError) { + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Missing required headers should cause 400. + Buffer::OwnedImpl request(makeHttpRequest("GET", "/reverse_connections/request", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); + // Stats: parse_error should increment. + auto parse_error = TestUtility::findCounter(stats_store_, "reverse_tunnel.handshake.parse_error"); + ASSERT_NE(nullptr, parse_error); + EXPECT_EQ(1, parse_error->value()); +} + +TEST_F(ReverseTunnelFilterUnitTest, NotFoundForNonReverseTunnelPath) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + Buffer::OwnedImpl request(makeHttpRequest("GET", "/health")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); +} + +TEST_F(ReverseTunnelFilterUnitTest, AutoCloseConnectionsClosesAfterAccept) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + cfg.set_auto_close_connections(true); + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + // Expect close on accept. + EXPECT_CALL(callbacks_.connection_, close(Network::ConnectionCloseType::FlushWrite)); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Exercise RequestDecoder interface methods by obtaining the decoder via +// ReverseTunnelFilter::newStream (avoids accessing the private impl type). +TEST_F(ReverseTunnelFilterUnitTest, RequestDecoderInterfaceCoverageViaNewStream) { + // Ensure filter has callbacks initialized so decoder can access time source. + filter_->initializeReadFilterCallbacks(callbacks_); + + // Get a decoder instance via newStream. + Http::MockResponseEncoder encoder; + Http::RequestDecoder& decoder = filter_->newStream(encoder, false); + + // Provide minimal headers so processIfComplete paths are safe if triggered. + auto headers = Http::RequestHeaderMapImpl::create(); + decoder.decodeHeaders(std::move(headers), false); + + // Call decodeMetadata (no-op) explicitly. + Http::MetadataMapPtr meta; + decoder.decodeMetadata(std::move(meta)); + + // Accessor methods. + auto& si = decoder.streamInfo(); + (void)si; + auto logs = decoder.accessLogHandlers(); + EXPECT_TRUE(logs.empty()); + auto handle = decoder.getRequestDecoderHandle(); + EXPECT_EQ(nullptr, handle.get()); +} + +// Test configuration with custom ping interval. +TEST_F(ReverseTunnelFilterUnitTest, ConfigurationCustomPingInterval) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + proto_config.mutable_ping_interval()->set_seconds(10); + proto_config.set_auto_close_connections(true); + proto_config.set_request_path("/custom/path"); + proto_config.set_request_method(envoy::config::core::v3::PUT); + + ReverseTunnelFilterConfig config(proto_config, factory_context_); + EXPECT_EQ(std::chrono::milliseconds(10000), config.pingInterval()); + EXPECT_TRUE(config.autoCloseConnections()); + EXPECT_EQ("/custom/path", config.requestPath()); + EXPECT_EQ("PUT", config.requestMethod()); +} + +// Ensure defaults remain stable. +TEST_F(ReverseTunnelFilterUnitTest, ConfigurationDefaultsRemainStable) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + ReverseTunnelFilterConfig config(proto_config, factory_context_); + EXPECT_EQ("/reverse_connections/request", config.requestPath()); +} + +// Test configuration with default values. +TEST_F(ReverseTunnelFilterUnitTest, ConfigurationDefaults) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel proto_config; + // Leave everything empty to test defaults. + + ReverseTunnelFilterConfig config(proto_config, factory_context_); + EXPECT_EQ(std::chrono::milliseconds(2000), config.pingInterval()); + EXPECT_FALSE(config.autoCloseConnections()); + EXPECT_EQ("/reverse_connections/request", config.requestPath()); + EXPECT_EQ("GET", config.requestMethod()); +} + +// Test RequestDecoder methods not fully covered. +TEST_F(ReverseTunnelFilterUnitTest, RequestDecoderImplMethods) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Create a request that will trigger decoder creation. + const std::string req = + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t"); + + // Split request into headers and body to test different decoder methods. + const auto hdr_end = req.find("\r\n\r\n"); + const std::string headers_part = req.substr(0, hdr_end + 4); + const std::string body_part = req.substr(hdr_end + 4); + + // First send headers. + Buffer::OwnedImpl header_buf(headers_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(header_buf, false)); + + // Then send body to test decodeData method. + Buffer::OwnedImpl body_buf(body_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(body_buf, false)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test decodeTrailers method. +TEST_F(ReverseTunnelFilterUnitTest, RequestDecoderImplDecodeTrailers) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Create a chunked request with trailers to trigger decodeTrailers. + const std::string headers_part = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + + makeRtHeaders("n", "c", "t") + + "Transfer-Encoding: chunked\r\n\r\n"; + + // Send headers first. + Buffer::OwnedImpl header_buf(headers_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(header_buf, false)); + + // Send chunk with data. + Buffer::OwnedImpl chunk1("5\r\nhello\r\n"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunk1, false)); + + // Send final chunk with trailers - this triggers decodeTrailers. + Buffer::OwnedImpl chunk2("0\r\nX-Trailer: value\r\n\r\n"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunk2, false)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test decodeTrailers triggers processIfComplete. +TEST_F(ReverseTunnelFilterUnitTest, DecodeTrailersTriggersCompletion) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Build a proper chunked request to ensure decodeTrailers is called. + std::string req = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + + makeRtHeaders("trail", "test", "complete") + + "Transfer-Encoding: chunked\r\n\r\n" + "0\r\n" // Zero-length chunk + "X-End: trailer\r\n" // Trailer header + "\r\n"; // End of trailers + + Buffer::OwnedImpl request(req); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test parsing with empty payload. +TEST_F(ReverseTunnelFilterUnitTest, ParseEmptyPayload) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request(makeHttpRequest("GET", "/reverse_connections/request", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); + + auto parse_error = TestUtility::findCounter(stats_store_, "reverse_tunnel.handshake.parse_error"); + ASSERT_NE(nullptr, parse_error); + EXPECT_EQ(1, parse_error->value()); +} + +TEST_F(ReverseTunnelFilterUnitTest, NonStringFilterStateIgnored) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +TEST_F(ReverseTunnelFilterUnitTest, ClusterIdMismatchIgnored) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +TEST_F(ReverseTunnelFilterUnitTest, TenantIdMissingIgnored) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test closed socket scenario. +TEST_F(ReverseTunnelFilterUnitTest, ProcessAcceptedConnectionClosedSocket) { + // Create a mock socket that reports as closed. + auto closed_socket = std::make_unique(); + EXPECT_CALL(*closed_socket, isOpen()).WillRepeatedly(testing::Return(false)); + + static Network::ConnectionSocketPtr stored_closed_socket; + stored_closed_socket = std::move(closed_socket); + EXPECT_CALL(callbacks_.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_closed_socket)); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test wrong HTTP method. +TEST_F(ReverseTunnelFilterUnitTest, WrongHttpMethod) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("PUT", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); +} + +// Test onGoAway method coverage. +TEST_F(ReverseTunnelFilterUnitTest, OnGoAway) { + // onGoAway is a no-op, but we need to test it for coverage. + filter_->onGoAway(Http::GoAwayErrorCode::NoError); + // No assertions needed as it's a no-op method. +} + +// Test sendLocalReply with different parameters. +TEST_F(ReverseTunnelFilterUnitTest, SendLocalReplyVariants) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Test sendLocalReply with empty body. + Buffer::OwnedImpl request(makeHttpRequest("GET", "/wrong/path", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); + EXPECT_THAT(written, testing::HasSubstr("Not a reverse tunnel request")); +} + +// Test invalid protobuf that fails parsing. +TEST_F(ReverseTunnelFilterUnitTest, InvalidProtobufData) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Body contents are ignored now; with proper headers we should accept. + std::string junk_body(100, '\xFF'); + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", + "c", "t", junk_body)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test request with headers only (no body). +TEST_F(ReverseTunnelFilterUnitTest, HeadersOnlyRequest) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + std::string headers_only = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 0\r\n\r\n"; + Buffer::OwnedImpl request(headers_only); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); +} + +// Test RequestDecoderImpl interface methods for coverage. +TEST_F(ReverseTunnelFilterUnitTest, RequestDecoderImplInterfaceMethods) { + // Create a decoder to test interface methods. + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Start a request to create the decoder. + // Use a non-empty body so the headers phase does not signal end_stream. + const std::string req = + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t"); + const auto hdr_end = req.find("\r\n\r\n"); + const std::string headers_part = req.substr(0, hdr_end + 4); + + Buffer::OwnedImpl header_buf(headers_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(header_buf, false)); + + // Continue with body to complete the request. + const std::string body_part = req.substr(hdr_end + 4); + Buffer::OwnedImpl body_buf(body_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(body_buf, false)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test wrong HTTP method leads to 404. +TEST_F(ReverseTunnelFilterUnitTest, WrongHttpMethodTest) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Test with wrong method (PUT instead of GET). + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("PUT", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); +} + +// Test successful request with response body. +TEST_F(ReverseTunnelFilterUnitTest, SuccessfulRequestWithResponseBody) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders( + "GET", "/reverse_connections/request", "test-node", "test-cluster", "test-tenant")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); + + // Check that accepted stat is incremented. + auto accepted = TestUtility::findCounter(stats_store_, "reverse_tunnel.handshake.accepted"); + ASSERT_NE(nullptr, accepted); + EXPECT_EQ(1, accepted->value()); +} + +// Test sendLocalReply with modify_headers function. +TEST_F(ReverseTunnelFilterUnitTest, SendLocalReplyWithHeaderModifier) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Send a request with wrong path to trigger sendLocalReply. + Buffer::OwnedImpl request(makeHttpRequest("GET", "/wrong/path", "test-body")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); +} + +// Explicitly call RequestDecoderImpl::sendLocalReply with a header modifier to +// test the modify_headers. +TEST_F(ReverseTunnelFilterUnitTest, RequestDecoderSendLocalReplyHeaderModifier) { + // Ensure callbacks are initialized to provide a time source. + filter_->initializeReadFilterCallbacks(callbacks_); + + // Mock encoder to capture headers set via modifier. + Http::MockResponseEncoder encoder; + bool saw_custom_header = false; + EXPECT_CALL(encoder, encodeHeaders(testing::_, testing::_)) + .WillOnce(testing::Invoke([&](const Http::ResponseHeaderMap& headers, bool) { + auto values = headers.get(Http::LowerCaseString("x-custom-mod")); + saw_custom_header = !values.empty() && values[0]->value().getStringView() == "v"; + })); + + // Obtain a decoder and call sendLocalReply with a modifier. + Http::RequestDecoder& decoder = filter_->newStream(encoder, false); + decoder.sendLocalReply( + Http::Code::Forbidden, "", + [](Http::ResponseHeaderMap& h) { h.addCopy(Http::LowerCaseString("x-custom-mod"), "v"); }, + absl::nullopt, "test"); + + EXPECT_TRUE(saw_custom_header); +} + +// Missing required headers should return 400. +TEST_F(ReverseTunnelFilterUnitTest, MissingReverseTunnelHeadersReturns400) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Missing required header should fail. + std::string req = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + "x-envoy-reverse-tunnel-cluster-id: c\r\n" + "x-envoy-reverse-tunnel-tenant-id: t\r\n" + "Content-Length: 0\r\n\r\n"; + Buffer::OwnedImpl request(req); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); +} + +// Test partial HTTP data processing. +TEST_F(ReverseTunnelFilterUnitTest, PartialHttpData) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + const std::string full_request = + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t"); + + // Send request in small chunks. + const size_t chunk_size = 10; + for (size_t i = 0; i < full_request.size(); i += chunk_size) { + const size_t actual_chunk_size = std::min(chunk_size, full_request.size() - i); + std::string chunk = full_request.substr(i, actual_chunk_size); + Buffer::OwnedImpl chunk_buf(chunk); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunk_buf, false)); + } + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test HTTP dispatch with complete body in single call. +TEST_F(ReverseTunnelFilterUnitTest, CompleteRequestSingleCall) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "single", "call", "test")); + + // Process complete request in one call. + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, true)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +TEST_F(ReverseTunnelFilterUnitTest, PartialStateIgnored) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test string parsing through HTTP path (parseHandshakeRequest is private). +TEST_F(ReverseTunnelFilterUnitTest, ParseHandshakeStringViaHttp) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Test with a valid protobuf serialized as string. + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "node", "cluster", "tenant")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test sendLocalReply with different paths. +TEST_F(ReverseTunnelFilterUnitTest, SendLocalReplyWithHeadersCallback) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Create a request with wrong path to trigger sendLocalReply. + Buffer::OwnedImpl request("GET / HTTP/1.1\r\nHost: test\r\n\r\n"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + + // Should get 404 since path doesn't match. + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); + EXPECT_THAT(written, testing::HasSubstr("Not a reverse tunnel request")); +} + +// Test processIfComplete early return paths. +TEST_F(ReverseTunnelFilterUnitTest, ProcessIfCompleteEarlyReturns) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + const std::string req = + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t", "x"); + + // Split request to send headers first without end_stream. + const auto hdr_end = req.find("\r\n\r\n"); + const std::string headers_part = req.substr(0, hdr_end + 4); + + // Send headers without end_stream - should not trigger processIfComplete. + Buffer::OwnedImpl header_buf(headers_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(header_buf, false)); + + // At this point, no response should have been written yet. + EXPECT_TRUE(written.empty()); + + // Now send the body with end_stream to complete. + const std::string body_part = req.substr(hdr_end + 4); + Buffer::OwnedImpl body_buf(body_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(body_buf, true)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test configuration with all branches. +TEST_F(ReverseTunnelFilterUnitTest, ConfigurationAllBranches) { + // Test config with ping_interval set. + { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + cfg.mutable_ping_interval()->set_seconds(5); + cfg.mutable_ping_interval()->set_nanos(500000000); + ReverseTunnelFilterConfig config(cfg, factory_context_); + EXPECT_EQ(std::chrono::milliseconds(5500), config.pingInterval()); + } + + // Test config without ping_interval (default). + { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + ReverseTunnelFilterConfig config(cfg, factory_context_); + EXPECT_EQ(std::chrono::milliseconds(2000), config.pingInterval()); + } + + // Test config with empty strings (should use defaults). + { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + cfg.set_request_path(""); + cfg.set_request_method(envoy::config::core::v3::METHOD_UNSPECIFIED); + ReverseTunnelFilterConfig config(cfg, factory_context_); + EXPECT_EQ("/reverse_connections/request", config.requestPath()); + EXPECT_EQ("GET", config.requestMethod()); + } +} + +// Test array parsing edge cases via HTTP (parseHandshakeRequestFromArray is private). +TEST_F(ReverseTunnelFilterUnitTest, ParseHandshakeArrayEdgeCases) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Test with empty body to trigger array parsing with null data. + Buffer::OwnedImpl empty_request(makeHttpRequest("GET", "/reverse_connections/request", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(empty_request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); +} + +// Test socket is null or not open scenarios. +TEST_F(ReverseTunnelFilterUnitTest, ProcessAcceptedConnectionNullSocket) { + // Create a mock connection that returns null socket. + NiceMock null_socket_callbacks; + EXPECT_CALL(null_socket_callbacks, connection()) + .WillRepeatedly(ReturnRef(null_socket_callbacks.connection_)); + + // Mock getSocket to return null. + static Network::ConnectionSocketPtr null_socket_ptr = nullptr; + EXPECT_CALL(null_socket_callbacks.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(null_socket_ptr)); + + ReverseTunnelFilter null_socket_filter(config_, *stats_store_.rootScope(), overload_manager_); + null_socket_filter.initializeReadFilterCallbacks(null_socket_callbacks); + + std::string written; + EXPECT_CALL(null_socket_callbacks.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, null_socket_filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test empty response body path. +TEST_F(ReverseTunnelFilterUnitTest, EmptyResponseBody) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + + // Should generate a response with non-empty body. + EXPECT_THAT(written, testing::HasSubstr("200 OK")); + // No protobuf body expected now. +} + +// Test codec dispatch error path. +TEST_F(ReverseTunnelFilterUnitTest, CodecDispatchError) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Send completely invalid HTTP data that will cause dispatch error. + Buffer::OwnedImpl invalid_data("\x00\x01\x02\x03INVALID HTTP"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(invalid_data, false)); + + // Should get no response since the filter returns early on dispatch error. +} + +TEST_F(ReverseTunnelFilterUnitTest, TenantIdMismatchIgnored2) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + auto local_config = std::make_shared(cfg, factory_context_); + ReverseTunnelFilter filter(local_config, *stats_store_.rootScope(), overload_manager_); + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(ReturnRef(callbacks_.connection_)); + filter.initializeReadFilterCallbacks(callbacks_); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter.onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test newStream with is_internally_created parameter via HTTP processing. +TEST_F(ReverseTunnelFilterUnitTest, NewStreamWithInternallyCreatedFlag) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // newStream is called internally when processing HTTP requests. + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test stats generation through actual filter operations. +TEST_F(ReverseTunnelFilterUnitTest, StatsGeneration) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Trigger parse error to verify stats are generated (missing headers). + Buffer::OwnedImpl invalid_request(makeHttpRequest("GET", "/reverse_connections/request", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(invalid_request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); + + // Verify parse_error stat was incremented. + auto parse_error = TestUtility::findCounter(stats_store_, "reverse_tunnel.handshake.parse_error"); + ASSERT_NE(nullptr, parse_error); + EXPECT_EQ(1, parse_error->value()); +} + +// Test configuration with ping_interval_ms deprecated field. +TEST_F(ReverseTunnelFilterUnitTest, ConfigurationDeprecatedField) { + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel cfg; + // Test the deprecated field if it exists. + cfg.set_auto_close_connections(false); + cfg.set_request_path("/test"); + cfg.set_request_method(envoy::config::core::v3::PUT); + // No extra options set to test defaults. + + ReverseTunnelFilterConfig config(cfg, factory_context_); + EXPECT_FALSE(config.autoCloseConnections()); + EXPECT_EQ("/test", config.requestPath()); + EXPECT_EQ("PUT", config.requestMethod()); +} + +// Test decodeData with multiple chunks. +TEST_F(ReverseTunnelFilterUnitTest, DecodeDataMultipleChunks) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + const std::string req = + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "n", "c", "t"); + + // Send headers first without end_stream. + const auto hdr_end = req.find("\r\n\r\n"); + const std::string headers_part = req.substr(0, hdr_end + 4); + Buffer::OwnedImpl header_buf(headers_part); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(header_buf, false)); + + // Send body in chunks without end_stream. + const std::string body_part = req.substr(hdr_end + 4); + const size_t chunk_size = body_part.size() / 3; + + Buffer::OwnedImpl chunk1(body_part.substr(0, chunk_size)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunk1, false)); + + Buffer::OwnedImpl chunk2(body_part.substr(chunk_size, chunk_size)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunk2, false)); + + // Send final chunk with end_stream. + Buffer::OwnedImpl chunk3(body_part.substr(chunk_size * 2)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunk3, true)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test RequestDecoderImpl interface methods with proper HTTP flow. +TEST_F(ReverseTunnelFilterUnitTest, RequestDecoderImplInterfaceMethodsCoverage) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Create a proper HTTP request with chunked encoding and trailers and headers-only body + std::string chunked_request = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + + makeRtHeaders("interface", "test", "coverage") + + "Transfer-Encoding: chunked\r\n\r\n"; + + // Send headers first + Buffer::OwnedImpl header_buf(chunked_request); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(header_buf, false)); + + // Send chunk end and trailers (no body required) + std::string end_chunk_and_trailers = "0\r\nX-Test-Trailer: value\r\n\r\n"; + Buffer::OwnedImpl trailer_buf(end_chunk_and_trailers); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(trailer_buf, false)); + + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test codec dispatch failure with truly malformed HTTP. +TEST_F(ReverseTunnelFilterUnitTest, CodecDispatchFailureDetailed) { + // Create HTTP data that will cause codec dispatch to fail and log error. + std::string malformed_http = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: \xFF\xFF\xFF\xFF\r\n\r\n"; // Invalid content length + + Buffer::OwnedImpl request(malformed_http); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); +} + +// Test more malformed HTTP to hit codec error paths. +TEST_F(ReverseTunnelFilterUnitTest, CodecDispatchMultipleErrorTypes) { + // Test 1: HTTP request with invalid headers + std::string invalid_headers = "GET /reverse_connections/request HTTP/1.1\r\n" + "Invalid Header Without Colon\r\n" + "\r\n"; + Buffer::OwnedImpl req1(invalid_headers); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(req1, false)); + + // Create new filter for second test + auto filter2 = + std::make_unique(config_, *stats_store_.rootScope(), overload_manager_); + NiceMock callbacks2; + EXPECT_CALL(callbacks2, connection()).WillRepeatedly(ReturnRef(callbacks2.connection_)); + auto socket2 = std::make_unique(); + EXPECT_CALL(*socket2, isOpen()).WillRepeatedly(testing::Return(true)); + static Network::ConnectionSocketPtr stored_socket2 = std::move(socket2); + EXPECT_CALL(callbacks2.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_socket2)); + filter2->initializeReadFilterCallbacks(callbacks2); + + // Test 2: Invalid HTTP version + std::string invalid_version = "GET /reverse_connections/request HTTP/9.9\r\n\r\n"; + Buffer::OwnedImpl req2(invalid_version); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter2->onData(req2, false)); +} + +// Ensure success path works without additional validations. +TEST_F(ReverseTunnelFilterUnitTest, SuccessPathCoverage) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Create a valid request; response verification occurs normally. + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "response-test", "cluster", "tenant")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + + // Ensure the success path works. + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test decodeMetadata method coverage. +TEST_F(ReverseTunnelFilterUnitTest, DecodeMetadataMethodCoverage) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // The decodeMetadata method is called internally when processing certain HTTP requests + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "meta", "data", "test")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test streamInfo method coverage. +TEST_F(ReverseTunnelFilterUnitTest, StreamInfoMethodCoverage) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "stream", "info", "test")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test accessLogHandlers method coverage. +TEST_F(ReverseTunnelFilterUnitTest, AccessLogHandlersMethodCoverage) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "access", "log", "test")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test getRequestDecoderHandle method coverage. +TEST_F(ReverseTunnelFilterUnitTest, GetRequestDecoderHandleMethodCoverage) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "decoder", "handle", "test")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test various HTTP malformations to hit codec error paths. +TEST_F(ReverseTunnelFilterUnitTest, VariousHttpMalformations) { + // Test different types of malformed HTTP to hit codec dispatch error paths + std::vector malformed_requests = { + // Missing HTTP version + "GET /reverse_connections/request\r\nHost: test\r\n\r\n", + // Invalid method + "INVALID_METHOD /reverse_connections/request HTTP/1.1\r\nHost: test\r\n\r\n", + // Binary garbage + std::string("\x00\x01\x02\x03\x04\x05", 6), + // Incomplete request line + "POS", + // Missing headers separator + "GET /reverse_connections/request HTTP/1.1\r\nHost: test", + // Invalid characters in headers + "GET /reverse_connections/request HTTP/1.1\r\nHo\x00st: test\r\n\r\n"}; + + for (size_t i = 0; i < malformed_requests.size(); ++i) { + // Create new filter for each test to avoid state issues + auto test_filter = std::make_unique(config_, *stats_store_.rootScope(), + overload_manager_); + NiceMock test_callbacks; + EXPECT_CALL(test_callbacks, connection()).WillRepeatedly(ReturnRef(test_callbacks.connection_)); + + auto test_socket = std::make_unique(); + EXPECT_CALL(*test_socket, isOpen()).WillRepeatedly(testing::Return(true)); + static std::vector stored_test_sockets; + stored_test_sockets.push_back(std::move(test_socket)); + EXPECT_CALL(test_callbacks.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_test_sockets.back())); + + test_filter->initializeReadFilterCallbacks(test_callbacks); + + Buffer::OwnedImpl request(malformed_requests[i]); + EXPECT_EQ(Network::FilterStatus::StopIteration, test_filter->onData(request, false)); + } +} + +// Test processAcceptedConnection with null TLS registry. +TEST_F(ReverseTunnelFilterUnitTest, ProcessAcceptedConnectionNullTlsRegistry) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Socket lifecycle is now managed by UpstreamReverseConnectionIOHandle wrapper. + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "null-tls", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test processAcceptedConnection when duplicate() returns null. +TEST_F(ReverseTunnelFilterUnitTest, ProcessAcceptedConnectionDuplicateFails) { + // Set up thread local slot for downstream socket interface. This is necessary + // for the socket manager to be initialized. + setupUpstreamExtension(); + setupUpstreamThreadLocalSlot(); + + // Create a mock socket that returns a null/closed handle on duplicate. + auto mock_socket = std::make_unique(); + auto mock_io_handle = std::make_unique(); + + // Setup IoHandle to return null on duplicate. + EXPECT_CALL(*mock_io_handle, duplicate()).WillOnce(testing::Return(nullptr)); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(testing::ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket, isOpen()).WillRepeatedly(testing::Return(true)); + + static Network::ConnectionSocketPtr stored_mock_socket; + static std::unique_ptr stored_io_handle; + stored_io_handle = std::move(mock_io_handle); + stored_mock_socket = std::move(mock_socket); + + EXPECT_CALL(callbacks_.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_mock_socket)); + // Socket lifecycle is now managed by UpstreamReverseConnectionIOHandle wrapper. + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "dup-fail", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test processAcceptedConnection when duplicated handle is not open. +TEST_F(ReverseTunnelFilterUnitTest, ProcessAcceptedConnectionDuplicatedHandleNotOpen) { + + // Set up thread local slot for downstream socket interface. This is necessary + // for the socket manager to be initialized. + setupUpstreamExtension(); + setupUpstreamThreadLocalSlot(); + + auto mock_socket = std::make_unique(); + auto mock_io_handle = std::make_unique(); + auto dup_io_handle = std::make_unique(); + + // Setup duplicated handle to report as not open. + EXPECT_CALL(*dup_io_handle, isOpen()).WillRepeatedly(testing::Return(false)); + EXPECT_CALL(*mock_io_handle, duplicate()) + .WillOnce(testing::Return(testing::ByMove(std::move(dup_io_handle)))); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(testing::ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket, isOpen()).WillRepeatedly(testing::Return(true)); + + static Network::ConnectionSocketPtr stored_mock_socket2; + static std::unique_ptr stored_io_handle2; + stored_io_handle2 = std::move(mock_io_handle); + stored_mock_socket2 = std::move(mock_socket); + + EXPECT_CALL(callbacks_.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_mock_socket2)); + // Socket lifecycle is now managed by UpstreamReverseConnectionIOHandle wrapper. + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request( + makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "dup-closed", "c", "t")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test systematic HTTP error patterns to trigger codec dispatch error paths. +TEST_F(ReverseTunnelFilterUnitTest, SystematicHttpErrorPatterns) { + auto patterns = HttpErrorHelper::getHttpErrorPatterns(); + + for (size_t i = 0; i < patterns.size(); ++i) { + // Create new filter for each test to avoid state pollution + auto error_filter = std::make_unique(config_, *stats_store_.rootScope(), + overload_manager_); + NiceMock error_callbacks; + EXPECT_CALL(error_callbacks, connection()) + .WillRepeatedly(ReturnRef(error_callbacks.connection_)); + + // Set up socket for each test + auto error_socket = std::make_unique(); + EXPECT_CALL(*error_socket, isOpen()).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*error_socket, ioHandle()) + .WillRepeatedly(testing::ReturnRef(*error_callbacks.socket_.io_handle_)); + + static std::vector stored_error_sockets; + stored_error_sockets.push_back(std::move(error_socket)); + EXPECT_CALL(error_callbacks.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_error_sockets.back())); + + error_filter->initializeReadFilterCallbacks(error_callbacks); + + // Test this error pattern + Buffer::OwnedImpl error_request(patterns[i]); + EXPECT_EQ(Network::FilterStatus::StopIteration, error_filter->onData(error_request, false)); + } +} + +// Test edge cases in HTTP/protobuf processing to maximize coverage. +TEST_F(ReverseTunnelFilterUnitTest, EdgeCaseHttpProtobufProcessing) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Test 1: Binary data that looks like protobuf but isn't + std::string fake_protobuf; + fake_protobuf.push_back(0x08); // Protobuf field tag + fake_protobuf.push_back(0x96); // Invalid varint continuation + fake_protobuf.push_back(0xFF); // More invalid data + fake_protobuf.push_back(0xFF); + fake_protobuf.push_back(0xFF); + + Buffer::OwnedImpl fake_request(makeHttpRequest("GET", "/reverse_connections/request", "")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(fake_request, false)); + EXPECT_THAT(written, testing::HasSubstr("400 Bad Request")); +} + +// Test to trigger specific interface methods for coverage. +TEST_F(ReverseTunnelFilterUnitTest, InterfaceMethodsCompleteCoverage) { + // Set up thread local slot for downstream socket interface. This is necessary + // for the socket manager to be initialized. + setupUpstreamExtension(); + setupUpstreamThreadLocalSlot(); + + // Set up mock socket with proper duplication mocking + auto mock_socket = std::make_unique(); + auto mock_io_handle = std::make_unique(); + auto dup_handle = std::make_unique(); + + // Mock successful duplication + EXPECT_CALL(*dup_handle, isOpen()).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*dup_handle, resetFileEvents()); + EXPECT_CALL(*dup_handle, fdDoNotUse()).WillRepeatedly(testing::Return(456)); + + EXPECT_CALL(*mock_io_handle, duplicate()) + .WillOnce(testing::Return(testing::ByMove(std::move(dup_handle)))); + EXPECT_CALL(*mock_socket, ioHandle()).WillRepeatedly(testing::ReturnRef(*mock_io_handle)); + EXPECT_CALL(*mock_socket, isOpen()).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(testing::Return(455)); + + // Store in static variables + static Network::ConnectionSocketPtr stored_interface_socket; + static std::unique_ptr stored_interface_handle; + stored_interface_handle = std::move(mock_io_handle); + stored_interface_socket = std::move(mock_socket); + + EXPECT_CALL(callbacks_.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_interface_socket)); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Create request with HTTP/1.1 Transfer-Encoding chunked to trigger trailers + std::string chunked_request = "GET /reverse_connections/request HTTP/1.1\r\n" + "Host: localhost\r\n" + + makeRtHeaders("interface", "methods", "test") + + "Transfer-Encoding: chunked\r\n\r\n"; + chunked_request += "0\r\n"; // End chunk + chunked_request += "X-Custom-Trailer: test-value\r\n"; // Trailer header + chunked_request += "\r\n"; // End trailers + + Buffer::OwnedImpl chunked_buf(chunked_request); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(chunked_buf, false)); + + // This should trigger decodeTrailers, decodeMetadata (if any), + // streamInfo, accessLogHandlers, and getRequestDecoderHandle methods + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test processIfComplete when already complete. +TEST_F(ReverseTunnelFilterUnitTest, ProcessIfCompleteAlreadyComplete) { + // Set up thread local slot for downstream socket interface. This is necessary + // for the socket manager to be initialized. + setupUpstreamExtension(); + // We don't need to setup thread local slot for this test since + // we are not testing socket duplication. + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Send a complete request. + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "double", "complete", "test")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + + // Verify we got the response. + EXPECT_THAT(written, testing::HasSubstr("200 OK")); + + // Try to send more data - should be ignored as already complete. + Buffer::OwnedImpl more_data("extra data"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(more_data, false)); +} + +// Test successful socket duplication with all operations succeeding. +TEST_F(ReverseTunnelFilterUnitTest, SuccessfulSocketDuplication) { + // Set up thread local slot for downstream socket interface. This is necessary + // for the socket manager to be initialized. + setupUpstreamExtension(); + setupUpstreamThreadLocalSlot(); + + auto socket_with_dup = std::make_unique(); + + // Mock successful duplication where everything succeeds. + auto mock_io_handle = std::make_unique(); + auto dup_handle = std::make_unique(); + + // The duplicated handle is open and operations succeed. + EXPECT_CALL(*dup_handle, isOpen()).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*dup_handle, resetFileEvents()); + EXPECT_CALL(*dup_handle, fdDoNotUse()).WillRepeatedly(testing::Return(123)); + + // Mock the duplicate() call to return the dup_handle. + EXPECT_CALL(*mock_io_handle, duplicate()) + .WillOnce(testing::Return(testing::ByMove(std::move(dup_handle)))); + + // Mock ioHandle() to return our mock handle. + EXPECT_CALL(*socket_with_dup, ioHandle()).WillRepeatedly(testing::ReturnRef(*mock_io_handle)); + EXPECT_CALL(*socket_with_dup, isOpen()).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*mock_io_handle, fdDoNotUse()).WillRepeatedly(testing::Return(122)); + + // Store socket and handle in static variables. + static Network::ConnectionSocketPtr stored_dup_socket; + static std::unique_ptr stored_dup_handle; + stored_dup_handle = std::move(mock_io_handle); + stored_dup_socket = std::move(socket_with_dup); + + // Set up the callbacks to use our mock socket. + EXPECT_CALL(callbacks_.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_dup_socket)); + + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + Buffer::OwnedImpl request(makeHttpRequestWithRtHeaders("GET", "/reverse_connections/request", + "dup", "success", "test")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("200 OK")); +} + +// Test modify_headers callback in sendLocalReply. +TEST_F(ReverseTunnelFilterUnitTest, SendLocalReplyWithModifyHeaders) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Send a request that will trigger a 404 response with modify_headers callback. + Buffer::OwnedImpl request(makeHttpRequest("GET", "/wrong/path")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + + // The sendLocalReply with modify_headers is called internally. + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); +} + +// Test sendLocalReply with all branches covered. +TEST_F(ReverseTunnelFilterUnitTest, SendLocalReplyAllBranches) { + std::string written; + EXPECT_CALL(callbacks_.connection_, write(testing::_, testing::_)) + .WillRepeatedly(testing::Invoke([&](Buffer::Instance& data, bool) { + written.append(data.toString()); + data.drain(data.length()); + })); + + // Test with wrong method to trigger 404. + Buffer::OwnedImpl request(makeHttpRequest("POST", "/reverse_connections/request")); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(request, false)); + EXPECT_THAT(written, testing::HasSubstr("404 Not Found")); + EXPECT_THAT(written, testing::HasSubstr("Not a reverse tunnel request")); +} + +// Test HTTP/1.1 codec initialization with different settings. +TEST_F(ReverseTunnelFilterUnitTest, CodecInitializationCoverage) { + // Create a new filter to test codec initialization. + auto test_filter = + std::make_unique(config_, *stats_store_.rootScope(), overload_manager_); + NiceMock test_callbacks; + EXPECT_CALL(test_callbacks, connection()).WillRepeatedly(ReturnRef(test_callbacks.connection_)); + + auto test_socket = std::make_unique(); + EXPECT_CALL(*test_socket, isOpen()).WillRepeatedly(testing::Return(true)); + static Network::ConnectionSocketPtr stored_codec_socket = std::move(test_socket); + EXPECT_CALL(test_callbacks.connection_, getSocket()) + .WillRepeatedly(testing::ReturnRef(stored_codec_socket)); + + test_filter->initializeReadFilterCallbacks(test_callbacks); + + // First call to onData initializes the codec. + Buffer::OwnedImpl data1("GET /test HTTP/1.1\r\n"); + EXPECT_EQ(Network::FilterStatus::StopIteration, test_filter->onData(data1, false)); + + // Second call uses existing codec. + Buffer::OwnedImpl data2("Host: test\r\n\r\n"); + EXPECT_EQ(Network::FilterStatus::StopIteration, test_filter->onData(data2, false)); +} + +} // namespace +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/reverse_tunnel/integration_test.cc b/test/extensions/filters/network/reverse_tunnel/integration_test.cc new file mode 100644 index 0000000000000..d9b6af8f4082d --- /dev/null +++ b/test/extensions/filters/network/reverse_tunnel/integration_test.cc @@ -0,0 +1,487 @@ +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.pb.h" +#include "envoy/extensions/transport_sockets/internal_upstream/v3/internal_upstream.pb.h" + +#include "source/common/protobuf/protobuf.h" + +#include "test/integration/integration.h" +#include "test/integration/utility.h" +#include "test/test_common/logging.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace ReverseTunnel { +namespace { + +class ReverseTunnelFilterIntegrationTest + : public testing::TestWithParam, + public BaseIntegrationTest { +public: + ReverseTunnelFilterIntegrationTest() + : BaseIntegrationTest(GetParam(), ConfigHelper::baseConfig()) {} + + void initialize() override { + // Add common bootstrap extensions that are used across multiple tests. + config_helper_.addBootstrapExtension(R"EOF( +name: envoy.bootstrap.reverse_tunnel.upstream_socket_interface +typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3.UpstreamReverseConnectionSocketInterface +)EOF"); + + config_helper_.addBootstrapExtension(R"EOF( +name: envoy.bootstrap.reverse_tunnel.downstream_socket_interface +typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface +)EOF"); + + // Call parent initialize to complete setup. + BaseIntegrationTest::initialize(); + } + +protected: + void addSetFilterStateFilter(const std::string& node_id = "integration-test-node", + const std::string& cluster_id = "integration-test-cluster", + const std::string& tenant_id = "integration-test-tenant") { + std::string on_new_connection = ""; + if (!node_id.empty()) { + on_new_connection += fmt::format(R"( + - object_key: node_id + factory_key: envoy.string + format_string: + text_format_source: + inline_string: "{}")", + node_id); + } + if (!cluster_id.empty()) { + on_new_connection += fmt::format(R"( + - object_key: cluster_id + factory_key: envoy.string + format_string: + text_format_source: + inline_string: "{}")", + cluster_id); + } + if (!tenant_id.empty()) { + on_new_connection += fmt::format(R"( + - object_key: tenant_id + factory_key: envoy.string + format_string: + text_format_source: + inline_string: "{}")", + tenant_id); + } + + const std::string set_filter_state = fmt::format(R"EOF( +name: envoy.filters.network.set_filter_state +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.set_filter_state.v3.Config + on_new_connection:{} +)EOF", + on_new_connection); + + config_helper_.addConfigModifier( + [set_filter_state](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + envoy::config::listener::v3::Filter filter; + TestUtility::loadFromYaml(set_filter_state, filter); + ASSERT_GT(bootstrap.mutable_static_resources()->listeners_size(), 0); + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + + // Create a filter chain if one doesn't exist, otherwise clear existing filters. + if (listener->filter_chains_size() == 0) { + listener->add_filter_chains(); + } else { + listener->mutable_filter_chains(0)->clear_filters(); + } + + // Add set_filter_state first. + listener->mutable_filter_chains(0)->add_filters()->Swap(&filter); + }); + } + + void addReverseTunnelFilter(bool auto_close_connections = false, + const std::string& request_path = "/reverse_connections/request", + const std::string& request_method = "GET") { + const std::string filter_config = + fmt::format(R"EOF( + name: envoy.filters.network.reverse_tunnel + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.reverse_tunnel.v3.ReverseTunnel + ping_interval: + seconds: 300 + auto_close_connections: {} + request_path: "{}" + request_method: {} +)EOF", + auto_close_connections ? "true" : "false", request_path, request_method); + + config_helper_.addConfigModifier( + [filter_config](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + envoy::config::listener::v3::Filter filter; + TestUtility::loadFromYaml(filter_config, filter); + ASSERT_GT(bootstrap.mutable_static_resources()->listeners_size(), 0); + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + + // Create a filter chain if one doesn't exist. + if (listener->filter_chains_size() == 0) { + listener->add_filter_chains(); + } + + // Add reverse tunnel filter (either as first filter or after existing filters). + listener->mutable_filter_chains(0)->add_filters()->Swap(&filter); + }); + } + + std::string createTestPayload(const std::string& node_uuid = "integration-test-node", + const std::string& cluster_uuid = "integration-test-cluster", + const std::string& tenant_uuid = "integration-test-tenant") { + UNREFERENCED_PARAMETER(node_uuid); + UNREFERENCED_PARAMETER(cluster_uuid); + UNREFERENCED_PARAMETER(tenant_uuid); + return std::string(); + } + + std::string createHttpRequest(const std::string& method, const std::string& path, + const std::string& body = "") { + std::string request = fmt::format("{} {} HTTP/1.1\r\n", method, path); + request += "Host: localhost\r\n"; + request += fmt::format("Content-Length: {}\r\n", body.length()); + request += "\r\n"; + request += body; + return request; + } + + std::string createHttpRequestWithRtHeaders(const std::string& method, const std::string& path, + const std::string& node, const std::string& cluster, + const std::string& tenant, + const std::string& body = "") { + std::string request = fmt::format("{} {} HTTP/1.1\r\n", method, path); + request += "Host: localhost\r\n"; + request += fmt::format("{}: {}\r\n", "x-envoy-reverse-tunnel-node-id", node); + request += fmt::format("{}: {}\r\n", "x-envoy-reverse-tunnel-cluster-id", cluster); + request += fmt::format("{}: {}\r\n", "x-envoy-reverse-tunnel-tenant-id", tenant); + request += fmt::format("Content-Length: {}\r\n", body.length()); + request += "\r\n"; + request += body; + return request; + } + + // Set log level to debug for this test class. + LogLevelSetter log_level_setter_ = LogLevelSetter(spdlog::level::trace); +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, ReverseTunnelFilterIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(ReverseTunnelFilterIntegrationTest, ValidReverseTunnelRequest) { + // Configure the reverse tunnel filter with default settings. + addReverseTunnelFilter(); + initialize(); + + std::string http_request = + createHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "integration-test-node", + "integration-test-cluster", "integration-test-tenant"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + if (!tcp_client->write(http_request)) { + // Server may have already sent the response and closed quickly; still verify response. + tcp_client->waitForData("HTTP/1.1 200 OK"); + return; + } + + // Should receive HTTP 200 OK response. + tcp_client->waitForData("HTTP/1.1 200 OK"); + + // Since auto_close_connections: false, we need to close the connection manually. + tcp_client->close(); +} + +TEST_P(ReverseTunnelFilterIntegrationTest, InvalidReverseTunnelRequest) { + // Configure the reverse tunnel filter with default settings. + addReverseTunnelFilter(); + initialize(); + + std::string http_request = createHttpRequest("GET", "/health"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + if (!tcp_client->write(http_request)) { + // Server may have already sent the response and closed. + tcp_client->waitForDisconnect(); + return; + } + // The request should pass through or be handled by other components; connection may close. + tcp_client->waitForDisconnect(); +} + +TEST_P(ReverseTunnelFilterIntegrationTest, PartialRequestHandling) { + // Configure the reverse tunnel filter with default settings. + addReverseTunnelFilter(); + initialize(); + + std::string http_request = createHttpRequestWithRtHeaders( + "GET", "/reverse_connections/request", "integration-test-node", "integration-test-cluster", + "integration-test-tenant", "abcdefghijklmno"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + + // Send request in chunks but ensure the body only completes on the third chunk. + // Split the HTTP request into headers and body, then stream body in parts. + const std::string::size_type hdr_end = http_request.find("\r\n\r\n"); + ASSERT_NE(hdr_end, std::string::npos); + const std::string headers = http_request.substr(0, hdr_end + 4); + const std::string body = http_request.substr(hdr_end + 4); + ASSERT_GT(body.size(), 8u); + + const size_t part = body.size() / 4; // Ensure first 2 parts are not enough to complete. + const std::string body1 = body.substr(0, part); + const std::string body2 = body.substr(part, part); + const std::string body3 = body.substr(2 * part); + + // First write: headers + small part of body. + if (!tcp_client->write(headers + body1, /*end_stream=*/false)) { + // Server may have already processed and responded; validate response and exit. + tcp_client->waitForData("HTTP/1.1 200 OK"); + return; + } + // Second write: more body but still not complete. If the server already completed,. + // the write can fail due to disconnect; treat that as acceptable and verify response. + if (!tcp_client->write(body2, /*end_stream=*/false)) { + tcp_client->waitForData("HTTP/1.1 200 OK"); + return; + } + // Third write: remaining body to complete the request. Same tolerance as above. + if (!tcp_client->write(body3, /*end_stream=*/false)) { + tcp_client->waitForData("HTTP/1.1 200 OK"); + return; + } + + // Should receive complete HTTP response. + tcp_client->waitForData("HTTP/1.1 200 OK"); + // Server may keep connection open (auto_close_connections: false). Close client side. + tcp_client->close(); +} + +TEST_P(ReverseTunnelFilterIntegrationTest, WrongPathReturns404) { + // Configure the reverse tunnel filter with default settings. + addReverseTunnelFilter(); + initialize(); + + // Test that requesting a different path than configured returns 404. + // The default configuration uses "/reverse_connections/request" path. + std::string http_request = + createHttpRequestWithRtHeaders("GET", "/custom/reverse", "integration-test-node", + "integration-test-cluster", "integration-test-tenant"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + if (!tcp_client->write(http_request)) { + // Server may have already sent the response and closed. + tcp_client->waitForDisconnect(); + return; + } + + // Should receive 404 Not Found response and connection should close. + tcp_client->waitForData("HTTP/1.1 404 Not Found"); + tcp_client->waitForDisconnect(); +} + +TEST_P(ReverseTunnelFilterIntegrationTest, MissingNodeUuidRejection) { + // Configure the reverse tunnel filter with default settings. + addReverseTunnelFilter(); + initialize(); + + // Missing node UUID header should trigger 400. + std::string http_request = + fmt::format("{} {} HTTP/1.1\r\nHost: localhost\r\n" + "x-envoy-reverse-tunnel-cluster-id: {}\r\n" + "x-envoy-reverse-tunnel-tenant-id: {}\r\nContent-Length: 0\r\n\r\n", + "GET", "/reverse_connections/request", "test-cluster", "test-tenant"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + if (!tcp_client->write(http_request)) { + // Server may have already sent the response and closed. + tcp_client->waitForData("HTTP/1.1 400 Bad Request"); + return; + } + + // Should receive HTTP 400 Bad Request response for missing node UUID. + tcp_client->waitForData("HTTP/1.1 400 Bad Request"); + tcp_client->waitForDisconnect(); +} + +// Filter accepts when method/path/headers match. +TEST_P(ReverseTunnelFilterIntegrationTest, AcceptsWhenHeadersPresent) { + addReverseTunnelFilter(); + initialize(); + + std::string http_request = + createHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "integration-test-node", + "integration-test-cluster", "integration-test-tenant"); + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(tcp_client->write(http_request)); + tcp_client->waitForData("HTTP/1.1 200 OK"); + tcp_client->close(); +} + +TEST_P(ReverseTunnelFilterIntegrationTest, IgnoresFilterStateValues) { + addReverseTunnelFilter(); + initialize(); + + std::string http_request = + createHttpRequestWithRtHeaders("GET", "/reverse_connections/request", "integration-test-node", + "integration-test-cluster", "integration-test-tenant"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(tcp_client->write(http_request)); + tcp_client->waitForData("HTTP/1.1 200 OK"); + tcp_client->close(); +} + +// Integration test that verifies basic reverse tunnel handshake. +TEST_P(ReverseTunnelFilterIntegrationTest, BasicReverseTunnelHandshake) { + // Configure the reverse tunnel filter with default settings. + addReverseTunnelFilter(); + initialize(); + + // Test reverse tunnel handshake and socket reuse functionality. + std::string http_request = createHttpRequestWithRtHeaders( + "GET", "/reverse_connections/request", "test-node", "test-cluster", "test-tenant"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(tcp_client->write(http_request)); + + // Should receive HTTP 200 OK response from the reverse tunnel filter. + tcp_client->waitForData("HTTP/1.1 200 OK"); + + // Verify stats show successful reverse tunnel handshake. + test_server_->waitForCounterGe("reverse_tunnel.handshake.accepted", 1); + + // Send a second request to test socket caching for different node IDs. + IntegrationTcpClientPtr tcp_client2 = makeTcpConnection(lookupPort("listener_0")); + std::string http_request2 = createHttpRequestWithRtHeaders( + "GET", "/reverse_connections/request", "test-node-2", "test-cluster-2", "test-tenant-2"); + + ASSERT_TRUE(tcp_client2->write(http_request2)); + tcp_client2->waitForData("HTTP/1.1 200 OK"); + + // Verify additional handshake was processed. + test_server_->waitForCounterGe("reverse_tunnel.handshake.accepted", 2); + + tcp_client->close(); + tcp_client2->close(); +} + +// End-to-end reverse connection handshake test where the downstream reverse connection listener +// (rc://) initiates a. connection to upstream listener running the reverse_tunnel filter. The +// downstream. side sends HTTP headers using the same helpers as the upstream expects, and the +// upstream. socket manager updates connection stats. We verify the gauges to confirm handshake +// success. The ping interval is kept at a very high value (5 minutes) to avoid ping timeout on +// accepted reverse connections. +TEST_P(ReverseTunnelFilterIntegrationTest, EndToEndReverseConnectionHandshake) { + DISABLE_IF_ADMIN_DISABLED; // Test requires admin interface for draining listener. + + // Use a deterministic port to avoid timing issues. + const uint32_t upstream_port = GetParam() == Network::Address::IpVersion::v4 ? 15000 : 15001; + const std::string loopback_addr = + GetParam() == Network::Address::IpVersion::v4 ? "127.0.0.1" : "::1"; + + // Configure listeners and clusters for the full reverse tunnel flow. + config_helper_.addConfigModifier([upstream_port, loopback_addr]( + envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // Clear existing listeners and add our custom setup. + bootstrap.mutable_static_resources()->clear_listeners(); + + // Ensure admin interface is configured. + if (!bootstrap.has_admin()) { + auto* admin = bootstrap.mutable_admin(); + auto* admin_address = admin->mutable_address()->mutable_socket_address(); + admin_address->set_address(loopback_addr); + admin_address->set_port_value(0); // Use ephemeral port + } + + // Listener 1: Upstream listener with reverse tunnel filter (accepts reverse connections). + auto* upstream_listener = bootstrap.mutable_static_resources()->add_listeners(); + upstream_listener->set_name("upstream_listener"); + upstream_listener->mutable_address()->mutable_socket_address()->set_address(loopback_addr); + upstream_listener->mutable_address()->mutable_socket_address()->set_port_value(upstream_port); + + auto* upstream_chain = upstream_listener->add_filter_chains(); + auto* rt_filter = upstream_chain->add_filters(); + rt_filter->set_name("envoy.filters.network.reverse_tunnel"); + + envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel rt_config; + rt_config.mutable_ping_interval()->set_seconds( + 300); // Set the ping interval to the max value to avoid ping timeout. + rt_config.set_auto_close_connections(false); + rt_config.set_request_path("/reverse_connections/request"); + rt_config.set_request_method(envoy::config::core::v3::GET); + rt_filter->mutable_typed_config()->PackFrom(rt_config); + + // Listener 2: Reverse connection listener (initiates reverse connections). + auto* rc_listener = bootstrap.mutable_static_resources()->add_listeners(); + rc_listener->set_name("reverse_connection_listener"); + auto* rc_address = rc_listener->mutable_address()->mutable_socket_address(); + // Use rc:// scheme to trigger reverse connection initiation. + rc_address->set_address("rc://e2e-node:e2e-cluster:e2e-tenant@upstream_cluster:1"); + rc_address->set_port_value(0); // Not used for rc:// addresses + rc_address->set_resolver_name("envoy.resolvers.reverse_connection"); + + // Add filter chain with echo filter to process the connection. + auto* rc_chain = rc_listener->add_filter_chains(); + auto* echo_filter = rc_chain->add_filters(); + echo_filter->set_name("envoy.filters.network.echo"); + auto* echo_config = echo_filter->mutable_typed_config(); + echo_config->set_type_url("type.googleapis.com/envoy.extensions.filters.network.echo.v3.Echo"); + + // Cluster that points to our upstream listener using regular TCP. + auto* cluster = bootstrap.mutable_static_resources()->add_clusters(); + cluster->set_name("upstream_cluster"); + cluster->set_type(envoy::config::cluster::v3::Cluster::STATIC); + cluster->mutable_load_assignment()->set_cluster_name("upstream_cluster"); + + // Point to the upstream listener's port. + auto* locality = cluster->mutable_load_assignment()->add_endpoints(); + auto* lb_endpoint = locality->add_lb_endpoints(); + auto* endpoint = lb_endpoint->mutable_endpoint(); + auto* addr = endpoint->mutable_address()->mutable_socket_address(); + addr->set_address(loopback_addr); + addr->set_port_value(upstream_port); + }); + + initialize(); + + // Register admin port after initialization since we cleared listeners. + registerTestServerPorts({}); + + ENVOY_LOG_MISC(info, "Waiting for reverse connections to be established."); + // Wait for reverse connections to be established. + timeSystem().advanceTimeWait(std::chrono::milliseconds(1000)); + + // Test that the full flow works by checking upstream socket interface metrics. + test_server_->waitForGaugeGe("reverse_connections.nodes.e2e-node", 1); + test_server_->waitForGaugeGe("reverse_connections.clusters.e2e-cluster", 1); + + // Verify stats show successful reverse tunnel handshake. + test_server_->waitForCounterGe("reverse_tunnel.handshake.accepted", 1); + + // Drain listeners to trigger proper cleanup of ReverseConnectionIOHandle. + BufferingStreamDecoderPtr admin_response = IntegrationUtil::makeSingleRequest( + lookupPort("admin"), "POST", "/drain_listeners", "", Http::CodecType::HTTP1, GetParam()); + EXPECT_TRUE(admin_response->complete()); + EXPECT_EQ("200", admin_response->headers().getStatusValue()); + + // Wait for listeners to be stopped, which triggers ReverseConnectionIOHandle cleanup. + test_server_->waitForCounterEq("listener_manager.listener_stopped", + 2); // 2 listeners in this test +} + +} // namespace +} // namespace ReverseTunnel +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/sni_dynamic_forward_proxy/BUILD b/test/extensions/filters/network/sni_dynamic_forward_proxy/BUILD index 7955f5267b4da..b2f243cf4829f 100644 --- a/test/extensions/filters/network/sni_dynamic_forward_proxy/BUILD +++ b/test/extensions/filters/network/sni_dynamic_forward_proxy/BUILD @@ -46,6 +46,7 @@ envoy_extension_cc_test( "//source/extensions/filters/listener/tls_inspector:config", "//source/extensions/filters/network/sni_dynamic_forward_proxy:config", "//source/extensions/filters/network/tcp_proxy:config", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:http_integration_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", diff --git a/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_integration_test.cc b/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_integration_test.cc index ec041d14c7e79..45ca8b15e3bc0 100644 --- a/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_integration_test.cc +++ b/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_integration_test.cc @@ -5,6 +5,7 @@ #include "source/common/tls/client_ssl_socket.h" #include "source/common/tls/context_config_impl.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/integration/http_integration.h" #include "test/integration/ssl_utility.h" @@ -49,6 +50,10 @@ name: envoy.filters.network.sni_dynamic_forward_proxy max_hosts: {} dns_cache_circuit_breaker: max_pending_requests: {} + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig port_value: {} )EOF", Network::Test::ipVersionToDnsFamily(GetParam()), max_hosts, @@ -73,6 +78,10 @@ name: envoy.clusters.dynamic_forward_proxy max_hosts: {} dns_cache_circuit_breaker: max_pending_requests: {} + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig )EOF", Network::Test::ipVersionToDnsFamily(GetParam()), max_hosts, max_pending_requests); @@ -145,5 +154,163 @@ TEST_P(SniDynamicProxyFilterIntegrationTest, CircuitBreakerInvokedUpstreamTls) { EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_rq_pending_overflow")->value()); } +// Test that verifies DNS cache statistics are properly recorded for successful resolution. +TEST_P(SniDynamicProxyFilterIntegrationTest, DnsCacheStatisticsSuccess) { + setup(); + fake_upstreams_[0]->setReadDisableOnNewConnection(false); + + // Initial state where we have no DNS queries yet. + EXPECT_EQ(0, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(0, test_server_->counter("dns_cache.foo.dns_query_success")->value()); + EXPECT_EQ(0, test_server_->counter("dns_cache.foo.host_added")->value()); + EXPECT_EQ(0, test_server_->gauge("dns_cache.foo.num_hosts")->value()); + + // First connection. It should trigger DNS resolution. + codec_client_ = makeHttpConnection( + makeSslClientConnection(Ssl::ClientSslTransportOptions().setSni("localhost"))); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + + // Verify DNS resolution statistics. + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_success")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); + EXPECT_EQ(1, test_server_->gauge("dns_cache.foo.num_hosts")->value()); + + // Send a request to complete the flow. + const Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", + fmt::format("localhost:{}", fake_upstreams_[0]->localAddress()->ip()->port())}}; + + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + checkSimpleRequestSuccess(0, 0, response.get()); + + // Close the connection. + codec_client_->close(); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + + // Second connection to the same host. It should use cached entry. + codec_client_ = makeHttpConnection( + makeSslClientConnection(Ssl::ClientSslTransportOptions().setSni("localhost"))); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + + // Verify no new DNS query was made. + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_success")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); + EXPECT_EQ(1, test_server_->gauge("dns_cache.foo.num_hosts")->value()); +} + +// Test that verifies DNS query failure statistics with invalid hostname. +TEST_P(SniDynamicProxyFilterIntegrationTest, DnsCacheQueryFailureStatistics) { + setup(); + + // Initial state. It should have no DNS queries yet. + EXPECT_EQ(0, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(0, test_server_->counter("dns_cache.foo.dns_query_failure")->value()); + + // Attempt connection with invalid hostname that will fail DNS resolution. + codec_client_ = + makeRawHttpConnection(makeSslClientConnection(Ssl::ClientSslTransportOptions().setSni( + "invalid.doesnotexist.example.com")), + absl::nullopt); + ASSERT_FALSE(codec_client_->connected()); + + // Verify DNS failure statistics. + test_server_->waitForCounterGe("dns_cache.foo.dns_query_attempt", 1); + test_server_->waitForCounterGe("dns_cache.foo.dns_query_failure", 1); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_failure")->value()); +} + +// Test that verifies DNS query timeout statistics. +TEST_P(SniDynamicProxyFilterIntegrationTest, DnsCacheQueryTimeoutStatistics) { + // Configure with very short DNS timeout to trigger timeout scenario. + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // Switch predefined cluster_0 to CDS filesystem sourcing. + bootstrap.mutable_dynamic_resources()->mutable_cds_config()->set_resource_api_version( + envoy::config::core::v3::ApiVersion::V3); + bootstrap.mutable_dynamic_resources() + ->mutable_cds_config() + ->mutable_path_config_source() + ->set_path(cds_helper_.cdsPath()); + bootstrap.mutable_static_resources()->clear_clusters(); + + const std::string filter = fmt::format( + R"EOF( +name: envoy.filters.network.sni_dynamic_forward_proxy +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.sni_dynamic_forward_proxy.v3.FilterConfig + dns_cache_config: + name: foo + dns_lookup_family: {} + max_hosts: 1024 + dns_query_timeout: 0.001s + dns_cache_circuit_breaker: + max_pending_requests: 1024 + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig + port_value: {} +)EOF", + Network::Test::ipVersionToDnsFamily(GetParam()), + fake_upstreams_[0]->localAddress()->ip()->port()); + config_helper_.addNetworkFilter(filter); + }); + + // Setup cluster with matching DNS config. + cluster_.mutable_connect_timeout()->CopyFrom( + Protobuf::util::TimeUtil::MillisecondsToDuration(100)); + cluster_.set_name("cluster_0"); + cluster_.set_lb_policy(envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED); + + const std::string cluster_type_config = fmt::format( + R"EOF( +name: envoy.clusters.dynamic_forward_proxy +typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig + dns_cache_config: + name: foo + dns_lookup_family: {} + max_hosts: 1024 + dns_query_timeout: 0.001s + dns_cache_circuit_breaker: + max_pending_requests: 1024 + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig +)EOF", + Network::Test::ipVersionToDnsFamily(GetParam())); + + TestUtility::loadFromYaml(cluster_type_config, *cluster_.mutable_cluster_type()); + + config_helper_.addListenerFilter(ConfigHelper::tlsInspectorFilter()); + cds_helper_.setCds({cluster_}); + HttpIntegrationTest::initialize(); + test_server_->waitForCounterEq("cluster_manager.cluster_added", 1); + test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 0); + + // Initial state. It should have no timeouts yet. + EXPECT_EQ(0, test_server_->counter("dns_cache.foo.dns_query_timeout")->value()); + + // Attempt connection with hostname that should trigger DNS timeout. + codec_client_ = makeRawHttpConnection( + makeSslClientConnection(Ssl::ClientSslTransportOptions().setSni("slowresolve.example.com")), + absl::nullopt); + ASSERT_FALSE(codec_client_->connected()); + + // Verify DNS timeout statistics. + test_server_->waitForCounterGe("dns_cache.foo.dns_query_attempt", 1); + // Note: timeout detection can be flaky in test environment, so we check attempts were made. + EXPECT_GE(test_server_->counter("dns_cache.foo.dns_query_attempt")->value(), 1); +} + } // namespace } // namespace Envoy diff --git a/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc b/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc index b5835c8ed9833..a53b2a0afbc87 100644 --- a/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc +++ b/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc @@ -212,7 +212,7 @@ stat_prefix: test config_, random_, filter_callbacks_.connection_.dispatcher_.timeSource(), drain_decision_); filter_->initializeReadFilterCallbacks(filter_callbacks_); ON_CALL(filter_callbacks_.connection_.stream_info_, setDynamicMetadata(_, _)) - .WillByDefault(Invoke([this](const std::string& key, const ProtobufWkt::Struct& obj) { + .WillByDefault(Invoke([this](const std::string& key, const Protobuf::Struct& obj) { (*filter_callbacks_.connection_.stream_info_.metadata_.mutable_filter_metadata())[key] .MergeFrom(obj); })); @@ -1433,6 +1433,37 @@ TEST_F(ThriftConnectionManagerTest, BadFunctionCallExceptionHandling) { EXPECT_EQ(access_log_data_, ""); } +TEST_F(ThriftConnectionManagerTest, + BadFunctionCallExceptionHandlingWithClosingDownstreamConnection) { + initializeFilter(); + + writeFramedBinaryMessage(buffer_, MessageType::Oneway, 0x0F); + + ThriftFilters::DecoderFilterCallbacks* callbacks{}; + EXPECT_CALL(*decoder_filter_, setDecoderFilterCallbacks(_)) + .WillOnce( + Invoke([&](ThriftFilters::DecoderFilterCallbacks& cb) -> void { callbacks = &cb; })); + EXPECT_CALL(*decoder_filter_, messageBegin(_)) + .WillOnce(Invoke([&](MessageMetadataSharedPtr) -> FilterStatus { + // mock that downstream connection is closing + filter_callbacks_.connection_.state_ = Network::Connection::State::Closing; + + std::function func; + func(); // throw bad_function_call + return FilterStatus::Continue; + })); + + // A local exception is sent by error handling. + EXPECT_CALL(*decoder_filter_, onLocalReply(_, _)); + EXPECT_EQ(filter_->onData(buffer_, false), Network::FilterStatus::StopIteration); + + EXPECT_EQ(1U, store_.counter("test.request_decoding_error").value()); + // Won't increase this counter as it's expected. + EXPECT_EQ(0U, store_.counter("test.request_internal_error").value()); + + EXPECT_EQ(access_log_data_, ""); +} + // Tests that a request is routed and a non-thrift response is handled. TEST_F(ThriftConnectionManagerTest, RequestAndGarbageResponse) { initializeFilter(); diff --git a/test/extensions/filters/network/thrift_proxy/driver/generate_fixture.sh b/test/extensions/filters/network/thrift_proxy/driver/generate_fixture.sh index bdf2e23569088..102cc93f57755 100755 --- a/test/extensions/filters/network/thrift_proxy/driver/generate_fixture.sh +++ b/test/extensions/filters/network/thrift_proxy/driver/generate_fixture.sh @@ -108,7 +108,7 @@ else SERVICE_FLAGS+=("--unix") "${DRIVER_DIR}/server" "${SERVICE_FLAGS[@]}" & SERVER_PID="$!" - while [[ ! -a "${SOCKET}" ]]; do + while [[ ! -e "${SOCKET}" ]]; do sleep 0.1 if ! kill -0 "${SERVER_PID}"; then diff --git a/test/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter_test.cc b/test/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter_test.cc index 29214e232c045..658cc0ba63965 100644 --- a/test/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter_test.cc +++ b/test/extensions/filters/network/thrift_proxy/filters/header_to_metadata/header_to_metadata_filter_test.cc @@ -19,7 +19,7 @@ namespace HeaderToMetadataFilter { namespace { MATCHER_P(MapEq, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); @@ -28,7 +28,7 @@ MATCHER_P(MapEq, rhs, "") { } MATCHER_P(MapEqNum, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).number_value(), entry.second); @@ -37,7 +37,7 @@ MATCHER_P(MapEqNum, rhs, "") { } MATCHER_P(MapEqValue, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_TRUE(TestUtility::protoEqual(obj.fields().at(entry.first), entry.second)); @@ -284,10 +284,10 @@ TEST_F(HeaderToMetadataTest, ProtobufValueTypeInBase64UrlTest) { )EOF"; initializeFilter(request_config_yaml); - ProtobufWkt::Value value; + Protobuf::Value value; auto* s = value.mutable_struct_value(); - ProtobufWkt::Value v; + Protobuf::Value v; v.set_string_value("blafoo"); (*s->mutable_fields())["k1"] = v; v.set_number_value(2019.07); @@ -295,7 +295,7 @@ TEST_F(HeaderToMetadataTest, ProtobufValueTypeInBase64UrlTest) { v.set_bool_value(true); (*s->mutable_fields())["k3"] = v; - std::map expected = {{"proto_key", value}}; + std::map expected = {{"proto_key", value}}; EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEqValue(expected))); std::string data; diff --git a/test/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter_test.cc b/test/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter_test.cc index 54ddca99d2d18..73e7fd2f2ac69 100644 --- a/test/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter_test.cc +++ b/test/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/payload_to_metadata_filter_test.cc @@ -20,7 +20,7 @@ namespace { using ::testing::Return; MATCHER_P(MapEq, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_NE(obj.fields().find(entry.first), obj.fields().end()); @@ -30,7 +30,7 @@ MATCHER_P(MapEq, rhs, "") { } MATCHER_P(MapEqNum, rhs, "") { - const ProtobufWkt::Struct& obj = arg; + const Protobuf::Struct& obj = arg; EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_NE(obj.fields().find(entry.first), obj.fields().end()); diff --git a/test/extensions/filters/network/thrift_proxy/filters/ratelimit/ratelimit_test.cc b/test/extensions/filters/network/thrift_proxy/filters/ratelimit/ratelimit_test.cc index 81c567dacf0d8..5cb3a7fc22059 100644 --- a/test/extensions/filters/network/thrift_proxy/filters/ratelimit/ratelimit_test.cc +++ b/test/extensions/filters/network/thrift_proxy/filters/ratelimit/ratelimit_test.cc @@ -331,13 +331,13 @@ TEST_F(ThriftRateLimitFilterTest, ErrorResponseWithDynamicMetadata) { EXPECT_EQ(ThriftProxy::FilterStatus::StopIteration, filter_->messageBegin(request_metadata_)); Filters::Common::RateLimit::DynamicMetadataPtr dynamic_metadata = - std::make_unique(); + std::make_unique(); auto* fields = dynamic_metadata->mutable_fields(); (*fields)["name"] = ValueUtil::stringValue("my-limit"); (*fields)["x"] = ValueUtil::numberValue(3); EXPECT_CALL(filter_callbacks_.stream_info_, setDynamicMetadata(_, _)) .WillOnce(Invoke([&dynamic_metadata](const std::string& ns, - const ProtobufWkt::Struct& returned_dynamic_metadata) { + const Protobuf::Struct& returned_dynamic_metadata) { EXPECT_EQ(ns, "envoy.filters.thrift.rate_limit"); EXPECT_TRUE(TestUtility::protoEqual(returned_dynamic_metadata, *dynamic_metadata)); })); diff --git a/test/extensions/filters/network/thrift_proxy/mocks.cc b/test/extensions/filters/network/thrift_proxy/mocks.cc index ad491a3a968a1..276ad6f0409c8 100644 --- a/test/extensions/filters/network/thrift_proxy/mocks.cc +++ b/test/extensions/filters/network/thrift_proxy/mocks.cc @@ -13,9 +13,9 @@ using testing::ReturnRef; namespace Envoy { -// Provide a specialization for ProtobufWkt::Struct (for MockFilterConfigFactory) +// Provide a specialization for Protobuf::Struct (for MockFilterConfigFactory) template <> -void MessageUtil::validate(const ProtobufWkt::Struct&, ProtobufMessage::ValidationVisitor&, bool) {} +void MessageUtil::validate(const Protobuf::Struct&, ProtobufMessage::ValidationVisitor&, bool) {} namespace Extensions { namespace NetworkFilters { @@ -187,7 +187,7 @@ FilterFactoryCb MockDecoderFilterConfigFactory::createFilterFactoryFromProto( Server::Configuration::FactoryContext& context) { UNREFERENCED_PARAMETER(context); - config_struct_ = dynamic_cast(proto_config); + config_struct_ = dynamic_cast(proto_config); config_stat_prefix_ = stats_prefix; return [this](FilterChainFactoryCallbacks& callbacks) -> void { @@ -207,7 +207,7 @@ FilterFactoryCb MockEncoderFilterConfigFactory::createFilterFactoryFromProto( Server::Configuration::FactoryContext& context) { UNREFERENCED_PARAMETER(context); - config_struct_ = dynamic_cast(proto_config); + config_struct_ = dynamic_cast(proto_config); config_stat_prefix_ = stats_prefix; return [this](FilterChainFactoryCallbacks& callbacks) -> void { @@ -227,7 +227,7 @@ FilterFactoryCb MockBidirectionalFilterConfigFactory::createFilterFactoryFromPro Server::Configuration::FactoryContext& context) { UNREFERENCED_PARAMETER(context); - config_struct_ = dynamic_cast(proto_config); + config_struct_ = dynamic_cast(proto_config); config_stat_prefix_ = stats_prefix; return [this](FilterChainFactoryCallbacks& callbacks) -> void { diff --git a/test/extensions/filters/network/thrift_proxy/mocks.h b/test/extensions/filters/network/thrift_proxy/mocks.h index f81af7f674049..36cba4bcfb0c4 100644 --- a/test/extensions/filters/network/thrift_proxy/mocks.h +++ b/test/extensions/filters/network/thrift_proxy/mocks.h @@ -417,12 +417,12 @@ class MockDecoderFilterConfigFactory : public NamedThriftFilterConfigFactory { Server::Configuration::FactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return name_; } - ProtobufWkt::Struct config_struct_; + Protobuf::Struct config_struct_; std::string config_stat_prefix_; private: @@ -441,12 +441,12 @@ class MockEncoderFilterConfigFactory : public NamedThriftFilterConfigFactory { Server::Configuration::FactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return name_; } - ProtobufWkt::Struct config_struct_; + Protobuf::Struct config_struct_; std::string config_stat_prefix_; private: @@ -465,12 +465,12 @@ class MockBidirectionalFilterConfigFactory : public NamedThriftFilterConfigFacto Server::Configuration::FactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return name_; } - ProtobufWkt::Struct config_struct_; + Protobuf::Struct config_struct_; std::string config_stat_prefix_; private: diff --git a/test/extensions/filters/network/thrift_proxy/route_matcher_test.cc b/test/extensions/filters/network/thrift_proxy/route_matcher_test.cc index 92d944aca0539..65731f90b5e23 100644 --- a/test/extensions/filters/network/thrift_proxy/route_matcher_test.cc +++ b/test/extensions/filters/network/thrift_proxy/route_matcher_test.cc @@ -647,7 +647,7 @@ name: config criteria->metadataMatchCriteria(); EXPECT_EQ(2, mmc.size()); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); @@ -703,7 +703,7 @@ name: config auto matcher = createMatcher(yaml); MessageMetadata metadata; metadata.setMethodName("method1"); - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("v1"); v2.set_string_value("v2"); v3.set_string_value("v3"); @@ -790,7 +790,7 @@ name: config auto matcher = createMatcher(yaml); MessageMetadata metadata; metadata.setMethodName("method1"); - ProtobufWkt::Value v1, v2, v3; + Protobuf::Value v1, v2, v3; v1.set_string_value("v1"); v2.set_string_value("v2"); v3.set_string_value("v3"); @@ -901,7 +901,7 @@ TEST_F(ThriftRouteMatcherTest, ClusterHeaderMetadataMatch) { criteria->metadataMatchCriteria(); EXPECT_EQ(2, mmc.size()); - ProtobufWkt::Value v1, v2; + Protobuf::Value v1, v2; v1.set_string_value("v1"); v2.set_string_value("v2"); HashedValue hv1(v1), hv2(v2); diff --git a/test/extensions/filters/network/thrift_proxy/router_test.cc b/test/extensions/filters/network/thrift_proxy/router_test.cc index cd1de06c40da9..80d300e994cd8 100644 --- a/test/extensions/filters/network/thrift_proxy/router_test.cc +++ b/test/extensions/filters/network/thrift_proxy/router_test.cc @@ -151,8 +151,8 @@ class ThriftRouterTestBase { } void verifyMetadataMatchCriteriaFromRequest(bool route_entry_has_match) { - ProtobufWkt::Struct request_struct; - ProtobufWkt::Value val; + Protobuf::Struct request_struct; + Protobuf::Value val; // Populate metadata like StreamInfo.setDynamicMetadata() would. auto& fields_map = *request_struct.mutable_fields(); @@ -202,8 +202,8 @@ class ThriftRouterTestBase { } void verifyMetadataMatchCriteriaFromRoute(bool route_entry_has_match) { - ProtobufWkt::Struct route_struct; - ProtobufWkt::Value val; + Protobuf::Struct route_struct; + Protobuf::Value val; auto& fields_map = *route_struct.mutable_fields(); val.set_string_value("v3.1"); diff --git a/test/extensions/filters/network/wasm/BUILD b/test/extensions/filters/network/wasm/BUILD index ad56dd8ec8527..b561b390e23bb 100644 --- a/test/extensions/filters/network/wasm/BUILD +++ b/test/extensions/filters/network/wasm/BUILD @@ -18,13 +18,13 @@ envoy_package() envoy_extension_cc_test( name = "config_test", - size = "enormous", + size = "large", srcs = ["config_test.cc"], data = envoy_select_wasm_cpp_tests([ "//test/extensions/filters/network/wasm/test_data:test_cpp.wasm", ]), extension_names = ["envoy.filters.network.wasm"], - rbe_pool = "6gig", + rbe_pool = "4core", tags = ["skip_on_windows"], deps = [ "//source/common/common:base64_lib", @@ -42,7 +42,7 @@ envoy_extension_cc_test( envoy_extension_cc_test( name = "wasm_filter_test", - size = "enormous", + size = "large", srcs = ["wasm_filter_test.cc"], data = envoy_select_wasm_cpp_tests([ "//test/extensions/filters/network/wasm/test_data:test_cpp.wasm", @@ -53,7 +53,7 @@ envoy_extension_cc_test( "//test/extensions/filters/network/wasm/test_data:resume_call_rust.wasm", ]), extension_names = ["envoy.filters.network.wasm"], - rbe_pool = "6gig", + rbe_pool = "4core", tags = ["skip_on_windows"], deps = [ "//source/extensions/filters/network/wasm:wasm_filter_lib", diff --git a/test/extensions/filters/network/zookeeper_proxy/config_test.cc b/test/extensions/filters/network/zookeeper_proxy/config_test.cc index 07799f663caf5..cd0ce589ab49d 100644 --- a/test/extensions/filters/network/zookeeper_proxy/config_test.cc +++ b/test/extensions/filters/network/zookeeper_proxy/config_test.cc @@ -20,7 +20,7 @@ using ZooKeeperProxyProtoConfig = class ZookeeperFilterConfigTest : public testing::Test { public: - std::string populateFullConfig(const ProtobufWkt::EnumDescriptor* opcode_descriptor) { + std::string populateFullConfig(const Protobuf::EnumDescriptor* opcode_descriptor) { std::string yaml = R"EOF( stat_prefix: test_prefix max_packet_bytes: 1048576 @@ -185,7 +185,7 @@ stat_prefix: test_prefix EXPECT_EQ(proto_config_.enable_per_opcode_decoder_error_metrics(), false); EXPECT_EQ(proto_config_.enable_latency_threshold_metrics(), false); EXPECT_EQ(proto_config_.default_latency_threshold(), - ProtobufWkt::util::TimeUtil::SecondsToDuration(0)); + Protobuf::util::TimeUtil::SecondsToDuration(0)); EXPECT_EQ(proto_config_.latency_threshold_overrides_size(), 0); Network::FilterFactoryCb cb = @@ -208,7 +208,7 @@ default_latency_threshold: "0.15s" EXPECT_EQ(proto_config_.enable_per_opcode_decoder_error_metrics(), false); EXPECT_EQ(proto_config_.enable_latency_threshold_metrics(), false); EXPECT_EQ(proto_config_.default_latency_threshold(), - ProtobufWkt::util::TimeUtil::MillisecondsToDuration(150)); + Protobuf::util::TimeUtil::MillisecondsToDuration(150)); EXPECT_EQ(proto_config_.latency_threshold_overrides_size(), 0); Network::FilterFactoryCb cb = @@ -233,12 +233,11 @@ stat_prefix: test_prefix EXPECT_EQ(proto_config_.enable_per_opcode_decoder_error_metrics(), false); EXPECT_EQ(proto_config_.enable_latency_threshold_metrics(), false); EXPECT_EQ(proto_config_.default_latency_threshold(), - ProtobufWkt::util::TimeUtil::SecondsToDuration(0)); + Protobuf::util::TimeUtil::SecondsToDuration(0)); EXPECT_EQ(proto_config_.latency_threshold_overrides_size(), 1); LatencyThresholdOverride threshold_override = proto_config_.latency_threshold_overrides().at(0); EXPECT_EQ(threshold_override.opcode(), LatencyThresholdOverride::Connect); - EXPECT_EQ(threshold_override.threshold(), - ProtobufWkt::util::TimeUtil::MillisecondsToDuration(151)); + EXPECT_EQ(threshold_override.threshold(), Protobuf::util::TimeUtil::MillisecondsToDuration(151)); Network::FilterFactoryCb cb = factory_.createFilterFactoryFromProto(proto_config_, context_).value(); @@ -247,7 +246,7 @@ stat_prefix: test_prefix } TEST_F(ZookeeperFilterConfigTest, FullConfig) { - const ProtobufWkt::EnumDescriptor* opcode_descriptor = envoy::extensions::filters::network:: + const Protobuf::EnumDescriptor* opcode_descriptor = envoy::extensions::filters::network:: zookeeper_proxy::v3::LatencyThresholdOverride_Opcode_descriptor(); std::string yaml = populateFullConfig(opcode_descriptor); TestUtility::loadFromYamlAndValidate(yaml, proto_config_); @@ -259,7 +258,7 @@ TEST_F(ZookeeperFilterConfigTest, FullConfig) { EXPECT_EQ(proto_config_.enable_per_opcode_decoder_error_metrics(), true); EXPECT_EQ(proto_config_.enable_latency_threshold_metrics(), true); EXPECT_EQ(proto_config_.default_latency_threshold(), - ProtobufWkt::util::TimeUtil::MillisecondsToDuration(100)); + Protobuf::util::TimeUtil::MillisecondsToDuration(100)); EXPECT_EQ(proto_config_.latency_threshold_overrides_size(), 27); for (int i = 0; i < opcode_descriptor->value_count(); i++) { @@ -270,7 +269,7 @@ TEST_F(ZookeeperFilterConfigTest, FullConfig) { EXPECT_EQ(opcode_name, opcode_tuple->name()); uint64_t threshold_delta = static_cast(opcode_tuple->number()); EXPECT_EQ(threshold_override.threshold(), - ProtobufWkt::util::TimeUtil::MillisecondsToDuration(150 + threshold_delta)); + Protobuf::util::TimeUtil::MillisecondsToDuration(150 + threshold_delta)); } Network::FilterFactoryCb cb = diff --git a/test/extensions/filters/network/zookeeper_proxy/filter_test.cc b/test/extensions/filters/network/zookeeper_proxy/filter_test.cc index 4136112f463b6..9aed2549a4b01 100644 --- a/test/extensions/filters/network/zookeeper_proxy/filter_test.cc +++ b/test/extensions/filters/network/zookeeper_proxy/filter_test.cc @@ -16,7 +16,7 @@ namespace Extensions { namespace NetworkFilters { namespace ZooKeeperProxy { -bool protoMapEq(const ProtobufWkt::Struct& obj, const std::map& rhs) { +bool protoMapEq(const Protobuf::Struct& obj, const std::map& rhs) { EXPECT_TRUE(!rhs.empty()); for (auto const& entry : rhs) { EXPECT_EQ(obj.fields().at(entry.first).string_value(), entry.second); @@ -584,7 +584,7 @@ class ZooKeeperFilterTest : public testing::Test { auto& call = EXPECT_CALL(stream_info_, setDynamicMetadata(_, _)); for (const auto& value : values) { - call.WillOnce(Invoke([value](const std::string& key, const ProtobufWkt::Struct& obj) -> void { + call.WillOnce(Invoke([value](const std::string& key, const Protobuf::Struct& obj) -> void { EXPECT_STREQ(key.c_str(), "envoy.filters.network.zookeeper_proxy"); protoMapEq(obj, value); })); diff --git a/test/extensions/filters/udp/dns_filter/BUILD b/test/extensions/filters/udp/dns_filter/BUILD index 1c2e6b73bc8b6..b04b9ae9f8892 100644 --- a/test/extensions/filters/udp/dns_filter/BUILD +++ b/test/extensions/filters/udp/dns_filter/BUILD @@ -36,6 +36,7 @@ envoy_extension_cc_test( deps = [ ":dns_filter_test_lib", "//source/extensions/filters/udp/dns_filter:dns_filter_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/mocks/server:instance_mocks", "//test/mocks/server:listener_factory_context_mocks", "//test/mocks/upstream:upstream_mocks", @@ -55,7 +56,9 @@ envoy_extension_cc_test( ":dns_filter_test_lib", "//source/extensions/filters/udp/dns_filter:config", "//source/extensions/filters/udp/dns_filter:dns_filter_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:integration_lib", + "//test/test_common:threadsafe_singleton_injector_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", ], ) diff --git a/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc b/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc index 9ed168b4b3729..fde6bf587350f 100644 --- a/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc +++ b/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc @@ -1,9 +1,11 @@ #include "envoy/config/bootstrap/v3/bootstrap.pb.h" #include "source/extensions/filters/udp/dns_filter/dns_filter.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/integration/integration.h" #include "test/test_common/network_utility.h" +#include "test/test_common/threadsafe_singleton_injector.h" #include "dns_filter_test_utils.h" @@ -15,6 +17,65 @@ namespace { using ResponseValidator = Utils::DnsResponseValidator; +// Mock OS getaddrinfo. +class OsSysCallsWithMockedDns : public Api::OsSysCallsImpl { +public: + static addrinfo* makeAddrInfo(const Network::Address::InstanceConstSharedPtr& addr) { + addrinfo* ai = reinterpret_cast(malloc(sizeof(addrinfo))); + memset(ai, 0, sizeof(addrinfo)); + ai->ai_protocol = IPPROTO_UDP; + ai->ai_socktype = SOCK_DGRAM; + if (addr->ip()->ipv4() != nullptr) { + ai->ai_family = AF_INET; + } else { + ai->ai_family = AF_INET6; + } + sockaddr_storage* storage = + reinterpret_cast(malloc(sizeof(sockaddr_storage))); + ai->ai_addr = reinterpret_cast(storage); + memcpy(ai->ai_addr, addr->sockAddr(), addr->sockAddrLen()); + ai->ai_addrlen = addr->sockAddrLen(); + return ai; + } + + Api::SysCallIntResult getaddrinfo(const char* node, const char* /*service*/, + const addrinfo* /*hints*/, addrinfo** res) override { + *res = nullptr; + if (absl::string_view{"www.google.com"} == node) { + if (ip_version_ == Network::Address::IpVersion::v6) { + static const Network::Address::InstanceConstSharedPtr* objectptr = + new Network::Address::InstanceConstSharedPtr{ + new Network::Address::Ipv6Instance("2607:42:42::42:42", 0, nullptr)}; + *res = makeAddrInfo(*objectptr); + } else { + static const Network::Address::InstanceConstSharedPtr* objectptr = + new Network::Address::InstanceConstSharedPtr{ + new Network::Address::Ipv4Instance("42.42.42.42", 0, nullptr)}; + *res = makeAddrInfo(*objectptr); + } + return {0, 0}; + } + if (nonexisting_addresses_.find(node) != nonexisting_addresses_.end()) { + return {EAI_NONAME, 0}; + } + std::cerr << "Mock DNS does not have entry for: " << node << std::endl; + return {-1, 128}; + } + void freeaddrinfo(addrinfo* ai) override { + while (ai != nullptr) { + addrinfo* p = ai; + ai = ai->ai_next; + free(p->ai_addr); + free(p); + } + } + + void setIpVersion(Network::Address::IpVersion version) { ip_version_ = version; } + Network::Address::IpVersion ip_version_ = Network::Address::IpVersion::v4; + absl::flat_hash_set nonexisting_addresses_ = {"doesnotexist.example.com", + "itdoesnotexist"}; +}; + class DnsFilterIntegrationTest : public testing::TestWithParam, public BaseIntegrationTest { public: @@ -86,16 +147,11 @@ name: listener_0 client_config: resolver_timeout: 1s typed_dns_resolver_config: - name: envoy.network.dns_resolver.cares + name: envoy.network.dns_resolver.getaddrinfo typed_config: - "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.cares.v3.CaresDnsResolverConfig - resolvers: - - socket_address: - address: {} - port_value: {} - dns_resolver_options: - use_tcp_for_dns_lookups: false - no_default_search_domain: false + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig + num_retries: + value: 1 max_pending_lookups: 256 server_config: inline_dns_table: @@ -178,6 +234,15 @@ name: listener_1 void setup(uint32_t upstream_count) { setUdpFakeUpstream(FakeUpstreamConfig::UdpConfig()); + // Adding bootstrap default DNS resolver config. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* typed_dns_resolver_config = bootstrap.mutable_typed_dns_resolver_config(); + typed_dns_resolver_config->set_name("envoy.network.dns_resolver.getaddrinfo"); + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + config; + config.mutable_num_retries()->set_value(1); + typed_dns_resolver_config->mutable_typed_config()->PackFrom(config); + }); if (upstream_count > 1) { setDeterministicValue(); setUpstreamCount(upstream_count); @@ -199,8 +264,18 @@ name: listener_1 config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto addr_port = getListenerBindAddressAndPortNoThrow(); auto listener_0 = getListener0(addr_port); + auto* listener0 = bootstrap.mutable_static_resources()->add_listeners(); + listener0->MergeFrom(listener_0); + // Remove client_config for cluster lookup test cases. + if (cluster_lookup_test_) { + auto* listener_filter = listener0->mutable_listener_filters(0); + envoy::extensions::filters::udp::dns_filter::v3::DnsFilterConfig dns_filter_config; + listener_filter->typed_config().UnpackTo(&dns_filter_config); + + dns_filter_config.clear_client_config(); + listener_filter->mutable_typed_config()->PackFrom(dns_filter_config); + } auto listener_1 = getListener1(addr_port); - bootstrap.mutable_static_resources()->add_listeners()->MergeFrom(listener_0); bootstrap.mutable_static_resources()->add_listeners()->MergeFrom(listener_1); }); @@ -215,6 +290,25 @@ name: listener_1 client.recv(response_datagram); } + void dnsLookupTest(Network::Address::IpVersion ip_version, const std::string listener, + uint16_t rec_type) { + mock_os_sys_calls_.setIpVersion(ip_version); + setup(0); + const uint32_t port = lookupPort(listener); + const auto listener_address = *Network::Utility::resolveUrl( + fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(version_), port)); + + Network::UdpRecvData response; + std::string query = Utils::buildQueryForDomain("www.google.com", rec_type, DNS_RECORD_CLASS_IN); + requestResponseWithListenerAddress(*listener_address, query, response); + + response_ctx_ = ResponseValidator::createResponseContext(response, counters_); + EXPECT_TRUE(response_ctx_->parse_status_); + + EXPECT_EQ(1, response_ctx_->answers_.size()); + EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); + } + Api::ApiPtr api_; NiceMock histogram_; NiceMock random_; @@ -225,6 +319,9 @@ name: listener_1 NiceMock queries_with_ans_or_authority_rrs_; DnsParserCounters counters_; DnsQueryContextPtr response_ctx_; + OsSysCallsWithMockedDns mock_os_sys_calls_; + TestThreadsafeSingletonInjector os_calls_{&mock_os_sys_calls_}; + bool cluster_lookup_test_ = false; }; INSTANTIATE_TEST_SUITE_P(IpVersions, DnsFilterIntegrationTest, @@ -232,39 +329,21 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, DnsFilterIntegrationTest, TestUtility::ipTestParamsToString); TEST_P(DnsFilterIntegrationTest, ExternalLookupTest) { - setup(0); - const uint32_t port = lookupPort("listener_0"); - const auto listener_address = *Network::Utility::resolveUrl( - fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(version_), port)); - - Network::UdpRecvData response; - std::string query = - Utils::buildQueryForDomain("www.google.com", DNS_RECORD_TYPE_A, DNS_RECORD_CLASS_IN); - requestResponseWithListenerAddress(*listener_address, query, response); - - response_ctx_ = ResponseValidator::createResponseContext(response, counters_); - EXPECT_TRUE(response_ctx_->parse_status_); + // Sending request to listener_0 triggers external DNS lookup. + dnsLookupTest(Network::Address::IpVersion::v4, "listener_0", DNS_RECORD_TYPE_A); +} - EXPECT_EQ(1, response_ctx_->answers_.size()); - EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); +TEST_P(DnsFilterIntegrationTest, InternalLookupTest) { + // Sending request to listener_1 triggers internal DNS lookup. + dnsLookupTest(Network::Address::IpVersion::v4, "listener_1", DNS_RECORD_TYPE_A); } TEST_P(DnsFilterIntegrationTest, ExternalLookupTestIPv6) { - setup(0); - const uint32_t port = lookupPort("listener_0"); - const auto listener_address = *Network::Utility::resolveUrl( - fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(version_), port)); - - Network::UdpRecvData response; - std::string query = - Utils::buildQueryForDomain("www.google.com", DNS_RECORD_TYPE_AAAA, DNS_RECORD_CLASS_IN); - requestResponseWithListenerAddress(*listener_address, query, response); - - response_ctx_ = ResponseValidator::createResponseContext(response, counters_); - EXPECT_TRUE(response_ctx_->parse_status_); + dnsLookupTest(Network::Address::IpVersion::v6, "listener_0", DNS_RECORD_TYPE_AAAA); +} - EXPECT_EQ(1, response_ctx_->answers_.size()); - EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); +TEST_P(DnsFilterIntegrationTest, InternalLookupTestIPv6) { + dnsLookupTest(Network::Address::IpVersion::v6, "listener_1", DNS_RECORD_TYPE_AAAA); } TEST_P(DnsFilterIntegrationTest, LocalLookupTest) { @@ -286,6 +365,7 @@ TEST_P(DnsFilterIntegrationTest, LocalLookupTest) { } TEST_P(DnsFilterIntegrationTest, ClusterLookupTest) { + cluster_lookup_test_ = true; setup(2); const uint32_t port = lookupPort("listener_0"); const auto listener_address = *Network::Utility::resolveUrl( diff --git a/test/extensions/filters/udp/dns_filter/dns_filter_test.cc b/test/extensions/filters/udp/dns_filter/dns_filter_test.cc index 07fda53f33520..a707e7f9fcc79 100644 --- a/test/extensions/filters/udp/dns_filter/dns_filter_test.cc +++ b/test/extensions/filters/udp/dns_filter/dns_filter_test.cc @@ -4,6 +4,7 @@ #include "source/common/common/logger.h" #include "source/extensions/filters/udp/dns_filter/dns_filter_constants.h" #include "source/extensions/filters/udp/dns_filter/dns_filter_utils.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/mocks/event/mocks.h" #include "test/mocks/server/instance.h" @@ -2563,6 +2564,43 @@ stat_prefix: "my_prefix" EXPECT_EQ(1, config_->stats().known_domain_queries_.value()); } +// Test that the bootstrap typed_dns_resolver_config is used when client_config is not set. +TEST_F(DnsFilterTest, BootstrapTypedDnsResolverTest) { + // Create bootstrap config with typed DNS resolver configuration. + const std::string dns_config_yaml = R"EOF( +name: envoy.network.dns_resolver.getaddrinfo +typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig +)EOF"; + envoy::config::core::v3::TypedExtensionConfig typed_config; + TestUtility::loadFromYaml(dns_config_yaml, typed_config); + auto& bootstrap = listener_factory_.server_factory_context_.bootstrap(); + bootstrap.mutable_typed_dns_resolver_config()->MergeFrom(typed_config); + + // Create DNS filter configuration. + const std::string filter_config_yaml = R"EOF( +stat_prefix: bar +server_config: + inline_dns_table: + virtual_domains: + - name: www.foo.com + endpoint: + address_list: + address: + - 10.0.0.1 +)EOF"; + envoy::extensions::filters::udp::dns_filter::v3::DnsFilterConfig filter_config; + TestUtility::loadFromYaml(filter_config_yaml, filter_config); + DnsFilterEnvoyConfig envoy_config(listener_factory_, filter_config); + + // Verify the filter is configured with bootstrap DNS resolver configuration. + const auto& resolver_config = envoy_config.typedDnsResolverConfig(); + EXPECT_EQ(resolver_config.name(), "envoy.network.dns_resolver.getaddrinfo"); + EXPECT_EQ(resolver_config.typed_config().type_url(), + "type.googleapis.com/" + "envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig"); +} + } // namespace } // namespace DnsFilter } // namespace UdpFilters diff --git a/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/BUILD b/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/BUILD index 0be85dec055b4..c592e2a428f62 100644 --- a/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/BUILD +++ b/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/BUILD @@ -61,6 +61,7 @@ envoy_extension_cc_test( "//source/extensions/filters/udp/udp_proxy:config", "//source/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy:config", "//source/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy:proxy_filter_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/extensions/filters/udp/udp_proxy/session_filters:psc_setter_filter_config_lib", "//test/extensions/filters/udp/udp_proxy/session_filters:psc_setter_filter_proto_cc_proto", "//test/integration:integration_lib", diff --git a/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/proxy_filter_integration_test.cc b/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/proxy_filter_integration_test.cc index 97ca9b93b041f..f56f2771e825c 100644 --- a/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/proxy_filter_integration_test.cc +++ b/test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/proxy_filter_integration_test.cc @@ -6,6 +6,7 @@ #include "source/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/config.h" #include "source/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/proxy_filter.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/common/upstream/utility.h" #include "test/extensions/filters/udp/udp_proxy/session_filters/dynamic_forward_proxy/dfp_setter.h" @@ -85,6 +86,10 @@ name: udp_proxy stat_prefix: foo dns_cache_config: name: foo + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig dns_lookup_family: {} max_hosts: {} dns_cache_circuit_breaker: @@ -142,6 +147,10 @@ name: envoy.clusters.dynamic_forward_proxy "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig dns_cache_config: name: foo + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig dns_lookup_family: {} max_hosts: {} dns_cache_circuit_breaker: diff --git a/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc b/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc index a951ef0b97636..3bb1a51c3e25f 100644 --- a/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc +++ b/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc @@ -1851,7 +1851,7 @@ stat_prefix: foo auto session = filter_->createTunnelingSession(); EXPECT_NO_THROW(session->onAboveWriteBufferHighWatermark()); - session->onSessionComplete(); + filter_.reset(); } TEST_F(UdpProxyFilterTest, TunnelingSessionUpstreamClosedDuringFlush) { diff --git a/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc b/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc index 71e85456e17fb..8f45bb7825204 100644 --- a/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc +++ b/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc @@ -51,7 +51,7 @@ class UdpReverseFilterConfigFactory } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "test.udp_listener.reverse"; } @@ -338,30 +338,6 @@ TEST_P(UdpProxyIntegrationTest, DownstreamDrop) { } } -// Verify upstream drops are handled correctly with stats. -TEST_P(UdpProxyIntegrationTest, UpstreamDrop) { - if (Runtime::runtimeFeatureEnabled( - "envoy.reloadable_features.udp_socket_apply_aggregated_read_limit")) { - return; - } - setup(1); - const uint32_t port = lookupPort("listener_0"); - const auto listener_address = *Network::Utility::resolveUrl( - fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(version_), port)); - Network::Test::UdpSyncPeer client(version_); - - client.write("hello", *listener_address); - Network::UdpRecvData request_datagram; - ASSERT_TRUE(fake_upstreams_[0]->waitForUdpDatagram(request_datagram)); - EXPECT_EQ("hello", request_datagram.buffer_->toString()); - - const uint64_t large_datagram_size = - (Network::DEFAULT_UDP_MAX_DATAGRAM_SIZE * Network::NUM_DATAGRAMS_PER_RECEIVE) + 1024; - fake_upstreams_[0]->sendUdpDatagram(std::string(large_datagram_size, 'a'), - request_datagram.addresses_.peer_); - test_server_->waitForCounterEq("cluster.cluster_0.udp.sess_rx_datagrams_dropped", 1); -} - // Test with large packet sizes. TEST_P(UdpProxyIntegrationTest, LargePacketSizesOnLoopback) { // The following tests large packets end to end. We use a size larger than diff --git a/test/extensions/filters/udp/udp_proxy/udp_session_extension_discovery_integration_test.cc b/test/extensions/filters/udp/udp_proxy/udp_session_extension_discovery_integration_test.cc index adc539a23aca9..15e39ca37d720 100644 --- a/test/extensions/filters/udp/udp_proxy/udp_session_extension_discovery_integration_test.cc +++ b/test/extensions/filters/udp/udp_proxy/udp_session_extension_discovery_integration_test.cc @@ -207,7 +207,7 @@ name: udp_proxy void sendLdsResponse(const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); response.add_resources()->PackFrom(listener_config_); lds_stream_->sendGrpcMessage(response); } diff --git a/test/extensions/formatter/cel/cel_test.cc b/test/extensions/formatter/cel/cel_test.cc index 044ca4aac4937..9afffa779496e 100644 --- a/test/extensions/formatter/cel/cel_test.cc +++ b/test/extensions/formatter/cel/cel_test.cc @@ -42,14 +42,151 @@ TEST_F(CELFormatterTest, TestNodeId) { auto formatter = cel_parser->parse("CEL", "xds.node.id", max_length); EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), ProtoEq(ValueUtil::stringValue("node_name"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "xds.node.id", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("node_name"))); +} + +TEST_F(CELFormatterTest, TestFormatWithContext) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "xds.node.id", max_length); + EXPECT_THAT(formatter->formatWithContext(formatter_context_, stream_info_), "node_name"); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "xds.node.id", max_length); + EXPECT_THAT(typed_formatter->formatWithContext(formatter_context_, stream_info_), "node_name"); } -TEST_F(CELFormatterTest, TestFormatValue) { +TEST_F(CELFormatterTest, TestFormatStringValue) { auto cel_parser = std::make_unique(); absl::optional max_length = absl::nullopt; auto formatter = cel_parser->parse("CEL", "request.headers[':method']", max_length); EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), ProtoEq(ValueUtil::stringValue("GET"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "request.headers[':method']", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("GET"))); +} + +TEST_F(CELFormatterTest, TestFormatNumberValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "request.headers[':method'].size()", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("3"))); + + auto typed_formatter = + cel_parser->parse("TYPED_CEL", "request.headers[':method'].size()", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::numberValue(3))); +} + +TEST_F(CELFormatterTest, TestFormatNullValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "request.headers.nope", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::nullValue())); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "request.headers.nope", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::nullValue())); +} + +TEST_F(CELFormatterTest, TestFormatBoolValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "request.headers[':method'] == 'GET'", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("true"))); + + auto typed_formatter = + cel_parser->parse("TYPED_CEL", "request.headers[':method'] == 'GET'", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::boolValue(true))); +} + +TEST_F(CELFormatterTest, TestFormatDurationValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "duration(\"1h30m\")", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("1h30m"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "duration(\"1h30m\")", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("5400s"))); +} + +TEST_F(CELFormatterTest, TestFormatTimestampValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "timestamp(\"2023-08-26T12:39:00-07:00\")", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("2023-08-26T19:39:00+00:00"))); + + auto typed_formatter = + cel_parser->parse("TYPED_CEL", "timestamp(\"2023-08-26T12:39:00-07:00\")", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("2023-08-26T19:39:00Z"))); +} + +TEST_F(CELFormatterTest, TestFormatBytesValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "bytes(\"hello\")", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("hello"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "bytes(\"hello\")", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("aGVsbG8="))); +} + +TEST_F(CELFormatterTest, TestFormatListValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "[\"foo\", 42, true]", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("CelList value"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "[\"foo\", 42, true]", max_length); + EXPECT_THAT( + typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::listValue({ValueUtil::stringValue("foo"), ValueUtil::numberValue(42), + ValueUtil::boolValue(true)}))); +} + +TEST_F(CELFormatterTest, TestFormatMapValue) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + auto formatter = cel_parser->parse("CEL", "{\"foo\": \"42\"}", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("CelMap value"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "{\"foo\": \"42\"}", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::structValue(MessageUtil::keyValueStruct("foo", "42")))); + + // Test something that fails to format. For whatever reason, + // ExportAsProtoValue will not tolerate boolean keys. + auto invalid_typed_formatter = cel_parser->parse("TYPED_CEL", "{true: \"42\"}", max_length); + EXPECT_THAT(invalid_typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::nullValue())); +} + +TEST_F(CELFormatterTest, TestTruncation) { + auto cel_parser = std::make_unique(); + absl::optional max_length = 2; + auto formatter = cel_parser->parse("CEL", "request.headers[':method']", max_length); + EXPECT_THAT(formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("GE"))); + + auto typed_formatter = cel_parser->parse("TYPED_CEL", "request.headers[':method']", max_length); + EXPECT_THAT(typed_formatter->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("GE"))); } TEST_F(CELFormatterTest, TestParseFail) { @@ -67,6 +204,28 @@ TEST_F(CELFormatterTest, TestNullFormatValue) { ProtoEq(ValueUtil::nullValue())); } +TEST_F(CELFormatterTest, TestFormatConversionV1AlphaToDevCel) { + auto cel_parser = std::make_unique(); + absl::optional max_length = absl::nullopt; + + // Test with a basic path expression + auto formatter1 = cel_parser->parse("CEL", "request.path", max_length); + EXPECT_THAT(formatter1->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("/request/path?secret=parameter"))); + + // Test with a more complex expression + auto formatter2 = cel_parser->parse("CEL", "request.headers[':method'] == 'GET'", max_length); + // The formatter returns boolean expressions as strings + EXPECT_THAT(formatter2->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("true"))); + + // Test with string operations + auto formatter3 = cel_parser->parse("CEL", "request.path.startsWith('/request')", max_length); + // The formatter returns boolean expressions as strings + EXPECT_THAT(formatter3->formatValueWithContext(formatter_context_, stream_info_), + ProtoEq(ValueUtil::stringValue("true"))); +} + TEST_F(CELFormatterTest, TestRequestHeaderWithLegacyConfiguration) { const std::string yaml = R"EOF( text_format_source: @@ -156,7 +315,7 @@ TEST_F(CELFormatterTest, TestComplexCelExpression) { EXPECT_EQ("true /original false", formatter->formatWithContext(formatter_context_, stream_info_)); } -TEST_F(CELFormatterTest, TestInvalidExpression) { +TEST_F(CELFormatterTest, TestUntypedInvalidExpression) { const std::string yaml = R"EOF( text_format_source: inline_string: "%CEL(+++++)%" @@ -165,7 +324,31 @@ TEST_F(CELFormatterTest, TestInvalidExpression) { EXPECT_THROW_WITH_REGEX( *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_), - EnvoyException, "Not able to parse filter expression: .*"); + EnvoyException, "Not able to parse expression: .*"); +} + +TEST_F(CELFormatterTest, TestTypedInvalidExpression) { + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%TYPED_CEL(+++++)%" +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + EXPECT_THROW_WITH_REGEX( + *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_), + EnvoyException, "Not able to parse expression: .*"); +} + +TEST_F(CELFormatterTest, TestInvalidSemanticExpression) { + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%CEL(f())%" +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + EXPECT_THROW_WITH_REGEX( + *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_), + EnvoyException, "failed to create an expression: .*"); } TEST_F(CELFormatterTest, TestRegexExtFunctions) { @@ -179,6 +362,49 @@ TEST_F(CELFormatterTest, TestRegexExtFunctions) { *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); EXPECT_EQ("true ", formatter->formatWithContext(formatter_context_, stream_info_)); } + +TEST_F(CELFormatterTest, TestRegexExtFunctionsWithActualExtraction) { + const std::string yaml = R"EOF( + text_format_source: + inline_string: "%CEL(re.extract(request.host, '(.+?)\\\\:(\\\\d+)', '\\\\2'))%" +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + request_headers_.addCopy("host", "example.com:443"); + auto formatter = + *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("443", formatter->formatWithContext(formatter_context_, stream_info_)); +} + +TEST_F(CELFormatterTest, TestUntypedJsonFormat) { + const std::string yaml = R"EOF( + json_format: + methodSize: "%CEL(request.headers[':method'].size())%" + shortMethod: "%CEL(request.headers[':method']):2%" + missingHeaderUnusedMaxLength: "%CEL(request.headers.missing):2%" +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("{\"methodSize\":\"3\",\"missingHeaderUnusedMaxLength\":null,\"shortMethod\":\"GE\"}\n", + formatter->formatWithContext(formatter_context_, stream_info_)); +} + +TEST_F(CELFormatterTest, TestTypedJsonFormat) { + const std::string yaml = R"EOF( + json_format: + methodSize: "%TYPED_CEL(request.headers[':method'].size())%" + shortMethod: "%TYPED_CEL(request.headers[':method']):2%" + missingHeaderUnusedMaxLength: "%TYPED_CEL(request.headers.missing):2%" +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = + *Envoy::Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config_, context_); + EXPECT_EQ("{\"methodSize\":3,\"missingHeaderUnusedMaxLength\":null,\"shortMethod\":\"GE\"}\n", + formatter->formatWithContext(formatter_context_, stream_info_)); +} #endif } // namespace Formatter diff --git a/test/extensions/formatter/metadata/integration_test.cc b/test/extensions/formatter/metadata/integration_test.cc index 56903d7a75c52..663c7223971f9 100644 --- a/test/extensions/formatter/metadata/integration_test.cc +++ b/test/extensions/formatter/metadata/integration_test.cc @@ -24,7 +24,7 @@ class IntegrationTest : public testing::TestWithParam()) { // Create metadata object with test values. - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; auto& fields_map = *struct_obj.mutable_fields(); fields_map["test_key"] = ValueUtil::stringValue("test_value"); (*metadata_->mutable_filter_metadata())["metadata.test"] = struct_obj; diff --git a/test/extensions/formatter/xfcc_value/BUILD b/test/extensions/formatter/xfcc_value/BUILD new file mode 100644 index 0000000000000..afc8460577947 --- /dev/null +++ b/test/extensions/formatter/xfcc_value/BUILD @@ -0,0 +1,40 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_fuzz_test", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "xfcc_value_test", + srcs = ["xfcc_value_test.cc"], + extension_names = ["envoy.built_in_formatters.xfcc_value"], + rbe_pool = "6gig", + deps = [ + "//source/common/formatter:substitution_formatter_lib", + "//source/extensions/formatter/xfcc_value:config", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_cc_fuzz_test( + name = "xfcc_value_fuzz_test", + srcs = ["xfcc_value_fuzz_test.cc"], + corpus = "xfcc_value_corpus", + rbe_pool = "6gig", + deps = [ + "//source/extensions/formatter/xfcc_value:config", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/formatter/xfcc_value/xfcc_value_corpus/example b/test/extensions/formatter/xfcc_value/xfcc_value_corpus/example new file mode 100644 index 0000000000000..e46bcf5c2ddda --- /dev/null +++ b/test/extensions/formatter/xfcc_value/xfcc_value_corpus/example @@ -0,0 +1,8 @@ +URI="abc,=,";DNS=example.com +By=spiffe://lyft.com/frontend;By=http://frontend.lyft.com;Hash=123456;URI=spiffe://lyft.com/testclient" +By=spiffe://lyft.com/backend-team;By=http://backend.lyft.com;Hash=xxxyyyzzz +By=spiffe://lyft.com/backend-team;By="http://backend.lyft.com";Hash=xxxyyyzzz +DNS=lyft.com";DNS=www.lyft.com +Subject="emailAddress=frontend-team@lyft.com,CN=Test Frontend Team,OU=Lyft Engineering,O=Lyft,L=San Francisco,ST=California,C=US" +By="a\\";URI="spiffe://good" +By="a\\\"b";URI="spiffe://ns/svc" diff --git a/test/extensions/formatter/xfcc_value/xfcc_value_fuzz_test.cc b/test/extensions/formatter/xfcc_value/xfcc_value_fuzz_test.cc new file mode 100644 index 0000000000000..4bf775d43ef43 --- /dev/null +++ b/test/extensions/formatter/xfcc_value/xfcc_value_fuzz_test.cc @@ -0,0 +1,32 @@ +#include "source/common/common/logger.h" +#include "source/common/formatter/http_formatter_context.h" +#include "source/extensions/formatter/xfcc_value/xfcc_value.h" + +#include "test/fuzz/fuzz_runner.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { +namespace { + +DEFINE_FUZZER(const uint8_t* buf, size_t len) { + Http::HeaderStringValidator::disable_validation_for_tests_ = true; + absl::string_view sv(reinterpret_cast(buf), len); + + // We just want to make sure that the parser doesn't crash with any input. + XfccValueFormatterCommandParser parser; + auto formatter = parser.parse("XFCC_VALUE", "uri", absl::nullopt); + + Http::TestRequestHeaderMapImpl request_headers{}; + request_headers.setForwardedClientCert(sv); + StreamInfo::MockStreamInfo stream_info; + + formatter->formatValueWithContext({&request_headers}, stream_info); +} + +} // namespace +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/formatter/xfcc_value/xfcc_value_test.cc b/test/extensions/formatter/xfcc_value/xfcc_value_test.cc new file mode 100644 index 0000000000000..4e4a5f8b37f26 --- /dev/null +++ b/test/extensions/formatter/xfcc_value/xfcc_value_test.cc @@ -0,0 +1,110 @@ +#include "envoy/config/core/v3/substitution_format_string.pb.validate.h" + +#include "source/common/formatter/substitution_format_string.h" +#include "source/common/formatter/substitution_formatter.h" + +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Formatter { +namespace { + +class XfccValueTest : public ::testing::Test { +public: + StreamInfo::MockStreamInfo stream_info_; + NiceMock context_; +}; + +TEST_F(XfccValueTest, UnknownCommand) { + auto formatter_or_error = Envoy::Formatter::SubstitutionFormatParser::parse("%UNKNOWN_COMMAND%"); + EXPECT_EQ("Not supported field in StreamInfo: UNKNOWN_COMMAND", + formatter_or_error.status().message()); +} + +TEST_F(XfccValueTest, MissingSubcommand) { + EXPECT_THROW_WITH_MESSAGE( + { auto error = Envoy::Formatter::SubstitutionFormatParser::parse("%XFCC_VALUE%"); }, + EnvoyException, "XFCC_VALUE command requires a subcommand"); +} + +TEST_F(XfccValueTest, UnsupportedSubcommand) { + EXPECT_THROW_WITH_MESSAGE( + { + auto error = + Envoy::Formatter::SubstitutionFormatParser::parse("%XFCC_VALUE(unsupported_key)%"); + }, + EnvoyException, "XFCC_VALUE command does not support subcommand: unsupported_key"); +} + +TEST_F(XfccValueTest, Test) { + auto formatter = + std::move(Envoy::Formatter::SubstitutionFormatParser::parse("%XFCC_VALUE(uri)%").value()[0]); + + { + // No XFCC header. + Http::TestRequestHeaderMapImpl headers{}; + EXPECT_TRUE(formatter->formatValueWithContext({&headers}, stream_info_).has_null_value()); + } + + { + // Normal value. + Http::TestRequestHeaderMapImpl headers{ + {"x-forwarded-client-cert", "By=test;URI=abc;DNS=example.com"}}; + EXPECT_EQ(formatter->formatValueWithContext({&headers}, stream_info_).string_value(), "abc"); + } + + // Normal value with special characters. + { + Http::TestRequestHeaderMapImpl headers{ + {"x-forwarded-client-cert", R"(By=test;URI="a,b,c;\"e;f;g=x";DNS=example.com)"}}; + EXPECT_EQ(formatter->formatValueWithContext({&headers}, stream_info_).string_value(), + R"(a,b,c;"e;f;g=x)"); + } + + { + // Multiple elements. + Http::TestRequestHeaderMapImpl headers{ + {"x-forwarded-client-cert", + R"(By=test;DNS=example.com,By=test;URI="a,b,c;\"e;f;g=x";DNS=example.com)"}}; + EXPECT_EQ(formatter->formatValueWithContext({&headers}, stream_info_).string_value(), + R"(a,b,c;"e;f;g=x)"); + } + + { + // With escaped backslash. + Http::TestRequestHeaderMapImpl headers{ + {"x-forwarded-client-cert", R"(By=test;DNS=example.com,By=test;URI="\\";DNS=example.com)"}}; + EXPECT_EQ(formatter->formatValueWithContext({&headers}, stream_info_).string_value(), R"(\)"); + } + + { + // With escaped backslash and escaped quote. + Http::TestRequestHeaderMapImpl headers{ + {"x-forwarded-client-cert", + R"(By=test;DNS=example.com,By=test;URI="\\\"";DNS=example.com)"}}; + EXPECT_EQ(formatter->formatValueWithContext({&headers}, stream_info_).string_value(), R"(\")"); + } + + { + // Unclosed quotes in XFCC header. + Http::TestRequestHeaderMapImpl headers{ + {"x-forwarded-client-cert", R"(By=test;URI="abc;DNS=example.com)"}}; + EXPECT_TRUE(formatter->formatValueWithContext({&headers}, stream_info_).has_null_value()); + } + + { + // No required key. + Http::TestRequestHeaderMapImpl headers{{"x-forwarded-client-cert", "By=test;DNS=example.com"}}; + EXPECT_TRUE(formatter->formatValueWithContext({&headers}, stream_info_).has_null_value()); + } +} + +} // namespace +} // namespace Formatter +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/geoip_providers/maxmind/geoip_provider_test.cc b/test/extensions/geoip_providers/maxmind/geoip_provider_test.cc index 70d1f4f00ba4f..f05691e0c269c 100644 --- a/test/extensions/geoip_providers/maxmind/geoip_provider_test.cc +++ b/test/extensions/geoip_providers/maxmind/geoip_provider_test.cc @@ -148,13 +148,22 @@ class GeoipProviderTestBase { } void expectStats(const absl::string_view& db_type, const uint32_t total_count = 1, - const uint32_t hit_count = 1, const uint32_t error_count = 0) { + const uint32_t hit_count = 1, const uint32_t error_count = 0, + const uint64_t build_epoch = 0) { auto& provider_scope = GeoipProviderPeer::providerScope(provider_); EXPECT_EQ(provider_scope.counterFromString(absl::StrCat(db_type, ".total")).value(), total_count); EXPECT_EQ(provider_scope.counterFromString(absl::StrCat(db_type, ".hit")).value(), hit_count); EXPECT_EQ(provider_scope.counterFromString(absl::StrCat(db_type, ".lookup_error")).value(), error_count); + + if (build_epoch > 0) { + EXPECT_EQ(provider_scope + .gaugeFromString(absl::StrCat(db_type, ".db_build_epoch"), + Stats::Gauge::ImportMode::Accumulate) + .value(), + build_epoch); + } } void expectReloadStats(const absl::string_view& db_type, const uint32_t reload_success_count = 0, @@ -196,7 +205,7 @@ TEST_F(GeoipProviderTest, ValidConfigCityAndAsnDbsSuccessfulLookup) { )EOF"; initializeProvider(config_yaml, cb_added_nullopt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("89.160.20.112"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -204,13 +213,13 @@ TEST_F(GeoipProviderTest, ValidConfigCityAndAsnDbsSuccessfulLookup) { provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); EXPECT_EQ(4, captured_lookup_response_.size()); const auto& city_it = captured_lookup_response_.find("x-geo-city"); - EXPECT_EQ("Boxford", city_it->second); + EXPECT_EQ("Linköping", city_it->second); const auto& region_it = captured_lookup_response_.find("x-geo-region"); - EXPECT_EQ("ENG", region_it->second); + EXPECT_EQ("E", region_it->second); const auto& country_it = captured_lookup_response_.find("x-geo-country"); - EXPECT_EQ("GB", country_it->second); + EXPECT_EQ("SE", country_it->second); const auto& asn_it = captured_lookup_response_.find("x-geo-asn"); - EXPECT_EQ("15169", asn_it->second); + EXPECT_EQ("29518", asn_it->second); expectStats("city_db"); expectStats("asn_db"); } @@ -224,7 +233,7 @@ TEST_F(GeoipProviderTest, ValidConfigAsnDbsSuccessfulLookup) { )EOF"; initializeProvider(config_yaml, cb_added_nullopt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("89.160.20.112"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -232,7 +241,7 @@ TEST_F(GeoipProviderTest, ValidConfigAsnDbsSuccessfulLookup) { provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); EXPECT_EQ(1, captured_lookup_response_.size()); const auto& asn_it = captured_lookup_response_.find("x-geo-asn"); - EXPECT_EQ("15169", asn_it->second); + EXPECT_EQ("29518", asn_it->second); expectStats("asn_db"); } @@ -270,7 +279,7 @@ TEST_F(GeoipProviderTest, ValidConfigUsingAsnAndIspDbsSuccessfulLookup) { initializeProvider(config_yaml, cb_added_nullopt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("2c0f:ff80::"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -278,10 +287,10 @@ TEST_F(GeoipProviderTest, ValidConfigUsingAsnAndIspDbsSuccessfulLookup) { provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); EXPECT_EQ(2, captured_lookup_response_.size()); const auto& asn_it = captured_lookup_response_.find("x-geo-asn"); - EXPECT_EQ("15169", asn_it->second); + EXPECT_EQ("237", asn_it->second); expectStats("asn_db"); const auto& isp_it = captured_lookup_response_.find("x-geo-isp"); - EXPECT_EQ("TOT Public Company Limited", isp_it->second); + EXPECT_EQ("Merit Network Inc.", isp_it->second); expectStats("isp_db"); } @@ -328,7 +337,7 @@ TEST_F(GeoipProviderTest, AsnLookupFallsBackToIspDb) { initializeProvider(config_yaml, cb_added_nullopt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("::1.128.0.1"); + Network::Utility::parseInternetAddressNoThrow("::1.128.0.0"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -355,7 +364,7 @@ TEST_F(GeoipProviderTest, ValidConfigUsingAsnDbNotReadingIspDbsSuccessfulLookup) initializeProvider(config_yaml, cb_added_nullopt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("1.0.0.123"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -465,6 +474,26 @@ TEST_F(GeoipProviderTest, ValidConfigAnonHostingSuccessfulLookup) { expectStats("anon_db"); } +TEST_F(GeoipProviderTest, ValidConfigUsingCityDbNoHeadersAddedWhenIpIsNotInDb) { + const std::string config_yaml = R"EOF( + common_provider_config: + geo_headers_to_add: + country: "x-geo-country" + city_db_path: "{{ test_rundir }}/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test.mmdb" + )EOF"; + + initializeProvider(config_yaml, cb_added_nullopt); + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.4"); + Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; + testing::MockFunction lookup_cb; + auto lookup_cb_std = lookup_cb.AsStdFunction(); + EXPECT_CALL(lookup_cb, Call(_)).WillRepeatedly(SaveArg<0>(&captured_lookup_response_)); + provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); + EXPECT_EQ(0, captured_lookup_response_.size()); + expectStats("city_db", 1, 0, 1); +} + TEST_F(GeoipProviderTest, ValidConfigAnonTorNodeSuccessfulLookup) { const std::string config_yaml = R"EOF( common_provider_config: @@ -529,7 +558,7 @@ TEST_F(GeoipProviderTest, ValidConfigEmptyLookupResult) { TEST_F(GeoipProviderTest, ValidConfigCityMultipleLookups) { initializeProvider(default_city_config_yaml, cb_added_nullopt); Network::Address::InstanceConstSharedPtr remote_address1 = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("2.125.160.216"); Geolocation::LookupRequest lookup_rq1{std::move(remote_address1)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -538,7 +567,7 @@ TEST_F(GeoipProviderTest, ValidConfigCityMultipleLookups) { EXPECT_EQ(3, captured_lookup_response_.size()); // Another lookup request. Network::Address::InstanceConstSharedPtr remote_address2 = - Network::Utility::parseInternetAddressNoThrow("63.25.243.11"); + Network::Utility::parseInternetAddressNoThrow("81.2.69.144"); Geolocation::LookupRequest lookup_rq2{std::move(remote_address2)}; testing::MockFunction lookup_cb2; auto lookup_cb_std2 = lookup_cb2.AsStdFunction(); @@ -568,7 +597,7 @@ TEST_F(GeoipProviderTest, DbReloadedOnMmdbFileUpdate) { auto cb_added_opt = absl::make_optional(); initializeProvider(formatted_config, cb_added_opt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("81.2.69.144"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -576,7 +605,7 @@ TEST_F(GeoipProviderTest, DbReloadedOnMmdbFileUpdate) { provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); EXPECT_EQ(3, captured_lookup_response_.size()); const auto& city_it = captured_lookup_response_.find("x-geo-city"); - EXPECT_EQ("Boxford", city_it->second); + EXPECT_EQ("London", city_it->second); TestEnvironment::renameFile(city_db_path, city_db_path + "1"); TestEnvironment::renameFile(reloaded_city_db_path, city_db_path); cb_added_opt.value().waitReady(); @@ -587,7 +616,7 @@ TEST_F(GeoipProviderTest, DbReloadedOnMmdbFileUpdate) { expectReloadStats("city_db", 1, 0); captured_lookup_response_.clear(); EXPECT_EQ(0, captured_lookup_response_.size()); - remote_address = Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + remote_address = Network::Utility::parseInternetAddressNoThrow("81.2.69.144"); Geolocation::LookupRequest lookup_rq2{std::move(remote_address)}; testing::MockFunction lookup_cb2; auto lookup_cb_std2 = lookup_cb2.AsStdFunction(); @@ -601,6 +630,39 @@ TEST_F(GeoipProviderTest, DbReloadedOnMmdbFileUpdate) { TestEnvironment::renameFile(city_db_path + "1", city_db_path); } +TEST_F(GeoipProviderTest, DbEpochGaugeUpdatesWhenReloadedOnMmdbFileUpdate) { + constexpr absl::string_view config_yaml = R"EOF( + common_provider_config: + geo_headers_to_add: + city: "x-geo-city" + city_db_path: {} + )EOF"; + std::string city_db_path = TestEnvironment::substitute( + "{{ test_rundir " + "}}/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test.mmdb"); + std::string reloaded_city_db_path = TestEnvironment::substitute( + "{{ test_rundir " + "}}/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test-Updated.mmdb"); + const std::string formatted_config = + fmt::format(config_yaml, TestEnvironment::substitute(city_db_path)); + auto cb_added_opt = absl::make_optional(); + initializeProvider(formatted_config, cb_added_opt); + expectStats("city_db", 0, 0, 0, 1671567063); + TestEnvironment::renameFile(city_db_path, city_db_path + "1"); + TestEnvironment::renameFile(reloaded_city_db_path, city_db_path); + cb_added_opt.value().waitReady(); + { + absl::ReaderMutexLock guard(&mutex_); + EXPECT_TRUE(on_changed_cbs_[0](Filesystem::Watcher::Events::MovedTo).ok()); + } + expectReloadStats("city_db", 1, 0); + expectStats("city_db", 0, 0, 0, 1753263760); + + // Clean up modifications to mmdb file names. + TestEnvironment::renameFile(city_db_path, reloaded_city_db_path); + TestEnvironment::renameFile(city_db_path + "1", city_db_path); +} + TEST_F(GeoipProviderTest, DbReloadError) { constexpr absl::string_view config_yaml = R"EOF( common_provider_config: @@ -622,7 +684,7 @@ TEST_F(GeoipProviderTest, DbReloadError) { auto cb_added_opt = absl::make_optional(); initializeProvider(formatted_config, cb_added_opt); Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + Network::Utility::parseInternetAddressNoThrow("81.2.69.144"); Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; testing::MockFunction lookup_cb; auto lookup_cb_std = lookup_cb.AsStdFunction(); @@ -630,7 +692,7 @@ TEST_F(GeoipProviderTest, DbReloadError) { provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); EXPECT_EQ(3, captured_lookup_response_.size()); const auto& city_it = captured_lookup_response_.find("x-geo-city"); - EXPECT_EQ("Boxford", city_it->second); + EXPECT_EQ("London", city_it->second); TestEnvironment::renameFile(city_db_path, city_db_path + "1"); TestEnvironment::renameFile(reloaded_invalid_city_db_path, city_db_path); cb_added_opt.value().waitReady(); @@ -642,14 +704,14 @@ TEST_F(GeoipProviderTest, DbReloadError) { expectReloadStats("city_db", 0, 1); captured_lookup_response_.clear(); EXPECT_EQ(0, captured_lookup_response_.size()); - remote_address = Network::Utility::parseInternetAddressNoThrow("78.26.243.166"); + remote_address = Network::Utility::parseInternetAddressNoThrow("81.2.69.144"); Geolocation::LookupRequest lookup_rq2{std::move(remote_address)}; testing::MockFunction lookup_cb2; auto lookup_cb_std2 = lookup_cb2.AsStdFunction(); EXPECT_CALL(lookup_cb2, Call(_)).WillRepeatedly(SaveArg<0>(&captured_lookup_response_)); provider_->lookup(std::move(lookup_rq2), std::move(lookup_cb_std2)); const auto& city1_it = captured_lookup_response_.find("x-geo-city"); - EXPECT_EQ("Boxford", city1_it->second); + EXPECT_EQ("London", city1_it->second); // Clean up modifications to mmdb file names. TestEnvironment::renameFile(city_db_path, reloaded_invalid_city_db_path); TestEnvironment::renameFile(city_db_path + "1", city_db_path); @@ -854,51 +916,13 @@ TEST_P(MmdbReloadImplTest, MmdbReloadedInFlightReadsNotAffected) { TestEnvironment::renameFile(source_db_file_path + "1", source_db_file_path); } -TEST_P(MmdbReloadImplTest, MmdbNotReloadedRuntimeFeatureDisabled) { - TestScopedRuntime scoped_runtime_; - scoped_runtime_.mergeValues({{"envoy.reloadable_features.mmdb_files_reload_enabled", "false"}}); - MmdbReloadTestCase test_case = GetParam(); - initializeProvider(test_case.yaml_config_, cb_added_nullopt); - Network::Address::InstanceConstSharedPtr remote_address = - Network::Utility::parseInternetAddressNoThrow(test_case.ip_); - Geolocation::LookupRequest lookup_rq{std::move(remote_address)}; - testing::MockFunction lookup_cb; - auto lookup_cb_std = lookup_cb.AsStdFunction(); - EXPECT_CALL(lookup_cb, Call(_)).WillRepeatedly(SaveArg<0>(&captured_lookup_response_)); - provider_->lookup(std::move(lookup_rq), std::move(lookup_cb_std)); - const auto& geoip_header_it = captured_lookup_response_.find(test_case.expected_header_name_); - EXPECT_EQ(test_case.expected_header_value_, geoip_header_it->second); - expectStats(test_case.db_type_, 1, 1); - std::string source_db_file_path = TestEnvironment::substitute(test_case.source_db_file_path_); - std::string reloaded_db_file_path = TestEnvironment::substitute(test_case.reloaded_db_file_path_); - TestEnvironment::renameFile(source_db_file_path, source_db_file_path + "1"); - TestEnvironment::renameFile(reloaded_db_file_path, source_db_file_path); - { - absl::ReaderMutexLock guard(&mutex_); - EXPECT_EQ(0, on_changed_cbs_.size()); - } - expectReloadStats(test_case.db_type_, 0, 0); - captured_lookup_response_.clear(); - remote_address = Network::Utility::parseInternetAddressNoThrow(test_case.ip_); - Geolocation::LookupRequest lookup_rq2{std::move(remote_address)}; - testing::MockFunction lookup_cb2; - auto lookup_cb_std2 = lookup_cb2.AsStdFunction(); - EXPECT_CALL(lookup_cb2, Call(_)).WillRepeatedly(SaveArg<0>(&captured_lookup_response_)); - provider_->lookup(std::move(lookup_rq2), std::move(lookup_cb_std2)); - const auto& geoip_header1_it = captured_lookup_response_.find(test_case.expected_header_name_); - EXPECT_EQ(test_case.expected_header_value_, geoip_header1_it->second); - // Clean up modifications to mmdb file names. - TestEnvironment::renameFile(source_db_file_path, reloaded_db_file_path); - TestEnvironment::renameFile(source_db_file_path + "1", source_db_file_path); -} - struct MmdbReloadTestCase mmdb_reload_test_cases[] = { {default_city_config_yaml, "city_db", default_city_db_path, default_updated_city_db_path, - "x-geo-city", "Boxford", "BoxfordImaginary", "78.26.243.166"}, + "x-geo-city", "London", "BoxfordImaginary", "81.2.69.144"}, {default_isp_config_yaml, "isp_db", default_isp_db_path, default_updated_isp_db_path, "x-geo-isp", "AT&T Services", "AT&T Services Special", "::12.96.16.1"}, {default_asn_config_yaml, "asn_db", default_asn_db_path, default_updated_asn_db_path, - "x-geo-asn", "15169", "77777", "78.26.243.166"}, + "x-geo-asn", "237", "23742", "2806:2000::"}, {default_anon_config_yaml, "anon_db", default_anon_db_path, default_updated_anon_db_path, "x-geo-anon", "true", "false", "65.4.3.2"}, }; diff --git a/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-ASN-Test-Updated.mmdb b/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-ASN-Test-Updated.mmdb index c8b6fe8466321..fc7adc0053ad3 100644 Binary files a/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-ASN-Test-Updated.mmdb and b/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-ASN-Test-Updated.mmdb differ diff --git a/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test-Updated.mmdb b/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test-Updated.mmdb index 58b6aa62baa07..2d2fb0eaf2150 100644 Binary files a/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test-Updated.mmdb and b/test/extensions/geoip_providers/maxmind/test_data/GeoLite2-City-Test-Updated.mmdb differ diff --git a/test/extensions/health_check/event_sinks/file/file_sink_impl_test.cc b/test/extensions/health_check/event_sinks/file/file_sink_impl_test.cc index fc404286c9bfe..11515145aa250 100644 --- a/test/extensions/health_check/event_sinks/file/file_sink_impl_test.cc +++ b/test/extensions/health_check/event_sinks/file/file_sink_impl_test.cc @@ -26,7 +26,7 @@ TEST(HealthCheckEventFileSinkFactory, createHealthCheckEventSink) { envoy::extensions::health_check::event_sinks::file::v3::HealthCheckEventFileSink config; config.set_event_log_path("test_path"); - Envoy::ProtobufWkt::Any typed_config; + Envoy::Protobuf::Any typed_config; typed_config.PackFrom(config); NiceMock context; diff --git a/test/extensions/http/early_header_mutation/header_mutation/BUILD b/test/extensions/http/early_header_mutation/header_mutation/BUILD index 1d631b3a27100..ca1e85566c3f8 100644 --- a/test/extensions/http/early_header_mutation/header_mutation/BUILD +++ b/test/extensions/http/early_header_mutation/header_mutation/BUILD @@ -21,6 +21,7 @@ envoy_extension_cc_test( deps = [ "//source/common/formatter:formatter_extension_lib", "//source/extensions/http/early_header_mutation/header_mutation:header_mutation_lib", + "//test/mocks/server:server_factory_context_mocks", "//test/mocks/stream_info:stream_info_mocks", "//test/test_common:utility_lib", ], diff --git a/test/extensions/http/early_header_mutation/header_mutation/config_test.cc b/test/extensions/http/early_header_mutation/header_mutation/config_test.cc index 4812abc5fb8e7..c863110c31008 100644 --- a/test/extensions/http/early_header_mutation/header_mutation/config_test.cc +++ b/test/extensions/http/early_header_mutation/header_mutation/config_test.cc @@ -38,7 +38,7 @@ TEST(FactoryTest, FactoryTest) { ProtoHeaderMutation proto_mutation; TestUtility::loadFromYaml(config, proto_mutation); - ProtobufWkt::Any any_config; + Protobuf::Any any_config; any_config.PackFrom(proto_mutation); EXPECT_NE(nullptr, factory->createExtension(any_config, context)); diff --git a/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc b/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc index 0705d8c58019e..756b63c289b7f 100644 --- a/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc +++ b/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc @@ -1,6 +1,7 @@ #include "source/common/http/header_map_impl.h" #include "source/extensions/http/early_header_mutation/header_mutation/header_mutation.h" +#include "test/mocks/server/server_factory_context.h" #include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" @@ -42,10 +43,12 @@ TEST(HeaderMutationTest, TestAll) { append_action: "OVERWRITE_IF_EXISTS_OR_ADD" )EOF"; + Server::Configuration::MockServerFactoryContext context; + ProtoHeaderMutation proto_mutation; TestUtility::loadFromYaml(config, proto_mutation); - HeaderMutation mutation(proto_mutation); + HeaderMutation mutation(proto_mutation, context); NiceMock stream_info; Envoy::Http::TestRequestHeaderMapImpl headers = { diff --git a/test/extensions/io_socket/user_space/io_handle_impl_test.cc b/test/extensions/io_socket/user_space/io_handle_impl_test.cc index bea47e6bdd96c..615c512115f6d 100644 --- a/test/extensions/io_socket/user_space/io_handle_impl_test.cc +++ b/test/extensions/io_socket/user_space/io_handle_impl_test.cc @@ -1205,8 +1205,8 @@ class TestObject : public StreamInfo::FilterState::Object { TEST_F(IoHandleImplTest, PassthroughState) { auto source_metadata = std::make_unique(); - ProtobufWkt::Struct& map = (*source_metadata->mutable_filter_metadata())["envoy.test"]; - ProtobufWkt::Value val; + Protobuf::Struct& map = (*source_metadata->mutable_filter_metadata())["envoy.test"]; + Protobuf::Value val; val.set_string_value("val"); (*map.mutable_fields())["key"] = val; StreamInfo::FilterState::Objects source_filter_state; @@ -1293,6 +1293,28 @@ TEST_F(IoHandleImplNotImplementedTest, ErrorOnGetOption) { TEST_F(IoHandleImplNotImplementedTest, ErrorOnIoctl) { EXPECT_THAT(io_handle_->ioctl(0, nullptr, 0, nullptr, 0, nullptr), IsNotSupportedResult()); } + +class TestPassthroughState : public PassthroughStateImpl {}; + +TEST(IoHandleFactoryTest, UseExistingPassthroughState) { + { + auto [io_handle, io_handle_peer] = + IoHandleFactory::createIoHandlePair(std::make_unique()); + EXPECT_NE(std::dynamic_pointer_cast(io_handle->passthroughState()), + nullptr); + EXPECT_NE(std::dynamic_pointer_cast(io_handle_peer->passthroughState()), + nullptr); + } + { + auto [io_handle, io_handle_peer] = IoHandleFactory::createBufferLimitedIoHandlePair( + 1024, std::make_unique()); + EXPECT_NE(std::dynamic_pointer_cast(io_handle->passthroughState()), + nullptr); + EXPECT_NE(std::dynamic_pointer_cast(io_handle_peer->passthroughState()), + nullptr); + } +} + } // namespace } // namespace UserSpace } // namespace IoSocket diff --git a/test/extensions/load_balancing_policies/client_side_weighted_round_robin/integration_test.cc b/test/extensions/load_balancing_policies/client_side_weighted_round_robin/integration_test.cc index 98da83e1c2008..a4a587d7267fb 100644 --- a/test/extensions/load_balancing_policies/client_side_weighted_round_robin/integration_test.cc +++ b/test/extensions/load_balancing_policies/client_side_weighted_round_robin/integration_test.cc @@ -217,8 +217,8 @@ class ClientSideWeightedRoundRobinXdsIntegrationTest // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for // cluster_1. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "55"); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 2); @@ -329,9 +329,9 @@ TEST_P(ClientSideWeightedRoundRobinXdsIntegrationTest, ClusterUpDownUp) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {FirstClusterName}, "42"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {FirstClusterName}, "42"); // We can continue the test once we're sure that Envoy's ClusterManager has // made use of the DiscoveryResponse that says cluster_1 is gone. test_server_->waitForCounterGe("cluster_manager.cluster_removed", 1); @@ -347,8 +347,8 @@ TEST_P(ClientSideWeightedRoundRobinXdsIntegrationTest, ClusterUpDownUp) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is back. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "42", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "42", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "413"); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 2); @@ -368,9 +368,9 @@ TEST_P(ClientSideWeightedRoundRobinXdsIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_2 is here. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); // Wait for the cluster to be active (two upstream clusters plus the CDS // cluster). test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); @@ -381,9 +381,9 @@ TEST_P(ClientSideWeightedRoundRobinXdsIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "42", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "42", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster2_}, {}, {FirstClusterName}, "43"); + Config::TestTypeUrl::get().Cluster, {cluster2_}, {}, {FirstClusterName}, "43"); // We can continue the test once we're sure that Envoy's ClusterManager has // made use of the DiscoveryResponse that says cluster_1 is gone. test_server_->waitForCounterGe("cluster_manager.cluster_removed", 1); @@ -393,9 +393,9 @@ TEST_P(ClientSideWeightedRoundRobinXdsIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is back. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "43", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "43", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_}, {}, "413"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_}, {}, "413"); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); testRouterHeaderOnlyRequestAndResponse(nullptr, FirstUpstreamIndex, "/cluster1"); cleanupUpstreamAndDownstream(); diff --git a/test/extensions/load_balancing_policies/common/BUILD b/test/extensions/load_balancing_policies/common/BUILD index 93d5c4b6b0849..71249aca2029e 100644 --- a/test/extensions/load_balancing_policies/common/BUILD +++ b/test/extensions/load_balancing_policies/common/BUILD @@ -27,6 +27,20 @@ envoy_cc_test_library( ], ) +envoy_cc_test( + name = "locality_wrr_test", + srcs = ["locality_wrr_test.cc"], + deps = [ + "//envoy/upstream:upstream_interface", + "//source/common/upstream:upstream_includes", + "//source/extensions/load_balancing_policies/common:locality_wrr_lib", + "//test/common/upstream:utility_lib", + "//test/mocks/upstream:cluster_info_mocks", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:test_runtime_lib", + ], +) + envoy_cc_test( name = "bounded_load_hlb_test", srcs = ["bounded_load_hlb_test.cc"], diff --git a/test/extensions/load_balancing_policies/common/benchmark_base_tester.cc b/test/extensions/load_balancing_policies/common/benchmark_base_tester.cc index 368999143e0a1..8278cff6d4a5f 100644 --- a/test/extensions/load_balancing_policies/common/benchmark_base_tester.cc +++ b/test/extensions/load_balancing_policies/common/benchmark_base_tester.cc @@ -13,9 +13,9 @@ BaseTester::BaseTester(uint64_t num_hosts, uint32_t weighted_subset_percent, uin const auto effective_weight = should_weight ? weight : 1; if (attach_metadata) { envoy::config::core::v3::Metadata metadata; - ProtobufWkt::Value value; + Protobuf::Value value; value.set_number_value(i); - ProtobufWkt::Struct& map = + Protobuf::Struct& map = (*metadata.mutable_filter_metadata())[Config::MetadataFilters::get().ENVOY_LB]; (*map.mutable_fields())[std::string(metadata_key)] = value; @@ -30,10 +30,10 @@ BaseTester::BaseTester(uint64_t num_hosts, uint32_t weighted_subset_percent, uin Upstream::makeHostsPerLocality({hosts}); priority_set_.updateHosts( 0, Upstream::HostSetImpl::partitionHosts(updated_hosts, hosts_per_locality), {}, hosts, {}, - random_.random(), absl::nullopt); + absl::nullopt); local_priority_set_.updateHosts( 0, Upstream::HostSetImpl::partitionHosts(updated_hosts, hosts_per_locality), {}, hosts, {}, - random_.random(), absl::nullopt); + absl::nullopt); } } // namespace Upstream diff --git a/test/extensions/load_balancing_policies/common/locality_wrr_test.cc b/test/extensions/load_balancing_policies/common/locality_wrr_test.cc new file mode 100644 index 0000000000000..ecb70b2fc5d48 --- /dev/null +++ b/test/extensions/load_balancing_policies/common/locality_wrr_test.cc @@ -0,0 +1,368 @@ +#include "envoy/upstream/upstream.h" + +#include "source/common/upstream/upstream_impl.h" +#include "source/extensions/load_balancing_policies/common/locality_wrr.h" + +#include "test/common/upstream/utility.h" +#include "test/mocks/common.h" +#include "test/mocks/upstream/cluster_info.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/test_runtime.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Upstream { +namespace { + +class LocalityWrrTest : public Event::TestUsingSimulatedTime, public ::testing::Test { +public: + LocalityWrrTest() { + host_set_ = std::make_unique(0, false, kDefaultOverProvisioningFactor); + } + + absl::optional chooseDegradedLocality() { + return locality_wrr_->chooseDegradedLocality(); + } + + absl::optional chooseHealthyLocality() { + return locality_wrr_->chooseHealthyLocality(); + } + + std::unique_ptr host_set_; + std::unique_ptr locality_wrr_ = nullptr; + std::shared_ptr info_{new NiceMock()}; +}; + +TEST_F(LocalityWrrTest, HostSetEmpty) { + locality_wrr_ = std::make_unique(*host_set_, 0); + + EXPECT_EQ(chooseHealthyLocality(), absl::nullopt); + EXPECT_EQ(chooseDegradedLocality(), absl::nullopt); +} + +TEST_F(LocalityWrrTest, AllHostsUnhealthy) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + envoy::config::core::v3::Locality zone_c; + zone_c.set_zone("C"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), + makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; + + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1, 1}}; + auto hosts_const_shared = std::make_shared(hosts); + host_set_->updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + locality_wrr_ = std::make_unique(*host_set_, 0); + + EXPECT_FALSE(chooseHealthyLocality().has_value()); +} + +// When a locality has endpoints that have not yet been warmed, weight calculation should ignore +// these hosts. +TEST_F(LocalityWrrTest, NotWarmedHostsLocality) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:82", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:83", zone_b), + makeTestHost(info_, "tcp://127.0.0.1:84", zone_b)}; + + // We have two localities with 3 hosts in A, 2 hosts in B. Two of the hosts in A are not + // warmed yet, so even though they are unhealthy we should not adjust the locality weight. + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0], hosts[1], hosts[2]}, {hosts[3], hosts[4]}}); + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1}}; + auto hosts_const_shared = std::make_shared(hosts); + HostsPerLocalitySharedPtr healthy_hosts_per_locality = + makeHostsPerLocality({{hosts[0]}, {hosts[3], hosts[4]}}); + HostsPerLocalitySharedPtr excluded_hosts_per_locality = + makeHostsPerLocality({{hosts[1], hosts[2]}, {}}); + + host_set_->updateHosts( + HostSetImpl::updateHostsParams( + hosts_const_shared, hosts_per_locality, + makeHostsFromHostsPerLocality(healthy_hosts_per_locality), + healthy_hosts_per_locality, std::make_shared(), + HostsPerLocalityImpl::empty(), + makeHostsFromHostsPerLocality(excluded_hosts_per_locality), + excluded_hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + locality_wrr_ = std::make_unique(*host_set_, 0); + + // We should RR between localities with equal weight. + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); +} + +TEST_F(LocalityWrrTest, AllZeroWeights) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b)}; + + HostsPerLocalitySharedPtr hosts_per_locality = makeHostsPerLocality({{hosts[0]}, {hosts[1]}}); + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{0, 0}}; + auto hosts_const_shared = std::make_shared(hosts); + host_set_->updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, + std::make_shared(hosts), + hosts_per_locality), + locality_weights, {}, {}, 0); + locality_wrr_ = std::make_unique(*host_set_, 0); + + EXPECT_FALSE(chooseHealthyLocality().has_value()); +} + +TEST_F(LocalityWrrTest, UnweightedLocalities) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + envoy::config::core::v3::Locality zone_c; + zone_c.set_zone("C"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), + makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; + + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1, 1}}; + auto hosts_const_shared = std::make_shared(hosts); + host_set_->updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, + std::make_shared(hosts), + hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + + locality_wrr_ = std::make_unique(*host_set_, 0); + + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); + EXPECT_EQ(2, chooseHealthyLocality().value()); + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); + EXPECT_EQ(2, chooseHealthyLocality().value()); +} + +// When locality weights differ, we have weighted RR behavior. +TEST_F(LocalityWrrTest, WeightedLocalities) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b)}; + + HostsPerLocalitySharedPtr hosts_per_locality = makeHostsPerLocality({{hosts[0]}, {hosts[1]}}); + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 2}}; + auto hosts_const_shared = std::make_shared(hosts); + host_set_->updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, + std::make_shared(hosts), + hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + + locality_wrr_ = std::make_unique(*host_set_, 0); + + EXPECT_EQ(1, chooseHealthyLocality().value()); + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(1, chooseHealthyLocality().value()); +} +// Localities with no weight assignment are never picked. +TEST_F(LocalityWrrTest, MissingWeight) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + envoy::config::core::v3::Locality zone_c; + zone_c.set_zone("C"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), + makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; + + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 0, 1}}; + auto hosts_const_shared = std::make_shared(hosts); + host_set_->updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, + std::make_shared(hosts), + hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + locality_wrr_ = std::make_unique(*host_set_, 0); + + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(2, chooseHealthyLocality().value()); + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(2, chooseHealthyLocality().value()); + EXPECT_EQ(0, chooseHealthyLocality().value()); + EXPECT_EQ(2, chooseHealthyLocality().value()); +} + +// Validates that with weighted initialization all localities are chosen +// proportionally to their weight. +TEST_F(LocalityWrrTest, WeightedAllChosen) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + envoy::config::core::v3::Locality zone_c; + zone_b.set_zone("C"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b), + makeTestHost(info_, "tcp://127.0.0.1:82", zone_c)}; + + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0]}, {hosts[1]}, {hosts[2]}}); + // Set weights of 10%, 60% and 30% to the three zones. + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 6, 3}}; + + // Keep track of how many times each locality is picked, initialized to 0. + uint32_t locality_picked_count[] = {0, 0, 0}; + + // Create the load-balancer 10 times, each with a different seed number (from + // 0 to 10), do a single pick, and validate that the number of picks equals + // to the weights assigned to the localities. + auto hosts_const_shared = std::make_shared(hosts); + for (uint32_t i = 0; i < 10; ++i) { + host_set_->updateHosts(updateHostsParams(hosts_const_shared, hosts_per_locality, + std::make_shared(hosts), + hosts_per_locality), + locality_weights, {}, {}, i, absl::nullopt); + locality_wrr_ = std::make_unique(*host_set_, i); + + locality_picked_count[chooseHealthyLocality().value()]++; + } + EXPECT_EQ(locality_picked_count[0], 1); + EXPECT_EQ(locality_picked_count[1], 6); + EXPECT_EQ(locality_picked_count[2], 3); +} + +// Gentle failover between localities as health diminishes. +TEST_F(LocalityWrrTest, UnhealthyFailover) { + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + HostVector hosts{makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:82", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:83", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:84", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:85", zone_b)}; + + locality_wrr_ = std::make_unique(*host_set_, 0); + + const auto setHealthyHostCount = [this, hosts](uint32_t host_count) { + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 2}}; + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0], hosts[1], hosts[2], hosts[3], hosts[4]}, {hosts[5]}}); + HostVector healthy_hosts; + for (uint32_t i = 0; i < host_count; ++i) { + healthy_hosts.emplace_back(hosts[i]); + } + HostsPerLocalitySharedPtr healthy_hosts_per_locality = + makeHostsPerLocality({healthy_hosts, {hosts[5]}}); + + auto hosts = makeHostsFromHostsPerLocality(hosts_per_locality); + host_set_->updateHosts(updateHostsParams(hosts, hosts_per_locality, + makeHostsFromHostsPerLocality( + healthy_hosts_per_locality), + healthy_hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + locality_wrr_ = std::make_unique(*host_set_, 0); + }; + + const auto expectPicks = [this](uint32_t locality_0_picks, uint32_t locality_1_picks) { + uint32_t count[2] = {0, 0}; + for (uint32_t i = 0; i < 100; ++i) { + const uint32_t locality_index = chooseHealthyLocality().value(); + ASSERT_LT(locality_index, 2); + ++count[locality_index]; + } + ENVOY_LOG_MISC(debug, "Locality picks {} {}", count[0], count[1]); + EXPECT_EQ(locality_0_picks, count[0]); + EXPECT_EQ(locality_1_picks, count[1]); + }; + + setHealthyHostCount(5); + expectPicks(33, 67); + setHealthyHostCount(4); + expectPicks(33, 67); + setHealthyHostCount(3); + expectPicks(29, 71); + setHealthyHostCount(2); + expectPicks(22, 78); + setHealthyHostCount(1); + expectPicks(12, 88); + setHealthyHostCount(0); + expectPicks(0, 100); +} + +TEST(OverProvisioningFactorTest, LocalityPickChanges) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.restart_features.move_locality_schedulers_to_lb", "false"}}); + auto setUpHostSetWithOPFAndTestPicks = [](const uint32_t overprovisioning_factor, + const uint32_t pick_0, const uint32_t pick_1) { + HostSetImpl host_set(0, false, overprovisioning_factor); + std::shared_ptr cluster_info{new NiceMock()}; + auto time_source = std::make_unique>(); + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + HostVector hosts{makeTestHost(cluster_info, "tcp://127.0.0.1:80", zone_a), + makeTestHost(cluster_info, "tcp://127.0.0.1:81", zone_a), + makeTestHost(cluster_info, "tcp://127.0.0.1:82", zone_b)}; + LocalityWeightsConstSharedPtr locality_weights{new LocalityWeights{1, 1}}; + HostsPerLocalitySharedPtr hosts_per_locality = + makeHostsPerLocality({{hosts[0], hosts[1]}, {hosts[2]}}); + // Healthy ratio: (1/2, 1). + HostsPerLocalitySharedPtr healthy_hosts_per_locality = + makeHostsPerLocality({{hosts[0]}, {hosts[2]}}); + auto healthy_hosts = + makeHostsFromHostsPerLocality(healthy_hosts_per_locality); + host_set.updateHosts(updateHostsParams(std::make_shared(hosts), + hosts_per_locality, healthy_hosts, + healthy_hosts_per_locality), + locality_weights, {}, {}, absl::nullopt); + LocalityWrr locality_wrr(host_set, 0); + uint32_t cnts[] = {0, 0}; + for (uint32_t i = 0; i < 100; ++i) { + absl::optional locality_index = locality_wrr.chooseHealthyLocality(); + if (!locality_index.has_value()) { + // It's possible locality scheduler is nullptr (when factor is 0). + continue; + } + ASSERT_LT(locality_index.value(), 2); + ++cnts[locality_index.value()]; + } + EXPECT_EQ(pick_0, cnts[0]); + EXPECT_EQ(pick_1, cnts[1]); + }; + + // NOTE: effective locality weight: weight * min(1, factor * healthy-ratio). + + // Picks in localities match to weight(1) * healthy-ratio when + // overprovisioning factor is 1. + setUpHostSetWithOPFAndTestPicks(100, 33, 67); + // Picks in localities match to weights as factor * healthy-ratio > 1. + setUpHostSetWithOPFAndTestPicks(200, 50, 50); +}; + +} // namespace +} // namespace Upstream +} // namespace Envoy diff --git a/test/extensions/load_balancing_policies/least_request/least_request_lb_simulation_test.cc b/test/extensions/load_balancing_policies/least_request/least_request_lb_simulation_test.cc index 17b75e5f3c6df..7fb0423115009 100644 --- a/test/extensions/load_balancing_policies/least_request/least_request_lb_simulation_test.cc +++ b/test/extensions/load_balancing_policies/least_request/least_request_lb_simulation_test.cc @@ -74,7 +74,7 @@ void leastRequestLBWeightTest(LRLBTestParams params) { updateHostsParams(updated_hosts, updated_locality_hosts, std::make_shared(*updated_hosts), updated_locality_hosts), - {}, hosts, {}, random.random(), absl::nullopt); + {}, hosts, {}, absl::nullopt); Stats::IsolatedStoreImpl stats_store; ClusterLbStatNames stat_names(stats_store.symbolTable()); diff --git a/test/extensions/load_balancing_policies/override_host/config_test.cc b/test/extensions/load_balancing_policies/override_host/config_test.cc index 0e634db89b1b6..6ef6d61ff94b4 100644 --- a/test/extensions/load_balancing_policies/override_host/config_test.cc +++ b/test/extensions/load_balancing_policies/override_host/config_test.cc @@ -73,7 +73,7 @@ TEST(OverrideHostLbonfigTest, NoPrimaryOverideSources) { config.set_name("envoy.load_balancers.override_host"); OverrideHost config_msg; - ProtobufWkt::Struct invalid_policy; + Protobuf::Struct invalid_policy; auto* typed_extension_config = config_msg.mutable_fallback_policy()->add_policies()->mutable_typed_extension_config(); typed_extension_config->mutable_typed_config()->PackFrom(invalid_policy); @@ -100,7 +100,7 @@ TEST(OverrideHostLbonfigTest, FirstValidFallbackPolicyIsUsed) { OverrideHost config_msg; config_msg.add_override_host_sources()->set_header("x-foo"); - ProtobufWkt::Struct invalid_policy; + Protobuf::Struct invalid_policy; auto* typed_extension_config = config_msg.mutable_fallback_policy()->add_policies()->mutable_typed_extension_config(); typed_extension_config->mutable_typed_config()->PackFrom(invalid_policy); @@ -128,7 +128,7 @@ TEST(OverrideHostLbonfigTest, EmptyPrimaryOverrideSource) { // Do not set either host or metadata keys config_msg.add_override_host_sources(); - ProtobufWkt::Struct invalid_policy; + Protobuf::Struct invalid_policy; auto* typed_extension_config = config_msg.mutable_fallback_policy()->add_policies()->mutable_typed_extension_config(); typed_extension_config->mutable_typed_config()->PackFrom(invalid_policy); @@ -161,7 +161,7 @@ TEST(OverrideHostLbonfigTest, HeaderAndMetadataInTheSameOverrideSource) { metadata_key->set_key("x-bar"); metadata_key->add_path()->set_key("a/b/c"); - ProtobufWkt::Struct invalid_policy; + Protobuf::Struct invalid_policy; auto* typed_extension_config = config_msg.mutable_fallback_policy()->add_policies()->mutable_typed_extension_config(); typed_extension_config->mutable_typed_config()->PackFrom(invalid_policy); diff --git a/test/extensions/load_balancing_policies/override_host/load_balancer_test.cc b/test/extensions/load_balancing_policies/override_host/load_balancer_test.cc index 2476ec4a8b84f..596a3a533767b 100644 --- a/test/extensions/load_balancing_policies/override_host/load_balancer_test.cc +++ b/test/extensions/load_balancing_policies/override_host/load_balancer_test.cc @@ -127,7 +127,7 @@ class OverrideHostLoadBalancerTest : public ::testing::Test { void setSelectedEndpointsMetadata(absl::string_view key, absl::string_view selected_endpoints_text_proto) { - Envoy::ProtobufWkt::Struct selected_endpoints; + Envoy::Protobuf::Struct selected_endpoints; EXPECT_TRUE( Protobuf::TextFormat::ParseFromString(selected_endpoints_text_proto, &selected_endpoints)); (*metadata_.mutable_filter_metadata())[key] = selected_endpoints; diff --git a/test/extensions/load_balancing_policies/random/random_lb_simulation_test.cc b/test/extensions/load_balancing_policies/random/random_lb_simulation_test.cc index c4a79a2773174..bdb0ff6bb03f7 100644 --- a/test/extensions/load_balancing_policies/random/random_lb_simulation_test.cc +++ b/test/extensions/load_balancing_policies/random/random_lb_simulation_test.cc @@ -107,7 +107,7 @@ class DISABLED_SimulationTest : public testing::Test { // NOLINT(readability-ide updateHostsParams(originating_hosts, per_zone_local_shared, std::make_shared(*originating_hosts), per_zone_local_shared), - {}, empty_vector_, empty_vector_, random_.random(), absl::nullopt); + {}, empty_vector_, empty_vector_, absl::nullopt); HostConstSharedPtr selected = lb.chooseHost(nullptr).host; hits[selected->address()->asString()]++; diff --git a/test/extensions/load_balancing_policies/round_robin/round_robin_lb_test.cc b/test/extensions/load_balancing_policies/round_robin/round_robin_lb_test.cc index 6ad3b1d3a9b0e..83e8e3b84ac98 100644 --- a/test/extensions/load_balancing_policies/round_robin/round_robin_lb_test.cc +++ b/test/extensions/load_balancing_policies/round_robin/round_robin_lb_test.cc @@ -335,23 +335,13 @@ TEST_P(RoundRobinLoadBalancerTest, Locality) { hostSet().healthy_hosts_ = *hosts; hostSet().healthy_hosts_per_locality_ = hosts_per_locality; init(false, true); - // chooseHealthyLocality() return value determines which locality we use. - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(0)); - EXPECT_EQ(hostSet().healthy_hosts_[1], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(1)); - EXPECT_EQ(hostSet().healthy_hosts_[0], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(0)); - EXPECT_EQ(hostSet().healthy_hosts_[1], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(1)); + + // Round robin through all localities. EXPECT_EQ(hostSet().healthy_hosts_[0], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(0)); EXPECT_EQ(hostSet().healthy_hosts_[1], lb_->chooseHost(nullptr).host); - // When there is no locality, we RR over all available hosts. - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(absl::optional())); + EXPECT_EQ(hostSet().healthy_hosts_[2], lb_->chooseHost(nullptr).host); EXPECT_EQ(hostSet().healthy_hosts_[0], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(absl::optional())); EXPECT_EQ(hostSet().healthy_hosts_[1], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(absl::optional())); EXPECT_EQ(hostSet().healthy_hosts_[2], lb_->chooseHost(nullptr).host); } @@ -380,13 +370,12 @@ TEST_P(RoundRobinLoadBalancerTest, DegradedLocality) { hostSet().degraded_hosts_per_locality_ = degraded_hosts_per_locality; init(false, true); - EXPECT_CALL(random_, random()).WillOnce(Return(50)).WillOnce(Return(0)); + EXPECT_CALL(random_, random()).WillOnce(Return(50)).WillOnce(Return(0)).WillOnce(Return(51)); // Since we're split between healthy and degraded, the LB should call into both // chooseHealthyLocality and chooseDegradedLocality. - EXPECT_CALL(hostSet(), chooseDegradedLocality()).WillOnce(Return(1)); EXPECT_EQ(hostSet().degraded_hosts_[0], lb_->chooseHost(nullptr).host); - EXPECT_CALL(hostSet(), chooseHealthyLocality()).WillOnce(Return(0)); EXPECT_EQ(hostSet().healthy_hosts_[0], lb_->chooseHost(nullptr).host); + EXPECT_EQ(hostSet().degraded_hosts_[1], lb_->chooseHost(nullptr).host); } TEST_P(RoundRobinLoadBalancerTest, Weighted) { @@ -966,6 +955,83 @@ TEST_P(RoundRobinLoadBalancerTest, ZoneAwareDifferentZoneSize) { EXPECT_EQ(2U, stats_.lb_zone_routing_cross_zone_.value()); } +TEST_P(RoundRobinLoadBalancerTest, ZoneAwareUseHostWeight) { + if (&hostSet() == &failover_host_set_) { // P = 1 does not support zone-aware routing. + return; + } + envoy::config::core::v3::Locality zone_a; + zone_a.set_zone("A"); + envoy::config::core::v3::Locality zone_b; + zone_b.set_zone("B"); + + // Setup is: + // L = local envoy + // U = upstream host + // + // Zone A: 2L with 100 weight each, 1U with 100 weight + // Zone B: 1L with 200 weight, 1U with 100 weight + + HostVectorSharedPtr upstream_hosts( + new HostVector({makeTestHost(info_, "tcp://127.0.0.1:80", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b)})); + HostVectorSharedPtr local_hosts( + new HostVector({makeTestHost(info_, "tcp://127.0.0.1:0", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:1", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:2", zone_b)})); + HostsPerLocalitySharedPtr upstream_hosts_per_locality = + makeHostsPerLocality({{// zone A + makeTestHost(info_, "tcp://127.0.0.1:80", zone_a)}, + {// zone B + makeTestHost(info_, "tcp://127.0.0.1:81", zone_b)}}); + HostsPerLocalitySharedPtr local_hosts_per_locality = + makeHostsPerLocality({{// zone A + makeTestHost(info_, "tcp://127.0.0.1:0", zone_a), + makeTestHost(info_, "tcp://127.0.0.1:1", zone_a)}, + {// zone B + makeTestHost(info_, "tcp://127.0.0.1:2", zone_b)}}); + + local_hosts_per_locality->get()[0][0]->weight(100); + local_hosts_per_locality->get()[0][1]->weight(100); + local_hosts_per_locality->get()[1][0]->weight(200); + upstream_hosts_per_locality->get()[0][0]->weight(100); + upstream_hosts_per_locality->get()[1][0]->weight(100); + + hostSet().healthy_hosts_ = *upstream_hosts; + hostSet().hosts_ = *upstream_hosts; + hostSet().healthy_hosts_per_locality_ = upstream_hosts_per_locality; + common_config_.mutable_healthy_panic_threshold()->set_value(100); + round_robin_lb_config_.mutable_locality_lb_config() + ->mutable_zone_aware_lb_config() + ->mutable_routing_enabled() + ->set_value(100); + round_robin_lb_config_.mutable_locality_lb_config() + ->mutable_zone_aware_lb_config() + ->mutable_min_cluster_size() + ->set_value(2); + round_robin_lb_config_.mutable_locality_lb_config() + ->mutable_zone_aware_lb_config() + ->set_locality_basis(envoy::extensions::load_balancing_policies::common::v3:: + LocalityLbConfig::ZoneAwareLbConfig::HEALTHY_HOSTS_WEIGHT); + init(true); + updateHosts(local_hosts, local_hosts_per_locality); + + EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.healthy_panic_threshold", 100)) + .WillRepeatedly(Return(50)); + EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.zone_routing.enabled", 100)) + .WillRepeatedly(Return(true)); + EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.force_local_zone.min_size", 0)) + .WillRepeatedly(Return(0)); + EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.min_cluster_size", 2)) + .WillRepeatedly(Return(2)); + + // Although there are two local hosts in zone A, the zone A and zone B has the same total weight + // in total. So all traffic should go directly to the same zone. + EXPECT_EQ(hostSet().healthy_hosts_per_locality_->get()[0][0], lb_->chooseHost(nullptr).host); + EXPECT_EQ(1U, stats_.lb_zone_routing_all_directly_.value()); + EXPECT_EQ(hostSet().healthy_hosts_per_locality_->get()[0][0], lb_->chooseHost(nullptr).host); + EXPECT_EQ(2U, stats_.lb_zone_routing_all_directly_.value()); +} + TEST_P(RoundRobinLoadBalancerTest, ZoneAwareRoutingLargeZoneSwitchOnOff) { if (&hostSet() == &failover_host_set_) { // P = 1 does not support zone-aware routing. return; diff --git a/test/extensions/load_balancing_policies/subset/subset_benchmark.cc b/test/extensions/load_balancing_policies/subset/subset_benchmark.cc index 3abb46573b05e..836d8ff59fac0 100644 --- a/test/extensions/load_balancing_policies/subset/subset_benchmark.cc +++ b/test/extensions/load_balancing_policies/subset/subset_benchmark.cc @@ -68,10 +68,10 @@ class SubsetLbTester : public Upstream::BaseTester { void update() { priority_set_.updateHosts( 0, Upstream::HostSetImpl::partitionHosts(smaller_hosts_, smaller_locality_hosts_), nullptr, - {}, host_moved_, random_.random(), absl::nullopt); + {}, host_moved_, absl::nullopt); priority_set_.updateHosts( 0, Upstream::HostSetImpl::partitionHosts(orig_hosts_, orig_locality_hosts_), nullptr, - host_moved_, {}, random_.random(), absl::nullopt); + host_moved_, {}, absl::nullopt); } std::unique_ptr subset_config_; diff --git a/test/extensions/load_balancing_policies/subset/subset_test.cc b/test/extensions/load_balancing_policies/subset/subset_test.cc index e84b9eac54206..369bb1bcced98 100644 --- a/test/extensions/load_balancing_policies/subset/subset_test.cc +++ b/test/extensions/load_balancing_policies/subset/subset_test.cc @@ -53,7 +53,7 @@ class MockLoadBalancerSubsetInfo : public LoadBalancerSubsetInfo { fallbackPolicy, (), (const)); MOCK_METHOD(envoy::config::cluster::v3::Cluster::LbSubsetConfig::LbSubsetMetadataFallbackPolicy, metadataFallbackPolicy, (), (const)); - MOCK_METHOD(const ProtobufWkt::Struct&, defaultSubset, (), (const)); + MOCK_METHOD(const Protobuf::Struct&, defaultSubset, (), (const)); MOCK_METHOD(const std::vector&, subsetSelectors, (), (const)); MOCK_METHOD(bool, localityWeightAware, (), (const)); MOCK_METHOD(bool, scaleLocalityWeight, (), (const)); @@ -68,7 +68,7 @@ MockLoadBalancerSubsetInfo::MockLoadBalancerSubsetInfo() { ON_CALL(*this, isEnabled()).WillByDefault(Return(true)); ON_CALL(*this, fallbackPolicy()) .WillByDefault(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::ANY_ENDPOINT)); - ON_CALL(*this, defaultSubset()).WillByDefault(ReturnRef(ProtobufWkt::Struct::default_instance())); + ON_CALL(*this, defaultSubset()).WillByDefault(ReturnRef(Protobuf::Struct::default_instance())); ON_CALL(*this, subsetSelectors()).WillByDefault(ReturnRef(subset_selectors_)); } @@ -91,7 +91,7 @@ class TestMetadataMatchCriteria : public Router::MetadataMatchCriteria { public: TestMetadataMatchCriteria(const std::map matches) { for (const auto& it : matches) { - ProtobufWkt::Value v; + Protobuf::Value v; v.set_string_value(it.second); matches_.emplace_back( @@ -99,7 +99,7 @@ class TestMetadataMatchCriteria : public Router::MetadataMatchCriteria { } } - TestMetadataMatchCriteria(const std::map matches) { + TestMetadataMatchCriteria(const std::map matches) { for (const auto& it : matches) { matches_.emplace_back( std::make_shared(it.first, HashedValue(it.second))); @@ -112,7 +112,7 @@ class TestMetadataMatchCriteria : public Router::MetadataMatchCriteria { } Router::MetadataMatchCriteriaConstPtr - mergeMatchCriteria(const ProtobufWkt::Struct& override) const override { + mergeMatchCriteria(const Protobuf::Struct& override) const override { auto new_criteria = std::make_unique(*this); // TODO: this is copied from MetadataMatchCriteriaImpl::extractMetadataMatchCriteria. @@ -265,7 +265,7 @@ TEST(LoadBalancerSubsetInfoImplTest, DefaultConfigIsDiabled) { } TEST(LoadBalancerSubsetInfoImplTest, SubsetConfig) { - auto subset_value = ProtobufWkt::Value(); + auto subset_value = Protobuf::Value(); subset_value.set_string_value("the value"); auto subset_config = envoy::config::cluster::v3::Cluster::LbSubsetConfig::default_instance(); @@ -306,9 +306,9 @@ class TestLoadBalancerContext : public LoadBalancerContextBase { new TestMetadataMatchCriteria(std::map(metadata_matches))) {} TestLoadBalancerContext( - std::initializer_list::value_type> metadata_matches) + std::initializer_list::value_type> metadata_matches) : matches_(new TestMetadataMatchCriteria( - std::map(metadata_matches))) {} + std::map(metadata_matches))) {} // Upstream::LoadBalancerContext absl::optional computeHashKey() override { return {}; } @@ -475,7 +475,7 @@ class SubsetLoadBalancerTest : public Event::TestUsingSimulatedTime, std::make_shared(*local_hosts_), local_hosts_per_locality_, std::make_shared(), HostsPerLocalityImpl::empty(), std::make_shared(), HostsPerLocalityImpl::empty()), - {}, {}, {}, 0, absl::nullopt); + {}, {}, {}, absl::nullopt); initLbConfigAndLB(nullptr, true); } @@ -514,12 +514,12 @@ class SubsetLoadBalancerTest : public Event::TestUsingSimulatedTime, return makeTestHost(info_, url, m); } - ProtobufWkt::Struct makeDefaultSubset(HostMetadata metadata) { - ProtobufWkt::Struct default_subset; + Protobuf::Struct makeDefaultSubset(HostMetadata metadata) { + Protobuf::Struct default_subset; auto* fields = default_subset.mutable_fields(); for (const auto& it : metadata) { - ProtobufWkt::Value v; + Protobuf::Value v; v.set_string_value(it.second); fields->insert({it.first, v}); } @@ -624,7 +624,7 @@ class SubsetLoadBalancerTest : public Event::TestUsingSimulatedTime, updateHostsParams(local_hosts_, local_hosts_per_locality_, std::make_shared(*local_hosts_), local_hosts_per_locality_), - {}, {}, remove, 0, absl::nullopt); + {}, {}, remove, absl::nullopt); } for (const auto& host : add) { @@ -641,7 +641,7 @@ class SubsetLoadBalancerTest : public Event::TestUsingSimulatedTime, updateHostsParams(local_hosts_, local_hosts_per_locality_, std::make_shared(*local_hosts_), local_hosts_per_locality_), - {}, add, {}, 0, absl::nullopt); + {}, add, {}, absl::nullopt); } } else if (!add.empty() || !remove.empty()) { local_priority_set_.updateHosts( @@ -649,7 +649,7 @@ class SubsetLoadBalancerTest : public Event::TestUsingSimulatedTime, updateHostsParams(local_hosts_, local_hosts_per_locality_, std::make_shared(*local_hosts_), local_hosts_per_locality_), - {}, add, remove, 0, absl::nullopt); + {}, add, remove, absl::nullopt); } } @@ -707,8 +707,8 @@ class SubsetLoadBalancerTest : public Event::TestUsingSimulatedTime, return std::make_shared(metadata); } - ProtobufWkt::Value valueFromJson(std::string json) { - ProtobufWkt::Value v; + Protobuf::Value valueFromJson(std::string json) { + Protobuf::Value v; TestUtility::loadFromJson(json, v); return v; } @@ -805,7 +805,7 @@ TEST_F(SubsetLoadBalancerTest, FallbackDefaultSubset) { EXPECT_CALL(subset_info_, fallbackPolicy()) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::DEFAULT_SUBSET)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "default"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "default"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); init({ @@ -824,7 +824,7 @@ TEST_F(SubsetLoadBalancerTest, FallbackPanicMode) { EXPECT_CALL(subset_info_, panicModeAny()).WillRepeatedly(Return(true)); // The default subset will be empty. - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "none"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "none"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); init({ @@ -844,7 +844,7 @@ TEST_P(SubsetLoadBalancerTest, FallbackPanicModeWithUpdates) { EXPECT_CALL(subset_info_, panicModeAny()).WillRepeatedly(Return(true)); // The default subset will be empty. - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "none"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "none"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); init({{"tcp://127.0.0.1:80", {{"version", "default"}}}}); @@ -862,7 +862,7 @@ TEST_P(SubsetLoadBalancerTest, FallbackDefaultSubsetAfterUpdate) { EXPECT_CALL(subset_info_, fallbackPolicy()) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::DEFAULT_SUBSET)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "default"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "default"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); init({ @@ -885,7 +885,7 @@ TEST_F(SubsetLoadBalancerTest, FallbackEmptyDefaultSubsetConvertsToAnyEndpoint) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::DEFAULT_SUBSET)); EXPECT_CALL(subset_info_, defaultSubset()) - .WillRepeatedly(ReturnRef(ProtobufWkt::Struct::default_instance())); + .WillRepeatedly(ReturnRef(Protobuf::Struct::default_instance())); init(); @@ -1154,7 +1154,7 @@ TEST_P(SubsetLoadBalancerTest, OnlyMetadataChanged) { EXPECT_CALL(subset_info_, subsetSelectors()).WillRepeatedly(ReturnRef(subset_selectors)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"default", "true"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"default", "true"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); EXPECT_CALL(subset_info_, fallbackPolicy()) @@ -1342,7 +1342,7 @@ TEST_P(SubsetLoadBalancerTest, MetadataChangedHostsAddedRemoved) { TestLoadBalancerContext context_13({{"version", "1.3"}}); TestLoadBalancerContext context_14({{"version", "1.4"}}); TestLoadBalancerContext context_default({{"default", "true"}}); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"default", "true"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"default", "true"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); EXPECT_CALL(subset_info_, fallbackPolicy()) @@ -1837,7 +1837,7 @@ TEST_F(SubsetLoadBalancerTest, ZoneAwareFallbackDefaultSubset) { EXPECT_CALL(subset_info_, fallbackPolicy()) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::DEFAULT_SUBSET)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "default"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "default"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); std::vector subset_selectors = {makeSelector( @@ -1896,7 +1896,7 @@ TEST_P(SubsetLoadBalancerTest, ZoneAwareFallbackDefaultSubsetAfterUpdate) { EXPECT_CALL(subset_info_, fallbackPolicy()) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::DEFAULT_SUBSET)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "default"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "default"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); std::vector subset_selectors = {makeSelector( @@ -2280,10 +2280,10 @@ TEST_F(SubsetLoadBalancerTest, DescribeMetadata) { .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::NO_FALLBACK)); init(); - ProtobufWkt::Value str_value; + Protobuf::Value str_value; str_value.set_string_value("abc"); - ProtobufWkt::Value num_value; + Protobuf::Value num_value; num_value.set_number_value(100); EXPECT_EQ("version=\"abc\"", SubsetLoadBalancer::describeMetadata({{"version", str_value}})); @@ -2718,7 +2718,7 @@ TEST_P(SubsetLoadBalancerTest, SubsetSelectorDefaultAnyFallbackPerSelector) { EXPECT_CALL(subset_info_, subsetSelectors()).WillRepeatedly(ReturnRef(subset_selectors)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"bar", "default"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"bar", "default"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); // Add hosts initial hosts. @@ -2744,7 +2744,7 @@ TEST_P(SubsetLoadBalancerTest, SubsetSelectorDefaultAfterUpdate) { EXPECT_CALL(subset_info_, fallbackPolicy()) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::DEFAULT_SUBSET)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"version", "default"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"version", "default"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); std::vector subset_selectors = {makeSelector( @@ -2799,7 +2799,7 @@ TEST_P(SubsetLoadBalancerTest, SubsetSelectorAnyAfterUpdate) { TEST_P(SubsetLoadBalancerTest, FallbackForCompoundSelector) { EXPECT_CALL(subset_info_, fallbackPolicy()) .WillRepeatedly(Return(envoy::config::cluster::v3::Cluster::LbSubsetConfig::ANY_ENDPOINT)); - const ProtobufWkt::Struct default_subset = makeDefaultSubset({{"foo", "bar"}}); + const Protobuf::Struct default_subset = makeDefaultSubset({{"foo", "bar"}}); EXPECT_CALL(subset_info_, defaultSubset()).WillRepeatedly(ReturnRef(default_subset)); std::vector subset_selectors = { @@ -2954,8 +2954,8 @@ TEST_P(SubsetLoadBalancerTest, MetadataFallbackList) { // if fallback_list is not a list, it should be ignored // regular metadata is in effect - ProtobufWkt::Value null_value; - null_value.set_null_value(ProtobufWkt::NullValue::NULL_VALUE); + Protobuf::Value null_value; + null_value.set_null_value(Protobuf::NullValue::NULL_VALUE); TestLoadBalancerContext context_with_invalid_fallback_list_null( {{"version", valueFromJson("\"3.0\"")}, {"fallback_list", null_value}}); @@ -3309,7 +3309,7 @@ INSTANTIATE_TEST_SUITE_P(UpdateOrderings, SubsetLoadBalancerSingleHostPerSubsetT TEST(LoadBalancerContextWrapperTest, LoadBalancingContextWrapperTest) { testing::NiceMock mock_context; - ProtobufWkt::Struct empty_struct; + Protobuf::Struct empty_struct; Router::MetadataMatchCriteriaImpl match_criteria(empty_struct); ON_CALL(mock_context, metadataMatchCriteria()).WillByDefault(testing::Return(&match_criteria)); diff --git a/test/extensions/matching/actions/format_string/config_test.cc b/test/extensions/matching/actions/format_string/config_test.cc index 8bbb3027572fe..7028674e75f2d 100644 --- a/test/extensions/matching/actions/format_string/config_test.cc +++ b/test/extensions/matching/actions/format_string/config_test.cc @@ -23,10 +23,8 @@ TEST(ConfigTest, TestConfig) { testing::NiceMock factory_context; ActionFactory factory; - auto action_cb = factory.createActionFactoryCb(config, factory_context, - ProtobufMessage::getStrictValidationVisitor()); - ASSERT_NE(nullptr, action_cb); - auto action = action_cb(); + auto action = + factory.createAction(config, factory_context, ProtobufMessage::getStrictValidationVisitor()); ASSERT_NE(nullptr, action); const auto& typed_action = action->getTyped(); diff --git a/test/extensions/matching/input_matchers/cel_matcher/cel_matcher_test.cc b/test/extensions/matching/input_matchers/cel_matcher/cel_matcher_test.cc index 898e93cdcf0f9..14d61060a626e 100644 --- a/test/extensions/matching/input_matchers/cel_matcher/cel_matcher_test.cc +++ b/test/extensions/matching/input_matchers/cel_matcher/cel_matcher_test.cc @@ -267,7 +267,7 @@ TEST_F(CelMatcherTest, CelMatcherDynamicMetadataNotMatched) { TEST_F(CelMatcherTest, CelMatcherTypedDynamicMetadataMatched) { ::envoy::config::core::v3::Pipe pipe; pipe.set_path("/foo/bar/baz.fads"); - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(pipe); stream_info_.metadata_.mutable_typed_filter_metadata()->insert( {std::string(kFilterNamespace), typed_metadata}); @@ -473,8 +473,55 @@ TEST_F(CelMatcherTest, CelMatcherRequestResponseNotMatchedWithParsedExprUseCel) } TEST_F(CelMatcherTest, NoCelExpression) { - EXPECT_DEATH(buildMatcherTree(RequestHeaderCelExprString, ExpressionType::NoExpression), - ".*panic: unset oneof.*"); + EXPECT_THROW_WITH_REGEX( + buildMatcherTree(RequestHeaderCelExprString, ExpressionType::NoExpression), EnvoyException, + ".*CEL expression not set.*"); +} + +// Add a test case specifically for testing format conversion +TEST_F(CelMatcherTest, FormatConversionV1AlphaToDevCel) { + // Use RequestHeaderCelExprString which is already defined and works + auto matcher_tree = buildMatcherTree(RequestHeaderCelExprString); + + TestRequestHeaderMapImpl request_headers = default_headers_; + buildCustomHeader({{"authenticated_user", "staging"}}, request_headers); + data_.onRequestHeaders(request_headers); + + const auto result = matcher_tree->match(data_); + // The match was complete, match found since user is "staging" + EXPECT_TRUE(result.isMatch()); + EXPECT_NE(result.action(), nullptr); +} + +// Test that we can parse and evaluate expressions in dev.cel format +TEST_F(CelMatcherTest, DevCelExpressionFormat) { + // Use the same RequestHeaderCelExprString with use_cel=true + auto matcher_tree = + buildMatcherTree(RequestHeaderCelExprString, ExpressionType::ParsedExpression, true); + + TestRequestHeaderMapImpl request_headers = default_headers_; + buildCustomHeader({{"authenticated_user", "staging"}}, request_headers); + data_.onRequestHeaders(request_headers); + + const auto result = matcher_tree->match(data_); + // The match was complete, match found + EXPECT_TRUE(result.isMatch()); + EXPECT_NE(result.action(), nullptr); +} + +// Test with different types of expressions and formats +TEST_F(CelMatcherTest, MixedFormatExpressions) { + // Use RequestHeaderCelExprString with CheckedExpression format + auto matcher_tree1 = + buildMatcherTree(RequestHeaderCelExprString, ExpressionType::CheckedExpression); + + TestRequestHeaderMapImpl request_headers = default_headers_; + buildCustomHeader({{"authenticated_user", "staging"}}, request_headers); + data_.onRequestHeaders(request_headers); + + const auto result1 = matcher_tree1->match(data_); + EXPECT_TRUE(result1.isMatch()); + EXPECT_NE(result1.action(), nullptr); } } // namespace CelMatcher diff --git a/test/extensions/matching/network/common/inputs_integration_test.cc b/test/extensions/matching/network/common/inputs_integration_test.cc index 75c335611f113..fd461f81ca1a4 100644 --- a/test/extensions/matching/network/common/inputs_integration_test.cc +++ b/test/extensions/matching/network/common/inputs_integration_test.cc @@ -229,7 +229,7 @@ TEST_F(InputsIntegrationTest, DynamicMetadataInput) { std::string label_key("label_key"); auto label = MessageUtil::keyValueStruct(label_key, "bar"); metadata.mutable_filter_metadata()->insert( - Protobuf::MapPair(metadata_key, label)); + Protobuf::MapPair(metadata_key, label)); auto stored_metadata = data.dynamicMetadata().filter_metadata(); EXPECT_EQ(label.fields_size(), 1); EXPECT_EQ(stored_metadata[metadata_key].fields_size(), 1); diff --git a/test/extensions/matching/network/common/inputs_test.cc b/test/extensions/matching/network/common/inputs_test.cc index dc1cdec7386c4..324fda87b751c 100644 --- a/test/extensions/matching/network/common/inputs_test.cc +++ b/test/extensions/matching/network/common/inputs_test.cc @@ -450,6 +450,114 @@ TEST(UdpMatchingData, UdpSourcePortInput) { } } +TEST(MatchingData, NetworkNamespaceInput) { + NetworkNamespaceInput input; + MockConnectionSocket socket; + StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection); + envoy::config::core::v3::Metadata metadata; + MatchingDataImpl data(socket, filter_state, metadata); + + // Test with no network namespace (default case). + { + socket.connection_info_provider_->setLocalAddress( + std::make_shared("127.0.0.1", 8080)); + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_TRUE(absl::holds_alternative(result.data_)); + } + + // Test with network namespace. + { + socket.connection_info_provider_->setLocalAddress( + std::make_shared( + "127.0.0.1", 8080, nullptr, absl::make_optional(std::string("/var/run/netns/ns1")))); + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_EQ(absl::get(result.data_), "/var/run/netns/ns1"); + } + + // Test with empty network namespace. + { + socket.connection_info_provider_->setLocalAddress( + std::make_shared("127.0.0.1", 8080, nullptr, + absl::make_optional(std::string("")))); + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_TRUE(absl::holds_alternative(result.data_)); + } + + // Test with IPv6 address and network namespace. + { + socket.connection_info_provider_->setLocalAddress( + std::make_shared( + "::1", 8080, nullptr, true, absl::make_optional(std::string("/var/run/netns/ns2")))); + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_EQ(absl::get(result.data_), "/var/run/netns/ns2"); + } + + // Test with pipe address. This should return monostate since pipes don't have network namespaces. + { + socket.connection_info_provider_->setLocalAddress( + *Network::Address::PipeInstance::create("/pipe/path")); + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_TRUE(absl::holds_alternative(result.data_)); + } +} + +TEST(MatchingData, HttpNetworkNamespaceInput) { + auto connection_info_provider = std::make_shared( + std::make_shared( + "127.0.0.1", 8080, nullptr, absl::make_optional(std::string("/var/run/netns/http_ns"))), + std::make_shared("10.0.0.1", 9090)); + + StreamInfo::StreamInfoImpl stream_info( + Http::Protocol::Http2, Event::GlobalTimeSystem().timeSystem(), connection_info_provider, + StreamInfo::FilterState::LifeSpan::FilterChain); + Http::Matching::HttpMatchingDataImpl data(stream_info); + + NetworkNamespaceInput input; + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_EQ(absl::get(result.data_), "/var/run/netns/http_ns"); +} + +TEST(UdpMatchingData, UdpNetworkNamespaceInput) { + NetworkNamespaceInput input; + + // Test with network namespace. + { + const Address::Ipv4Instance local_ip("127.0.0.1", 8080, nullptr, + absl::make_optional(std::string("/var/run/netns/udp_ns"))); + const Address::Ipv4Instance remote_ip("10.0.0.1", 9090); + UdpMatchingDataImpl data(local_ip, remote_ip); + + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_EQ(absl::get(result.data_), "/var/run/netns/udp_ns"); + } + + // Test without network namespace. + { + const Address::Ipv4Instance local_ip("127.0.0.1", 8080); + const Address::Ipv4Instance remote_ip("10.0.0.1", 9090); + UdpMatchingDataImpl data(local_ip, remote_ip); + + const auto result = input.get(data); + EXPECT_EQ(result.data_availability_, + Matcher::DataInputGetResult::DataAvailability::AllDataAvailable); + EXPECT_TRUE(absl::holds_alternative(result.data_)); + } +} + } // namespace Matching } // namespace Network } // namespace Envoy diff --git a/test/extensions/network/dns_resolver/cares/BUILD b/test/extensions/network/dns_resolver/cares/BUILD index 92405960686f2..783c531855cf4 100644 --- a/test/extensions/network/dns_resolver/cares/BUILD +++ b/test/extensions/network/dns_resolver/cares/BUILD @@ -39,3 +39,14 @@ envoy_cc_test( "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) + +envoy_cc_test( + name = "dns_impl_integration_test", + srcs = ["dns_impl_integration_test.cc"], + tags = ["fails_on_clang_cl"], + deps = [ + "//source/extensions/clusters/dns:dns_cluster_lib", + "//source/extensions/network/dns_resolver/cares:config", + "//test/integration:http_integration_lib", + ], +) diff --git a/test/extensions/network/dns_resolver/cares/dns_impl_integration_test.cc b/test/extensions/network/dns_resolver/cares/dns_impl_integration_test.cc new file mode 100644 index 0000000000000..cfea13859c3a9 --- /dev/null +++ b/test/extensions/network/dns_resolver/cares/dns_impl_integration_test.cc @@ -0,0 +1,206 @@ +#include "source/common/protobuf/protobuf.h" +#include "source/extensions/network/dns_resolver/cares/dns_impl.h" + +#include "test/integration/http_integration.h" +#include "test/test_common/simulated_time_system.h" + +namespace Envoy { +namespace Network { +namespace { + +class DnsImplIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + DnsImplIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP2, GetParam()) {} +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, DnsImplIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(DnsImplIntegrationTest, LogicalDnsWithCaresResolver) { + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + RELEASE_ASSERT(bootstrap.mutable_static_resources()->clusters_size() == 1, ""); + auto& cluster = *bootstrap.mutable_static_resources()->mutable_clusters(0); + cluster.set_type(envoy::config::cluster::v3::Cluster::LOGICAL_DNS); + cluster.set_dns_lookup_family(envoy::config::cluster::v3::Cluster::ALL); + }); + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + auto* route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); + route->mutable_route()->mutable_auto_host_rewrite()->set_value(true); + }); + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0); + + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +TEST_P(DnsImplIntegrationTest, StrictDnsWithCaresResolver) { + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + RELEASE_ASSERT(bootstrap.mutable_static_resources()->clusters_size() == 1, ""); + auto& cluster = *bootstrap.mutable_static_resources()->mutable_clusters(0); + cluster.set_type(envoy::config::cluster::v3::Cluster::STRICT_DNS); + cluster.set_dns_lookup_family(envoy::config::cluster::v3::Cluster::ALL); + }); + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0); + + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +// Test UDP Channel Refresh Behavior +class DnsResolverUdpChannelRefreshIntegrationTest : public testing::Test { +public: + DnsResolverUdpChannelRefreshIntegrationTest() + : api_(Api::createApiForTest(stats_store_, simulated_time_system_)), + dispatcher_(api_->allocateDispatcher("test_thread")) {} + + void SetUp() override { + resolver_address_ = Network::Utility::parseInternetAddressAndPortNoThrow("127.0.0.1:5353"); + ASSERT_NE(nullptr, resolver_address_); + } + + std::shared_ptr + createResolver(std::chrono::milliseconds refresh_duration = std::chrono::milliseconds::zero()) { + envoy::extensions::network::dns_resolver::cares::v3::CaresDnsResolverConfig config; + config.mutable_dns_resolver_options()->set_use_tcp_for_dns_lookups(false); + + // Add resolver address. + envoy::config::core::v3::Address resolver_addr; + Network::Utility::addressToProtobufAddress(*resolver_address_, resolver_addr); + config.add_resolvers()->CopyFrom(resolver_addr); + + // Set UDP channel refresh duration if specified. + if (refresh_duration > std::chrono::milliseconds::zero()) { + config.mutable_max_udp_channel_duration()->CopyFrom( + Protobuf::util::TimeUtil::MillisecondsToDuration(refresh_duration.count())); + } + + auto csv_or_error = DnsResolverImpl::maybeBuildResolversCsv({resolver_address_}); + EXPECT_TRUE(csv_or_error.ok()); + return std::make_shared(config, *dispatcher_, csv_or_error.value(), + *stats_store_.rootScope()); + } + + Stats::TestUtil::TestStore stats_store_; + Event::SimulatedTimeSystem simulated_time_system_; + Api::ApiPtr api_; + Event::DispatcherPtr dispatcher_; + Network::Address::InstanceConstSharedPtr resolver_address_; +}; + +// Test that UDP channel refresh actually triggers periodic reinitializations. +TEST_F(DnsResolverUdpChannelRefreshIntegrationTest, PeriodicRefreshWorks) { + // Create resolver with 2-second refresh interval. + auto resolver = createResolver(std::chrono::seconds(2)); + + // Verify initial state: no reinitializations. + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); + + // Advance time but not enough to trigger refresh. + simulated_time_system_.advanceTimeAndRun(std::chrono::milliseconds(1500), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); + + // Advance time to trigger first refresh. + simulated_time_system_.advanceTimeAndRun(std::chrono::milliseconds(600), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(1, stats_store_.counter("dns.cares.reinits").value()); + + // Advance time to trigger second refresh. + simulated_time_system_.advanceTimeAndRun(std::chrono::seconds(2), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(2, stats_store_.counter("dns.cares.reinits").value()); + + // Advance time to trigger third refresh. + simulated_time_system_.advanceTimeAndRun(std::chrono::seconds(2), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(3, stats_store_.counter("dns.cares.reinits").value()); +} + +// Test that without UDP channel refresh configured, no periodic reinitialization happens. +TEST_F(DnsResolverUdpChannelRefreshIntegrationTest, NoPeriodicRefreshWhenDisabled) { + // Create resolver without refresh configuration. This is the default behavior. + auto resolver = createResolver(); + + // Verify initial state i.e., no reinitializations. + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); + + // Advance time significantly. + simulated_time_system_.advanceTimeAndRun(std::chrono::seconds(10), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Should still be zero since periodic refresh is disabled. + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); + + // Advance more time to be sure. + simulated_time_system_.advanceTimeAndRun(std::chrono::seconds(30), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); +} + +// Test that different refresh durations work correctly. +TEST_F(DnsResolverUdpChannelRefreshIntegrationTest, DifferentRefreshDurationsWork) { + // Test with a very short refresh interval (500ms). + auto resolver = createResolver(std::chrono::milliseconds(500)); + + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); + + // Should trigger refresh after 500ms. + simulated_time_system_.advanceTimeAndRun(std::chrono::milliseconds(550), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(1, stats_store_.counter("dns.cares.reinits").value()); + + // Should trigger again after another 500ms. + simulated_time_system_.advanceTimeAndRun(std::chrono::milliseconds(500), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_EQ(2, stats_store_.counter("dns.cares.reinits").value()); +} + +// Test that refresh works alongside actual DNS queries. +TEST_F(DnsResolverUdpChannelRefreshIntegrationTest, RefreshWorksWithDnsQueries) { + // Create resolver with 1-second refresh interval. + auto resolver = createResolver(std::chrono::seconds(1)); + // Verify initial state i.e., no reinitializations yet. + EXPECT_EQ(0, stats_store_.counter("dns.cares.reinits").value()); + + // Perform a DNS query. This will likely fail due to no real DNS server, but that's OK. + bool callback_called = false; + resolver->resolve("example.com", DnsLookupFamily::V4Only, + [&](DnsResolver::ResolutionStatus, absl::string_view, + std::list&&) { callback_called = true; }); + + // Advance time to trigger refresh. + simulated_time_system_.advanceTimeAndRun(std::chrono::milliseconds(1100), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Should see reinitialization even with active DNS queries. + EXPECT_GE(stats_store_.counter("dns.cares.reinits").value(), 1); + + // Advance time again. + simulated_time_system_.advanceTimeAndRun(std::chrono::seconds(1), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_GE(stats_store_.counter("dns.cares.reinits").value(), 2); +} + +} // namespace +} // namespace Network +} // namespace Envoy diff --git a/test/extensions/network/dns_resolver/cares/dns_impl_test.cc b/test/extensions/network/dns_resolver/cares/dns_impl_test.cc index 1e61dd5752dfd..b59e32722b8a5 100644 --- a/test/extensions/network/dns_resolver/cares/dns_impl_test.cc +++ b/test/extensions/network/dns_resolver/cares/dns_impl_test.cc @@ -21,6 +21,7 @@ #include "source/common/network/listen_socket_impl.h" #include "source/common/network/tcp_listener_impl.h" #include "source/common/network/utility.h" +#include "source/common/protobuf/protobuf.h" #include "source/common/stream_info/stream_info_impl.h" #include "source/extensions/network/dns_resolver/cares/dns_impl.h" @@ -727,6 +728,11 @@ class DnsImplTest : public testing::TestWithParam { cares.set_allocated_udp_max_queries(udpMaxQueries()); cares.set_rotate_nameservers(setRotateNameservers()); + // Set EDNS0 configuration if specified + if (getEdns0MaxPayloadSize() > 0) { + cares.mutable_edns0_max_payload_size()->set_value(getEdns0MaxPayloadSize()); + } + // Copy over the dns_resolver_options_. cares.mutable_dns_resolver_options()->MergeFrom(dns_resolver_options); // setup the typed config @@ -975,7 +981,8 @@ class DnsImplTest : public testing::TestWithParam { virtual bool setResolverInConstructor() const { return false; } virtual bool filterUnroutableFamilies() const { return false; } virtual bool setRotateNameservers() const { return false; } - virtual ProtobufWkt::UInt32Value* udpMaxQueries() const { return nullptr; } + virtual Protobuf::UInt32Value* udpMaxQueries() const { return nullptr; } + virtual uint32_t getEdns0MaxPayloadSize() const { return 0; } Stats::TestUtil::TestStore stats_store_; NiceMock runtime_; std::unique_ptr server_; @@ -2198,10 +2205,10 @@ TEST_F(DnsImplConstructor, VerifyCustomTimeoutAndTries) { dns_resolvers); envoy::extensions::network::dns_resolver::cares::v3::CaresDnsResolverConfig cares; cares.add_resolvers()->MergeFrom(dns_resolvers); - auto query_timeout_seconds = std::make_unique(); + auto query_timeout_seconds = std::make_unique(); query_timeout_seconds->set_value(9); cares.set_allocated_query_timeout_seconds(query_timeout_seconds.release()); - auto query_tries = std::make_unique(); + auto query_tries = std::make_unique(); query_tries->set_value(7); cares.set_allocated_query_tries(query_tries.release()); Network::Utility::addressToProtobufAddress( @@ -2243,10 +2250,10 @@ TEST_F(DnsImplConstructor, VerifyCustomTimeoutAndTries) { class DnsImplAresFlagsForMaxUdpQueriesinTest : public DnsImplTest { protected: bool tcpOnly() const override { return false; } - ProtobufWkt::UInt32Value* udpMaxQueries() const override { - auto udp_max_queries = std::make_unique(); + Protobuf::UInt32Value* udpMaxQueries() const override { + auto udp_max_queries = std::make_unique(); udp_max_queries->set_value(100); - return dynamic_cast(udp_max_queries.release()); + return dynamic_cast(udp_max_queries.release()); } }; @@ -2314,5 +2321,34 @@ TEST_P(DnsImplAresFlagsForNoNameserverRotationTest, NameserverRotationDisabled) ares_destroy_options(&opts); } +// EDNS0 configuration test + +class DnsImplEdns0Test : public DnsImplTest { +protected: + bool tcpOnly() const override { return false; } + uint32_t getEdns0MaxPayloadSize() const override { return 4096; } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, DnsImplEdns0Test, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Test: Verify EDNS0 configuration is applied to c-ares options +// Note: EDNS0 is only relevant for UDP DNS queries. +// The DNS tests in this file use TCP-only mode to avoid instability and flakiness from UDP. +// Therefore, this test only verifies that the EDNS0 configuration flag is set in c-ares, +// not its functional behavior. +TEST_P(DnsImplEdns0Test, Edns0ConfigurationApplied) { + ares_options opts{}; + int optmask = 0; + EXPECT_EQ(ARES_SUCCESS, ares_save_options(peer_->channel(), &opts, &optmask)); + + // Verify EDNS0 payload size flag is set and value is correct + EXPECT_TRUE((optmask & ARES_OPT_EDNSPSZ) == ARES_OPT_EDNSPSZ); + EXPECT_EQ(opts.ednspsz, 4096); + + ares_destroy_options(&opts); +} + } // namespace Network } // namespace Envoy diff --git a/test/extensions/quic/connection_id_generator/quic_lb/quic_lb_test.cc b/test/extensions/quic/connection_id_generator/quic_lb/quic_lb_test.cc index c5a980f0e6f20..106a49743442a 100644 --- a/test/extensions/quic/connection_id_generator/quic_lb/quic_lb_test.cc +++ b/test/extensions/quic/connection_id_generator/quic_lb/quic_lb_test.cc @@ -189,6 +189,38 @@ TEST(QuicLbTest, Unencrypted) { absl::Span(expected, sizeof(expected))); } +TEST(QuicLbTest, Base64ServerId) { + constexpr absl::string_view id_data_base64 = "dGVzdHRlc3Q="; + constexpr absl::string_view id_data = "testtest"; + + envoy::extensions::quic::connection_id_generator::quic_lb::v3::Config cfg; + cfg.set_unsafe_unencrypted_testing_mode(true); + cfg.mutable_server_id()->set_inline_string(id_data_base64); + cfg.set_server_id_base64_encoded(true); + cfg.set_expected_server_id_length(id_data.length()); + cfg.set_nonce_length_bytes(8); + cfg.mutable_encryption_parameters()->set_name(kSecretName); + + testing::NiceMock factory_context; + + auto status = factory_context.server_factory_context_.secretManager().addStaticSecret( + encryptionParamaters(0)); + absl::StatusOr> factory_or_status = + Factory::create(cfg, factory_context); + auto generator = createTypedIdGenerator(*factory_or_status.value()); + auto new_cid = generator->GenerateNextConnectionId(quic::QuicConnectionId{}); + EXPECT_TRUE(new_cid.has_value()); + uint8_t expected[1 + id_data.size()]; + expected[0] = 16; // Configured length of encoded portion of CID. Zero version means the high bits + // are all unset. + memcpy(expected + 1, id_data.data(), id_data.size()); + ASSERT_GT(new_cid->length(), sizeof(expected)); + + // First bytes should be the version followed by unencrypted server ID. + EXPECT_EQ(absl::Span(reinterpret_cast(new_cid->data()), sizeof(expected)), + absl::Span(expected, sizeof(expected))); +} + TEST(QuicLbTest, TooLong) { uint8_t id_data[] = {0xab, 0xcd, 0xef, 0x12, 0x34, 0x56}; envoy::extensions::quic::connection_id_generator::quic_lb::v3::Config cfg; diff --git a/test/extensions/quic/proof_source/pending_proof_source_factory_impl.h b/test/extensions/quic/proof_source/pending_proof_source_factory_impl.h index 35b77043d3859..0643b371c51fe 100644 --- a/test/extensions/quic/proof_source/pending_proof_source_factory_impl.h +++ b/test/extensions/quic/proof_source/pending_proof_source_factory_impl.h @@ -13,7 +13,7 @@ class PendingProofSourceFactoryImpl : public EnvoyQuicProofSourceFactoryInterfac public: ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "envoy.quic.proof_source.pending_signing"; } diff --git a/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc b/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc index 9c86871ff79a9..ba870b0b65463 100644 --- a/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc +++ b/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc @@ -25,7 +25,7 @@ TEST_F(FixedServerPreferredAddressConfigTest, Validation) { cfg.mutable_ipv4_config()->mutable_address()->set_address("not an address"); cfg.mutable_ipv4_config()->mutable_address()->set_port_value(1); EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, context_), - EnvoyException, ".*Invalid address socket_address.*"); + EnvoyException, "(?s).*Invalid address.*socket_address.*"); } { // Bad address. diff --git a/test/extensions/rate_limit_descriptors/expr/config_test.cc b/test/extensions/rate_limit_descriptors/expr/config_test.cc index b455fc29a9c97..49810f6e6c490 100644 --- a/test/extensions/rate_limit_descriptors/expr/config_test.cc +++ b/test/extensions/rate_limit_descriptors/expr/config_test.cc @@ -88,7 +88,7 @@ TEST_F(RateLimitPolicyEntryTest, ExpressionText) { testing::ContainerEq(descriptors_)); } -TEST_F(RateLimitPolicyEntryTest, ExpressionTextMalformed) { +TEST_F(RateLimitPolicyEntryTest, FormatConversionV1AlphaToDevCel) { const std::string yaml = R"EOF( actions: - extension: @@ -96,58 +96,83 @@ TEST_F(RateLimitPolicyEntryTest, ExpressionTextMalformed) { typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor descriptor_key: my_descriptor_name - text: undefined_ext(false) + text: request.headers[":method"] == "GET" )EOF"; - EXPECT_THROW_WITH_REGEX(setupTest(yaml), EnvoyException, "failed to create an expression: .*"); + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":method", "GET"}}; + + rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + EXPECT_THAT(std::vector({{{{"my_descriptor_name", "true"}}}}), + testing::ContainerEq(descriptors_)); } -TEST_F(RateLimitPolicyEntryTest, ExpressionUnparsable) { +TEST_F(RateLimitPolicyEntryTest, ExpressionWithDifferentDataTypes) { const std::string yaml = R"EOF( actions: - extension: - name: custom_descriptor + name: string_descriptor typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor - descriptor_key: my_descriptor_name - text: ++ + descriptor_key: string_value + text: request.headers[":method"] )EOF"; - EXPECT_THROW_WITH_REGEX(setupTest(yaml), EnvoyException, - "Unable to parse descriptor expression: .*"); + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":method", "GET"}}; + + rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + + EXPECT_EQ(1, descriptors_.size()); + // Check the descriptor has the correct value + EXPECT_EQ("GET", descriptors_[0].entries_[0].value_); } -#endif -TEST_F(RateLimitPolicyEntryTest, ExpressionParsed) { +// Test boolean expression evaluation +TEST_F(RateLimitPolicyEntryTest, BooleanExpressionEvaluation) { const std::string yaml = R"EOF( actions: - extension: - name: custom_descriptor + name: boolean_descriptor typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor - descriptor_key: my_descriptor_name - parsed: - call_expr: - function: _==_ - args: - - select_expr: - operand: - ident_expr: - name: request - field: method - - const_expr: - string_value: GET + descriptor_key: boolean_value + text: request.headers[":method"] == "GET" )EOF"; setupTest(yaml); Http::TestRequestHeaderMapImpl header{{":method", "GET"}}; rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); - EXPECT_THAT(std::vector({{{{"my_descriptor_name", "true"}}}}), - testing::ContainerEq(descriptors_)); + + EXPECT_EQ(1, descriptors_.size()); + // Check the descriptor has the correct value - boolean results are converted to strings + EXPECT_EQ("true", descriptors_[0].entries_[0].value_); } -TEST_F(RateLimitPolicyEntryTest, ExpressionParsedMalformed) { +// Test numeric expression evaluation +TEST_F(RateLimitPolicyEntryTest, NumericExpressionEvaluation) { + const std::string yaml = R"EOF( +actions: +- extension: + name: number_descriptor + typed_config: + "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor + descriptor_key: number_value + text: size(request.headers[":method"]) + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":method", "GET"}}; + + rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + + EXPECT_EQ(1, descriptors_.size()); + // The numeric result is converted to a string + EXPECT_EQ("3", descriptors_[0].entries_[0].value_); +} + +TEST_F(RateLimitPolicyEntryTest, ExpressionTextMalformed) { const std::string yaml = R"EOF( actions: - extension: @@ -155,18 +180,52 @@ TEST_F(RateLimitPolicyEntryTest, ExpressionParsedMalformed) { typed_config: "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor descriptor_key: my_descriptor_name - parsed: - call_expr: - function: undefined_extent - args: - - const_expr: - bool_value: false + text: undefined_ext(false) )EOF"; EXPECT_THROW_WITH_REGEX(setupTest(yaml), EnvoyException, "failed to create an expression: .*"); } -#if defined(USE_CEL_PARSER) +TEST_F(RateLimitPolicyEntryTest, ExpressionUnparsable) { + const std::string yaml = R"EOF( +actions: +- extension: + name: custom_descriptor + typed_config: + "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor + descriptor_key: my_descriptor_name + text: ++ + )EOF"; + + EXPECT_THROW_WITH_REGEX(setupTest(yaml), EnvoyException, + "Unable to parse descriptor expression: .*"); +} + +TEST_F(RateLimitPolicyEntryTest, ComplexExpressionWithConditionals) { + const std::string yaml = R"EOF( +actions: +- extension: + name: complex_descriptor + typed_config: + "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor + descriptor_key: processed_path + text: "request.headers[\":path\"] == \"/api/users\" ? \"api_path\" : \"no_path\"" + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":path", "/api/users"}}; + + rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + EXPECT_EQ(1, descriptors_.size()); + + // Test with a different path + descriptors_.clear(); + Http::TestRequestHeaderMapImpl header2{{":path", "/other/path"}}; + + rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header2, stream_info_); + EXPECT_EQ(1, descriptors_.size()); +} + TEST_F(RateLimitPolicyEntryTest, ExpressionTextError) { const std::string yaml = R"EOF( actions: @@ -216,6 +275,69 @@ TEST_F(RateLimitPolicyEntryTest, ExpressionTextErrorSkip) { } #endif +TEST_F(RateLimitPolicyEntryTest, ExpressionParsed) { + const std::string yaml = R"EOF( +actions: +- extension: + name: custom_descriptor + typed_config: + "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor + descriptor_key: my_descriptor_name + parsed: + call_expr: + function: _==_ + args: + - select_expr: + operand: + ident_expr: + name: request + field: method + - const_expr: + string_value: GET + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":method", "GET"}}; + + rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + EXPECT_THAT(std::vector({{{{"my_descriptor_name", "true"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyEntryTest, ExpressionParsedMalformed) { + const std::string yaml = R"EOF( +actions: +- extension: + name: custom_descriptor + typed_config: + "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor + descriptor_key: my_descriptor_name + parsed: + call_expr: + function: undefined_extent + args: + - const_expr: + bool_value: false + )EOF"; + + EXPECT_THROW_WITH_REGEX(setupTest(yaml), EnvoyException, "failed to create an expression: .*"); +} + +TEST_F(RateLimitPolicyEntryTest, ExprSpecifierNotSet) { + const std::string yaml = R"EOF( +actions: +- extension: + name: custom_descriptor + typed_config: + "@type": type.googleapis.com/envoy.extensions.rate_limit_descriptors.expr.v3.Descriptor + descriptor_key: test_key + )EOF"; + + EXPECT_THROW_WITH_REGEX( + setupTest(yaml), EnvoyException, + "Rate limit descriptor extension failed: expression specifier is not set"); +} + } // namespace } // namespace Expr } // namespace RateLimitDescriptors diff --git a/test/extensions/retry/host/omit_canary_hosts/config_test.cc b/test/extensions/retry/host/omit_canary_hosts/config_test.cc index aae083d6ac8f7..2927aa70a0db8 100644 --- a/test/extensions/retry/host/omit_canary_hosts/config_test.cc +++ b/test/extensions/retry/host/omit_canary_hosts/config_test.cc @@ -22,7 +22,7 @@ TEST(OmitCanaryHostsRetryPredicateTest, PredicateTest) { ASSERT_NE(nullptr, factory); - ProtobufWkt::Struct config; + Protobuf::Struct config; auto predicate = factory->createHostPredicate(config, 3); auto host1 = std::make_shared>(); diff --git a/test/extensions/retry/host/previous_hosts/config_test.cc b/test/extensions/retry/host/previous_hosts/config_test.cc index 5e24b59fc2895..281e3af00a047 100644 --- a/test/extensions/retry/host/previous_hosts/config_test.cc +++ b/test/extensions/retry/host/previous_hosts/config_test.cc @@ -23,7 +23,7 @@ TEST(PreviousHostsRetryPredicateConfigTest, PredicateTest) { ASSERT_NE(nullptr, factory); - ProtobufWkt::Struct config; + Protobuf::Struct config; auto predicate = factory->createHostPredicate(config, 3); auto host1 = std::make_shared>(); diff --git a/test/extensions/stats_sinks/open_telemetry/config_test.cc b/test/extensions/stats_sinks/open_telemetry/config_test.cc index 8f33a43f7ff89..109e9251a8fd7 100644 --- a/test/extensions/stats_sinks/open_telemetry/config_test.cc +++ b/test/extensions/stats_sinks/open_telemetry/config_test.cc @@ -47,7 +47,7 @@ TEST(OpenTelemetryConfigTest, OpenTelemetrySinkType) { TEST(OpenTelemetryConfigTest, OtlpOptionsTest) { { envoy::extensions::stat_sinks::open_telemetry::v3::SinkConfig sink_config; - OtlpOptions options(sink_config); + OtlpOptions options(sink_config, Tracers::OpenTelemetry::Resource()); // Default options EXPECT_FALSE(options.reportCountersAsDeltas()); @@ -55,6 +55,7 @@ TEST(OpenTelemetryConfigTest, OtlpOptionsTest) { EXPECT_TRUE(options.emitTagsAsAttributes()); EXPECT_TRUE(options.useTagExtractedName()); EXPECT_EQ("", options.statPrefix()); + EXPECT_TRUE(options.resource_attributes().empty()); } { @@ -63,12 +64,17 @@ TEST(OpenTelemetryConfigTest, OtlpOptionsTest) { sink_config.mutable_use_tag_extracted_name()->set_value(false); sink_config.set_prefix("prefix"); - OtlpOptions options(sink_config); + Tracers::OpenTelemetry::Resource resource; + resource.attributes_["key"] = "value"; + OtlpOptions options(sink_config, resource); EXPECT_FALSE(options.reportCountersAsDeltas()); EXPECT_FALSE(options.reportHistogramsAsDeltas()); EXPECT_FALSE(options.emitTagsAsAttributes()); EXPECT_FALSE(options.useTagExtractedName()); EXPECT_EQ("prefix.", options.statPrefix()); + ASSERT_EQ(1, options.resource_attributes().size()); + EXPECT_EQ("key", options.resource_attributes()[0].key()); + EXPECT_EQ("value", options.resource_attributes()[0].value().string_value()); } } diff --git a/test/extensions/stats_sinks/open_telemetry/open_telemetry_impl_test.cc b/test/extensions/stats_sinks/open_telemetry/open_telemetry_impl_test.cc index 32e13c57188ee..e745afc0d23c8 100644 --- a/test/extensions/stats_sinks/open_telemetry/open_telemetry_impl_test.cc +++ b/test/extensions/stats_sinks/open_telemetry/open_telemetry_impl_test.cc @@ -35,19 +35,22 @@ class OpenTelemetryStatsSinkTests : public testing::Test { } } - const OtlpOptionsSharedPtr otlpOptions(bool report_counters_as_deltas = false, - bool report_histograms_as_deltas = false, - bool emit_tags_as_attributes = true, - bool use_tag_extracted_name = true, - const std::string& stat_prefix = "") { + const OtlpOptionsSharedPtr + otlpOptions(bool report_counters_as_deltas = false, bool report_histograms_as_deltas = false, + bool emit_tags_as_attributes = true, bool use_tag_extracted_name = true, + const std::string& stat_prefix = "", + absl::flat_hash_map resource_attributes = {}) { envoy::extensions::stat_sinks::open_telemetry::v3::SinkConfig sink_config; sink_config.set_report_counters_as_deltas(report_counters_as_deltas); sink_config.set_report_histograms_as_deltas(report_histograms_as_deltas); sink_config.mutable_emit_tags_as_attributes()->set_value(emit_tags_as_attributes); sink_config.mutable_use_tag_extracted_name()->set_value(use_tag_extracted_name); sink_config.set_prefix(stat_prefix); - - return std::make_shared(sink_config); + Tracers::OpenTelemetry::Resource resource; + for (const auto& [key, value] : resource_attributes) { + resource.attributes_[key] = value; + } + return std::make_shared(sink_config, resource); } std::string getTagExtractedName(const std::string name) { return name + "-tagged"; } @@ -433,6 +436,20 @@ TEST_F(OtlpMetricsFlusherTests, DeltaHistogramMetric) { expectHistogram(metricAt(1, metrics), getTagExtractedName("test_histogram2"), true); } +TEST_F(OtlpMetricsFlusherTests, SetResourceAttributes) { + OtlpMetricsFlusherImpl flusher( + otlpOptions(true, false, true, true, "", {{"key_foo", "val_foo"}})); + addCounterToSnapshot("test_counter1", 1, 1); + MetricsExportRequestSharedPtr metrics = flusher.flush(snapshot_); + expectMetricsCount(metrics, 1); + expectSum(metricAt(0, metrics), getTagExtractedName("test_counter1"), 1, true); + EXPECT_EQ(1, metrics->resource_metrics().size()); + EXPECT_EQ(1, metrics->resource_metrics()[0].resource().attributes().size()); + EXPECT_EQ("key_foo", metrics->resource_metrics()[0].resource().attributes()[0].key()); + EXPECT_EQ("val_foo", + metrics->resource_metrics()[0].resource().attributes()[0].value().string_value()); +} + class MockOpenTelemetryGrpcMetricsExporter : public OpenTelemetryGrpcMetricsExporter { public: MOCK_METHOD(void, send, (MetricsExportRequestPtr&&)); diff --git a/test/extensions/tracers/datadog/span_test.cc b/test/extensions/tracers/datadog/span_test.cc index 6104989bd7c90..764df76f83ad0 100644 --- a/test/extensions/tracers/datadog/span_test.cc +++ b/test/extensions/tracers/datadog/span_test.cc @@ -377,6 +377,21 @@ TEST_F(DatadogTracerSpanTest, SetSampledFalse) { EXPECT_EQ(-1, found->second); } +TEST_F(DatadogTracerSpanTest, UseLocalDecisionDefault) { + Span span{std::move(span_)}; + EXPECT_EQ(false, span.useLocalDecision()); +} + +TEST_F(DatadogTracerSpanTest, UseLocalDecisionTrue) { + Span span{std::move(span_), true}; + EXPECT_EQ(true, span.useLocalDecision()); +} + +TEST_F(DatadogTracerSpanTest, UseLocalDecisionFalse) { + Span span{std::move(span_), false}; + EXPECT_EQ(false, span.useLocalDecision()); +} + TEST_F(DatadogTracerSpanTest, Baggage) { // Baggage is not supported by dd-trace-cpp, so `Span::getBaggage` and // `Span::setBaggage` do nothing. diff --git a/test/extensions/tracers/datadog/tracer_test.cc b/test/extensions/tracers/datadog/tracer_test.cc index 8f2fa2ac04189..7b56589732485 100644 --- a/test/extensions/tracers/datadog/tracer_test.cc +++ b/test/extensions/tracers/datadog/tracer_test.cc @@ -18,6 +18,7 @@ #include "datadog/optional.h" #include "datadog/propagation_style.h" #include "datadog/sampling_priority.h" +#include "datadog/tags.h" #include "datadog/trace_segment.h" #include "datadog/tracer_config.h" #include "gtest/gtest.h" @@ -206,6 +207,55 @@ TEST_F(DatadogTracerTest, ExtractionSuccess) { EXPECT_EQ(5678, *dd_span.parent_id()); } +TEST_F(DatadogTracerTest, UseLocalDecisionTrue) { + datadog::tracing::TracerConfig config; + config.service = "envoy"; + + Tracer tracer("fake_cluster", "test_host", config, cluster_manager_, *store_.rootScope(), + thread_local_slot_allocator_, time_); + + const std::string operation_name = "do.thing"; + const SystemTime start = time_.timeSystem().systemTime(); + ON_CALL(stream_info_, startTime()).WillByDefault(testing::Return(start)); + + // trace context in the Datadog style + Tracing::TestTraceContextImpl context{}; + + const Tracing::SpanPtr span = + tracer.startSpan(Tracing::MockConfig{}, context, stream_info_, operation_name, + {Tracing::Reason::NotTraceable, false}); + + // The `useLocalDecision` method is true because the span has no external trace sampling + // decision. + EXPECT_EQ(true, span->useLocalDecision()); +} + +TEST_F(DatadogTracerTest, UseLocalDecisionFalse) { + datadog::tracing::TracerConfig config; + config.service = "envoy"; + + Tracer tracer("fake_cluster", "test_host", config, cluster_manager_, *store_.rootScope(), + thread_local_slot_allocator_, time_); + + const std::string operation_name = "do.thing"; + const SystemTime start = time_.timeSystem().systemTime(); + ON_CALL(stream_info_, startTime()).WillByDefault(testing::Return(start)); + + // trace context in the Datadog style + Tracing::TestTraceContextImpl context{ + {"x-datadog-trace-id", "1234"}, + {"x-datadog-parent-id", "5678"}, + {"x-datadog-sampling-priority", "0"}, + }; + + const Tracing::SpanPtr span = + tracer.startSpan(Tracing::MockConfig{}, context, stream_info_, operation_name, + {Tracing::Reason::NotTraceable, false}); + // The `useLocalDecision` method is false because the span has an external trace sampling + // decision. + EXPECT_EQ(false, span->useLocalDecision()); +} + TEST_F(DatadogTracerTest, ExtractionFailure) { // Verify that if there is invalid trace information in the `TraceContext` // supplied to `startSpan`, that the resulting span is nonetheless valid (it diff --git a/test/extensions/tracers/fluentd/tracer_impl_test.cc b/test/extensions/tracers/fluentd/tracer_impl_test.cc index 980dfad50f7bc..aef674bdac222 100644 --- a/test/extensions/tracers/fluentd/tracer_impl_test.cc +++ b/test/extensions/tracers/fluentd/tracer_impl_test.cc @@ -17,7 +17,6 @@ #include "msgpack.hpp" using testing::Return; -using testing::ReturnRef; namespace Envoy { namespace Extensions { @@ -70,7 +69,7 @@ class FluentdTracerImplTest : public testing::Test { std::map option_ = {{"fluent_signal", "2"}, {"TimeFormat", "DateTime"}}; packer.pack(option_); - return std::string(buffer.data(), buffer.size()); + return {buffer.data(), buffer.size()}; } std::string tag_ = "test.tag"; @@ -435,19 +434,19 @@ using StatusHelpers::HasStatusMessage; constexpr absl::string_view version = "00"; constexpr absl::string_view trace_id = "00000000000000000000000000000001"; -constexpr absl::string_view parent_id = "0000000000000003"; +constexpr absl::string_view span_id = "0000000000000003"; constexpr absl::string_view trace_flags = "01"; TEST(SpanContextExtractorTest, ExtractSpanContext) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); EXPECT_OK(span_context); EXPECT_EQ(span_context->traceId(), trace_id); - EXPECT_EQ(span_context->parentId(), parent_id); + EXPECT_EQ(span_context->spanId(), span_id); EXPECT_EQ(span_context->version(), version); EXPECT_TRUE(span_context->sampled()); } @@ -456,13 +455,13 @@ TEST(SpanContextExtractorTest, ExtractSpanContextNotSampled) { const std::string trace_flags_unsampled{"00"}; Tracing::TestTraceContextImpl request_headers{ {"traceparent", - fmt::format("{}-{}-{}-{}", version, trace_id, parent_id, trace_flags_unsampled)}}; + fmt::format("{}-{}-{}-{}", version, trace_id, span_id, trace_flags_unsampled)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); EXPECT_OK(span_context); EXPECT_EQ(span_context->traceId(), trace_id); - EXPECT_EQ(span_context->parentId(), parent_id); + EXPECT_EQ(span_context->spanId(), span_id); EXPECT_EQ(span_context->version(), version); EXPECT_FALSE(span_context->sampled()); } @@ -479,7 +478,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithoutHeader) { TEST(SpanContextExtractorTest, ThrowsExceptionWithTooLongHeader) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("000{}-{}-{}-{}", version, trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("000{}-{}-{}-{}", version, trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -490,7 +489,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithTooLongHeader) { TEST(SpanContextExtractorTest, ThrowsExceptionWithTooShortHeader) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("{}-{}-{}", trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}-{}-{}", trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -501,7 +500,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithTooShortHeader) { TEST(SpanContextExtractorTest, ThrowsExceptionWithInvalidHyphenation) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("{}{}-{}-{}", version, trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}{}-{}-{}", version, trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -512,7 +511,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithInvalidHyphenation) { TEST(SpanContextExtractorTest, ThrowExceptionWithInvalidHyphenation) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("{}-{}-{}---", version, trace_id, parent_id)}}; + {"traceparent", fmt::format("{}-{}-{}---", version, trace_id, span_id)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -526,7 +525,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithInvalidSizes) { const std::string invalid_trace_flags{"001"}; Tracing::TestTraceContextImpl request_headers{ {"traceparent", - fmt::format("{}-{}-{}-{}", invalid_version, trace_id, parent_id, invalid_trace_flags)}}; + fmt::format("{}-{}-{}-{}", invalid_version, trace_id, span_id, invalid_trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -538,8 +537,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithInvalidSizes) { TEST(SpanContextExtractorTest, ThrowsExceptionWithInvalidHex) { const std::string invalid_version{"ZZ"}; Tracing::TestTraceContextImpl request_headers{ - {"traceparent", - fmt::format("{}-{}-{}-{}", invalid_version, trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}-{}-{}-{}", invalid_version, trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -551,8 +549,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithInvalidHex) { TEST(SpanContextExtractorTest, ThrowsExceptionWithAllZeroTraceId) { const std::string invalid_trace_id{"00000000000000000000000000000000"}; Tracing::TestTraceContextImpl request_headers{ - {"traceparent", - fmt::format("{}-{}-{}-{}", version, invalid_trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}-{}-{}-{}", version, invalid_trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -562,10 +559,9 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithAllZeroTraceId) { } TEST(SpanContextExtractorTest, ThrowsExceptionWithAllZeroParentId) { - const std::string invalid_parent_id{"0000000000000000"}; + const std::string invalid_span_id{"0000000000000000"}; Tracing::TestTraceContextImpl request_headers{ - {"traceparent", - fmt::format("{}-{}-{}-{}", version, trace_id, invalid_parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, invalid_span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -576,7 +572,7 @@ TEST(SpanContextExtractorTest, ThrowsExceptionWithAllZeroParentId) { TEST(SpanContextExtractorTest, ExtractSpanContextWithEmptyTracestate) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, parent_id, trace_flags)}}; + {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, span_id, trace_flags)}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -586,7 +582,7 @@ TEST(SpanContextExtractorTest, ExtractSpanContextWithEmptyTracestate) { TEST(SpanContextExtractorTest, ExtractSpanContextWithTracestate) { Tracing::TestTraceContextImpl request_headers{ - {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, parent_id, trace_flags)}, + {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, span_id, trace_flags)}, {"tracestate", "sample-tracestate"}}; SpanContextExtractor span_context_extractor(request_headers); absl::StatusOr span_context = span_context_extractor.extractSpanContext(); @@ -606,7 +602,7 @@ TEST(SpanContextExtractorTest, IgnoreTracestateWithoutTraceparent) { TEST(SpanContextExtractorTest, ExtractSpanContextWithMultipleTracestateEntries) { Http::TestRequestHeaderMapImpl request_headers{ - {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, parent_id, trace_flags)}, + {"traceparent", fmt::format("{}-{}-{}-{}", version, trace_id, span_id, trace_flags)}, {"tracestate", "sample-tracestate"}, {"tracestate", "sample-tracestate-2"}}; Tracing::HttpTraceContext trace_context(request_headers); diff --git a/test/extensions/tracers/fluentd/tracer_integration_test.cc b/test/extensions/tracers/fluentd/tracer_integration_test.cc index b6c1e8662c96e..b6425949889eb 100644 --- a/test/extensions/tracers/fluentd/tracer_integration_test.cc +++ b/test/extensions/tracers/fluentd/tracer_integration_test.cc @@ -13,8 +13,6 @@ #include "gtest/gtest.h" #include "msgpack.hpp" -using testing::AssertionResult; - namespace Envoy { namespace Extensions { namespace Tracers { @@ -98,6 +96,10 @@ TEST_F(FluentdTracerIntegrationTest, Span) { EXPECT_EQ(span->getTraceId(), trace_id_hex); + // The `useLocalDecision` method is false because the span has an external trace sampling + // decision. + EXPECT_EQ(false, span->useLocalDecision()); + // Test Span functions span->setOperation("test_new"); span->setTag("test_tag", "test_value"); @@ -141,6 +143,10 @@ TEST_F(FluentdTracerIntegrationTest, ParseSpanContextFromHeadersTest) { EXPECT_EQ(span->getTraceId(), trace_id_hex); + // The `useLocalDecision` method is false because the span has an external trace sampling + // decision. + EXPECT_EQ(false, span->useLocalDecision()); + // Remove headers, then inject context into header from the span. trace_context.remove(FluentdConstants::get().TRACE_PARENT.key()); trace_context.remove(FluentdConstants::get().TRACE_STATE.key()); @@ -183,6 +189,10 @@ TEST_F(FluentdTracerIntegrationTest, GenerateSpanContextWithoutHeadersTest) { Tracing::SpanPtr span = driver_->startSpan(mock_tracing_config_, trace_context, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); + // The `useLocalDecision` method is true because the span has no external trace sampling + // decision. + EXPECT_EQ(true, span->useLocalDecision()); + // Remove headers, then inject context into header from the span. trace_context.remove(FluentdConstants::get().TRACE_PARENT.key()); span->injectContext(trace_context, Tracing::UpstreamContext()); diff --git a/test/extensions/tracers/opentelemetry/grpc_trace_exporter_integration_test.cc b/test/extensions/tracers/opentelemetry/grpc_trace_exporter_integration_test.cc index 6f510c142e89d..cb354785b20c0 100644 --- a/test/extensions/tracers/opentelemetry/grpc_trace_exporter_integration_test.cc +++ b/test/extensions/tracers/opentelemetry/grpc_trace_exporter_integration_test.cc @@ -97,7 +97,7 @@ class OpenTelemetryTraceExporterIntegrationTest } FakeUpstream* grpc_receiver_upstream_{}; - ProtobufWkt::Struct otel_runtime_config_; + Protobuf::Struct otel_runtime_config_; FakeHttpConnectionPtr connection_; std::vector streams_; diff --git a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc index f91cfd4f96932..628a73283724c 100644 --- a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc +++ b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc @@ -32,8 +32,10 @@ using testing::ReturnRef; class MockResourceProvider : public ResourceProvider { public: MOCK_METHOD(Resource, getResource, - (const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, - Server::Configuration::TracerFactoryContext& context), + (const Protobuf::RepeatedPtrField& + resource_detectors, + Server::Configuration::ServerFactoryContext& context, + absl::string_view service_name), (const)); }; @@ -57,7 +59,7 @@ class OpenTelemetryDriverTest : public testing::Test { resource.attributes_.insert(std::pair("key1", "val1")); auto mock_resource_provider = NiceMock(); - EXPECT_CALL(mock_resource_provider, getResource(_, _)).WillRepeatedly(Return(resource)); + EXPECT_CALL(mock_resource_provider, getResource(_, _, _)).WillRepeatedly(Return(resource)); driver_ = std::make_unique(opentelemetry_config, context_, mock_resource_provider); } @@ -864,6 +866,48 @@ TEST_F(OpenTelemetryDriverTest, IgnoreNotSampledSpan) { EXPECT_EQ(0U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +TEST_F(OpenTelemetryDriverTest, UseLocalDecisionTrue) { + setupValidDriver(); + Tracing::TestTraceContextImpl request_headers{ + {":authority", "test.com"}, {":path", "/"}, {":method", "GET"}}; + + Tracing::SpanPtr span = driver_->startSpan(mock_tracing_config_, request_headers, stream_info_, + operation_name_, {Tracing::Reason::Sampling, true}); + // The `useLocalDecision` should be true because there is no traceparent header in the request. + EXPECT_TRUE(span->useLocalDecision()); + + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.opentelemetry.min_flush_spans", 5U)) + .Times(1) + .WillRepeatedly(Return(1)); + EXPECT_CALL(*mock_client_, sendRaw(_, _, _, _, _, _)); + span->finishSpan(); + EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); +} + +TEST_F(OpenTelemetryDriverTest, UseLocalDecisionFalse) { + setupValidDriver(); + Tracing::TestTraceContextImpl request_headers{ + {":authority", "test.com"}, + {":path", "/"}, + {":method", "GET"}, + {"traceparent", "00-00000000000000010000000000000002-0000000000000003-01"}}; + + // The traceparent header indicates the span is sampled and the Envoy tracing decision is + // ignored. + Tracing::SpanPtr span = + driver_->startSpan(mock_tracing_config_, request_headers, stream_info_, operation_name_, + {Tracing::Reason::NotTraceable, false}); + // The `useLocalDecision` should be false because there is a traceparent header in the request. + EXPECT_FALSE(span->useLocalDecision()); + + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.opentelemetry.min_flush_spans", 5U)) + .Times(1) + .WillRepeatedly(Return(1)); + EXPECT_CALL(*mock_client_, sendRaw(_, _, _, _, _, _)); + span->finishSpan(); + EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); +} + // Verifies tracer is "disabled" when no exporter is configured TEST_F(OpenTelemetryDriverTest, NoExportWithoutGrpcService) { const std::string yaml_string = "{}"; diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config_test.cc index 9d769cc62dc62..6891bd2da4204 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/dynatrace/config_test.cc @@ -25,7 +25,7 @@ TEST(DynatraceResourceDetectorFactoryTest, Basic) { )EOF"; TestUtility::loadFromYaml(yaml, typed_config); - NiceMock context; + NiceMock context; EXPECT_NE(factory->createResourceDetector(typed_config.typed_config(), context), nullptr); EXPECT_STREQ(factory->name().c_str(), "envoy.tracers.opentelemetry.resource_detectors.dynatrace"); } diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc index 7e9ada0850eb1..1d7180d604c13 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc @@ -26,8 +26,10 @@ TEST(EnvironmentResourceDetectorFactoryTest, Basic) { )EOF"; TestUtility::loadFromYaml(yaml, typed_config); - NiceMock context; - EXPECT_NE(factory->createResourceDetector(typed_config.typed_config(), context), nullptr); + NiceMock server_factory_context; + + EXPECT_NE(factory->createResourceDetector(typed_config.typed_config(), server_factory_context), + nullptr); } } // namespace OpenTelemetry diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc index cc62387f4a983..0bd4fc7b1fc75 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc @@ -28,7 +28,8 @@ TEST(EnvironmentResourceDetectorTest, EnvVariableNotPresent) { envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: EnvironmentResourceDetectorConfig config; - auto detector = std::make_unique(config, context); + auto detector = + std::make_unique(config, context.serverFactoryContext()); Resource resource = detector->detect(); EXPECT_EQ(resource.schema_url_, ""); @@ -44,7 +45,8 @@ TEST(EnvironmentResourceDetectorTest, EnvVariablePresentButEmpty) { envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: EnvironmentResourceDetectorConfig config; - auto detector = std::make_unique(config, context); + auto detector = + std::make_unique(config, context.serverFactoryContext()); Resource resource = detector->detect(); EXPECT_EQ(resource.schema_url_, ""); @@ -64,7 +66,8 @@ TEST(EnvironmentResourceDetectorTest, EnvVariablePresentAndWithAttributes) { envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: EnvironmentResourceDetectorConfig config; - auto detector = std::make_unique(config, context); + auto detector = + std::make_unique(config, context.serverFactoryContext()); Resource resource = detector->detect(); EXPECT_EQ(resource.schema_url_, ""); @@ -91,7 +94,8 @@ TEST(EnvironmentResourceDetectorTest, EnvVariablePresentAndWithAttributesWrongFo envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: EnvironmentResourceDetectorConfig config; - auto detector = std::make_unique(config, context); + auto detector = + std::make_unique(config, context.serverFactoryContext()); Resource resource = detector->detect(); EXPECT_EQ(resource.schema_url_, ""); diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc index 9f44804643137..cd08b58e55878 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc @@ -30,10 +30,10 @@ class DetectorFactoryA : public ResourceDetectorFactory { public: MOCK_METHOD(ResourceDetectorPtr, createResourceDetector, (const Protobuf::Message& message, - Server::Configuration::TracerFactoryContext& context)); + Server::Configuration::ServerFactoryContext& context)); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.tracers.opentelemetry.resource_detectors.a"; } @@ -43,10 +43,10 @@ class DetectorFactoryB : public ResourceDetectorFactory { public: MOCK_METHOD(ResourceDetectorPtr, createResourceDetector, (const Protobuf::Message& message, - Server::Configuration::TracerFactoryContext& context)); + Server::Configuration::ServerFactoryContext& context)); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.tracers.opentelemetry.resource_detectors.b"; } @@ -60,7 +60,7 @@ class ResourceProviderTest : public testing::Test { resource_a_.attributes_.insert(std::pair("key1", "val1")); resource_b_.attributes_.insert(std::pair("key2", "val2")); } - NiceMock context_; + testing::NiceMock server_factory_context_; Resource resource_a_; Resource resource_b_; }; @@ -78,7 +78,9 @@ TEST_F(ResourceProviderTest, NoResourceDetectorsConfigured) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); + Resource resource = + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()); EXPECT_EQ(resource.schema_url_, ""); @@ -99,29 +101,6 @@ TEST_F(ResourceProviderTest, NoResourceDetectorsConfigured) { } } -// Verifies a resource with the default service name is returned when no detectors + static service -// name are configured -TEST_F(ResourceProviderTest, ServiceNameNotProvided) { - const std::string yaml_string = R"EOF( - grpc_service: - envoy_grpc: - cluster_name: fake-cluster - timeout: 0.250s - )EOF"; - envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; - TestUtility::loadFromYaml(yaml_string, opentelemetry_config); - - ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); - - EXPECT_EQ(resource.schema_url_, ""); - - // service.name receives the unknown value when not configured - EXPECT_EQ(4, resource.attributes_.size()); - auto service_name = resource.attributes_.find("service.name"); - EXPECT_EQ("unknown_service:envoy", service_name->second); -} - // Verifies it is possible to configure multiple resource detectors TEST_F(ResourceProviderTest, MultipleResourceDetectorsConfigured) { auto detector_a = std::make_unique>(); @@ -168,7 +147,9 @@ TEST_F(ResourceProviderTest, MultipleResourceDetectorsConfigured) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); + Resource resource = + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()); EXPECT_EQ(resource.schema_url_, ""); @@ -202,7 +183,9 @@ TEST_F(ResourceProviderTest, UnknownResourceDetectors) { ResourceProviderImpl resource_provider; EXPECT_THROW_WITH_MESSAGE( - resource_provider.getResource(opentelemetry_config, context_), EnvoyException, + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()), + EnvoyException, "Resource detector factory not found: " "'envoy.tracers.opentelemetry.resource_detectors.UnkownResourceDetector'"); } @@ -231,7 +214,9 @@ TEST_F(ResourceProviderTest, ProblemCreatingResourceDetector) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - EXPECT_THROW_WITH_MESSAGE(resource_provider.getResource(opentelemetry_config, context_), + EXPECT_THROW_WITH_MESSAGE(resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, + opentelemetry_config.service_name()), EnvoyException, "Resource detector could not be created: " "'envoy.tracers.opentelemetry.resource_detectors.a'"); @@ -281,7 +266,9 @@ TEST_F(ResourceProviderTest, OldSchemaEmptyUpdatingSet) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); + Resource resource = + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()); // OTel spec says the updating schema should be used EXPECT_EQ(expected_schema_url, resource.schema_url_); @@ -331,7 +318,9 @@ TEST_F(ResourceProviderTest, OldSchemaSetUpdatingEmpty) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); + Resource resource = + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()); // OTel spec says the updating schema should be used EXPECT_EQ(expected_schema_url, resource.schema_url_); @@ -381,7 +370,9 @@ TEST_F(ResourceProviderTest, OldAndUpdatingSchemaAreEqual) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); + Resource resource = + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()); EXPECT_EQ(expected_schema_url, resource.schema_url_); } @@ -430,7 +421,9 @@ TEST_F(ResourceProviderTest, OldAndUpdatingSchemaAreDifferent) { TestUtility::loadFromYaml(yaml_string, opentelemetry_config); ResourceProviderImpl resource_provider; - Resource resource = resource_provider.getResource(opentelemetry_config, context_); + Resource resource = + resource_provider.getResource(opentelemetry_config.resource_detectors(), + server_factory_context_, opentelemetry_config.service_name()); // OTel spec says Old schema should be used EXPECT_EQ(expected_schema_url, resource.schema_url_); diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/static/config_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/static/config_test.cc index 76b7c6e2b0e12..59e40fda2e30e 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/static/config_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/static/config_test.cc @@ -31,7 +31,7 @@ TEST(StaticConfigResourceDetectorFactoryTest, Basic) { )EOF"; TestUtility::loadFromYaml(yaml, typed_config); - NiceMock context; + NiceMock context; EXPECT_NE(factory->createResourceDetector(typed_config.typed_config(), context), nullptr); EXPECT_STREQ(factory->name().c_str(), "envoy.tracers.opentelemetry.resource_detectors.static_config"); diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_integration_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_integration_test.cc index 1380e1bd27187..cdbc8fea07fd5 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_integration_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_integration_test.cc @@ -114,9 +114,9 @@ TEST_P(StaticConfigResourceDetectorIntegrationTest, TestResourceAttributeSet) { ASSERT_TRUE(backend_request_->waitForEndStream(*dispatcher_)); // Sanity checking that we sent the expected data. - EXPECT_THAT(backend_request_->headers(), HeaderValueOf(Http::Headers::get().Method, "POST")); + EXPECT_THAT(backend_request_->headers(), ContainsHeader(Http::Headers::get().Method, "POST")); EXPECT_THAT(backend_request_->headers(), - HeaderValueOf(Http::Headers::get().Path, "/api/v2/traces")); + ContainsHeader(Http::Headers::get().Path, "/api/v2/traces")); backend_request_->encodeHeaders(default_response_headers_, true /*end_stream*/); diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_test.cc index ba2243d47111f..c142c335735d9 100644 --- a/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_test.cc +++ b/test/extensions/tracers/opentelemetry/resource_detectors/static/static_config_resource_detector_test.cc @@ -22,7 +22,7 @@ namespace OpenTelemetry { // Test detector when when attributes is empty TEST(StaticConfigResourceDetectorTest, EmptyAttributesMap) { - NiceMock context; + NiceMock context; envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: StaticConfigResourceDetectorConfig config; @@ -52,7 +52,8 @@ TEST(StaticConfigResourceDetectorTest, EmptyAttributesAreIgnored) { StaticConfigResourceDetectorConfig proto_config; TestUtility::loadFromYamlAndValidate(yaml, proto_config); - auto detector = std::make_unique(proto_config, context); + auto detector = + std::make_unique(proto_config, context.server_factory_context_); Resource resource = detector->detect(); EXPECT_EQ(resource.schema_url_, ""); @@ -84,7 +85,8 @@ TEST(StaticConfigResourceDetectorTest, ValidAttributes) { StaticConfigResourceDetectorConfig proto_config; TestUtility::loadFromYamlAndValidate(yaml, proto_config); - auto detector = std::make_unique(proto_config, context); + auto detector = + std::make_unique(proto_config, context.server_factory_context_); Resource resource = detector->detect(); EXPECT_EQ(resource.schema_url_, ""); diff --git a/test/extensions/tracers/opentelemetry/samplers/sampler_test.cc b/test/extensions/tracers/opentelemetry/samplers/sampler_test.cc index 7b67d8fe88ab5..cbd3302210ac6 100644 --- a/test/extensions/tracers/opentelemetry/samplers/sampler_test.cc +++ b/test/extensions/tracers/opentelemetry/samplers/sampler_test.cc @@ -41,7 +41,7 @@ class TestSamplerFactory : public SamplerFactory { Server::Configuration::TracerFactoryContext& context)); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.tracers.opentelemetry.samplers.testsampler"; } @@ -156,7 +156,7 @@ TEST_F(SamplerFactoryTest, TestWithSampler) { // So the dynamic_cast should be safe. std::unique_ptr span(dynamic_cast(tracing_span.release())); EXPECT_TRUE(span->sampled()); - EXPECT_STREQ(span->tracestate().c_str(), "this_is=tracesate"); + EXPECT_EQ(span->tracestate(), "this_is=tracesate"); // shouldSamples return a result containing additional attributes and Decision::Drop EXPECT_CALL(*test_sampler, shouldSample(_, _, _, _, _, _, _)) @@ -184,7 +184,7 @@ TEST_F(SamplerFactoryTest, TestWithSampler) { {Tracing::Reason::Sampling, true}); std::unique_ptr unsampled_span(dynamic_cast(tracing_span.release())); EXPECT_FALSE(unsampled_span->sampled()); - EXPECT_STREQ(unsampled_span->tracestate().c_str(), "this_is=another_tracesate"); + EXPECT_EQ(unsampled_span->tracestate(), "this_is=another_tracesate"); auto proto_span = unsampled_span->spanForTest(); auto get_attr_value = diff --git a/test/extensions/tracers/opentelemetry/span_context_extractor_test.cc b/test/extensions/tracers/opentelemetry/span_context_extractor_test.cc index b87f984768ebe..5515d760d6bd9 100644 --- a/test/extensions/tracers/opentelemetry/span_context_extractor_test.cc +++ b/test/extensions/tracers/opentelemetry/span_context_extractor_test.cc @@ -30,7 +30,7 @@ TEST(SpanContextExtractorTest, ExtractSpanContext) { EXPECT_OK(span_context); EXPECT_EQ(span_context->traceId(), trace_id); - EXPECT_EQ(span_context->parentId(), parent_id); + EXPECT_EQ(span_context->spanId(), parent_id); EXPECT_EQ(span_context->version(), version); EXPECT_TRUE(span_context->sampled()); } @@ -45,7 +45,7 @@ TEST(SpanContextExtractorTest, ExtractSpanContextNotSampled) { EXPECT_OK(span_context); EXPECT_EQ(span_context->traceId(), trace_id); - EXPECT_EQ(span_context->parentId(), parent_id); + EXPECT_EQ(span_context->spanId(), parent_id); EXPECT_EQ(span_context->version(), version); EXPECT_FALSE(span_context->sampled()); } diff --git a/test/extensions/tracers/skywalking/tracer_test.cc b/test/extensions/tracers/skywalking/tracer_test.cc index 1dab0f94550fd..de8ce15d44d67 100644 --- a/test/extensions/tracers/skywalking/tracer_test.cc +++ b/test/extensions/tracers/skywalking/tracer_test.cc @@ -97,6 +97,9 @@ TEST_F(TracerTest, TracerTestCreateNewSpanWithNoPropagationHeaders) { span->setSampled(false); EXPECT_TRUE(span->spanEntity()->skipAnalysis()); + EXPECT_FALSE(span->useLocalDecision()); // Always false for now. + EXPECT_TRUE(span->spanEntity()->skipAnalysis()); + // The initial operation name is consistent with the 'operation' parameter in the 'startSpan' // method call. EXPECT_EQ("/downstream/path", span->spanEntity()->operationName()); diff --git a/test/extensions/tracers/xray/tracer_test.cc b/test/extensions/tracers/xray/tracer_test.cc index c451fb993f1f6..ebf0d0002f7a1 100644 --- a/test/extensions/tracers/xray/tracer_test.cc +++ b/test/extensions/tracers/xray/tracer_test.cc @@ -60,7 +60,7 @@ class XRayTracerTest : public ::testing::Test { expected_(std::make_unique( "Service 1", "AWS::Service::Proxy", "test_value", "egress hostname", "POST", "/first/second", "Mozilla/5.0 (Macintosh; Intel Mac OS X)", "egress")) {} - absl::flat_hash_map aws_metadata_; + absl::flat_hash_map aws_metadata_; NiceMock server_; NiceMock config_; std::unique_ptr broker_; @@ -584,7 +584,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, XRayDaemonTest, TEST_P(XRayDaemonTest, VerifyUdpPacketContents) { NiceMock config_; ON_CALL(config_, operationName()).WillByDefault(Return(Tracing::OperationName::Ingress)); - absl::flat_hash_map aws_metadata; + absl::flat_hash_map aws_metadata; NiceMock server; Network::Test::UdpSyncPeer xray_fake_daemon(GetParam()); const std::string daemon_endpoint = xray_fake_daemon.localAddress()->asString(); diff --git a/test/extensions/tracers/xray/xray_tracer_impl_test.cc b/test/extensions/tracers/xray/xray_tracer_impl_test.cc index 4a7a2c995c328..cf4e85b2a1fe6 100644 --- a/test/extensions/tracers/xray/xray_tracer_impl_test.cc +++ b/test/extensions/tracers/xray/xray_tracer_impl_test.cc @@ -34,7 +34,7 @@ class XRayDriverTest : public ::testing::Test { // The MockStreamInfo will register the singleton time system to SimulatedTimeSystem and ignore // the TestRealTimeSystem in the MockTracerFactoryContext. NiceMock stream_info_; - absl::flat_hash_map aws_metadata_; + absl::flat_hash_map aws_metadata_; NiceMock context_; NiceMock tls_; NiceMock tracing_config_; diff --git a/test/extensions/tracers/zipkin/BUILD b/test/extensions/tracers/zipkin/BUILD index b1d325d570a6b..eb34d6390cf32 100644 --- a/test/extensions/tracers/zipkin/BUILD +++ b/test/extensions/tracers/zipkin/BUILD @@ -34,13 +34,10 @@ envoy_extension_cc_test( "//source/extensions/tracers/zipkin:zipkin_lib", "//test/mocks:common_lib", "//test/mocks/http:http_mocks", - "//test/mocks/local_info:local_info_mocks", - "//test/mocks/runtime:runtime_mocks", + "//test/mocks/server:server_factory_context_mocks", "//test/mocks/stats:stats_mocks", "//test/mocks/stream_info:stream_info_mocks", - "//test/mocks/thread_local:thread_local_mocks", "//test/mocks/tracing:tracing_mocks", - "//test/mocks/upstream:cluster_manager_mocks", "//test/mocks/upstream:thread_local_cluster_mocks", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", diff --git a/test/extensions/tracers/zipkin/config_test.cc b/test/extensions/tracers/zipkin/config_test.cc index 073d9af2246cc..fe99f404c17e5 100644 --- a/test/extensions/tracers/zipkin/config_test.cc +++ b/test/extensions/tracers/zipkin/config_test.cc @@ -1,12 +1,9 @@ #include "envoy/config/trace/v3/http_tracer.pb.h" -#include "envoy/config/trace/v3/zipkin.pb.h" -#include "envoy/config/trace/v3/zipkin.pb.validate.h" -#include "envoy/registry/registry.h" #include "source/extensions/tracers/zipkin/config.h" -#include "test/mocks/server/tracer_factory.h" #include "test/mocks/server/tracer_factory_context.h" +#include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -65,6 +62,75 @@ TEST(ZipkinTracerConfigTest, ZipkinHttpTracerWithTypedConfig) { EXPECT_NE(nullptr, zipkin_tracer); } +TEST(ZipkinTracerConfigTest, ZipkinHttpTracerWithHttpService) { + NiceMock context; + context.server_factory_context_.cluster_manager_.initializeClusters({"fake_cluster"}, {}); + + const std::string yaml_string = R"EOF( + http: + name: zipkin + typed_config: + "@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig + collector_cluster: fake_cluster + collector_endpoint: /api/v2/spans + collector_endpoint_version: HTTP_JSON + collector_service: + http_uri: + uri: "https://zipkin-collector.example.com/api/v2/spans" + cluster: fake_cluster + timeout: 5s + request_headers_to_add: + - header: + key: "Authorization" + value: "Bearer token123" + - header: + key: "X-Custom-Header" + value: "custom-value" + - header: + key: "X-API-Key" + value: "api-key-123" + )EOF"; + + envoy::config::trace::v3::Tracing configuration; + TestUtility::loadFromYaml(yaml_string, configuration); + + ZipkinTracerFactory factory; + auto message = Config::Utility::translateToFactoryConfig( + configuration.http(), ProtobufMessage::getStrictValidationVisitor(), factory); + auto zipkin_tracer = factory.createTracerDriver(*message, context); + EXPECT_NE(nullptr, zipkin_tracer); +} + +TEST(ZipkinTracerConfigTest, ZipkinHttpTracerWithHttpServiceEmptyHeaders) { + NiceMock context; + context.server_factory_context_.cluster_manager_.initializeClusters({"fake_cluster"}, {}); + + const std::string yaml_string = R"EOF( + http: + name: zipkin + typed_config: + "@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig + collector_cluster: fake_cluster + collector_endpoint: /api/v2/spans + collector_endpoint_version: HTTP_JSON + collector_service: + http_uri: + uri: "https://zipkin-collector.example.com/api/v2/spans" + cluster: fake_cluster + timeout: 5s + request_headers_to_add: [] + )EOF"; + + envoy::config::trace::v3::Tracing configuration; + TestUtility::loadFromYaml(yaml_string, configuration); + + ZipkinTracerFactory factory; + auto message = Config::Utility::translateToFactoryConfig( + configuration.http(), ProtobufMessage::getStrictValidationVisitor(), factory); + auto zipkin_tracer = factory.createTracerDriver(*message, context); + EXPECT_NE(nullptr, zipkin_tracer); +} + } // namespace } // namespace Zipkin } // namespace Tracers diff --git a/test/extensions/tracers/zipkin/span_buffer_test.cc b/test/extensions/tracers/zipkin/span_buffer_test.cc index 5a5154a242eac..d266d371a1b9e 100644 --- a/test/extensions/tracers/zipkin/span_buffer_test.cc +++ b/test/extensions/tracers/zipkin/span_buffer_test.cc @@ -19,6 +19,21 @@ namespace Tracers { namespace Zipkin { namespace { +class EmptyTracer : public TracerInterface { +public: + SpanPtr startSpan(const Tracing::Config&, const std::string&, SystemTime) override { + return nullptr; + } + SpanPtr startSpan(const Tracing::Config&, const std::string&, SystemTime, + const SpanContext&) override { + return nullptr; + } + void reportSpan(Span&&) override {} + envoy::config::trace::v3::ZipkinConfig::TraceContextOption traceContextOption() const override { + return envoy::config::trace::v3::ZipkinConfig::USE_B3; + } +}; + // If this default timestamp is wrapped as double (using ValueUtil::numberValue()) and then it is // serialized using Protobuf::util::MessageToJsonString, it renders as: 1.58432429547687e+15. constexpr uint64_t DEFAULT_TEST_TIMESTAMP = 1584324295476870; @@ -68,7 +83,8 @@ BinaryAnnotation createTag() { Span createSpan(const std::vector& annotation_values, const IpType ip_type) { Event::SimulatedTimeSystem simulated_time_system; - Span span(simulated_time_system); + EmptyTracer tracer; + Span span(simulated_time_system, tracer); span.setId(1); span.setTraceId(1); span.setDuration(DEFAULT_TEST_DURATION); @@ -114,8 +130,10 @@ void expectSerializedBuffer(SpanBuffer& buffer, const bool delay_allocation, buffer.allocateBuffer(expected_list.size() + 1); } + EmptyTracer tracer; + // Add span after allocation, but missing required annotations should be false. - EXPECT_FALSE(buffer.addSpan(Span(test_time.timeSystem()))); + EXPECT_FALSE(buffer.addSpan(Span(test_time.timeSystem(), tracer))); EXPECT_FALSE(buffer.addSpan(createSpan({"aa"}, IpType::V4))); for (uint64_t i = 0; i < expected_list.size(); i++) { @@ -145,7 +163,7 @@ template std::string serializedMessageToJson(const std::string& TEST(ZipkinSpanBufferTest, TestSerializeTimestamp) { const std::string default_timestamp_string = std::to_string(DEFAULT_TEST_TIMESTAMP); - ProtobufWkt::Struct object; + Protobuf::Struct object; auto* fields = object.mutable_fields(); Util::Replacements replacements; (*fields)["timestamp"] = Util::uint64Value(DEFAULT_TEST_TIMESTAMP, "timestamp", replacements); @@ -447,7 +465,7 @@ TEST(ZipkinSpanBufferTest, SerializeSpan) { } TEST(ZipkinSpanBufferTest, TestSerializeTimestampInTheFuture) { - ProtobufWkt::Struct objectWithScientificNotation; + Protobuf::Struct objectWithScientificNotation; auto* objectWithScientificNotationFields = objectWithScientificNotation.mutable_fields(); (*objectWithScientificNotationFields)["timestamp"] = ValueUtil::numberValue( DEFAULT_TEST_TIMESTAMP); // the value of DEFAULT_TEST_TIMESTAMP is 1584324295476870. @@ -457,7 +475,7 @@ TEST(ZipkinSpanBufferTest, TestSerializeTimestampInTheFuture) { // see the value is rendered with scientific notation (1.58432429547687e+15). EXPECT_EQ(R"({"timestamp":1.58432429547687e+15})", objectWithScientificNotationJson); - ProtobufWkt::Struct object; + Protobuf::Struct object; auto* objectFields = object.mutable_fields(); Util::Replacements replacements; (*objectFields)["timestamp"] = @@ -471,7 +489,7 @@ TEST(ZipkinSpanBufferTest, TestSerializeTimestampInTheFuture) { SpanBuffer bufferDeprecatedJsonV1(envoy::config::trace::v3::ZipkinConfig::HTTP_JSON, true, 2); bufferDeprecatedJsonV1.addSpan(createSpan({"cs"}, IpType::V4)); - // We do "HasSubstr" here since we could not compare the serialized JSON of a ProtobufWkt::Struct + // We do "HasSubstr" here since we could not compare the serialized JSON of a Protobuf::Struct // object, since the positions of keys are not consistent between calls. EXPECT_THAT(bufferDeprecatedJsonV1.serialize(), HasSubstr(R"("timestamp":1584324295476871)")); EXPECT_THAT(bufferDeprecatedJsonV1.serialize(), diff --git a/test/extensions/tracers/zipkin/span_context_extractor_test.cc b/test/extensions/tracers/zipkin/span_context_extractor_test.cc index 393b995f71679..06c916e22d416 100644 --- a/test/extensions/tracers/zipkin/span_context_extractor_test.cc +++ b/test/extensions/tracers/zipkin/span_context_extractor_test.cc @@ -31,7 +31,7 @@ TEST(ZipkinSpanContextExtractorTest, Largest) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(9, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_TRUE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, WithoutParentDebug) { @@ -46,7 +46,7 @@ TEST(ZipkinSpanContextExtractorTest, WithoutParentDebug) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(9, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_TRUE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, MalformedUuid) { @@ -54,7 +54,7 @@ TEST(ZipkinSpanContextExtractorTest, MalformedUuid) { SpanContextExtractor extractor(request_headers); EXPECT_THROW_WITH_MESSAGE(extractor.extractSpanContext(true), ExtractorException, "Invalid input: invalid trace id b970dafd-0d95-40"); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().has_value()); } TEST(ZipkinSpanContextExtractorTest, MiddleOfString) { @@ -63,7 +63,7 @@ TEST(ZipkinSpanContextExtractorTest, MiddleOfString) { SpanContextExtractor extractor(request_headers); EXPECT_THROW_WITH_MESSAGE(extractor.extractSpanContext(true), ExtractorException, "Invalid input: truncated"); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().has_value()); } TEST(ZipkinSpanContextExtractorTest, DebugOnly) { @@ -77,7 +77,7 @@ TEST(ZipkinSpanContextExtractorTest, DebugOnly) { EXPECT_EQ(0, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_FALSE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_TRUE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, Sampled) { @@ -91,7 +91,7 @@ TEST(ZipkinSpanContextExtractorTest, Sampled) { EXPECT_EQ(0, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_FALSE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_TRUE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, SampledFalse) { @@ -105,7 +105,7 @@ TEST(ZipkinSpanContextExtractorTest, SampledFalse) { EXPECT_EQ(0, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_FALSE(context.first.sampled()); - EXPECT_FALSE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, IdNotYetSampled128) { @@ -120,7 +120,7 @@ TEST(ZipkinSpanContextExtractorTest, IdNotYetSampled128) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(9, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_FALSE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_FALSE(extractor.extractSampled().has_value()); } TEST(ZipkinSpanContextExtractorTest, IdsUnsampled) { @@ -134,7 +134,7 @@ TEST(ZipkinSpanContextExtractorTest, IdsUnsampled) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_FALSE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, ParentUnsampled) { @@ -149,7 +149,7 @@ TEST(ZipkinSpanContextExtractorTest, ParentUnsampled) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_FALSE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, ParentDebug) { @@ -164,7 +164,7 @@ TEST(ZipkinSpanContextExtractorTest, ParentDebug) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_TRUE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, IdsWithDebug) { @@ -178,7 +178,7 @@ TEST(ZipkinSpanContextExtractorTest, IdsWithDebug) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_TRUE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_TRUE(extractor.extractSampled().value()); } TEST(ZipkinSpanContextExtractorTest, WithoutSampled) { @@ -192,7 +192,7 @@ TEST(ZipkinSpanContextExtractorTest, WithoutSampled) { EXPECT_EQ(1, context.first.traceId()); EXPECT_EQ(0, context.first.traceIdHigh()); EXPECT_FALSE(context.first.sampled()); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().has_value()); } TEST(ZipkinSpanContextExtractorTest, TooBig) { @@ -202,7 +202,7 @@ TEST(ZipkinSpanContextExtractorTest, TooBig) { SpanContextExtractor extractor(request_headers); EXPECT_THROW_WITH_MESSAGE(extractor.extractSpanContext(true), ExtractorException, "Invalid input: too long"); - EXPECT_FALSE(extractor.extractSampled({Tracing::Reason::Sampling, false})); + EXPECT_FALSE(extractor.extractSampled().has_value()); } { @@ -310,7 +310,7 @@ TEST(ZipkinSpanContextExtractorTest, InvalidInput) { { Tracing::TestTraceContextImpl request_headers{{"b3", "-"}}; SpanContextExtractor extractor(request_headers); - EXPECT_TRUE(extractor.extractSampled({Tracing::Reason::Sampling, true})); + EXPECT_FALSE(extractor.extractSampled().has_value()); EXPECT_THROW_WITH_MESSAGE(extractor.extractSpanContext(true), ExtractorException, "Invalid input: invalid sampling flag -"); } @@ -407,6 +407,227 @@ TEST(ZipkinSpanContextExtractorTest, Truncated) { } } +// Test W3C fallback functionality +TEST(ZipkinSpanContextExtractorTest, W3CFallbackDisabledByDefault) { + // Test that W3C headers are ignored when w3c_fallback is disabled (default) + Tracing::TestTraceContextImpl request_headers{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor(request_headers); // w3c_fallback disabled by default + auto context = extractor.extractSpanContext(true); + EXPECT_FALSE(context.second); // Should not extract context from W3C headers + EXPECT_FALSE(extractor.extractSampled().has_value()); +} + +TEST(ZipkinSpanContextExtractorTest, W3CFallbackEnabled) { + // Test that W3C headers are used when w3c_fallback is enabled + Tracing::TestTraceContextImpl request_headers{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor(request_headers, true); // w3c_fallback enabled + auto context = extractor.extractSpanContext(true); + EXPECT_TRUE(context.second); // Should extract context from W3C headers + EXPECT_TRUE(extractor.extractSampled().value()); + + // Verify the converted values + EXPECT_EQ(0xb7ad6b7169203331, context.first.id()); // W3C span-id becomes span-id + EXPECT_EQ(0, context.first.parentId()); // No parent in W3C conversion + EXPECT_TRUE(context.first.is128BitTraceId()); + EXPECT_EQ(0x8448eb211c80319c, context.first.traceId()); // Low 64 bits + EXPECT_EQ(0x0af7651916cd43dd, context.first.traceIdHigh()); // High 64 bits +} + +TEST(ZipkinSpanContextExtractorTest, B3TakesPrecedenceOverW3C) { + // Test that B3 headers take precedence over W3C headers when both are present + Tracing::TestTraceContextImpl request_headers{ + {"b3", fmt::format("{}-{}-1", trace_id, span_id)}, + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor(request_headers, true); // w3c_fallback enabled + auto context = extractor.extractSpanContext(true); + EXPECT_TRUE(context.second); + + // Should use B3 values, not W3C values + EXPECT_EQ(3, context.first.id()); // From B3 span_id + EXPECT_EQ(0, context.first.parentId()); + EXPECT_FALSE(context.first.is128BitTraceId()); // B3 uses 64-bit in this test + EXPECT_EQ(1, context.first.traceId()); // From B3 trace_id +} + +TEST(ZipkinSpanContextExtractorTest, W3CFallbackWithInvalidHeaders) { + // Test that invalid W3C headers are handled gracefully + Tracing::TestTraceContextImpl request_headers{{"traceparent", "invalid-header-format"}}; + SpanContextExtractor extractor(request_headers, true); // w3c_fallback enabled + auto context = extractor.extractSpanContext(true); + EXPECT_FALSE(context.second); // Should not extract context from invalid W3C headers + EXPECT_FALSE(extractor.extractSampled().has_value()); +} + +TEST(ZipkinSpanContextExtractorTest, W3CFallbackWithInvalidTraceIdLength) { + // Test invalid W3C trace ID length (too short) + Tracing::TestTraceContextImpl request_headers{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor(request_headers, true); + auto context = extractor.extractSpanContext(true); + EXPECT_FALSE(context.second); + + // Test invalid W3C trace ID length (too long) + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c123-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); +} + +TEST(ZipkinSpanContextExtractorTest, W3CTraceIdLengthValidation) { + // Test that invalid W3C trace ID lengths are properly rejected + // Invalid headers should not extract a valid context (context.second should be false) + + // Too short trace ID (31 chars instead of 32) + Tracing::TestTraceContextImpl request_headers1{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor1(request_headers1, true); + auto context1 = extractor1.extractSpanContext(true); + EXPECT_FALSE(context1.second); // Should not extract context from invalid trace ID length + + // Too long trace ID (33 chars instead of 32) + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c1-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); // Should not extract context from invalid trace ID length + + // Empty trace ID + Tracing::TestTraceContextImpl request_headers3{{"traceparent", "00--b7ad6b7169203331-01"}}; + SpanContextExtractor extractor3(request_headers3, true); + auto context3 = extractor3.extractSpanContext(true); + EXPECT_FALSE(context3.second); // Should not extract context from empty trace ID +} + +TEST(ZipkinSpanContextExtractorTest, W3CFallbackWithInvalidSpanIdLength) { + // Test invalid W3C span ID length (too short) + Tracing::TestTraceContextImpl request_headers{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b716920331-01"}}; + SpanContextExtractor extractor(request_headers, true); + auto context = extractor.extractSpanContext(true); + EXPECT_FALSE(context.second); + + // Test invalid W3C span ID length (too long) + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331123-01"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); +} + +TEST(ZipkinSpanContextExtractorTest, W3CFallbackWithInvalidHexCharacters) { + // Test invalid hex characters in trace ID + Tracing::TestTraceContextImpl request_headers{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319g-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor(request_headers, true); + auto context = extractor.extractSpanContext(true); + EXPECT_FALSE(context.second); + + // Test invalid hex characters in span ID + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331g-01"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); +} + +TEST(ZipkinSpanContextExtractorTest, W3CTraceIdHexValidation) { + // Test that invalid hex characters in W3C trace IDs are properly rejected + // Invalid headers should not extract a valid context (context.second should be false) + + // Invalid hex character 'g' in high part of trace ID + Tracing::TestTraceContextImpl request_headers1{ + {"traceparent", "00-0af7651916cd43dg8448eb211c80319c-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor1(request_headers1, true); + auto context1 = extractor1.extractSpanContext(true); + EXPECT_FALSE(context1.second); // Should not extract context from invalid hex in trace ID + + // Invalid hex character 'z' in low part of trace ID + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319z-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); // Should not extract context from invalid hex in trace ID + + // Invalid character at start of trace ID + Tracing::TestTraceContextImpl request_headers3{ + {"traceparent", "00-xaf7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor3(request_headers3, true); + auto context3 = extractor3.extractSpanContext(true); + EXPECT_FALSE(context3.second); // Should not extract context from invalid hex in trace ID + + // Invalid character at end of trace ID + Tracing::TestTraceContextImpl request_headers4{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319x-b7ad6b7169203331-01"}}; + SpanContextExtractor extractor4(request_headers4, true); + auto context4 = extractor4.extractSpanContext(true); + EXPECT_FALSE(context4.second); // Should not extract context from invalid hex in trace ID +} + +TEST(ZipkinSpanContextExtractorTest, W3CSpanIdHexValidation) { + // Test that invalid hex characters in W3C span IDs are properly rejected + // Invalid headers should not extract a valid context (context.second should be false) + + // Invalid hex character 'g' in span ID + Tracing::TestTraceContextImpl request_headers1{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331g-01"}}; + SpanContextExtractor extractor1(request_headers1, true); + auto context1 = extractor1.extractSpanContext(true); + EXPECT_FALSE(context1.second); // Should not extract context from invalid hex in span ID + + // Invalid hex character 'z' in span ID + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331z-01"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); // Should not extract context from invalid hex in span ID + + // Invalid character at start of span ID + Tracing::TestTraceContextImpl request_headers3{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-x7ad6b7169203331-01"}}; + SpanContextExtractor extractor3(request_headers3, true); + auto context3 = extractor3.extractSpanContext(true); + EXPECT_FALSE(context3.second); // Should not extract context from invalid hex in span ID + + // Invalid character in middle of span ID + Tracing::TestTraceContextImpl request_headers4{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b71692x3331-01"}}; + SpanContextExtractor extractor4(request_headers4, true); + auto context4 = extractor4.extractSpanContext(true); + EXPECT_FALSE(context4.second); // Should not extract context from invalid hex in span ID + + // Non-hex character like space + Tracing::TestTraceContextImpl request_headers5{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203 31-01"}}; + SpanContextExtractor extractor5(request_headers5, true); + auto context5 = extractor5.extractSpanContext(true); + EXPECT_FALSE(context5.second); // Should not extract context from invalid hex in span ID +} + +TEST(ZipkinSpanContextExtractorTest, W3CFallbackWithMalformedTraceparent) { + // Test missing components + Tracing::TestTraceContextImpl request_headers{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c"}}; + SpanContextExtractor extractor(request_headers, true); + auto context = extractor.extractSpanContext(true); + EXPECT_FALSE(context.second); + + // Test wrong number of dashes + Tracing::TestTraceContextImpl request_headers2{ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"}}; + SpanContextExtractor extractor2(request_headers2, true); + auto context2 = extractor2.extractSpanContext(true); + EXPECT_FALSE(context2.second); + + // Test empty traceparent + Tracing::TestTraceContextImpl request_headers3{{"traceparent", ""}}; + SpanContextExtractor extractor3(request_headers3, true); + auto context3 = extractor3.extractSpanContext(true); + EXPECT_FALSE(context3.second); +} + } // namespace Zipkin } // namespace Tracers } // namespace Extensions diff --git a/test/extensions/tracers/zipkin/tracer_test.cc b/test/extensions/tracers/zipkin/tracer_test.cc index b49783a63734c..27e9f14552af1 100644 --- a/test/extensions/tracers/zipkin/tracer_test.cc +++ b/test/extensions/tracers/zipkin/tracer_test.cc @@ -81,9 +81,6 @@ TEST_F(ZipkinTracerTest, SpanCreation) { Endpoint endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), root_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(root_span->isSetDuration()); @@ -93,7 +90,7 @@ TEST_F(ZipkinTracerTest, SpanCreation) { ON_CALL(config, operationName()).WillByDefault(Return(Tracing::OperationName::Ingress)); - SpanContext root_span_context(*root_span); + SpanContext root_span_context = root_span->spanContext(); SpanPtr server_side_shared_context_span = tracer.startSpan(config, "my_span", timestamp, root_span_context); @@ -125,9 +122,6 @@ TEST_F(ZipkinTracerTest, SpanCreation) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), server_side_shared_context_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(server_side_shared_context_span->isSetDuration()); @@ -137,7 +131,7 @@ TEST_F(ZipkinTracerTest, SpanCreation) { ON_CALL(config, operationName()).WillByDefault(Return(Tracing::OperationName::Egress)); ON_CALL(random_generator, random()).WillByDefault(Return(2000)); - SpanContext server_side_context(*server_side_shared_context_span); + SpanContext server_side_context = server_side_shared_context_span->spanContext(); SpanPtr child_span = tracer.startSpan(config, "my_child_span", timestamp, server_side_context); EXPECT_EQ("my_child_span", child_span->name()); @@ -171,9 +165,6 @@ TEST_F(ZipkinTracerTest, SpanCreation) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), child_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(child_span->isSetDuration()); @@ -218,9 +209,6 @@ TEST_F(ZipkinTracerTest, SpanCreation) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), new_shared_context_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(new_shared_context_span->isSetDuration()); } @@ -266,9 +254,6 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxy) { Endpoint endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), root_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(root_span->isSetDuration()); @@ -278,7 +263,7 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxy) { // ============== ON_CALL(random_generator, random()).WillByDefault(Return(2000)); - SpanContext root_span_context(*root_span); + SpanContext root_span_context = root_span->spanContext(); SpanPtr child_span = tracer.startSpan(config, "my_child_span", timestamp, root_span_context); EXPECT_EQ("my_child_span", child_span->name()); @@ -312,9 +297,6 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxy) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), child_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(child_span->isSetDuration()); @@ -322,7 +304,13 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxy) { // Test the downstream span with parent context and the shared context is enabled. If the // independent proxy is set to true, the downstream span will be server span. // ============== - SpanContext child_span_context(*child_span, false); + SpanContext child_span_context = child_span->spanContext(); + + // By default the context that from an existing span is an inner context. But here we want to + // test the case there the context is an external context from the downstream request. So + // we set the inner context to false manually for test. + child_span_context.setInnerContextForTest(false); + SpanPtr server_side_shared_context_span = tracer.startSpan(config, "my_span", timestamp, child_span_context); @@ -354,9 +342,6 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxy) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), server_side_shared_context_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(server_side_shared_context_span->isSetDuration()); } @@ -402,9 +387,6 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxyByTracingConfig) { Endpoint endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), root_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(root_span->isSetDuration()); @@ -414,7 +396,7 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxyByTracingConfig) { // ============== ON_CALL(random_generator, random()).WillByDefault(Return(2000)); - SpanContext root_span_context(*root_span); + SpanContext root_span_context = root_span->spanContext(); SpanPtr child_span = tracer.startSpan(config, "my_child_span", timestamp, root_span_context); EXPECT_EQ("my_child_span", child_span->name()); @@ -448,9 +430,6 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxyByTracingConfig) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), child_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(child_span->isSetDuration()); @@ -458,7 +437,13 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxyByTracingConfig) { // Test the downstream span with parent context and the shared context is enabled. If the // independent proxy is set to true, the downstream span will be server span. // ============== - SpanContext child_span_context(*child_span, false); + SpanContext child_span_context = child_span->spanContext(); + + // By default the context that from an existing span is an inner context. But here we want to + // test the case there the context is an external context from the downstream request. So + // we set the inner context to false manually for test. + child_span_context.setInnerContextForTest(false); + SpanPtr server_side_shared_context_span = tracer.startSpan(config, "my_span", timestamp, child_span_context); @@ -490,9 +475,6 @@ TEST_F(ZipkinTracerTest, SpanCreationWithIndependentProxyByTracingConfig) { endpoint = ann.endpoint(); EXPECT_EQ("my_service_name", endpoint.serviceName()); - // The tracer must have been properly set - EXPECT_EQ(dynamic_cast(&tracer), server_side_shared_context_span->tracer()); - // Duration is not set at span-creation time EXPECT_FALSE(server_side_shared_context_span->isSetDuration()); } @@ -516,7 +498,7 @@ TEST_F(ZipkinTracerTest, FinishSpan) { span->setSampled(true); // Finishing a root span with a CS annotation must add a CR annotation - span->finish(); + span->finishSpan(); EXPECT_EQ(2ULL, span->annotations().size()); // Check the CS annotation added at span-creation time @@ -545,7 +527,7 @@ TEST_F(ZipkinTracerTest, FinishSpan) { ON_CALL(config, operationName()).WillByDefault(Return(Tracing::OperationName::Ingress)); - SpanContext context(*span); + SpanContext context = span->spanContext(); SpanPtr server_side = tracer.startSpan(config, "my_span", timestamp, context); // Associate a reporter with the tracer @@ -554,7 +536,7 @@ TEST_F(ZipkinTracerTest, FinishSpan) { tracer.setReporter(std::move(reporter_ptr)); // Finishing a server-side span with an SR annotation must add an SS annotation - server_side->finish(); + server_side->finishSpan(); EXPECT_EQ(2ULL, server_side->annotations().size()); // Test if the reporter's reportSpan method was actually called upon finishing the span @@ -602,7 +584,7 @@ TEST_F(ZipkinTracerTest, FinishNotSampledSpan) { // Creates a root-span with a CS annotation SpanPtr span = tracer.startSpan(config, "my_span", timestamp); span->setSampled(false); - span->finish(); + span->finishSpan(); // Test if the reporter's reportSpan method was NOT called upon finishing the span EXPECT_EQ(0ULL, reporter_object->reportedSpans().size()); @@ -622,14 +604,14 @@ TEST_F(ZipkinTracerTest, SpanSampledPropagatedToChild) { SpanPtr parent_span = tracer.startSpan(config, "parent_span", timestamp); parent_span->setSampled(true); - SpanContext parent_context1(*parent_span); + SpanContext parent_context1 = parent_span->spanContext(); SpanPtr child_span1 = tracer.startSpan(config, "child_span 1", timestamp, parent_context1); // Test that child span sampled flag is true EXPECT_TRUE(child_span1->sampled()); parent_span->setSampled(false); - SpanContext parent_context2(*parent_span); + SpanContext parent_context2 = parent_span->spanContext(); SpanPtr child_span2 = tracer.startSpan(config, "child_span 2", timestamp, parent_context2); // Test that sampled flag is false @@ -670,7 +652,7 @@ TEST_F(ZipkinTracerTest, SharedSpanContext) { // Create parent span SpanPtr parent_span = tracer.startSpan(config, "parent_span", timestamp); - SpanContext parent_context(*parent_span); + SpanContext parent_context = parent_span->spanContext(); // An CS annotation must have been added EXPECT_EQ(1ULL, parent_span->annotations().size()); @@ -706,7 +688,7 @@ TEST_F(ZipkinTracerTest, NotSharedSpanContext) { // Create parent span SpanPtr parent_span = tracer.startSpan(config, "parent_span", timestamp); - SpanContext parent_context(*parent_span); + SpanContext parent_context = parent_span->spanContext(); // An CS annotation must have been added EXPECT_EQ(1ULL, parent_span->annotations().size()); diff --git a/test/extensions/tracers/zipkin/zipkin_core_types_test.cc b/test/extensions/tracers/zipkin/zipkin_core_types_test.cc index 5bd447121258e..850a279455f9b 100644 --- a/test/extensions/tracers/zipkin/zipkin_core_types_test.cc +++ b/test/extensions/tracers/zipkin/zipkin_core_types_test.cc @@ -7,6 +7,7 @@ #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" namespace Envoy { @@ -350,9 +351,23 @@ TEST(ZipkinCoreTypesBinaryAnnotationTest, assignmentOperator) { EXPECT_EQ(ann.annotationType(), ann2.annotationType()); } +class MockTracer : public TracerInterface { +public: + MOCK_METHOD(SpanPtr, startSpan, + (const Tracing::Config&, const std::string& span_name, SystemTime timestamp), ()); + MOCK_METHOD(SpanPtr, startSpan, + (const Tracing::Config&, const std::string& span_name, SystemTime timestamp, + const SpanContext& parent_context), + ()); + MOCK_METHOD(void, reportSpan, (Span && span), ()); + MOCK_METHOD(envoy::config::trace::v3::ZipkinConfig::TraceContextOption, traceContextOption, (), + (const)); +}; + TEST(ZipkinCoreTypesSpanTest, defaultConstructor) { Event::SimulatedTimeSystem test_time; - Span span(test_time.timeSystem()); + MockTracer tracer; + Span span(test_time.timeSystem(), tracer); Util::Replacements replacements; EXPECT_EQ(0ULL, span.id()); @@ -575,85 +590,10 @@ TEST(ZipkinCoreTypesSpanTest, defaultConstructor) { EXPECT_EQ(6, replacements.size()); } -TEST(ZipkinCoreTypesSpanTest, copyConstructor) { - Event::SimulatedTimeSystem test_time; - Span span(test_time.timeSystem()); - Util::Replacements replacements; - - uint64_t id = Util::generateRandom64(test_time.timeSystem()); - std::string id_hex = Hex::uint64ToHex(id); - span.setId(id); - span.setParentId(id); - span.setTraceId(id); - int64_t timestamp = std::chrono::duration_cast( - test_time.timeSystem().systemTime().time_since_epoch()) - .count(); - span.setTimestamp(timestamp); - span.setDuration(3000LL); - span.setName("span_name"); - - Span span2(span); - - EXPECT_EQ(span.id(), span2.id()); - EXPECT_EQ(span.parentId(), span2.parentId()); - EXPECT_EQ(span.traceId(), span2.traceId()); - EXPECT_EQ(span.name(), span2.name()); - EXPECT_EQ(span.annotations().size(), span2.annotations().size()); - EXPECT_EQ(span.binaryAnnotations().size(), span2.binaryAnnotations().size()); - EXPECT_EQ(span.idAsHexString(), span2.idAsHexString()); - EXPECT_EQ(span.parentIdAsHexString(), span2.parentIdAsHexString()); - EXPECT_EQ(span.traceIdAsHexString(), span2.traceIdAsHexString()); - EXPECT_EQ(span.timestamp(), span2.timestamp()); - EXPECT_EQ(span.duration(), span2.duration()); - EXPECT_EQ(span.startTime(), span2.startTime()); - EXPECT_EQ(span.debug(), span2.debug()); - EXPECT_EQ(span.isSetDuration(), span2.isSetDuration()); - EXPECT_EQ(span.isSetParentId(), span2.isSetParentId()); - EXPECT_EQ(span.isSetTimestamp(), span2.isSetTimestamp()); - EXPECT_EQ(span.isSetTraceIdHigh(), span2.isSetTraceIdHigh()); -} - -TEST(ZipkinCoreTypesSpanTest, assignmentOperator) { - Event::SimulatedTimeSystem test_time; - Span span(test_time.timeSystem()); - Util::Replacements replacements; - - uint64_t id = Util::generateRandom64(test_time.timeSystem()); - std::string id_hex = Hex::uint64ToHex(id); - span.setId(id); - span.setParentId(id); - span.setTraceId(id); - int64_t timestamp = std::chrono::duration_cast( - test_time.timeSystem().systemTime().time_since_epoch()) - .count(); - span.setTimestamp(timestamp); - span.setDuration(3000LL); - span.setName("span_name"); - - Span span2 = span; - - EXPECT_EQ(span.id(), span2.id()); - EXPECT_EQ(span.parentId(), span2.parentId()); - EXPECT_EQ(span.traceId(), span2.traceId()); - EXPECT_EQ(span.name(), span2.name()); - EXPECT_EQ(span.annotations().size(), span2.annotations().size()); - EXPECT_EQ(span.binaryAnnotations().size(), span2.binaryAnnotations().size()); - EXPECT_EQ(span.idAsHexString(), span2.idAsHexString()); - EXPECT_EQ(span.parentIdAsHexString(), span2.parentIdAsHexString()); - EXPECT_EQ(span.traceIdAsHexString(), span2.traceIdAsHexString()); - EXPECT_EQ(span.timestamp(), span2.timestamp()); - EXPECT_EQ(span.duration(), span2.duration()); - EXPECT_EQ(span.startTime(), span2.startTime()); - EXPECT_EQ(span.debug(), span2.debug()); - EXPECT_EQ(span.isSetDuration(), span2.isSetDuration()); - EXPECT_EQ(span.isSetParentId(), span2.isSetParentId()); - EXPECT_EQ(span.isSetTimestamp(), span2.isSetTimestamp()); - EXPECT_EQ(span.isSetTraceIdHigh(), span2.isSetTraceIdHigh()); -} - TEST(ZipkinCoreTypesSpanTest, setTag) { Event::SimulatedTimeSystem test_time; - Span span(test_time.timeSystem()); + MockTracer tracer; + Span span(test_time.timeSystem(), tracer); span.setTag("key1", "value1"); span.setTag("key2", "value2"); diff --git a/test/extensions/tracers/zipkin/zipkin_tracer_impl_test.cc b/test/extensions/tracers/zipkin/zipkin_tracer_impl_test.cc index e4292aa904527..f492eaff21f5b 100644 --- a/test/extensions/tracers/zipkin/zipkin_tracer_impl_test.cc +++ b/test/extensions/tracers/zipkin/zipkin_tracer_impl_test.cc @@ -5,23 +5,16 @@ #include "envoy/config/trace/v3/zipkin.pb.h" -#include "source/common/http/header_map_impl.h" #include "source/common/http/headers.h" #include "source/common/http/message_impl.h" -#include "source/common/runtime/runtime_impl.h" -#include "source/common/tracing/http_tracer_impl.h" #include "source/extensions/tracers/zipkin/zipkin_core_constants.h" #include "source/extensions/tracers/zipkin/zipkin_tracer_impl.h" #include "test/mocks/http/mocks.h" -#include "test/mocks/local_info/mocks.h" -#include "test/mocks/runtime/mocks.h" -#include "test/mocks/stats/mocks.h" +#include "test/mocks/server/server_factory_context.h" #include "test/mocks/stream_info/mocks.h" #include "test/mocks/thread_local/mocks.h" #include "test/mocks/tracing/mocks.h" -#include "test/mocks/upstream/cluster_manager.h" -#include "test/mocks/upstream/thread_local_cluster.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" @@ -57,8 +50,7 @@ class ZipkinDriverTest : public testing::Test { EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5000), _)); } - driver_ = std::make_unique(zipkin_config, cm_, *stats_.rootScope(), tls_, runtime_, - local_info_, random_, time_source_); + driver_ = std::make_unique(zipkin_config, context_); } void setupValidDriverWithHostname(const std::string& version, const std::string& hostname) { @@ -156,14 +148,16 @@ class ZipkinDriverTest : public testing::Test { {":authority", "api.lyft.com"}, {":path", "/"}, {":method", "GET"}, {"x-request-id", "foo"}}; NiceMock stream_info_; - NiceMock tls_; std::unique_ptr driver_; NiceMock* timer_; - NiceMock stats_; - NiceMock cm_; - NiceMock runtime_; - NiceMock local_info_; - NiceMock random_; + + NiceMock context_; + NiceMock& tls_{context_.thread_local_}; + NiceMock& stats_{context_.store_}; + NiceMock& cm_{context_.cluster_manager_}; + NiceMock& runtime_{context_.runtime_loader_}; + NiceMock& local_info_{context_.local_info_}; + NiceMock& random_{context_.api_.random_}; NiceMock config_; Event::SimulatedTimeSystem test_time_; @@ -206,6 +200,141 @@ TEST_F(ZipkinDriverTest, InitializeDriver) { } } +TEST_F(ZipkinDriverTest, TraceContextOptionConfiguration) { + cm_.initializeClusters({"fake_cluster"}, {}); + + { + // Test default trace_context_option value (USE_B3) - W3C fallback should be disabled. + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + collector_endpoint: /api/v2/spans + collector_endpoint_version: HTTP_JSON + )EOF"; + envoy::config::trace::v3::ZipkinConfig zipkin_config; + TestUtility::loadFromYaml(yaml_string, zipkin_config); + + setup(zipkin_config, true); + EXPECT_FALSE(driver_->w3cFallbackEnabled()); // W3C fallback should be disabled by default + EXPECT_EQ(driver_->traceContextOption(), envoy::config::trace::v3::ZipkinConfig::USE_B3); + } + + { + // Test trace_context_option explicitly set to USE_B3 - W3C fallback should be disabled. + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + collector_endpoint: /api/v2/spans + collector_endpoint_version: HTTP_JSON + trace_context_option: USE_B3 + )EOF"; + envoy::config::trace::v3::ZipkinConfig zipkin_config; + TestUtility::loadFromYaml(yaml_string, zipkin_config); + + setup(zipkin_config, true); + EXPECT_FALSE(driver_->w3cFallbackEnabled()); // W3C fallback should be disabled + EXPECT_EQ(driver_->traceContextOption(), envoy::config::trace::v3::ZipkinConfig::USE_B3); + } + + { + // Test trace_context_option set to USE_B3_WITH_W3C_PROPAGATION - W3C fallback should be + // enabled. + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + collector_endpoint: /api/v2/spans + collector_endpoint_version: HTTP_JSON + trace_context_option: USE_B3_WITH_W3C_PROPAGATION + )EOF"; + envoy::config::trace::v3::ZipkinConfig zipkin_config; + TestUtility::loadFromYaml(yaml_string, zipkin_config); + + setup(zipkin_config, true); + EXPECT_TRUE(driver_->w3cFallbackEnabled()); // W3C fallback should be enabled + EXPECT_EQ(driver_->traceContextOption(), + envoy::config::trace::v3::ZipkinConfig::USE_B3_WITH_W3C_PROPAGATION); + } +} + +TEST_F(ZipkinDriverTest, DualHeaderExtractionAndInjection) { + cm_.initializeClusters({"fake_cluster"}, {}); + + // Test complete dual header cycle: extract from B3 headers, then inject both B3 and W3C headers + const std::string yaml_string = R"EOF( + collector_cluster: fake_cluster + collector_endpoint: /api/v2/spans + collector_endpoint_version: HTTP_JSON + trace_context_option: USE_B3_WITH_W3C_PROPAGATION + )EOF"; + envoy::config::trace::v3::ZipkinConfig zipkin_config; + TestUtility::loadFromYaml(yaml_string, zipkin_config); + + setup(zipkin_config, true); + + // Step 1: Simulate incoming request with B3 headers (extraction phase) + Tracing::TestTraceContextImpl incoming_trace_context{ + {"x-b3-traceid", "463ac35c9f6413ad48485a3953bb6124"}, + {"x-b3-spanid", "a2fb4a1d1a96d312"}, + {"x-b3-sampled", "1"}}; + + // Create a span from the incoming B3 headers + Tracing::SpanPtr span = driver_->startSpan(config_, incoming_trace_context, stream_info_, + "test_operation", {Tracing::Reason::Sampling, true}); + + // Step 2: Inject context for outgoing request (injection phase) + Tracing::TestTraceContextImpl outgoing_trace_context{{}}; + Tracing::UpstreamContext upstream_context; + span->injectContext(outgoing_trace_context, upstream_context); + + // Step 3: Verify both B3 and W3C headers are injected + + // Verify B3 headers are injected + auto b3_traceid = outgoing_trace_context.get("x-b3-traceid"); + auto b3_spanid = outgoing_trace_context.get("x-b3-spanid"); + auto b3_sampled = outgoing_trace_context.get("x-b3-sampled"); + + EXPECT_TRUE(b3_traceid.has_value()); + EXPECT_TRUE(b3_spanid.has_value()); + EXPECT_TRUE(b3_sampled.has_value()); + + // Verify the trace ID is preserved from extraction + EXPECT_EQ(b3_traceid.value(), "463ac35c9f6413ad48485a3953bb6124"); + EXPECT_EQ(b3_sampled.value(), "1"); + + // Verify W3C traceparent header is also injected + auto traceparent = outgoing_trace_context.get("traceparent"); + EXPECT_TRUE(traceparent.has_value()); + EXPECT_FALSE(traceparent.value().empty()); + + // Verify traceparent format and contains the same trace ID + const std::string traceparent_value = std::string(traceparent.value()); + EXPECT_EQ(traceparent_value.length(), 55); // 2+1+32+1+16+1+2 + EXPECT_EQ(traceparent_value.substr(0, 3), "00-"); // version + EXPECT_EQ(traceparent_value.substr(3, 32), "463ac35c9f6413ad48485a3953bb6124"); // same trace ID + EXPECT_EQ(traceparent_value[35], '-'); // separator after trace-id + EXPECT_EQ(traceparent_value[52], '-'); // separator after span-id + EXPECT_EQ(traceparent_value.substr(53, 2), "01"); // sampled flag + + // Step 4: Test W3C extraction fallback when B3 headers are not present + Tracing::TestTraceContextImpl w3c_only_context{ + {"traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}}; + + Tracing::SpanPtr w3c_span = + driver_->startSpan(config_, w3c_only_context, stream_info_, "w3c_test_operation", + {Tracing::Reason::Sampling, true}); + + // Inject context for W3C extracted span + Tracing::TestTraceContextImpl w3c_outgoing_context{{}}; + w3c_span->injectContext(w3c_outgoing_context, upstream_context); + + // Verify both B3 and W3C headers are injected even when extracted from W3C + EXPECT_TRUE(w3c_outgoing_context.get("x-b3-traceid").has_value()); + EXPECT_TRUE(w3c_outgoing_context.get("x-b3-spanid").has_value()); + EXPECT_TRUE(w3c_outgoing_context.get("x-b3-sampled").has_value()); + EXPECT_TRUE(w3c_outgoing_context.get("traceparent").has_value()); + + // Verify the trace ID is preserved from W3C extraction + auto w3c_b3_traceid = w3c_outgoing_context.get("x-b3-traceid"); + EXPECT_EQ(w3c_b3_traceid.value(), "4bf92f3577b34da6a3ce929d0e0e4736"); +} + TEST_F(ZipkinDriverTest, AllowCollectorClusterToBeAddedViaApi) { cm_.initializeClusters({"fake_cluster"}, {}); ON_CALL(*cm_.active_clusters_["fake_cluster"]->info_, addedViaApi()).WillByDefault(Return(true)); @@ -503,8 +632,8 @@ TEST_F(ZipkinDriverTest, NoB3ContextSampledTrue) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_TRUE(zipkin_span->span().sampled()); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); + EXPECT_TRUE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, NoB3ContextSampledFalse) { @@ -517,8 +646,8 @@ TEST_F(ZipkinDriverTest, NoB3ContextSampledFalse) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, false}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_FALSE(zipkin_span->span().sampled()); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); + EXPECT_FALSE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, PropagateB3NoSampleDecisionSampleTrue) { @@ -533,8 +662,8 @@ TEST_F(ZipkinDriverTest, PropagateB3NoSampleDecisionSampleTrue) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_TRUE(zipkin_span->span().sampled()); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); + EXPECT_TRUE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, PropagateB3NoSampleDecisionSampleFalse) { @@ -549,8 +678,8 @@ TEST_F(ZipkinDriverTest, PropagateB3NoSampleDecisionSampleFalse) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, false}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_FALSE(zipkin_span->span().sampled()); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); + EXPECT_FALSE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, PropagateB3NotSampled) { @@ -630,8 +759,8 @@ TEST_F(ZipkinDriverTest, PropagateB3SampleFalse) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_FALSE(zipkin_span->span().sampled()); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); + EXPECT_FALSE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, ZipkinSpanTest) { @@ -647,13 +776,12 @@ TEST_F(ZipkinDriverTest, ZipkinSpanTest) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); zipkin_span->setTag("key", "value"); - Span& zipkin_zipkin_span = zipkin_span->span(); - EXPECT_EQ(1ULL, zipkin_zipkin_span.binaryAnnotations().size()); - EXPECT_EQ("key", zipkin_zipkin_span.binaryAnnotations()[0].key()); - EXPECT_EQ("value", zipkin_zipkin_span.binaryAnnotations()[0].value()); + EXPECT_EQ(1ULL, zipkin_span->binaryAnnotations().size()); + EXPECT_EQ("key", zipkin_span->binaryAnnotations()[0].key()); + EXPECT_EQ("value", zipkin_span->binaryAnnotations()[0].value()); // ==== // Test setTag() with SR annotated span @@ -670,29 +798,27 @@ TEST_F(ZipkinDriverTest, ZipkinSpanTest) { Tracing::SpanPtr span2 = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span2(dynamic_cast(span2.release())); + Zipkin::SpanPtr zipkin_span2(dynamic_cast(span2.release())); zipkin_span2->setTag("key2", "value2"); - Span& zipkin_zipkin_span2 = zipkin_span2->span(); - EXPECT_EQ(1ULL, zipkin_zipkin_span2.binaryAnnotations().size()); - EXPECT_EQ("key2", zipkin_zipkin_span2.binaryAnnotations()[0].key()); - EXPECT_EQ("value2", zipkin_zipkin_span2.binaryAnnotations()[0].value()); + EXPECT_EQ(1ULL, zipkin_span2->binaryAnnotations().size()); + EXPECT_EQ("key2", zipkin_span2->binaryAnnotations()[0].key()); + EXPECT_EQ("value2", zipkin_span2->binaryAnnotations()[0].value()); // ==== // Test setTag() with empty annotations vector // ==== Tracing::SpanPtr span3 = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span3(dynamic_cast(span3.release())); - Span& zipkin_zipkin_span3 = zipkin_span3->span(); + Zipkin::SpanPtr zipkin_span3(dynamic_cast(span3.release())); std::vector annotations; - zipkin_zipkin_span3.setAnnotations(annotations); + zipkin_span3->setAnnotations(annotations); zipkin_span3->setTag("key3", "value3"); - EXPECT_EQ(1ULL, zipkin_zipkin_span3.binaryAnnotations().size()); - EXPECT_EQ("key3", zipkin_zipkin_span3.binaryAnnotations()[0].key()); - EXPECT_EQ("value3", zipkin_zipkin_span3.binaryAnnotations()[0].value()); + EXPECT_EQ(1ULL, zipkin_span3->binaryAnnotations().size()); + EXPECT_EQ("key3", zipkin_span3->binaryAnnotations()[0].key()); + EXPECT_EQ("value3", zipkin_span3->binaryAnnotations()[0].value()); // ==== // Test effective log() @@ -706,11 +832,10 @@ TEST_F(ZipkinDriverTest, ZipkinSpanTest) { std::chrono::duration_cast(timestamp.time_since_epoch()).count(); span4->log(timestamp, "abc"); - ZipkinSpanPtr zipkin_span4(dynamic_cast(span4.release())); - Span& zipkin_zipkin_span4 = zipkin_span4->span(); - EXPECT_FALSE(zipkin_zipkin_span4.annotations().empty()); - EXPECT_EQ(timestamp_count, zipkin_zipkin_span4.annotations().back().timestamp()); - EXPECT_EQ("abc", zipkin_zipkin_span4.annotations().back().value()); + Zipkin::SpanPtr zipkin_span4(dynamic_cast(span4.release())); + EXPECT_FALSE(zipkin_span4->annotations().empty()); + EXPECT_EQ(timestamp_count, zipkin_span4->annotations().back().timestamp()); + EXPECT_EQ("abc", zipkin_span4->annotations().back().value()); // ==== // Test baggage noop @@ -745,12 +870,12 @@ TEST_F(ZipkinDriverTest, ZipkinSpanContextFromB3HeadersTest) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_EQ(trace_id, zipkin_span->span().traceIdAsHexString()); - EXPECT_EQ(span_id, zipkin_span->span().idAsHexString()); - EXPECT_EQ(parent_id, zipkin_span->span().parentIdAsHexString()); - EXPECT_TRUE(zipkin_span->span().sampled()); + EXPECT_EQ(trace_id, zipkin_span->traceIdAsHexString()); + EXPECT_EQ(span_id, zipkin_span->idAsHexString()); + EXPECT_EQ(parent_id, zipkin_span->parentIdAsHexString()); + EXPECT_TRUE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, ZipkinSpanContextFromB3HeadersEmptyParentSpanTest) { @@ -769,8 +894,8 @@ TEST_F(ZipkinDriverTest, ZipkinSpanContextFromB3HeadersEmptyParentSpanTest) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_TRUE(zipkin_span->span().sampled()); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); + EXPECT_TRUE(zipkin_span->sampled()); } TEST_F(ZipkinDriverTest, ZipkinSpanContextFromB3Headers128TraceIdTest) { @@ -791,14 +916,14 @@ TEST_F(ZipkinDriverTest, ZipkinSpanContextFromB3Headers128TraceIdTest) { Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, operation_name_, {Tracing::Reason::Sampling, true}); - ZipkinSpanPtr zipkin_span(dynamic_cast(span.release())); + Zipkin::SpanPtr zipkin_span(dynamic_cast(span.release())); - EXPECT_EQ(trace_id_high, zipkin_span->span().traceIdHigh()); - EXPECT_EQ(trace_id_low, zipkin_span->span().traceId()); - EXPECT_EQ(trace_id, zipkin_span->span().traceIdAsHexString()); - EXPECT_EQ(span_id, zipkin_span->span().idAsHexString()); - EXPECT_EQ(parent_id, zipkin_span->span().parentIdAsHexString()); - EXPECT_TRUE(zipkin_span->span().sampled()); + EXPECT_EQ(trace_id_high, zipkin_span->traceIdHigh()); + EXPECT_EQ(trace_id_low, zipkin_span->traceId()); + EXPECT_EQ(trace_id, zipkin_span->traceIdAsHexString()); + EXPECT_EQ(span_id, zipkin_span->idAsHexString()); + EXPECT_EQ(parent_id, zipkin_span->parentIdAsHexString()); + EXPECT_TRUE(zipkin_span->sampled()); EXPECT_EQ(trace_id, zipkin_span->getTraceId()); EXPECT_EQ("", zipkin_span->getSpanId()); } @@ -879,6 +1004,38 @@ TEST_F(ZipkinDriverTest, ExplicitlySetSampledTrue) { EXPECT_EQ(SAMPLED, sampled_entry.value()); } +TEST_F(ZipkinDriverTest, UseLocalDecisionTrue) { + setupValidDriver("HTTP_JSON"); + + Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, + operation_name_, {Tracing::Reason::Sampling, true}); + EXPECT_TRUE(span->useLocalDecision()); + + request_headers_.remove(ZipkinCoreConstants::get().X_B3_SAMPLED.key()); + + span->injectContext(request_headers_, Tracing::UpstreamContext()); + + auto sampled_entry = request_headers_.get(ZipkinCoreConstants::get().X_B3_SAMPLED.key()); + EXPECT_EQ(SAMPLED, sampled_entry.value()); +} + +TEST_F(ZipkinDriverTest, UseLocalDecisionFalse) { + setupValidDriver("HTTP_JSON"); + request_headers_.set(ZipkinCoreConstants::get().X_B3_SAMPLED.key(), NOT_SAMPLED); + + // Envoy tracing decision is ignored if the B3 sampled header is set to not sample. + Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, + operation_name_, {Tracing::Reason::Sampling, true}); + EXPECT_FALSE(span->useLocalDecision()); + + request_headers_.remove(ZipkinCoreConstants::get().X_B3_SAMPLED.key()); + + span->injectContext(request_headers_, Tracing::UpstreamContext()); + + auto sampled_entry = request_headers_.get(ZipkinCoreConstants::get().X_B3_SAMPLED.key()); + EXPECT_EQ(NOT_SAMPLED, sampled_entry.value()); +} + TEST_F(ZipkinDriverTest, DuplicatedHeader) { setupValidDriver("HTTP_JSON"); request_headers_.set(ZipkinCoreConstants::get().X_B3_TRACE_ID.key(), @@ -908,6 +1065,156 @@ TEST_F(ZipkinDriverTest, DuplicatedHeader) { }); } +TEST_F(ZipkinDriverTest, ReporterFlushWithHttpServiceHeadersVerifyHeaders) { + cm_.initializeClusters({"fake_cluster", "legacy_cluster"}, {}); + + const std::string yaml_string = R"EOF( + collector_cluster: legacy_cluster + collector_endpoint: /legacy/api/v1/spans + collector_service: + http_uri: + uri: "https://zipkin-collector.example.com/api/v2/spans" + cluster: fake_cluster + timeout: 5s + request_headers_to_add: + - header: + key: "Authorization" + value: "Bearer token123" + - header: + key: "X-Custom-Header" + value: "custom-value" + - header: + key: "X-API-Key" + value: "api-key-123" + collector_endpoint_version: HTTP_JSON + )EOF"; + + envoy::config::trace::v3::ZipkinConfig zipkin_config; + TestUtility::loadFromYaml(yaml_string, zipkin_config); + setup(zipkin_config, true); + + Http::MockAsyncClientRequest request(&cm_.thread_local_cluster_.async_client_); + Http::AsyncClient::Callbacks* callback; + const absl::optional timeout(std::chrono::seconds(5)); + + // Set up expectations for the HTTP request with custom headers + EXPECT_CALL(cm_.thread_local_cluster_.async_client_, + send_(_, _, Http::AsyncClient::RequestOptions().setTimeout(timeout))) + .WillOnce( + Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callback = &callbacks; + + // Verify standard headers are present + EXPECT_EQ("/api/v2/spans", message->headers().getPathValue()); + EXPECT_EQ("zipkin-collector.example.com", message->headers().getHostValue()); + EXPECT_EQ("application/json", message->headers().getContentTypeValue()); + + // Verify custom headers are present + auto auth_header = message->headers().get(Http::LowerCaseString("authorization")); + EXPECT_FALSE(auth_header.empty()); + EXPECT_EQ("Bearer token123", auth_header[0]->value().getStringView()); + + auto custom_header = message->headers().get(Http::LowerCaseString("x-custom-header")); + EXPECT_FALSE(custom_header.empty()); + EXPECT_EQ("custom-value", custom_header[0]->value().getStringView()); + + auto api_key_header = message->headers().get(Http::LowerCaseString("x-api-key")); + EXPECT_FALSE(api_key_header.empty()); + EXPECT_EQ("api-key-123", api_key_header[0]->value().getStringView()); + + return &request; + })); + + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.zipkin.min_flush_spans", 5)) + .WillOnce(Return(1)); + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.zipkin.request_timeout", 5000U)) + .WillOnce(Return(5000U)); + + Tracing::SpanPtr span = driver_->startSpan(config_, request_headers_, stream_info_, + operation_name_, {Tracing::Reason::Sampling, true}); + span->finishSpan(); + + Http::ResponseHeaderMapPtr response_headers{ + new Http::TestResponseHeaderMapImpl{{":status", "202"}}}; + callback->onSuccess(request, + std::make_unique(std::move(response_headers))); +} + +// Test URI parsing edge cases to improve coverage +TEST_F(ZipkinDriverTest, DriverWithHttpServiceUriParsing) { + cm_.initializeClusters({"fake_cluster"}, {}); + + // Test 1: URI without hostname (should fallback to cluster name) + const std::string yaml_string_no_host = R"EOF( + collector_service: + http_uri: + uri: "/api/v2/spans" + cluster: fake_cluster + timeout: 5s + collector_endpoint_version: HTTP_JSON + )EOF"; + + envoy::config::trace::v3::ZipkinConfig zipkin_config_no_host; + TestUtility::loadFromYaml(yaml_string_no_host, zipkin_config_no_host); + setup(zipkin_config_no_host, false); + EXPECT_EQ("fake_cluster", driver_->hostnameForTest()); // Should fallback to cluster name +} + +TEST_F(ZipkinDriverTest, DriverWithHttpServiceUriParsingNoPath) { + cm_.initializeClusters({"fake_cluster"}, {}); + + // Test 2: URI with hostname but no path (should use "/" as default) + const std::string yaml_string_no_path = R"EOF( + collector_service: + http_uri: + uri: "https://zipkin-collector.example.com" + cluster: fake_cluster + timeout: 5s + collector_endpoint_version: HTTP_JSON + )EOF"; + + envoy::config::trace::v3::ZipkinConfig zipkin_config_no_path; + TestUtility::loadFromYaml(yaml_string_no_path, zipkin_config_no_path); + setup(zipkin_config_no_path, false); + EXPECT_EQ("zipkin-collector.example.com", driver_->hostnameForTest()); +} + +TEST_F(ZipkinDriverTest, DriverWithHttpServiceUriParsingWithPort) { + cm_.initializeClusters({"fake_cluster"}, {}); + + // Test 3: URI with hostname and port + const std::string yaml_string_with_port = R"EOF( + collector_service: + http_uri: + uri: "http://zipkin-collector.example.com:9411/api/v2/spans" + cluster: fake_cluster + timeout: 5s + collector_endpoint_version: HTTP_JSON + )EOF"; + + envoy::config::trace::v3::ZipkinConfig zipkin_config_with_port; + TestUtility::loadFromYaml(yaml_string_with_port, zipkin_config_with_port); + setup(zipkin_config_with_port, false); + EXPECT_EQ("zipkin-collector.example.com:9411", driver_->hostnameForTest()); +} + +TEST_F(ZipkinDriverTest, DriverMissingCollectorConfiguration) { + cm_.initializeClusters({"fake_cluster"}, {}); + + // Test missing both collector_cluster and collector_service + const std::string yaml_string_missing = R"EOF( + collector_endpoint_version: HTTP_JSON + )EOF"; + + envoy::config::trace::v3::ZipkinConfig zipkin_config_missing; + TestUtility::loadFromYaml(yaml_string_missing, zipkin_config_missing); + + EXPECT_THROW_WITH_MESSAGE(setup(zipkin_config_missing, false), EnvoyException, + "collector_cluster and collector_endpoint must be specified when not " + "using collector_service"); +} + } // namespace } // namespace Zipkin } // namespace Tracers diff --git a/test/extensions/transport_sockets/http_11_proxy/BUILD b/test/extensions/transport_sockets/http_11_proxy/BUILD index 66c830693601c..ebd0658b51f53 100644 --- a/test/extensions/transport_sockets/http_11_proxy/BUILD +++ b/test/extensions/transport_sockets/http_11_proxy/BUILD @@ -38,6 +38,7 @@ envoy_extension_cc_test( "//source/extensions/filters/http/dynamic_forward_proxy:config", "//source/extensions/filters/network/tcp_proxy:config", "//source/extensions/key_value/file_based:config_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//source/extensions/transport_sockets/http_11_proxy:upstream_config", "//test/integration:http_integration_lib", "//test/integration:integration_lib", diff --git a/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc b/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc index d09f2a0354982..d58c516d52651 100644 --- a/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc +++ b/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc @@ -30,6 +30,10 @@ class Http11ConnectHttpIntegrationTest : public testing::TestWithParammutable_cluster_type()); cluster->clear_load_assignment(); diff --git a/test/extensions/transport_sockets/http_11_proxy/connect_test.cc b/test/extensions/transport_sockets/http_11_proxy/connect_test.cc index e6b9c3a1aefc4..20450732e6a3f 100644 --- a/test/extensions/transport_sockets/http_11_proxy/connect_test.cc +++ b/test/extensions/transport_sockets/http_11_proxy/connect_test.cc @@ -113,7 +113,7 @@ class Http11ConnectTest : public testing::TestWithParamset_address(proxy_info_hostname); addr_proto.mutable_socket_address()->set_port_value(1234); - ProtobufWkt::Any anypb; + Protobuf::Any anypb; anypb.PackFrom(addr_proto); metadata->mutable_typed_filter_metadata()->emplace(std::make_pair(metadata_key, anypb)); EXPECT_CALL(*host, metadata()).Times(AnyNumber()).WillRepeatedly(Return(metadata)); @@ -153,14 +153,6 @@ TEST_P(Http11ConnectTest, HostWithPort) { injectHeaderOnceTest(); } -TEST_P(Http11ConnectTest, ProxySslPortRuntimeGuardDisabled) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.proxy_ssl_port", "false"}}); - - initialize(); - injectHeaderOnceTest(); -} - // Test injects CONNECT only once. Configured via endpoint metadata. TEST_P(Http11ConnectTest, InjectsHeaderOnlyOnceEndpointMetadata) { initializeWithMetadataProxyAddr(); diff --git a/test/extensions/transport_sockets/internal_upstream/internal_upstream_test.cc b/test/extensions/transport_sockets/internal_upstream/internal_upstream_test.cc index 0736d9407f7b6..f43fdc8c125e7 100644 --- a/test/extensions/transport_sockets/internal_upstream/internal_upstream_test.cc +++ b/test/extensions/transport_sockets/internal_upstream/internal_upstream_test.cc @@ -83,8 +83,8 @@ TEST_F(InternalSocketTest, PassthroughStateInjected) { filter_state_objects_.push_back( {filter_state_object, StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::StreamSharingMayImpactPooling::SharedWithUpstreamConnection, "test.object"}); - ProtobufWkt::Struct& map = (*metadata_->mutable_filter_metadata())["envoy.test"]; - ProtobufWkt::Value val; + Protobuf::Struct& map = (*metadata_->mutable_filter_metadata())["envoy.test"]; + Protobuf::Value val; val.set_string_value("val"); (*map.mutable_fields())["key"] = val; diff --git a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc index 9c3190e72e9ab..8079ed6adc933 100644 --- a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc +++ b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc @@ -490,7 +490,7 @@ class ProxyProtocolTLVsIntegrationTest : public testing::TestWithParamset_type(tlv.first); entry->set_value(std::string(tlv.second.begin(), tlv.second.end())); } - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(tlvs_metadata); const std::string metadata_key = Config::MetadataFilters::get().ENVOY_TRANSPORT_SOCKETS_PROXY_PROTOCOL; diff --git a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc index 9c8abb2a695b6..5964ce747f2bc 100644 --- a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc +++ b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_test.cc @@ -781,7 +781,7 @@ TEST_F(ProxyProtocolTest, V2CustomTLVsFromHostMetadata) { host_added_tlvs->set_type(0x96); host_added_tlvs->set_value("moredata"); - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(host_metadata_config); metadata->mutable_typed_filter_metadata()->emplace(std::make_pair(metadata_key, typed_metadata)); EXPECT_CALL(*host, metadata()).Times(testing::AnyNumber()).WillRepeatedly(Return(metadata)); @@ -844,7 +844,7 @@ TEST_F(ProxyProtocolTest, V2CombinedPrecedenceHostConfigPassthrough) { host_added_tlvs->set_type(0x99); host_added_tlvs->set_value("hostValue"); - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(host_metadata_config); metadata->mutable_typed_filter_metadata()->emplace(std::make_pair(metadata_key, typed_metadata)); EXPECT_CALL(*host, metadata()).WillRepeatedly(Return(metadata)); @@ -908,7 +908,7 @@ TEST_F(ProxyProtocolTest, V2DuplicateTLVsInConfigAndMetadataHandledProperly) { auto duplicate_host_entry = host_metadata_config.add_added_tlvs(); duplicate_host_entry->set_type(0x98); duplicate_host_entry->set_value("d2"); // Last duplicate value - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(host_metadata_config); metadata->mutable_typed_filter_metadata()->emplace(std::make_pair(metadata_key, typed_metadata)); EXPECT_CALL(*host, metadata()).WillRepeatedly(Return(metadata)); @@ -979,7 +979,7 @@ TEST_F(ProxyProtocolTest, V2CustomTLVMetadataInvalidFormat) { envoy::config::core::v3::Address addr_proto; addr_proto.mutable_socket_address()->set_address("0.0.0.0"); addr_proto.mutable_socket_address()->set_port_value(1234); - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(addr_proto); metadata->mutable_typed_filter_metadata()->emplace(std::make_pair(metadata_key, typed_metadata)); EXPECT_CALL(*host, metadata()).Times(testing::AnyNumber()).WillRepeatedly(Return(metadata)); @@ -1034,7 +1034,7 @@ TEST_F(ProxyProtocolTest, V2CustomTLVHostMetadataMissing) { outbound-proxy-protocol: true )EOF", socket_match_metadata); - ProtobufWkt::Any typed_metadata; + Protobuf::Any typed_metadata; typed_metadata.PackFrom(socket_match_metadata); auto host = std::make_shared>(); diff --git a/test/extensions/transport_sockets/tap/tap_config_impl_test.cc b/test/extensions/transport_sockets/tap/tap_config_impl_test.cc index 5dd98221543fa..397e04eca2190 100644 --- a/test/extensions/transport_sockets/tap/tap_config_impl_test.cc +++ b/test/extensions/transport_sockets/tap/tap_config_impl_test.cc @@ -371,8 +371,8 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsFalse) { pegging_counter_ = local_pegging_counter; } -// Verify the full streaming flow for submiting tapped message on all cases -// When send_streamed_msg_on_configured_size_ is True. +// Verify the full streaming flow for submiting tapped message on all cases. +// When the send_streamed_msg_on_configured_size_ is True. TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrue) { // Keep the original value. bool local_output_conn_info_per_event = output_conn_info_per_event_; @@ -382,7 +382,6 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrue) { bool local_send_streamed_msg_on_configured_size_ = send_streamed_msg_on_configured_size_; send_streamed_msg_on_configured_size_ = true; bool local_default_min_buffered_bytes = default_min_buffered_bytes_; - default_min_buffered_bytes_ = 15; // Submit when the transport socket is created. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( @@ -403,7 +402,10 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrue) { InSequence s; - // Submit when the transport socket is gotten read event + // Submit the single read event. + default_min_buffered_bytes_ = 50; + EXPECT_CALL(*config_, minStreamedSentBytes()).WillRepeatedly(Return(default_min_buffered_bytes_)); + EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -426,7 +428,8 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrue) { )EOF"))); tapper_->onRead(Buffer::OwnedImpl("Test transport socket tap buffered data onRead submit"), 53); - // Submit when the transport socket is gotten write event + // Submit the single write event. + EXPECT_CALL(*config_, minStreamedSentBytes()).WillRepeatedly(Return(default_min_buffered_bytes_)); EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -452,7 +455,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrue) { tapper_->onWrite(Buffer::OwnedImpl("Test transport socket tap buffered data onWrite submit"), 54, true); - // Submit when the transport socket is gotten close event + // Submit when the transport socket is gotten close event. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -474,15 +477,16 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrue) { time_system_.setSystemTime(std::chrono::seconds(2)); tapper_->closeSocket(Network::ConnectionEvent::RemoteClose); - // Restore the value + // Restore the value. output_conn_info_per_event_ = local_output_conn_info_per_event; pegging_counter_ = local_pegging_counter; send_streamed_msg_on_configured_size_ = local_send_streamed_msg_on_configured_size_; default_min_buffered_bytes_ = local_default_min_buffered_bytes; } -// Verify the full streaming flow for submiting tapped message on all cases -// When send_streamed_msg_on_configured_size_ is True and two read events +// Verify the full streaming flow for submiting tapped message on all cases. +// When the send_streamed_msg_on_configured_size_ is True and two read events. +// and submitted because aged duration is reached threshold. TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEvents) { // Keep the original value. bool local_output_conn_info_per_event = output_conn_info_per_event_; @@ -492,9 +496,9 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEve bool local_send_streamed_msg_on_configured_size_ = send_streamed_msg_on_configured_size_; send_streamed_msg_on_configured_size_ = true; bool local_default_min_buffered_bytes = default_min_buffered_bytes_; - default_min_buffered_bytes_ = 100; + default_min_buffered_bytes_ = 120; - // Submit when the transport socket is created + // Submit when the transport socket is created. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -513,7 +517,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEve InSequence s; - // Submit when the transport socket is gotten read event + // Store the read event. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -533,7 +537,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEve socket_address: address: 10.0.0.3 port_value: 50000 - - timestamp: 1970-01-01T00:00:01Z + - timestamp: 1970-01-01T00:00:15Z read: data: as_bytes: VGVzdCB0cmFuc3BvcnQgc29ja2V0IHRhcCBidWZmZXJlZCBkYXRhIG9uUmVhZCBzdWJtaXQ= @@ -548,10 +552,10 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEve port_value: 50000 )EOF"))); tapper_->onRead(Buffer::OwnedImpl("Test transport socket tap buffered data onRead submit"), 53); - time_system_.setSystemTime(std::chrono::seconds(1)); + time_system_.setSystemTime(std::chrono::seconds(15)); tapper_->onRead(Buffer::OwnedImpl("Test transport socket tap buffered data onRead submit"), 53); - // Submit when the transport socket is gotten close event + // Submit when the transport socket is gotten close event. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -573,7 +577,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEve time_system_.setSystemTime(std::chrono::seconds(2)); tapper_->closeSocket(Network::ConnectionEvent::RemoteClose); - // Restore the value + // Restore the value. output_conn_info_per_event_ = local_output_conn_info_per_event; pegging_counter_ = local_pegging_counter; send_streamed_msg_on_configured_size_ = local_send_streamed_msg_on_configured_size_; @@ -581,7 +585,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueTwoReadEve } // Verify the full streaming flow for submiting tapped message on all cases -// When send_streamed_msg_on_configured_size_ is True and two write events +// When the send_streamed_msg_on_configured_size_ is True and two write events TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTruetwoWriteEvents) { // Keep the original value. bool local_output_conn_info_per_event = output_conn_info_per_event_; @@ -591,9 +595,9 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTruetwoWriteEv bool local_send_streamed_msg_on_configured_size_ = send_streamed_msg_on_configured_size_; send_streamed_msg_on_configured_size_ = true; bool local_default_min_buffered_bytes = default_min_buffered_bytes_; - default_min_buffered_bytes_ = 100; + default_min_buffered_bytes_ = 120; - // Submit when the transport socket is created + // Submit when the transport socket is created. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -612,7 +616,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTruetwoWriteEv InSequence s; - // Submit when the transport socket is gotten write event + // Submit when the aged duration is equal 12. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -633,7 +637,7 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTruetwoWriteEv socket_address: address: 10.0.0.3 port_value: 50000 - - timestamp: 1970-01-01T00:00:01Z + - timestamp: 1970-01-01T00:00:15Z write: data: as_bytes: VGVzdCB0cmFuc3BvcnQgc29ja2V0IHRhcCBidWZmZXJlZCBkYXRhIG9uV3JpdGUgc3VibWl0 @@ -650,11 +654,11 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTruetwoWriteEv )EOF"))); tapper_->onWrite(Buffer::OwnedImpl("Test transport socket tap buffered data onWrite submit"), 54, true); - time_system_.setSystemTime(std::chrono::seconds(1)); + time_system_.setSystemTime(std::chrono::seconds(15)); tapper_->onWrite(Buffer::OwnedImpl("Test transport socket tap buffered data onWrite submit"), 54, true); - // Submit when the transport socket is gotten close event + // Submit when the transport socket is gotten close event. EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( R"EOF( socket_streamed_trace_segment: @@ -676,7 +680,101 @@ TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTruetwoWriteEv time_system_.setSystemTime(std::chrono::seconds(2)); tapper_->closeSocket(Network::ConnectionEvent::RemoteClose); - // Restore the value + // Restore the value. + output_conn_info_per_event_ = local_output_conn_info_per_event; + pegging_counter_ = local_pegging_counter; + send_streamed_msg_on_configured_size_ = local_send_streamed_msg_on_configured_size_; + default_min_buffered_bytes_ = local_default_min_buffered_bytes; +} + +// All data are submitted in close event +TEST_F(PerSocketTapperImplTest, StreamingFlowWhenSendStreamedMsgIsTrueInCloseEvents) { + // Keep the original value. + bool local_output_conn_info_per_event = output_conn_info_per_event_; + output_conn_info_per_event_ = true; + bool local_pegging_counter = pegging_counter_; + pegging_counter_ = true; + bool local_send_streamed_msg_on_configured_size_ = send_streamed_msg_on_configured_size_; + send_streamed_msg_on_configured_size_ = true; + bool local_default_min_buffered_bytes = default_min_buffered_bytes_; + default_min_buffered_bytes_ = 128; + + // Submit when the transport socket is created. + EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( + R"EOF( +socket_streamed_trace_segment: + trace_id: 1 + connection: + local_address: + socket_address: + address: 127.0.0.1 + port_value: 1000 + remote_address: + socket_address: + address: 10.0.0.3 + port_value: 50000 +)EOF"))); + setup(true); + + InSequence s; + + time_system_.setSystemTime(std::chrono::seconds(1)); + EXPECT_CALL(*sink_manager_, submitTrace_(TraceEqual( + R"EOF( +socket_streamed_trace_segment: + trace_id: 1 + events: + events: + - timestamp: 1970-01-01T00:00:01Z + read: + data: + as_bytes: VGVzdCB0cmFuc3BvcnQgc29ja2V0IHRhcCBidWZmZXJlZCBkYXRhIG9uUmVhZCBzdWJtaXQ= + + connection: + local_address: + socket_address: + address: 127.0.0.1 + port_value: 1000 + remote_address: + socket_address: + address: 10.0.0.3 + port_value: 50000 + - timestamp: 1970-01-01T00:00:02Z + write: + data: + as_bytes: VGVzdCB0cmFuc3BvcnQgc29ja2V0IHRhcCBidWZmZXJlZCBkYXRhIG9uV3JpdGUgc3VibWl0 + + end_stream: true + connection: + local_address: + socket_address: + address: 127.0.0.1 + port_value: 1000 + remote_address: + socket_address: + address: 10.0.0.3 + port_value: 50000 + - timestamp: 1970-01-01T00:00:03Z + closed: {} + connection: + local_address: + socket_address: + address: 127.0.0.1 + port_value: 1000 + remote_address: + socket_address: + address: 10.0.0.3 + port_value: 50000 +)EOF"))); + tapper_->onRead(Buffer::OwnedImpl("Test transport socket tap buffered data onRead submit"), 53); + time_system_.setSystemTime(std::chrono::seconds(2)); + tapper_->onWrite(Buffer::OwnedImpl("Test transport socket tap buffered data onWrite submit"), 54, + true); + + time_system_.setSystemTime(std::chrono::seconds(3)); + tapper_->closeSocket(Network::ConnectionEvent::RemoteClose); + + // Restore the value. output_conn_info_per_event_ = local_output_conn_info_per_event; pegging_counter_ = local_pegging_counter; send_streamed_msg_on_configured_size_ = local_send_streamed_msg_on_configured_size_; diff --git a/test/extensions/transport_sockets/tls/cert_validator/spiffe/spiffe_validator_test.cc b/test/extensions/transport_sockets/tls/cert_validator/spiffe/spiffe_validator_test.cc index dfe9be63297fe..c874aa7a1f5ba 100644 --- a/test/extensions/transport_sockets/tls/cert_validator/spiffe/spiffe_validator_test.cc +++ b/test/extensions/transport_sockets/tls/cert_validator/spiffe/spiffe_validator_test.cc @@ -751,15 +751,16 @@ name: envoy.tls.cert_validator.spiffe bool foundTestCA = false; SSLContextPtr ctx = SSL_CTX_new(TLS_method()); ASSERT_TRUE(validator().addClientValidationContext(ctx.get(), false).ok()); - for (X509_NAME* name : SSL_CTX_get_client_CA_list(ctx.get())) { + for (const X509_NAME* name : SSL_CTX_get_client_CA_list(ctx.get())) { const int cn_index = X509_NAME_get_index_by_NID(name, NID_commonName, -1); EXPECT_TRUE(cn_index >= 0); - X509_NAME_ENTRY* cn_entry = X509_NAME_get_entry(name, cn_index); + const X509_NAME_ENTRY* cn_entry = X509_NAME_get_entry(name, cn_index); EXPECT_TRUE(cn_entry); - ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry); + const ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry); EXPECT_TRUE(cn_asn1); - auto cn_str = std::string(reinterpret_cast(ASN1_STRING_data(cn_asn1))); + auto cn_str = std::string(reinterpret_cast(ASN1_STRING_get0_data(cn_asn1)), + ASN1_STRING_length(cn_asn1)); if (cn_str == "Test Server") { foundTestServer = true; } else if (cn_str == "Test CA") { diff --git a/test/extensions/upstreams/http/config_test.cc b/test/extensions/upstreams/http/config_test.cc index 4e443f8fcafe7..155ad08367b99 100644 --- a/test/extensions/upstreams/http/config_test.cc +++ b/test/extensions/upstreams/http/config_test.cc @@ -21,6 +21,7 @@ namespace Extensions { namespace Upstreams { namespace Http { +using ::testing::ContainsRegex; using ::testing::InvokeWithoutArgs; using ::testing::NiceMock; using ::testing::StrictMock; @@ -98,11 +99,11 @@ TEST_F(ConfigTest, KvStoreConcurrencyFail) { ->mutable_alternate_protocols_cache_options() ->mutable_key_value_store_config(); server_context_.options_.concurrency_ = 2; - EXPECT_EQ( - ProtocolOptionsConfigImpl::createProtocolOptionsConfig(options_, server_context_) - .status() - .message(), - "options has key value store but Envoy has concurrency = 2 : key_value_store_config {\n}\n"); + EXPECT_THAT(ProtocolOptionsConfigImpl::createProtocolOptionsConfig(options_, server_context_) + .status() + .message(), + ContainsRegex("(?s)options has key value store but Envoy has concurrency = 2 " + ":.*key_value_store_config {\n}\n")); } namespace { @@ -147,7 +148,7 @@ class DefaultHeaderValidatorFactoryConfigOverride createFromProto(const Protobuf::Message& message, Server::Configuration::ServerFactoryContext& server_context) override { auto mptr = ::Envoy::Config::Utility::translateAnyToFactoryConfig( - dynamic_cast(message), server_context.messageValidationVisitor(), + dynamic_cast(message), server_context.messageValidationVisitor(), *this); const auto& proto_config = MessageUtil::downcastAndValidatefull_name() == "google.protobuf.Any") { - auto* any_message = Protobuf::DynamicCastMessage(&message); + auto* any_message = Protobuf::DynamicCastMessage(&message); inner_message = Helper::typeUrlToMessage(any_message->type_url()); target_type_url = any_message->type_url(); if (inner_message) { diff --git a/test/fuzz/utility.h b/test/fuzz/utility.h index d1ac39a70aa63..230f01f8a19e8 100644 --- a/test/fuzz/utility.h +++ b/test/fuzz/utility.h @@ -6,6 +6,7 @@ #include "source/common/network/resolver_impl.h" #include "source/common/network/socket_impl.h" #include "source/common/network/utility.h" +#include "source/common/protobuf/utility.h" #include "test/common/stream_info/test_util.h" #include "test/fuzz/common.pb.h" @@ -71,7 +72,7 @@ replaceInvalidStringValues(const envoy::config::core::v3::Metadata& upstream_met // This clears any invalid characters in string values. It may not be likely a coverage-driven // fuzzer will explore recursive structs, so this case is not handled here. for (auto& field : *metadata_struct.second.mutable_fields()) { - if (field.second.kind_case() == ProtobufWkt::Value::kStringValue) { + if (field.second.kind_case() == Protobuf::Value::kStringValue) { field.second.set_string_value(replaceInvalidCharacters(field.second.string_value())); } } @@ -208,8 +209,8 @@ inline std::vector parseHttpData(const test::fuzz::HttpData& data) data_chunks.push_back(http_data); } } else if (data.has_proto_body()) { - const std::string serialized = data.proto_body().message().value(); - data_chunks = absl::StrSplit(serialized, absl::ByLength(data.proto_body().chunk_size())); + data_chunks = absl::StrSplit(MessageUtil::bytesToString(data.proto_body().message().value()), + absl::ByLength(data.proto_body().chunk_size())); } return data_chunks; diff --git a/test/fuzz/validated_input_generator.cc b/test/fuzz/validated_input_generator.cc index 2d53ac0f406ac..9427b9a8feae9 100644 --- a/test/fuzz/validated_input_generator.cc +++ b/test/fuzz/validated_input_generator.cc @@ -164,8 +164,8 @@ void ValidatedInputGenerator::handleAnyRules( if (any_rules.has_required() && any_rules.required()) { // Stop creating any message when a certain depth is reached if (max_depth_ > 0 && current_depth_ > max_depth_) { - auto* any_message = Protobuf::DynamicCastMessage(msg); - any_message->PackFrom(ProtobufWkt::Struct()); + auto* any_message = Protobuf::DynamicCastMessage(msg); + any_message->PackFrom(Protobuf::Struct()); return; } const Protobuf::Descriptor* descriptor = msg->GetDescriptor(); @@ -178,7 +178,7 @@ void ValidatedInputGenerator::handleAnyRules( const std::string field_name = std::string(message_path_.back()); FieldToTypeUrls::const_iterator field_to_typeurls_cand = field_to_typeurls.find(field_name); if (field_to_typeurls_cand != any_map_cand->second.end()) { - auto* any_message = Protobuf::DynamicCastMessage(msg); + auto* any_message = Protobuf::DynamicCastMessage(msg); inner_message = ProtobufMessage::Helper::typeUrlToMessage(any_message->type_url()); if (!inner_message || !any_message->UnpackTo(inner_message.get())) { const TypeUrlAndFactory& randomed_typeurl = field_to_typeurls_cand->second.at( @@ -415,7 +415,7 @@ void ValidatedInputGenerator::onEnterMessage(Protobuf::Message& msg, const Protobuf::Descriptor* descriptor = msg.GetDescriptor(); message_path_.push_back(field_name); if (descriptor->full_name() == kAny) { - auto* any_message = Protobuf::DynamicCastMessage(&msg); + auto* any_message = Protobuf::DynamicCastMessage(&msg); std::unique_ptr inner_message = ProtobufMessage::Helper::typeUrlToMessage(any_message->type_url()); if (!inner_message || !any_message->UnpackTo(inner_message.get())) { @@ -464,7 +464,7 @@ void ValidatedInputGenerator::onLeaveMessage(Protobuf::Message&, ValidatedInputGenerator::AnyMap ValidatedInputGenerator::getDefaultAnyMap() { static const auto dummy_proto_msg = []() -> std::unique_ptr { - return std::make_unique(); + return std::make_unique(); }; static const ValidatedInputGenerator::ListOfTypeUrlAndFactory matchers = { diff --git a/test/integration/BUILD b/test/integration/BUILD index 4449d2516603f..86e56f222bc9f 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -81,6 +81,31 @@ envoy_cc_test( ], ) +envoy_cc_test_library( + name = "xdstp_config_sources_integration_lib", + hdrs = [ + "xdstp_config_sources_integration.h", + ], + rbe_pool = "2core", + deps = [ + ":ads_integration_lib", + ":http_integration_lib", + "//source/common/config:protobuf_link_hacks", + "//source/common/protobuf:utility_lib", + "//source/common/version:version_lib", + "//test/common/grpc:grpc_client_integration_lib", + "//test/test_common:network_utility_lib", + "//test/test_common:resources_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/endpoint/v3:pkg_cc_proto", + "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "xdstp_config_sources_integration_test", srcs = ["xdstp_config_sources_integration_test.cc"], @@ -89,8 +114,8 @@ envoy_cc_test( "cpu:3", ], deps = [ - ":ads_integration_lib", ":http_integration_lib", + ":xdstp_config_sources_integration_lib", "//source/common/config:protobuf_link_hacks", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", @@ -106,16 +131,15 @@ envoy_cc_test( ], ) -envoy_cc_test( - name = "ads_xdstp_config_sources_integration_test", - srcs = ["ads_xdstp_config_sources_integration_test.cc"], +envoy_cc_test_library( + name = "ads_xdstp_config_sources_integration_lib", + hdrs = ["ads_xdstp_config_sources_integration.h"], rbe_pool = "4core", tags = [ "cpu:3", ], deps = [ ":ads_integration_lib", - ":http_integration_lib", "//source/common/config:protobuf_link_hacks", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", @@ -131,6 +155,18 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "ads_xdstp_config_sources_integration_test", + srcs = ["ads_xdstp_config_sources_integration_test.cc"], + rbe_pool = "4core", + tags = [ + "cpu:3", + ], + deps = [ + ":ads_xdstp_config_sources_integration_lib", + ], +) + envoy_cc_test( name = "alpn_integration_test", size = "large", @@ -430,8 +466,8 @@ envoy_cc_test_binary( "//source/common/http:rds_lib", "//source/exe:envoy_main_common_with_core_extensions_lib", "//source/exe:platform_impl_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/static:static_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/load_balancing_policies/cluster_provided:config", "//source/extensions/load_balancing_policies/least_request:config", "//source/extensions/load_balancing_policies/random:config", @@ -903,9 +939,9 @@ envoy_cc_test_library( ":http_protocol_integration_lib", ":socket_interface_swap_lib", "//source/common/http:header_map_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/filters/http/buffer:config", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/common/http/http2:http2_frame", "//test/integration/filters:add_invalid_data_filter_lib", "//test/integration/filters:buffer_continue_filter_lib", @@ -1261,7 +1297,6 @@ envoy_cc_test_library( "//source/extensions/load_balancing_policies/maglev:config", "//source/extensions/load_balancing_policies/random:config", "//source/extensions/load_balancing_policies/round_robin:config", - "//source/extensions/network/dns_resolver/cares:config", ], ) @@ -2030,6 +2065,7 @@ envoy_cc_test( "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/access_loggers/file/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/tcp_proxy/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/request_id/uuid/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/upstreams/http/tcp/v3:pkg_cc_proto", ], ) @@ -2737,9 +2773,11 @@ envoy_cc_test( deps = [ ":http_integration_lib", ":integration_lib", + "//source/extensions/filters/http/header_mutation:config", "//test/integration/filters:repick_cluster_filter_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/header_mutation/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", ], ) @@ -2882,8 +2920,7 @@ envoy_cc_test( ":http_protocol_integration_lib", "//source/common/http:character_set_validation_lib", "//source/common/http:header_map_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/filters/http/buffer:config", "//source/extensions/http/header_validators/envoy_default:character_tables", "//test/test_common:logging_lib", diff --git a/test/integration/access_log_integration_test.cc b/test/integration/access_log_integration_test.cc index cce81f75187a5..cf3c54d7e0b4e 100644 --- a/test/integration/access_log_integration_test.cc +++ b/test/integration/access_log_integration_test.cc @@ -45,7 +45,7 @@ TEST_P(AccessLogIntegrationTest, ShouldReplaceInvalidUtf8) { auto* log_format = access_log_config.mutable_log_format(); auto* json = log_format->mutable_json_format(); - Envoy::ProtobufWkt::Value v; + Envoy::Protobuf::Value v; v.set_string_value("%REQ(X-FORWARDED-FOR)%"); auto fields = json->mutable_fields(); (*fields)["x_forwarded_for"] = v; diff --git a/test/integration/admin_html/BUILD b/test/integration/admin_html/BUILD index e3f777c8fcfad..e9d7912b2bef2 100644 --- a/test/integration/admin_html/BUILD +++ b/test/integration/admin_html/BUILD @@ -18,7 +18,7 @@ envoy_cc_test_binary( "//source/common/formatter:formatter_extension_lib", "//source/exe:envoy_main_common_with_core_extensions_lib", "//source/exe:platform_impl_lib", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/static:static_cluster_lib", "//source/server/admin:admin_html_util", "@com_google_absl//absl/debugging:symbolize", diff --git a/test/integration/ads_integration.cc b/test/integration/ads_integration.cc index 41c82b5565633..5a47325c9b61c 100644 --- a/test/integration/ads_integration.cc +++ b/test/integration/ads_integration.cc @@ -21,72 +21,75 @@ using testing::AssertionResult; namespace Envoy { -AdsIntegrationTest::AdsIntegrationTest() +AdsIntegrationTestBase::AdsIntegrationTestBase(Network::Address::IpVersion ip_version, + Grpc::SotwOrDelta sotw_or_delta) : HttpIntegrationTest( - Http::CodecType::HTTP2, ipVersion(), - ConfigHelper::adsBootstrap((sotwOrDelta() == Grpc::SotwOrDelta::Sotw) || - (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw) + Http::CodecType::HTTP2, ip_version, + ConfigHelper::adsBootstrap((sotw_or_delta == Grpc::SotwOrDelta::Sotw) || + (sotw_or_delta == Grpc::SotwOrDelta::UnifiedSotw) ? "GRPC" : "DELTA_GRPC")) { - commonInitialize(); + commonInitialize(sotw_or_delta); } -AdsIntegrationTest::AdsIntegrationTest(const std::string& config) - : HttpIntegrationTest(Http::CodecType::HTTP2, ipVersion(), config) { - commonInitialize(); +AdsIntegrationTestBase::AdsIntegrationTestBase(Network::Address::IpVersion ip_version, + Grpc::SotwOrDelta sotw_or_delta, + const std::string& config) + : HttpIntegrationTest(Http::CodecType::HTTP2, ip_version, config) { + commonInitialize(sotw_or_delta); } -void AdsIntegrationTest::commonInitialize() { +void AdsIntegrationTestBase::commonInitialize(Grpc::SotwOrDelta sotw_or_delta) { config_helper_.addRuntimeOverride("envoy.reloadable_features.unified_mux", - (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw || - sotwOrDelta() == Grpc::SotwOrDelta::UnifiedDelta) + (sotw_or_delta == Grpc::SotwOrDelta::UnifiedSotw || + sotw_or_delta == Grpc::SotwOrDelta::UnifiedDelta) ? "true" : "false"); use_lds_ = false; create_xds_upstream_ = true; tls_xds_upstream_ = true; - sotw_or_delta_ = sotwOrDelta(); + sotw_or_delta_ = sotw_or_delta; setUpstreamProtocol(Http::CodecType::HTTP2); } -void AdsIntegrationTest::TearDown() { cleanUpXdsConnection(); } - envoy::config::cluster::v3::Cluster -AdsIntegrationTest::buildCluster(const std::string& name, - envoy::config::cluster::v3::Cluster::LbPolicy lb_policy) { +AdsIntegrationTestBase::buildCluster(const std::string& name, + envoy::config::cluster::v3::Cluster::LbPolicy lb_policy) { return ConfigHelper::buildCluster(name, lb_policy); } -envoy::config::cluster::v3::Cluster AdsIntegrationTest::buildTlsCluster(const std::string& name) { +envoy::config::cluster::v3::Cluster +AdsIntegrationTestBase::buildTlsCluster(const std::string& name) { return ConfigHelper::buildTlsCluster(name, envoy::config::cluster::v3::Cluster::ROUND_ROBIN); } -envoy::config::cluster::v3::Cluster AdsIntegrationTest::buildRedisCluster(const std::string& name) { +envoy::config::cluster::v3::Cluster +AdsIntegrationTestBase::buildRedisCluster(const std::string& name) { return ConfigHelper::buildCluster(name, envoy::config::cluster::v3::Cluster::MAGLEV); } envoy::config::endpoint::v3::ClusterLoadAssignment -AdsIntegrationTest::buildClusterLoadAssignment(const std::string& name) { +AdsIntegrationTestBase::buildClusterLoadAssignment(const std::string& name) { return ConfigHelper::buildClusterLoadAssignment( name, Network::Test::getLoopbackAddressString(ipVersion()), fake_upstreams_[0]->localAddress()->ip()->port()); } envoy::config::endpoint::v3::ClusterLoadAssignment -AdsIntegrationTest::buildTlsClusterLoadAssignment(const std::string& name) { +AdsIntegrationTestBase::buildTlsClusterLoadAssignment(const std::string& name) { return ConfigHelper::buildClusterLoadAssignment( name, Network::Test::getLoopbackAddressString(ipVersion()), 8443); } envoy::config::endpoint::v3::ClusterLoadAssignment -AdsIntegrationTest::buildClusterLoadAssignmentWithLeds(const std::string& name, - const std::string& collection_name) { +AdsIntegrationTestBase::buildClusterLoadAssignmentWithLeds(const std::string& name, + const std::string& collection_name) { return ConfigHelper::buildClusterLoadAssignmentWithLeds(name, collection_name); } envoy::service::discovery::v3::Resource -AdsIntegrationTest::buildLbEndpointResource(const std::string& lb_endpoint_resource_name, - const std::string& version) { +AdsIntegrationTestBase::buildLbEndpointResource(const std::string& lb_endpoint_resource_name, + const std::string& version) { envoy::service::discovery::v3::Resource resource; resource.set_name(lb_endpoint_resource_name); resource.set_version(version); @@ -99,14 +102,14 @@ AdsIntegrationTest::buildLbEndpointResource(const std::string& lb_endpoint_resou } envoy::config::listener::v3::Listener -AdsIntegrationTest::buildListener(const std::string& name, const std::string& route_config, - const std::string& stat_prefix) { +AdsIntegrationTestBase::buildListener(const std::string& name, const std::string& route_config, + const std::string& stat_prefix) { return ConfigHelper::buildListener( name, route_config, Network::Test::getLoopbackAddressString(ipVersion()), stat_prefix); } envoy::config::listener::v3::Listener -AdsIntegrationTest::buildRedisListener(const std::string& name, const std::string& cluster) { +AdsIntegrationTestBase::buildRedisListener(const std::string& name, const std::string& cluster) { std::string redis = fmt::format( R"EOF( filters: @@ -126,19 +129,19 @@ AdsIntegrationTest::buildRedisListener(const std::string& name, const std::strin } envoy::config::route::v3::RouteConfiguration -AdsIntegrationTest::buildRouteConfig(const std::string& name, const std::string& cluster) { +AdsIntegrationTestBase::buildRouteConfig(const std::string& name, const std::string& cluster) { return ConfigHelper::buildRouteConfig(name, cluster); } -void AdsIntegrationTest::makeSingleRequest() { +void AdsIntegrationTestBase::makeSingleRequest() { registerTestServerPorts({"http"}); testRouterHeaderOnlyRequestAndResponse(); cleanupUpstreamAndDownstream(); } -void AdsIntegrationTest::initialize() { initializeAds(false); } +void AdsIntegrationTestBase::initialize() { initializeAds(false); } -void AdsIntegrationTest::initializeAds(const bool rate_limiting) { +void AdsIntegrationTestBase::initializeAds(const bool rate_limiting) { config_helper_.addConfigModifier([this, &rate_limiting]( envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* ads_config = bootstrap.mutable_dynamic_resources()->mutable_ads_config(); @@ -176,109 +179,111 @@ void AdsIntegrationTest::initializeAds(const bool rate_limiting) { } } -void AdsIntegrationTest::testBasicFlow() { +void AdsIntegrationTestBase::testBasicFlow() { // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); makeSingleRequest(); - const ProtobufWkt::Timestamp first_active_listener_ts_1 = + const Protobuf::Timestamp first_active_listener_ts_1 = getListenersConfigDump().dynamic_listeners(0).active_state().last_updated(); - const ProtobufWkt::Timestamp first_active_cluster_ts_1 = + const Protobuf::Timestamp first_active_cluster_ts_1 = getClustersConfigDump().dynamic_active_clusters()[0].last_updated(); - const ProtobufWkt::Timestamp first_route_config_ts_1 = + const Protobuf::Timestamp first_route_config_ts_1 = getRoutesConfigDump().dynamic_route_configs()[0].last_updated(); // Upgrade RDS/CDS/EDS to a newer config, validate we can process a request. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("cluster_1"), buildCluster("cluster_2")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_1"), buildCluster("cluster_2")}, {buildCluster("cluster_1"), buildCluster("cluster_2")}, {"cluster_0"}, "2"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 2); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_1"), buildClusterLoadAssignment("cluster_2")}, {buildClusterLoadAssignment("cluster_1"), buildClusterLoadAssignment("cluster_2")}, {"cluster_0"}, "2"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 0); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_2", "cluster_1"}, {"cluster_2", "cluster_1"}, {"cluster_0"})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "2", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"cluster_2", "cluster_1"}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_1")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_1")}, {buildRouteConfig("route_config_0", "cluster_1")}, {}, "2"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "2", {"route_config_0"}, {}, {})); makeSingleRequest(); - const ProtobufWkt::Timestamp first_active_listener_ts_2 = + const Protobuf::Timestamp first_active_listener_ts_2 = getListenersConfigDump().dynamic_listeners(0).active_state().last_updated(); - const ProtobufWkt::Timestamp first_active_cluster_ts_2 = + const Protobuf::Timestamp first_active_cluster_ts_2 = getClustersConfigDump().dynamic_active_clusters()[0].last_updated(); - const ProtobufWkt::Timestamp first_route_config_ts_2 = + const Protobuf::Timestamp first_route_config_ts_2 = getRoutesConfigDump().dynamic_route_configs()[0].last_updated(); // Upgrade LDS/RDS, validate we can process a request. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, + Config::TestTypeUrl::get().Listener, {buildListener("listener_1", "route_config_1"), buildListener("listener_2", "route_config_2")}, {buildListener("listener_1", "route_config_1"), buildListener("listener_2", "route_config_2")}, {"listener_0"}, "2"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "2", {"route_config_2", "route_config_1", "route_config_0"}, {"route_config_2", "route_config_1"}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "2", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "2", {"route_config_2", "route_config_1"}, {}, {"route_config_0"})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_1", "cluster_1"), buildRouteConfig("route_config_2", "cluster_1")}, {buildRouteConfig("route_config_1", "cluster_1"), buildRouteConfig("route_config_2", "cluster_1")}, {"route_config_0"}, "3"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "3", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "3", {"route_config_2", "route_config_1"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 2); makeSingleRequest(); - const ProtobufWkt::Timestamp first_active_listener_ts_3 = + const Protobuf::Timestamp first_active_listener_ts_3 = getListenersConfigDump().dynamic_listeners(0).active_state().last_updated(); - const ProtobufWkt::Timestamp first_active_cluster_ts_3 = + const Protobuf::Timestamp first_active_cluster_ts_3 = getClustersConfigDump().dynamic_active_clusters()[0].last_updated(); - const ProtobufWkt::Timestamp first_route_config_ts_3 = + const Protobuf::Timestamp first_route_config_ts_3 = getRoutesConfigDump().dynamic_route_configs()[0].last_updated(); // Expect last_updated timestamps to be updated in a predictable way @@ -293,19 +298,19 @@ void AdsIntegrationTest::testBasicFlow() { EXPECT_GT(first_route_config_ts_3, first_route_config_ts_2); } -envoy::admin::v3::ClustersConfigDump AdsIntegrationTest::getClustersConfigDump() { +envoy::admin::v3::ClustersConfigDump AdsIntegrationTestBase::getClustersConfigDump() { auto message_ptr = test_server_->server().admin()->getConfigTracker().getCallbacksMap().at( "clusters")(Matchers::UniversalStringMatcher()); return dynamic_cast(*message_ptr); } -envoy::admin::v3::ListenersConfigDump AdsIntegrationTest::getListenersConfigDump() { +envoy::admin::v3::ListenersConfigDump AdsIntegrationTestBase::getListenersConfigDump() { auto message_ptr = test_server_->server().admin()->getConfigTracker().getCallbacksMap().at( "listeners")(Matchers::UniversalStringMatcher()); return dynamic_cast(*message_ptr); } -envoy::admin::v3::RoutesConfigDump AdsIntegrationTest::getRoutesConfigDump() { +envoy::admin::v3::RoutesConfigDump AdsIntegrationTestBase::getRoutesConfigDump() { auto message_ptr = test_server_->server().admin()->getConfigTracker().getCallbacksMap().at( "routes")(Matchers::UniversalStringMatcher()); return dynamic_cast(*message_ptr); diff --git a/test/integration/ads_integration.h b/test/integration/ads_integration.h index 5f6ec180be4e6..28b910faf0161 100644 --- a/test/integration/ads_integration.h +++ b/test/integration/ads_integration.h @@ -37,13 +37,12 @@ class AdsDeltaSotwIntegrationSubStateParamTest Grpc::SotwOrDelta sotwOrDelta() const { return std::get<2>(GetParam()); } }; -class AdsIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, - public HttpIntegrationTest { +class AdsIntegrationTestBase : public Grpc::BaseGrpcClientIntegrationParamTest, + public HttpIntegrationTest { public: - AdsIntegrationTest(); - AdsIntegrationTest(const std::string& config); - - void TearDown() override; + AdsIntegrationTestBase(Network::Address::IpVersion ip_version, Grpc::SotwOrDelta sotw_or_delta); + AdsIntegrationTestBase(Network::Address::IpVersion ip_version, Grpc::SotwOrDelta sotw_or_delta, + const std::string& config); envoy::config::cluster::v3::Cluster buildCluster(const std::string& name, envoy::config::cluster::v3::Cluster::LbPolicy lb_policy = @@ -87,7 +86,31 @@ class AdsIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, envoy::admin::v3::RoutesConfigDump getRoutesConfigDump(); private: - void commonInitialize(); + void commonInitialize(Grpc::SotwOrDelta sotw_or_delta); +}; + +class AdsIntegrationTest + : public AdsIntegrationTestBase, + public testing::TestWithParam< + std::tuple> { +public: + AdsIntegrationTest() : AdsIntegrationTestBase(ipVersion(), sotwOrDelta()) {} + AdsIntegrationTest(const std::string& config) + : AdsIntegrationTestBase(ipVersion(), sotwOrDelta(), config) {} + + void TearDown() override { cleanUpXdsConnection(); } + + static std::string protocolTestParamsToString( + const ::testing::TestParamInfo< + std::tuple>& p) { + return fmt::format( + "{}_{}_{}", TestUtility::ipVersionToString(std::get<0>(p.param)), + std::get<1>(p.param) == Grpc::ClientType::GoogleGrpc ? "GoogleGrpc" : "EnvoyGrpc", + std::get<2>(p.param) == Grpc::SotwOrDelta::Delta ? "Delta" : "StateOfTheWorld"); + } + Network::Address::IpVersion ipVersion() const override { return std::get<0>(GetParam()); } + Grpc::ClientType clientType() const override { return std::get<1>(GetParam()); } + Grpc::SotwOrDelta sotwOrDelta() const { return std::get<2>(GetParam()); } }; // When old delta subscription state goes away, we could replace this macro back with diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc index dfb00421efd85..2996ff26950e5 100644 --- a/test/integration/ads_integration_test.cc +++ b/test/integration/ads_integration_test.cc @@ -69,13 +69,13 @@ TEST_P(AdsIntegrationTest, BasicClusterInitialWarmingWithResourceWrapper) { EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", - {{"test", ProtobufWkt::Any()}}); + {{"test", Protobuf::Any()}}); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); test_server_->waitForGaugeEq("cluster.cluster_0.warming_state", 1); EXPECT_TRUE(compareDiscoveryRequest(eds_type_url, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( eds_type_url, {buildClusterLoadAssignment("cluster_0")}, - {buildClusterLoadAssignment("cluster_0")}, {}, "1", {{"test", ProtobufWkt::Any()}}); + {buildClusterLoadAssignment("cluster_0")}, {}, "1", {{"test", Protobuf::Any()}}); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 0); test_server_->waitForGaugeGe("cluster_manager.active_clusters", 2); @@ -455,68 +455,69 @@ TEST_P(AdsIntegrationTest, Failure) { // Send initial configuration, failing each xDS once (via a type mismatch), validate we can // process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().Cluster, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().Cluster, "", {}, {}, {}, false, - Grpc::Status::WellKnownGrpcStatus::Internal, - fmt::format("does not match the message-wide type URL {}", Config::TypeUrl::get().Cluster))); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, false, + Grpc::Status::WellKnownGrpcStatus::Internal, + fmt::format("does not match the message-wide type URL {}", + Config::TestTypeUrl::get().Cluster))); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildCluster("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", - {"cluster_0"}, {}, {}, false, - Grpc::Status::WellKnownGrpcStatus::Internal, - fmt::format("does not match the message-wide type URL {}", - Config::TypeUrl::get().ClusterLoadAssignment))); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE( + compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, + {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Internal, + fmt::format("does not match the message-wide type URL {}", + Config::TestTypeUrl::get().ClusterLoadAssignment))); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildRouteConfig("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildRouteConfig("listener_0", "route_config_0")}, {buildRouteConfig("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().Listener, "", {}, {}, {}, false, - Grpc::Status::WellKnownGrpcStatus::Internal, - fmt::format("does not match the message-wide type URL {}", Config::TypeUrl::get().Listener))); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false, + Grpc::Status::WellKnownGrpcStatus::Internal, + fmt::format("does not match the message-wide type URL {}", + Config::TestTypeUrl::get().Listener))); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildListener("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, {buildListener("route_config_0", "cluster_0")}, {buildListener("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Internal, fmt::format("does not match the message-wide type URL {}", - Config::TypeUrl::get().RouteConfiguration))); + Config::TestTypeUrl::get().RouteConfiguration))); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -527,19 +528,19 @@ TEST_P(AdsIntegrationTest, Failure) { // Regression test for https://github.com/envoyproxy/envoy/issues/9682. TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); // A second CDS request should be sent so that the node is cleared in the cached request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); xds_stream_->finishGrpcStream(Grpc::Status::Internal); AssertionResult result = xds_connection_->waitForNewStream(*dispatcher_, xds_stream_); @@ -549,8 +550,8 @@ TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { // In SotW cluster_0 will be in the resource_names, but in delta-xDS // resource_names_subscribe and resource_names_unsubscribe must be empty for // a wildcard request (cluster_0 will appear in initial_resource_versions). - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {"cluster_0"}, {}, + {}, true)); } // Verifies that upon stream reconnection: @@ -559,24 +560,24 @@ TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { // Regression test for https://github.com/envoyproxy/envoy/issues/16063. TEST_P(AdsIntegrationTest, ResourceNamesOnStreamReset) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); // A second CDS request should be sent so that the node is cleared in the cached request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // The LDS request, which returns no resources in the DiscoveryResponse. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Listener, {}, - {}, {}, "1"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Listener, + {}, {}, {}, "1"); xds_stream_->finishGrpcStream(Grpc::Status::Internal); AssertionResult result = xds_connection_->waitForNewStream(*dispatcher_, xds_stream_); @@ -589,11 +590,11 @@ TEST_P(AdsIntegrationTest, ResourceNamesOnStreamReset) { // In SotW cluster_0 will be in the resource_names, but in delta-xDS // resource_names_subscribe and resource_names_unsubscribe must be empty for // a wildcard request (cluster_0 will appear in initial_resource_versions). - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, {}, {}, true)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {"cluster_0"}, {}, + {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {"cluster_0"}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); } // Validate that the request with duplicate listeners is rejected. @@ -601,23 +602,23 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); // Send duplicate listeners and validate that the update is rejected. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, + Config::TestTypeUrl::get().Listener, {buildListener("duplicae_listener", "route_config_0"), buildListener("duplicae_listener", "route_config_0")}, {buildListener("duplicae_listener", "route_config_0"), @@ -630,7 +631,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { TEST_P(AdsIntegrationTest, DEPRECATED_FEATURE_TEST(RejectV2TransportConfigByDefault)) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); auto cluster = buildCluster("cluster_0"); auto* api_config_source = cluster.mutable_eds_cluster_config()->mutable_eds_config()->mutable_api_config_source(); @@ -638,7 +639,7 @@ TEST_P(AdsIntegrationTest, DEPRECATED_FEATURE_TEST(RejectV2TransportConfigByDefa api_config_source->set_transport_api_version(envoy::config::core::v3::V2); envoy::config::core::v3::GrpcService* grpc_service = api_config_source->add_grpc_services(); setGrpcService(*grpc_service, "ads_cluster", xds_upstream_->localAddress()); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster}, {cluster}, {}, "1"); test_server_->waitForCounterGe("cluster_manager.cds.update_rejected", 1); } @@ -648,17 +649,18 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithNoRdsChanges) { initialize(); // Send initial configuration. - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -667,13 +669,15 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithNoRdsChanges) { // Update existing LDS (change stat_prefix). sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0", "rds_crash")}, + Config::TestTypeUrl::get().Listener, + {buildListener("listener_0", "route_config_0", "rds_crash")}, {buildListener("listener_0", "route_config_0", "rds_crash")}, {}, "2"); test_server_->waitForCounterGe("listener_manager.listener_create_success", 2); // Update existing RDS (no changes). sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "2"); // Validate that we can process a request again @@ -684,54 +688,56 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithNoRdsChanges) { // an active cluster is replaced by a newer cluster undergoing warming. TEST_P(AdsIntegrationTest, CdsEdsReplacementWarming) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); makeSingleRequest(); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildTlsCluster("cluster_0")}, + Config::TestTypeUrl::get().Cluster, {buildTlsCluster("cluster_0")}, {buildTlsCluster("cluster_0")}, {}, "2"); // Inconsistent SotW and delta behaviors for warming, see // https://github.com/envoyproxy/envoy/issues/11477#issuecomment-657855029. // TODO (dmitri-d) this should be remove when legacy mux implementations have been removed. if (sotw_or_delta_ == Grpc::SotwOrDelta::Sotw) { - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); } sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildTlsClusterLoadAssignment("cluster_0")}, - {buildTlsClusterLoadAssignment("cluster_0")}, {}, "2"); + Config::TestTypeUrl::get().ClusterLoadAssignment, + {buildTlsClusterLoadAssignment("cluster_0")}, {buildTlsClusterLoadAssignment("cluster_0")}, + {}, "2"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "2", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"cluster_0"}, {}, {})); } @@ -742,38 +748,39 @@ TEST_P(AdsIntegrationTest, CdsKeepEdsAfterWarmingFailure) { // (only the runtime guard should be removed). config_helper_.addRuntimeOverride("envoy.restart_features.use_eds_cache_for_ads", "true"); initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); envoy::config::cluster::v3::Cluster cluster = buildCluster("cluster_0"); // Set a small EDS subscription expiration. cluster.mutable_eds_cluster_config() ->mutable_eds_config() ->mutable_initial_fetch_timeout() ->set_nanos(100 * 1000 * 1000); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster}, {cluster}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -781,13 +788,13 @@ TEST_P(AdsIntegrationTest, CdsKeepEdsAfterWarmingFailure) { // Update a cluster's field (connect_timeout) so the cluster in Envoy will be explicitly updated. cluster.mutable_connect_timeout()->set_seconds(7); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster}, {cluster}, {}, "2"); // Inconsistent SotW and delta behaviors for warming, see // https://github.com/envoyproxy/envoy/issues/11477#issuecomment-657855029. // TODO (dmitri-d) this should be remove when legacy mux implementations have been removed. if (sotw_or_delta_ == Grpc::SotwOrDelta::Sotw) { - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); } @@ -796,10 +803,10 @@ TEST_P(AdsIntegrationTest, CdsKeepEdsAfterWarmingFailure) { test_server_->waitForCounterGe("cluster.cluster_0.init_fetch_timeout", 1); if (sotw_or_delta_ == Grpc::SotwOrDelta::Sotw) { // Expect another EDS request after the previous one wasn't answered and timed out. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); } - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {}, {}, {})); // Envoy uses the cached resource. EXPECT_EQ(1, test_server_->counter("cluster.cluster_0.assignment_use_cached")->value()); @@ -815,7 +822,7 @@ TEST_P(AdsIntegrationTest, DoubleClustersCachedLoadAssignment) { // (only the runtime guard should be removed). config_helper_.addRuntimeOverride("envoy.restart_features.use_eds_cache_for_ads", "true"); initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); envoy::config::cluster::v3::Cluster cluster0 = buildCluster("cluster_0"); envoy::config::cluster::v3::Cluster cluster1 = buildCluster("cluster_1"); // Set a small EDS subscription expiration. @@ -831,30 +838,31 @@ TEST_P(AdsIntegrationTest, DoubleClustersCachedLoadAssignment) { cluster0.mutable_eds_cluster_config()->set_service_name("same_eds"); cluster1.mutable_eds_cluster_config()->set_service_name("same_eds"); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster0, cluster1}, {cluster0, cluster1}, {}, "1"); + Config::TestTypeUrl::get().Cluster, {cluster0, cluster1}, {cluster0, cluster1}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"same_eds"}, {"same_eds"}, {})); auto cla_0 = buildClusterLoadAssignment("same_eds"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cla_0}, {cla_0}, {}, "1"); + Config::TestTypeUrl::get().ClusterLoadAssignment, {cla_0}, {cla_0}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"same_eds"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); makeSingleRequest(); @@ -864,12 +872,12 @@ TEST_P(AdsIntegrationTest, DoubleClustersCachedLoadAssignment) { cluster0.mutable_connect_timeout()->set_seconds(7); cluster1.mutable_connect_timeout()->set_seconds(7); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster0, cluster1}, {cluster0, cluster1}, {}, "2"); + Config::TestTypeUrl::get().Cluster, {cluster0, cluster1}, {cluster0, cluster1}, {}, "2"); // Inconsistent SotW and delta behaviors for warming, see // https://github.com/envoyproxy/envoy/issues/11477#issuecomment-657855029. // TODO (dmitri-d) this should be remove when legacy mux implementations have been removed. if (sotw_or_delta_ == Grpc::SotwOrDelta::Sotw) { - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"same_eds"}, {}, {})); } @@ -879,12 +887,12 @@ TEST_P(AdsIntegrationTest, DoubleClustersCachedLoadAssignment) { if (sotw_or_delta_ == Grpc::SotwOrDelta::Sotw) { // Expect another EDS request after the previous one wasn't answered and timed out. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"same_eds"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"same_eds"}, {}, {})); } - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {}, {}, {})); // Envoy uses the cached resource. EXPECT_EQ(1, test_server_->counter("cluster.cluster_0.assignment_use_cached")->value()); @@ -894,7 +902,7 @@ TEST_P(AdsIntegrationTest, DoubleClustersCachedLoadAssignment) { // Now send an EDS update. cla_0.mutable_policy()->mutable_overprovisioning_factor()->set_value(141); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cla_0}, {cla_0}, {}, "2"); + Config::TestTypeUrl::get().ClusterLoadAssignment, {cla_0}, {cla_0}, {}, "2"); // Wait for ingesting the update. test_server_->waitForCounterEq("cluster.cluster_0.update_success", 2); @@ -910,9 +918,9 @@ TEST_P(AdsIntegrationTest, DuplicateInitialClusters) { // Send initial configuration, failing each xDS once (via a type mismatch), validate we can // process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, + Config::TestTypeUrl::get().Cluster, {buildCluster("duplicate_cluster"), buildCluster("duplicate_cluster")}, {buildCluster("duplicate_cluster"), buildCluster("duplicate_cluster")}, {}, "1"); @@ -925,33 +933,34 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingClusters) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -959,7 +968,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingClusters) { // Send duplicate warming clusters and validate that the update is rejected. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, + Config::TestTypeUrl::get().Cluster, {buildCluster("duplicate_cluster"), buildCluster("duplicate_cluster")}, {buildCluster("duplicate_cluster"), buildCluster("duplicate_cluster")}, {}, "2"); test_server_->waitForCounterGe("cluster_manager.cds.update_rejected", 1); @@ -970,33 +979,34 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -1004,31 +1014,31 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { // Send the first warming cluster. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("warming_cluster_1")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("warming_cluster_1")}, {buildCluster("warming_cluster_1")}, {"cluster_0"}, "2"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); test_server_->waitForGaugeEq("cluster.warming_cluster_1.warming_state", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"warming_cluster_1"}, {"warming_cluster_1"}, {"cluster_0"})); // Send the second warming cluster. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, + Config::TestTypeUrl::get().Cluster, {buildCluster("warming_cluster_1"), buildCluster("warming_cluster_2")}, {buildCluster("warming_cluster_1"), buildCluster("warming_cluster_2")}, {}, "3"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 2); test_server_->waitForGaugeEq("cluster.warming_cluster_2.warming_state", 1); // We would've got a Cluster discovery request with version 2 here, had the CDS not been paused. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"warming_cluster_2", "warming_cluster_1"}, {"warming_cluster_2"}, {})); // Finish warming the clusters. sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("warming_cluster_1"), buildClusterLoadAssignment("warming_cluster_2")}, {buildClusterLoadAssignment("warming_cluster_1"), @@ -1046,10 +1056,10 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { // Envoy will ACK both Cluster messages. Since they arrived while CDS was paused, they aren't // sent until CDS is unpaused. Since version 3 has already arrived by the time the version 2 // ACK goes out, they're both acknowledging version 3. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "3", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "3", {}, {}, {})); } - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "3", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "3", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"warming_cluster_2", "warming_cluster_1"}, {}, {})); } @@ -1057,33 +1067,34 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -1091,16 +1102,16 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { // Send the first warming cluster. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("warming_cluster_1")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("warming_cluster_1")}, {buildCluster("warming_cluster_1")}, {"cluster_0"}, "2"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"warming_cluster_1"}, {"warming_cluster_1"}, {"cluster_0"})); // Send the second warming cluster and remove the first cluster. - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("warming_cluster_2")}, {buildCluster("warming_cluster_2")}, // Delta: remove warming_cluster_1. @@ -1108,14 +1119,14 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); // We would've got a Cluster discovery request with version 2 here, had the CDS not been paused. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"warming_cluster_2"}, {"warming_cluster_2"}, {"warming_cluster_1"})); // Finish warming the clusters. Note that the first warming cluster is not included in the // response. sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("warming_cluster_2")}, {buildClusterLoadAssignment("warming_cluster_2")}, {"cluster_0"}, "2"); @@ -1129,10 +1140,10 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { // Envoy will ACK both Cluster messages. Since they arrived while CDS was paused, they aren't // sent until CDS is unpaused. Since version 3 has already arrived by the time the version 2 // ACK goes out, they're both acknowledging version 3. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "3", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "3", {}, {}, {})); } - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "3", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "2", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "3", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"warming_cluster_2"}, {}, {})); } // Validate that warming listeners are removed when left out of SOTW update. @@ -1140,33 +1151,34 @@ TEST_P(AdsIntegrationTest, RemoveWarmingListener) { initialize(); // Send initial configuration to start workers, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -1174,23 +1186,23 @@ TEST_P(AdsIntegrationTest, RemoveWarmingListener) { // Send a listener without its route, so it will be added as warming. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0"), buildListener("warming_listener_1", "nonexistent_route")}, {buildListener("warming_listener_1", "nonexistent_route")}, {}, "2"); test_server_->waitForGaugeEq("listener_manager.total_listeners_warming", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"nonexistent_route", "route_config_0"}, {"nonexistent_route"}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "2", {}, {}, {})); // Send a request removing the warming listener. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {"warming_listener_1"}, "3"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {"nonexistent_route"})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "3", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "3", {}, {}, {})); // The warming listener should be successfully removed. test_server_->waitForCounterEq("listener_manager.listener_removed", 1); @@ -1202,33 +1214,34 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -1236,27 +1249,27 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { // Send the first warming cluster. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("warming_cluster_1")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("warming_cluster_1")}, {buildCluster("warming_cluster_1")}, {"cluster_0"}, "2"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"warming_cluster_1"}, {"warming_cluster_1"}, {"cluster_0"})); // Send the second warming cluster. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, + Config::TestTypeUrl::get().Cluster, {buildCluster("warming_cluster_1"), buildCluster("warming_cluster_2")}, {buildCluster("warming_cluster_1"), buildCluster("warming_cluster_2")}, {}, "3"); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 2); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"warming_cluster_2", "warming_cluster_1"}, {"warming_cluster_2"}, {})); // Finish warming the first cluster. sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("warming_cluster_1")}, {buildClusterLoadAssignment("warming_cluster_1")}, {}, "2"); @@ -1277,7 +1290,7 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { // Finish warming the second cluster. sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("warming_cluster_2")}, {buildClusterLoadAssignment("warming_cluster_2")}, {}, "3"); @@ -1291,17 +1304,18 @@ TEST_P(AdsIntegrationTest, ListenerWarmingOnNamedResponse) { initialize(); // Send initial configuration. - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -1311,13 +1325,14 @@ TEST_P(AdsIntegrationTest, ListenerWarmingOnNamedResponse) { // Update existing listener - update stat prefix, use the same route name. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("cluster_1")}, {buildCluster("cluster_1")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_1")}, {buildCluster("cluster_1")}, {"cluster_0"}, "2"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_1")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_1")}, {buildClusterLoadAssignment("cluster_1")}, {"cluster_0"}, "2"); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0", "rds_test")}, + Config::TestTypeUrl::get().Listener, + {buildListener("listener_0", "route_config_0", "rds_test")}, {buildListener("listener_0", "route_config_0", "rds_test")}, {}, "2"); // Validate that listener is updated correctly and does not get in to warming state. @@ -1326,7 +1341,8 @@ TEST_P(AdsIntegrationTest, ListenerWarmingOnNamedResponse) { // Update listener with a new route. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_1", "rds_test")}, + Config::TestTypeUrl::get().Listener, + {buildListener("listener_0", "route_config_1", "rds_test")}, {buildListener("listener_0", "route_config_1", "rds_test")}, {}, "2"); // Validate that the listener gets in to warming state waiting for RDS. @@ -1335,7 +1351,8 @@ TEST_P(AdsIntegrationTest, ListenerWarmingOnNamedResponse) { // Send the new route and validate that listener finishes warming. sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_1", "cluster_1")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_1", "cluster_1")}, {buildRouteConfig("route_config_1", "cluster_1")}, {}, "2"); test_server_->waitForGaugeEq("listener_manager.total_listeners_warming", 0); test_server_->waitForCounterGe("listener_manager.listener_create_success", 3); @@ -1346,17 +1363,18 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithRdsChange) { initialize(); // Send initial configuration. - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -1365,19 +1383,21 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithRdsChange) { // Update existing LDS (change stat_prefix). sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("cluster_1")}, {buildCluster("cluster_1")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_1")}, {buildCluster("cluster_1")}, {"cluster_0"}, "2"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_1")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_1")}, {buildClusterLoadAssignment("cluster_1")}, {"cluster_0"}, "2"); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0", "rds_crash")}, + Config::TestTypeUrl::get().Listener, + {buildListener("listener_0", "route_config_0", "rds_crash")}, {buildListener("listener_0", "route_config_0", "rds_crash")}, {}, "2"); test_server_->waitForCounterGe("listener_manager.listener_create_success", 2); // Update existing RDS (migrate traffic to cluster_1). sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_1")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_1")}, {buildRouteConfig("route_config_0", "cluster_1")}, {}, "2"); // Validate that we can process a request after RDS update @@ -1395,36 +1415,37 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { // --------------------- // Initial request for any cluster, respond with cluster_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); // Initial request for load assignment for cluster_0, respond with version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); // Request for updates to cluster_0 version 1, no response - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // Initial request for any listener, respond with listener_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); // Request for updates to load assignment version 1, no response - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); // Initial request for route_config_0 (referenced by listener_0), respond with version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_0", "cluster_0")}, {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); // Wait for initial listener to be created successfully. Any subsequent listeners will then use @@ -1436,16 +1457,16 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { // Request for updates to listener_0 version 1, respond with version 2. Under the hood, this // registers RdsRouteConfigSubscription's init target with the new ListenerImpl instance. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_1")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_1")}, {buildListener("listener_0", "route_config_1")}, {}, "2"); // Request for updates to route_config_0 version 1, and initial request for route_config_1 // (referenced by listener_0), don't respond yet! - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_1", "route_config_0"}, {"route_config_1"}, {})); @@ -1455,16 +1476,17 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { // Request for updates to listener_0 version 2, respond with version 3 (updated stats prefix). // This should blow away the previous ListenerImpl instance, which is still waiting for // route_config_1... - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "2", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_1", "omg")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_1", "omg")}, {buildListener("listener_0", "route_config_1", "omg")}, {}, "3"); // Respond to prior request for route_config_1. Under the hood, this invokes // RdsRouteConfigSubscription::runInitializeCallbackIfAny, which references the defunct // ListenerImpl instance. We should not crash in this event! sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_1", "cluster_0")}, + Config::TestTypeUrl::get().RouteConfiguration, + {buildRouteConfig("route_config_1", "cluster_0")}, {buildRouteConfig("route_config_1", "cluster_0")}, {"route_config_0"}, "1"); test_server_->waitForCounterGe("listener_manager.listener_create_success", 2); @@ -1594,20 +1616,20 @@ TEST_P(AdsIntegrationTest, XdsBatching) { ASSERT_TRUE(xds_connection_->waitForNewStream(*dispatcher_, xds_stream_)); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"eds_cluster2", "eds_cluster"}, {"eds_cluster2", "eds_cluster"}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("eds_cluster"), buildClusterLoadAssignment("eds_cluster2")}, {buildClusterLoadAssignment("eds_cluster"), buildClusterLoadAssignment("eds_cluster2")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config2", "route_config"}, {"route_config2", "route_config"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config2", "eds_cluster2"), buildRouteConfig("route_config", "dummy_cluster")}, {buildRouteConfig("route_config2", "eds_cluster2"), @@ -1623,32 +1645,32 @@ TEST_P(AdsIntegrationTest, ListenerDrainBeforeServerStart) { initialize(); // Initial request for cluster, response for cluster_0. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); // Initial request for load assignment for cluster_0, respond with version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); // Request for updates to cluster_0 version 1, no response - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // Initial request for any listener, respond with listener_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); // Request for updates to load assignment version 1, no response - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {})); // Initial request for route_config_0 (referenced by listener_0), respond with version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); test_server_->waitForGaugeGe("listener_manager.total_listeners_active", 1); @@ -1658,9 +1680,9 @@ TEST_P(AdsIntegrationTest, ListenerDrainBeforeServerStart) { EXPECT_TRUE(getListenersConfigDump().dynamic_listeners(0).has_warming_state()); // Remove listener. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - sendDiscoveryResponse(Config::TypeUrl::get().Listener, {}, - {}, {"listener_0"}, "2"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + sendDiscoveryResponse(Config::TestTypeUrl::get().Listener, + {}, {}, {"listener_0"}, "2"); test_server_->waitForGaugeEq("listener_manager.total_listeners_active", 0); } @@ -1696,19 +1718,19 @@ TEST_P(AdsIntegrationTest, SetNodeAlways) { initialize(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, true)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, true)); }; // Check if EDS cluster defined in file is loaded before ADS request and used as xDS server @@ -1797,15 +1819,16 @@ TEST_P(AdsClusterFromFileIntegrationTest, BasicTestWidsAdsEndpointLoadedFromFile ASSERT_TRUE(xds_connection_->waitForNewStream(*dispatcher_, xds_stream_)); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"ads_eds_cluster"}, {"ads_eds_cluster"}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("ads_eds_cluster")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, + {buildClusterLoadAssignment("ads_eds_cluster")}, {buildClusterLoadAssignment("ads_eds_cluster")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"ads_eds_cluster"}, {}, {})); } @@ -1831,7 +1854,7 @@ class AdsIntegrationTestWithRtds : public AdsIntegrationTest { void testBasicFlow() { // Test that runtime discovery request comes first and cluster discovery request comes after // runtime was loaded. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "", {"ads_rtds_layer"}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "", {"ads_rtds_layer"}, {"ads_rtds_layer"}, {}, true)); auto some_rtds_layer = TestUtility::parseYaml(R"EOF( name: ads_rtds_layer @@ -1840,12 +1863,12 @@ class AdsIntegrationTestWithRtds : public AdsIntegrationTest { baz: meh )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {})); - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, + {}, {})); } }; @@ -1876,7 +1899,7 @@ class AdsIntegrationTestWithRtdsAndSecondaryClusters : public AdsIntegrationTest void testBasicFlow() { // Test that runtime discovery request comes first followed by the cluster load assignment // discovery request for secondary cluster and then CDS discovery request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "", {"ads_rtds_layer"}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "", {"ads_rtds_layer"}, {"ads_rtds_layer"}, {}, true)); auto some_rtds_layer = TestUtility::parseYaml(R"EOF( name: ads_rtds_layer @@ -1885,21 +1908,22 @@ class AdsIntegrationTestWithRtdsAndSecondaryClusters : public AdsIntegrationTest baz: meh )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"eds_cluster"}, {"eds_cluster"}, {}, false)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("eds_cluster")}, - {buildClusterLoadAssignment("eds_cluster")}, {}, "1"); + Config::TestTypeUrl::get().ClusterLoadAssignment, + {buildClusterLoadAssignment("eds_cluster")}, {buildClusterLoadAssignment("eds_cluster")}, + {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, {}, - {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, + {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, false)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, - {}, "1"); + Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, + {buildCluster("cluster_0")}, {}, "1"); } }; @@ -1916,53 +1940,58 @@ TEST_P(AdsIntegrationTest, ContextParameterUpdate) { initialize(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {}, false)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {}, false)); // Set a Cluster DCP. - test_server_->setDynamicContextParam(Config::TypeUrl::get().Cluster, "foo", "bar"); + test_server_->setDynamicContextParam(Config::TestTypeUrl::get().Cluster, "foo", "bar"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, true)); - EXPECT_EQ("bar", - last_node_.dynamic_parameters().at(Config::TypeUrl::get().Cluster).params().at("foo")); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {}, true)); + EXPECT_EQ( + "bar", + last_node_.dynamic_parameters().at(Config::TestTypeUrl::get().Cluster).params().at("foo")); // Modify Cluster DCP. - test_server_->setDynamicContextParam(Config::TypeUrl::get().Cluster, "foo", "baz"); + test_server_->setDynamicContextParam(Config::TestTypeUrl::get().Cluster, "foo", "baz"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, true)); - EXPECT_EQ("baz", - last_node_.dynamic_parameters().at(Config::TypeUrl::get().Cluster).params().at("foo")); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {}, true)); + EXPECT_EQ( + "baz", + last_node_.dynamic_parameters().at(Config::TestTypeUrl::get().Cluster).params().at("foo")); // Modify CLA DCP (some other resource type URL). - test_server_->setDynamicContextParam(Config::TypeUrl::get().ClusterLoadAssignment, "foo", "b"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + test_server_->setDynamicContextParam(Config::TestTypeUrl::get().ClusterLoadAssignment, "foo", + "b"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {}, true)); EXPECT_EQ("b", last_node_.dynamic_parameters() - .at(Config::TypeUrl::get().ClusterLoadAssignment) + .at(Config::TestTypeUrl::get().ClusterLoadAssignment) .params() .at("foo")); - EXPECT_EQ("baz", - last_node_.dynamic_parameters().at(Config::TypeUrl::get().Cluster).params().at("foo")); + EXPECT_EQ( + "baz", + last_node_.dynamic_parameters().at(Config::TestTypeUrl::get().Cluster).params().at("foo")); // Clear Cluster DCP. - test_server_->unsetDynamicContextParam(Config::TypeUrl::get().Cluster, "foo"); + test_server_->unsetDynamicContextParam(Config::TestTypeUrl::get().Cluster, "foo"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {}, true)); EXPECT_EQ( - 0, last_node_.dynamic_parameters().at(Config::TypeUrl::get().Cluster).params().count("foo")); + 0, + last_node_.dynamic_parameters().at(Config::TestTypeUrl::get().Cluster).params().count("foo")); } class XdsTpAdsIntegrationTest : public AdsIntegrationTest { @@ -2015,7 +2044,7 @@ TEST_P(XdsTpAdsIntegrationTest, Basic) { // Basic CDS/EDS xDS initialization (CDS via xdstp:// glob collection). const std::string cluster_wildcard = "xdstp://test/envoy.config.cluster.v3.Cluster/foo-cluster/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"; - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {cluster_wildcard}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {cluster_wildcard}, {cluster_wildcard}, {}, /*expect_node=*/true)); const std::string cluster_name = "xdstp://test/envoy.config.cluster.v3.Cluster/foo-cluster/" "baz?xds.node.cluster=cluster_name&xds.node.id=node_name"; @@ -2024,21 +2053,21 @@ TEST_P(XdsTpAdsIntegrationTest, Basic) { "xdstp://test/envoy.config.endpoint.v3.ClusterLoadAssignment/foo-cluster/baz"; cluster_resource.mutable_eds_cluster_config()->set_service_name(endpoints_name); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster_resource}, {cluster_resource}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().Cluster, {cluster_resource}, {cluster_resource}, {}, "1"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {endpoints_name}, {endpoints_name}, {})); const auto cluster_load_assignments = {buildClusterLoadAssignment(endpoints_name)}; sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, cluster_load_assignments, + Config::TestTypeUrl::get().ClusterLoadAssignment, cluster_load_assignments, cluster_load_assignments, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) const std::string listener_wildcard = "xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"; - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {listener_wildcard}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {listener_wildcard}, {listener_wildcard}, {})); const std::string route_name_0 = "xdstp://test/envoy.config.route.v3.RouteConfiguration/route_config_0"; @@ -2057,19 +2086,20 @@ TEST_P(XdsTpAdsIntegrationTest, Basic) { "baz?xds.node.cluster=cluster_name&xds.node.id=other_name", route_name_0), }; - sendDiscoveryResponse(Config::TypeUrl::get().Listener, + sendDiscoveryResponse(Config::TestTypeUrl::get().Listener, listeners, listeners, {}, "1"); EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", {}, {}, {})); + compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", {route_name_0}, - {route_name_0}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", + {route_name_0}, {route_name_0}, {})); const auto route_config = buildRouteConfig(route_name_0, cluster_name); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {route_config}, {route_config}, {}, "1"); + Config::TestTypeUrl::get().RouteConfiguration, {route_config}, {route_config}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE( + compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {}, {}, {})); test_server_->waitForCounterEq("listener_manager.listener_create_success", 1); makeSingleRequest(); @@ -2080,17 +2110,18 @@ TEST_P(XdsTpAdsIntegrationTest, Basic) { "baz?xds.node.cluster=cluster_name&xds.node.id=node_name", route_name_1); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {baz_listener}, {baz_listener}, {}, "2"); + Config::TestTypeUrl::get().Listener, {baz_listener}, {baz_listener}, {}, "2"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {route_name_1}, {route_name_1}, {})); const auto second_route_config = buildRouteConfig(route_name_1, cluster_name); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {second_route_config}, {second_route_config}, {}, - "2"); + Config::TestTypeUrl::get().RouteConfiguration, {second_route_config}, {second_route_config}, + {}, "2"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "2", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "2", {}, {}, {})); + EXPECT_TRUE( + compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "2", {}, {}, {})); test_server_->waitForCounterEq("listener_manager.listener_create_success", 2); makeSingleRequest(); @@ -2101,23 +2132,24 @@ TEST_P(XdsTpAdsIntegrationTest, Basic) { if (isSotw()) { // In SotW, removal consists of sending the other listeners, except for the one to be removed. - sendDiscoveryResponse(Config::TypeUrl::get().Listener, - {baz_listener}, {}, {}, "3"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "3", {}, {}, {})); + sendDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {baz_listener}, {}, {}, "3"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "3", {}, {}, {})); test_server_->waitForCounterEq("listener_manager.listener_removed", 1); makeSingleRequest(); } else { // Update bar listener in the foo namespace. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {}, {buildListener(bar_listener, route_name_1)}, {}, "3"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "3", {}, {}, {})); + Config::TestTypeUrl::get().Listener, {}, {buildListener(bar_listener, route_name_1)}, {}, + "3"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "3", {}, {}, {})); test_server_->waitForCounterEq("listener_manager.listener_in_place_updated", 1); makeSingleRequest(); // Remove bar listener from the foo namespace. - sendDiscoveryResponse(Config::TypeUrl::get().Listener, - {}, {}, {bar_listener}, "3"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "4", {}, {}, {})); + sendDiscoveryResponse( + Config::TestTypeUrl::get().Listener, {}, {}, {bar_listener}, "3"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "4", {}, {}, {})); test_server_->waitForCounterEq("listener_manager.listener_removed", 1); makeSingleRequest(); } @@ -2160,7 +2192,7 @@ TEST_P(XdsTpAdsIntegrationTest, BasicWithLeds) { {buildClusterLoadAssignmentWithLeds(endpoints_name, absl::StrCat(leds_resource_prefix, "*"))}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -2174,7 +2206,7 @@ TEST_P(XdsTpAdsIntegrationTest, BasicWithLeds) { const auto endpoint2_name = absl::StrCat(leds_resource_prefix, "endpoint_1", "?xds.node.cluster=cluster_name&xds.node.id=node_name"); sendExplicitResourcesDeltaDiscoveryResponse( - Config::TypeUrl::get().LbEndpoint, + Config::TestTypeUrl::get().LbEndpoint, {buildLbEndpointResource(endpoint1_name, "2"), buildLbEndpointResource(endpoint2_name, "2")}, {}); @@ -2186,7 +2218,7 @@ TEST_P(XdsTpAdsIntegrationTest, BasicWithLeds) { // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, + compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {"xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"}, {})); @@ -2236,7 +2268,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingEds) { absl::StrCat(leds_resource_prefix_foo, "*"))}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -2261,7 +2293,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingEds) { const auto endpoint2_name_foo = absl::StrCat(leds_resource_prefix_foo, "endpoint_1", "?xds.node.cluster=cluster_name&xds.node.id=node_name"); - sendExplicitResourcesDeltaDiscoveryResponse(Config::TypeUrl::get().LbEndpoint, + sendExplicitResourcesDeltaDiscoveryResponse(Config::TestTypeUrl::get().LbEndpoint, {buildLbEndpointResource(endpoint1_name_foo, "2"), buildLbEndpointResource(endpoint2_name_foo, "2")}, {}); @@ -2282,7 +2314,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingEds) { const auto endpoint2_name_bar = absl::StrCat(leds_resource_prefix_bar, "endpoint_1", "?xds.node.cluster=cluster_name&xds.node.id=node_name"); - sendExplicitResourcesDeltaDiscoveryResponse(Config::TypeUrl::get().LbEndpoint, + sendExplicitResourcesDeltaDiscoveryResponse(Config::TestTypeUrl::get().LbEndpoint, {buildLbEndpointResource(endpoint1_name_bar, "3"), buildLbEndpointResource(endpoint2_name_bar, "3")}, {}); @@ -2296,7 +2328,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingEds) { // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, + compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {"xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"}, {})); @@ -2344,7 +2376,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingCds) { absl::StrCat(leds_resource_prefix1, "*"))}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -2373,7 +2405,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingCds) { const auto endpoint2_name_cluster1 = absl::StrCat( leds_resource_prefix1, "endpoint_1", "?xds.node.cluster=cluster_name&xds.node.id=node_name"); sendExplicitResourcesDeltaDiscoveryResponse( - Config::TypeUrl::get().LbEndpoint, + Config::TestTypeUrl::get().LbEndpoint, {buildLbEndpointResource(endpoint1_name_cluster1, "2"), buildLbEndpointResource(endpoint2_name_cluster1, "2")}, {}); @@ -2395,7 +2427,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingCds) { "*?xds.node.cluster=cluster_name&xds.node.id=node_name")})); // Receive CDS ack. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {}, {}, {})); // Receive the EDS ack. EXPECT_TRUE(compareDiscoveryRequest(leds_type_url, "2", {}, {}, {})); @@ -2414,7 +2446,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingCds) { const auto endpoint2_name_cluster2 = absl::StrCat( leds_resource_prefix2, "endpoint_1", "?xds.node.cluster=cluster_name&xds.node.id=node_name"); sendExplicitResourcesDeltaDiscoveryResponse( - Config::TypeUrl::get().LbEndpoint, + Config::TestTypeUrl::get().LbEndpoint, {buildLbEndpointResource(endpoint1_name_cluster2, "2"), buildLbEndpointResource(endpoint2_name_cluster2, "2")}, {}); @@ -2424,7 +2456,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsClusterWarmingUpdatingCds) { // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, + compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {"xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"}, {})); @@ -2477,7 +2509,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsTimeout) { sendDiscoveryResponse( eds_type_url, {}, {cla_with_leds}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // Receive LEDS request, and wait for the initial fetch timeout. EXPECT_TRUE(compareDiscoveryRequest( leds_type_url, "", {}, @@ -2501,7 +2533,7 @@ TEST_P(XdsTpAdsIntegrationTest, LedsTimeout) { // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, + compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {"xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"}, {})); @@ -2537,39 +2569,40 @@ TEST_P(XdsTpAdsIntegrationTest, EdsAlternatingLedsUsage) { // Receive EDS request, and send ClusterLoadAssignment with one locality, // that doesn't use LEDS. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", {}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {}, {endpoints_name}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {}, {buildClusterLoadAssignment(endpoints_name)}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "1", {}, {}, {})); // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, + compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {"xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "*?xds.node.cluster=cluster_name&xds.node.id=node_name"}, {})); const std::string route_name_0 = "xdstp://test/envoy.config.route.v3.RouteConfiguration/route_config_0"; sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {}, + Config::TestTypeUrl::get().Listener, {}, {buildListener("xdstp://test/envoy.config.listener.v3.Listener/foo-listener/" "bar?xds.node.cluster=cluster_name&xds.node.id=node_name", route_name_0)}, {}, "1"); EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", {}, + compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {}, {route_name_0}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {}, {buildRouteConfig(route_name_0, cluster_name)}, - {}, "1"); + Config::TestTypeUrl::get().RouteConfiguration, {}, + {buildRouteConfig(route_name_0, cluster_name)}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE( + compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {}, {}, {})); test_server_->waitForCounterEq("listener_manager.listener_create_success", 1); makeSingleRequest(); @@ -2601,7 +2634,7 @@ TEST_P(XdsTpAdsIntegrationTest, EdsAlternatingLedsUsage) { const auto endpoint2_name = absl::StrCat(leds_resource_prefix, "endpoint_1", "?xds.node.cluster=cluster_name&xds.node.id=node_name"); sendExplicitResourcesDeltaDiscoveryResponse( - Config::TypeUrl::get().LbEndpoint, + Config::TestTypeUrl::get().LbEndpoint, {buildLbEndpointResource(endpoint1_name, "1"), buildLbEndpointResource(endpoint2_name, "1")}, {}); @@ -2614,7 +2647,7 @@ TEST_P(XdsTpAdsIntegrationTest, EdsAlternatingLedsUsage) { // Send a new EDS update that doesn't use LEDS. sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {}, {buildClusterLoadAssignment(endpoints_name)}, {}, "3"); // The server should remove interest in the old LEDS. @@ -2627,7 +2660,7 @@ TEST_P(XdsTpAdsIntegrationTest, EdsAlternatingLedsUsage) { EXPECT_TRUE(compareDiscoveryRequest(eds_type_url, "3", {}, {}, {})); // Remove the LEDS endpoints. - sendExplicitResourcesDeltaDiscoveryResponse(Config::TypeUrl::get().LbEndpoint, {}, + sendExplicitResourcesDeltaDiscoveryResponse(Config::TestTypeUrl::get().LbEndpoint, {}, {endpoint1_name, endpoint2_name}); // Receive the LEDS ack. @@ -2641,14 +2674,14 @@ TEST_P(XdsTpAdsIntegrationTest, EdsAlternatingLedsUsage) { // Make sure two listeners send only a single SRDS request. TEST_P(AdsIntegrationTest, SrdsPausedDuringLds) { initialize(); - const auto lds_type_url = Config::TypeUrl::get().Listener; - const auto cds_type_url = Config::TypeUrl::get().Cluster; - const auto eds_type_url = Config::TypeUrl::get().ClusterLoadAssignment; - const auto srds_type_url = Config::TypeUrl::get().ScopedRouteConfiguration; - const auto rds_type_url = Config::TypeUrl::get().RouteConfiguration; + const auto lds_type_url = Config::TestTypeUrl::get().Listener; + const auto cds_type_url = Config::TestTypeUrl::get().Cluster; + const auto eds_type_url = Config::TestTypeUrl::get().ClusterLoadAssignment; + const auto srds_type_url = Config::TestTypeUrl::get().ScopedRouteConfiguration; + const auto rds_type_url = Config::TestTypeUrl::get().RouteConfiguration; EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(eds_type_url, "", {"cluster_0"}, {"cluster_0"}, {})); @@ -2852,21 +2885,21 @@ TEST_P(AdsReplacementIntegrationTest, ReplaceAdsConfig) { initializeTwoAds(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, - "original1"); + Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, + {}, "original1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {}, false)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "original1"); EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "original1", {}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "original1", + compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "original1", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "original1", {"cluster_0"}, {}, {}, false)); // Prepare the second ADS server config. @@ -2902,59 +2935,61 @@ TEST_P(AdsReplacementIntegrationTest, ReplaceAdsConfig) { const absl::flat_hash_map cds_eds_initial_resource_versions_map{ {"cluster_0", "original1"}}; const absl::flat_hash_map empty_initial_resource_versions_map; + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true, + Grpc::Status::WellKnownGrpcStatus::Ok, "", + second_xds_stream_.get(), + makeOptRef(cds_eds_initial_resource_versions_map))); EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().Cluster, "", {}, {}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, - "", second_xds_stream_.get(), makeOptRef(cds_eds_initial_resource_versions_map))); - EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {}, false, + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get(), makeOptRef(cds_eds_initial_resource_versions_map))); - EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().Listener, "", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, - "", second_xds_stream_.get(), makeOptRef(empty_initial_resource_versions_map))); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {}, false, + Grpc::Status::WellKnownGrpcStatus::Ok, "", + second_xds_stream_.get(), + makeOptRef(empty_initial_resource_versions_map))); // Send a CDS response with new resources. sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {buildCluster("replaced_cluster")}, + Config::TestTypeUrl::get().Cluster, {buildCluster("replaced_cluster")}, {buildCluster("replaced_cluster")}, {}, "replaced1", {}, second_xds_stream_.get()); // Wait for an updated EDS request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "original1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "original1", {"replaced_cluster"}, {"replaced_cluster"}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get())); // Send an EDS response. sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("replaced_cluster")}, {buildClusterLoadAssignment("replaced_cluster")}, {}, "replaced1", {}, second_xds_stream_.get()); // Wait for a CDS and EDS ACKs. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "replaced1", {}, {}, {}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "replaced1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get())); EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "replaced1", {"replaced_cluster_1"}, {}, {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, "replaced1", {"replaced_cluster_1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get())); // Continue with LDS and RDS, and send a request-response. sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "replaced1", {}, second_xds_stream_.get()); EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {}, + Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get())); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {buildRouteConfig("route_config_0", "replaced_cluster")}, {buildRouteConfig("route_config_0", "replaced_cluster")}, {}, "replaced1", {}, second_xds_stream_.get()); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "replaced1", {}, {}, {}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "replaced1", {}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get())); EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().RouteConfiguration, "replaced1", {"route_config_0"}, {}, {}, false, + Config::TestTypeUrl::get().RouteConfiguration, "replaced1", {"route_config_0"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", second_xds_stream_.get())); makeSingleRequest(); diff --git a/test/integration/ads_xdstp_config_sources_integration.h b/test/integration/ads_xdstp_config_sources_integration.h new file mode 100644 index 0000000000000..32569ccfab0bb --- /dev/null +++ b/test/integration/ads_xdstp_config_sources_integration.h @@ -0,0 +1,233 @@ +#pragma once + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/endpoint/v3/endpoint.pb.h" +#include "envoy/config/listener/v3/listener.pb.h" +#include "envoy/config/route/v3/route.pb.h" +#include "envoy/grpc/status.h" + +#include "source/common/config/protobuf_link_hacks.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" +#include "source/common/tls/server_context_config_impl.h" +#include "source/common/tls/server_ssl_socket.h" +#include "source/common/version/version.h" + +#include "test/common/grpc/grpc_client_integration.h" +#include "test/config/v2_link_hacks.h" +#include "test/integration/ads_integration.h" +#include "test/integration/http_integration.h" +#include "test/integration/utility.h" +#include "test/test_common/network_utility.h" +#include "test/test_common/resources.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using testing::AssertionResult; + +namespace Envoy { + +// Tests for cases where both (old) ADS and xDS-TP based config sources are +// defined in the bootstrap. +class AdsXdsTpConfigsIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, + public HttpIntegrationTest { +public: + AdsXdsTpConfigsIntegrationTest() + : HttpIntegrationTest( + Http::CodecType::HTTP2, ipVersion(), + ConfigHelper::adsBootstrap((sotwOrDelta() == Grpc::SotwOrDelta::Sotw) || + (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw) + ? "GRPC" + : "DELTA_GRPC")) { + config_helper_.addRuntimeOverride("envoy.reloadable_features.unified_mux", + (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw || + sotwOrDelta() == Grpc::SotwOrDelta::UnifiedDelta) + ? "true" + : "false"); + use_lds_ = false; + // xds_upstream_ will be used for the ADS upstream. + create_xds_upstream_ = true; + // Not testing TLS in this case. + tls_xds_upstream_ = false; + sotw_or_delta_ = sotwOrDelta(); + setUpstreamProtocol(Http::CodecType::HTTP2); + } + + FakeUpstream* createAdsUpstream() { + ASSERT(!tls_xds_upstream_); + addFakeUpstream(Http::CodecType::HTTP2); + return fake_upstreams_.back().get(); + } + + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + // An upstream for authority1 (H/2), an upstream for the default_authority (H/2), and an + // upstream for a backend (H/1). + authority1_upstream_ = createAdsUpstream(); + default_authority_upstream_ = createAdsUpstream(); + } + + void TearDown() override { + cleanupXdsConnection(xds_connection_); + cleanupXdsConnection(authority1_xds_connection_); + cleanupXdsConnection(default_authority_xds_connection_); + } + + void cleanupXdsConnection(FakeHttpConnectionPtr& connection) { + if (connection != nullptr) { + AssertionResult result = connection->close(); + RELEASE_ASSERT(result, result.message()); + result = connection->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + connection.reset(); + } + } + + bool isSotw() const { + return sotwOrDelta() == Grpc::SotwOrDelta::Sotw || + sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw; + } + + // Adds config_source for authority1.com and a default_config_source for + // default_authority.com. + void initialize() override { + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions", "true"); + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // Add the first config_source. + { + auto* config_source1 = bootstrap.mutable_config_sources()->Add(); + config_source1->mutable_authorities()->Add()->set_name("authority1.com"); + auto* api_config_source = config_source1->mutable_api_config_source(); + api_config_source->set_api_type( + isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC + : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::V3); + api_config_source->set_set_node_on_first_message_only(true); + auto* grpc_service = api_config_source->add_grpc_services(); + setGrpcService(*grpc_service, "authority1_cluster", authority1_upstream_->localAddress()); + auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); + xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + xds_cluster->set_name("authority1_cluster"); + } + // Add the default config source. + { + auto* default_config_source = bootstrap.mutable_default_config_source(); + default_config_source->mutable_authorities()->Add()->set_name("default_authority.com"); + auto* api_config_source = default_config_source->mutable_api_config_source(); + api_config_source->set_api_type( + isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC + : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::V3); + api_config_source->set_set_node_on_first_message_only(true); + auto* grpc_service = api_config_source->add_grpc_services(); + setGrpcService(*grpc_service, "default_authority_cluster", + default_authority_upstream_->localAddress()); + auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); + xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + xds_cluster->set_name("default_authority_cluster"); + } + // Add the (old) ADS server. + { + auto* ads_config = bootstrap.mutable_dynamic_resources()->mutable_ads_config(); + if (ads_config_type_override_.has_value()) { + ads_config->set_api_type(ads_config_type_override_.value()); + } else { + ads_config->set_api_type(isSotw() ? envoy::config::core::v3::ApiConfigSource::GRPC + : envoy::config::core::v3::ApiConfigSource::DELTA_GRPC); + } + ads_config->set_transport_api_version(envoy::config::core::v3::V3); + ads_config->set_set_node_on_first_message_only(true); + auto* grpc_service = ads_config->add_grpc_services(); + setGrpcService(*grpc_service, "ads_cluster", xds_upstream_->localAddress()); + auto* ads_cluster = bootstrap.mutable_static_resources()->add_clusters(); + ads_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + ads_cluster->set_name("ads_cluster"); + } + }); + HttpIntegrationTest::initialize(); + connectAds(); + connectAuthority1(); + connectDefaultAuthority(); + } + + void connectAds() { + if (xds_stream_ == nullptr) { + AssertionResult result = xds_upstream_->waitForHttpConnection(*dispatcher_, xds_connection_); + RELEASE_ASSERT(result, result.message()); + result = xds_connection_->waitForNewStream(*dispatcher_, xds_stream_); + RELEASE_ASSERT(result, result.message()); + xds_stream_->startGrpcStream(); + } + } + + void connectAuthority1() { + AssertionResult result = + authority1_upstream_->waitForHttpConnection(*dispatcher_, authority1_xds_connection_); + RELEASE_ASSERT(result, result.message()); + result = authority1_xds_connection_->waitForNewStream(*dispatcher_, authority1_xds_stream_); + RELEASE_ASSERT(result, result.message()); + authority1_xds_stream_->startGrpcStream(); + } + + void connectDefaultAuthority() { + AssertionResult result = default_authority_upstream_->waitForHttpConnection( + *dispatcher_, default_authority_xds_connection_); + RELEASE_ASSERT(result, result.message()); + result = default_authority_xds_connection_->waitForNewStream(*dispatcher_, + default_authority_xds_stream_); + RELEASE_ASSERT(result, result.message()); + default_authority_xds_stream_->startGrpcStream(); + } + + envoy::config::endpoint::v3::ClusterLoadAssignment + buildClusterLoadAssignment(const std::string& name) { + // The first fake upstream is the emulated backend server. + return ConfigHelper::buildClusterLoadAssignment( + name, Network::Test::getLoopbackAddressString(ipVersion()), + fake_upstreams_[0].get()->localAddress()->ip()->port()); + } + + void setupClustersFromOldAds() { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + bootstrap.mutable_dynamic_resources()->mutable_cds_config()->mutable_ads(); + }); + } + + void setupListenersFromOldAds() { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + bootstrap.mutable_dynamic_resources()->mutable_lds_config()->mutable_ads(); + }); + } + + envoy::config::listener::v3::Listener buildListener(const std::string& name, + const std::string& route_config) { + return ConfigHelper::buildListener( + name, route_config, Network::Test::getLoopbackAddressString(ipVersion()), "ads_test"); + } + + void makeSingleRequest() { + registerTestServerPorts({"http"}); + testRouterHeaderOnlyRequestAndResponse(); + cleanupUpstreamAndDownstream(); + } + + // Data members that emulate the authority1 server. + FakeUpstream* authority1_upstream_; + FakeHttpConnectionPtr authority1_xds_connection_; + FakeStreamPtr authority1_xds_stream_; + + // Data members that emulate the default_authority server. + FakeUpstream* default_authority_upstream_; + FakeHttpConnectionPtr default_authority_xds_connection_; + FakeStreamPtr default_authority_xds_stream_; + + // An optional setting to overwrite the (old) ADS config type. By default it + // is not set, and the integration test param value will be used. + absl::optional ads_config_type_override_; +}; + +} // namespace Envoy diff --git a/test/integration/ads_xdstp_config_sources_integration_test.cc b/test/integration/ads_xdstp_config_sources_integration_test.cc index 8e2e45ee7c9c1..ec7118805520a 100644 --- a/test/integration/ads_xdstp_config_sources_integration_test.cc +++ b/test/integration/ads_xdstp_config_sources_integration_test.cc @@ -1,226 +1,9 @@ -#include "envoy/config/bootstrap/v3/bootstrap.pb.h" -#include "envoy/config/cluster/v3/cluster.pb.h" -#include "envoy/config/core/v3/base.pb.h" -#include "envoy/config/endpoint/v3/endpoint.pb.h" -#include "envoy/config/listener/v3/listener.pb.h" -#include "envoy/config/route/v3/route.pb.h" -#include "envoy/grpc/status.h" - -#include "source/common/config/protobuf_link_hacks.h" -#include "source/common/protobuf/protobuf.h" -#include "source/common/protobuf/utility.h" -#include "source/common/tls/server_context_config_impl.h" -#include "source/common/tls/server_ssl_socket.h" -#include "source/common/version/version.h" - -#include "test/common/grpc/grpc_client_integration.h" -#include "test/config/v2_link_hacks.h" -#include "test/integration/ads_integration.h" -#include "test/integration/http_integration.h" -#include "test/integration/utility.h" -#include "test/test_common/network_utility.h" -#include "test/test_common/resources.h" -#include "test/test_common/utility.h" - -#include "gtest/gtest.h" +#include "test/integration/ads_xdstp_config_sources_integration.h" using testing::AssertionResult; namespace Envoy { -// Tests for cases where both (old) ADS and xDS-TP based config sources are -// defined in the bootstrap. -class AdsXdsTpConfigsIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, - public HttpIntegrationTest { -public: - AdsXdsTpConfigsIntegrationTest() - : HttpIntegrationTest( - Http::CodecType::HTTP2, ipVersion(), - // ConfigHelper::httpProxyConfig(false)) { - ConfigHelper::adsBootstrap((sotwOrDelta() == Grpc::SotwOrDelta::Sotw) || - (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw) - ? "GRPC" - : "DELTA_GRPC")) { - config_helper_.addRuntimeOverride("envoy.reloadable_features.unified_mux", - (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw || - sotwOrDelta() == Grpc::SotwOrDelta::UnifiedDelta) - ? "true" - : "false"); - use_lds_ = false; - // xds_upstream_ will be used for the ADS upstream. - create_xds_upstream_ = true; - // Not testing TLS in this case. - tls_xds_upstream_ = false; - sotw_or_delta_ = sotwOrDelta(); - setUpstreamProtocol(Http::CodecType::HTTP2); - } - - FakeUpstream* createAdsUpstream() { - ASSERT(!tls_xds_upstream_); - addFakeUpstream(Http::CodecType::HTTP2); - return fake_upstreams_.back().get(); - } - - void createUpstreams() override { - HttpIntegrationTest::createUpstreams(); - // An upstream for authority1 (H/2), an upstream for the default_authority (H/2), and an - // upstream for a backend (H/1). - authority1_upstream_ = createAdsUpstream(); - default_authority_upstream_ = createAdsUpstream(); - } - - void TearDown() override { - cleanupXdsConnection(xds_connection_); - cleanupXdsConnection(authority1_xds_connection_); - cleanupXdsConnection(default_authority_xds_connection_); - } - - void cleanupXdsConnection(FakeHttpConnectionPtr& connection) { - if (connection != nullptr) { - AssertionResult result = connection->close(); - RELEASE_ASSERT(result, result.message()); - result = connection->waitForDisconnect(); - RELEASE_ASSERT(result, result.message()); - connection.reset(); - } - } - - bool isSotw() const { - return sotwOrDelta() == Grpc::SotwOrDelta::Sotw || - sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw; - } - - // Adds config_source for authority1.com and a default_config_source for - // default_authority.com. - void initialize() override { - config_helper_.addRuntimeOverride( - "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions", "true"); - config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - // Add the first config_source. - { - auto* config_source1 = bootstrap.mutable_config_sources()->Add(); - config_source1->mutable_authorities()->Add()->set_name("authority1.com"); - auto* api_config_source = config_source1->mutable_api_config_source(); - api_config_source->set_api_type( - isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC - : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); - api_config_source->set_transport_api_version(envoy::config::core::v3::V3); - api_config_source->set_set_node_on_first_message_only(true); - auto* grpc_service = api_config_source->add_grpc_services(); - setGrpcService(*grpc_service, "authority1_cluster", authority1_upstream_->localAddress()); - auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); - xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - xds_cluster->set_name("authority1_cluster"); - } - // Add the default config source. - { - auto* default_config_source = bootstrap.mutable_default_config_source(); - default_config_source->mutable_authorities()->Add()->set_name("default_authority.com"); - auto* api_config_source = default_config_source->mutable_api_config_source(); - api_config_source->set_api_type( - isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC - : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); - api_config_source->set_transport_api_version(envoy::config::core::v3::V3); - api_config_source->set_set_node_on_first_message_only(true); - auto* grpc_service = api_config_source->add_grpc_services(); - setGrpcService(*grpc_service, "default_authority_cluster", - default_authority_upstream_->localAddress()); - auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); - xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - xds_cluster->set_name("default_authority_cluster"); - } - // Add the (old) ADS server. - { - auto* ads_config = bootstrap.mutable_dynamic_resources()->mutable_ads_config(); - ads_config->set_api_type(isSotw() ? envoy::config::core::v3::ApiConfigSource::GRPC - : envoy::config::core::v3::ApiConfigSource::DELTA_GRPC); - ads_config->set_transport_api_version(envoy::config::core::v3::V3); - ads_config->set_set_node_on_first_message_only(true); - auto* grpc_service = ads_config->add_grpc_services(); - setGrpcService(*grpc_service, "ads_cluster", xds_upstream_->localAddress()); - auto* ads_cluster = bootstrap.mutable_static_resources()->add_clusters(); - ads_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - ads_cluster->set_name("ads_cluster"); - } - }); - HttpIntegrationTest::initialize(); - connectAds(); - connectAuthority1(); - connectDefaultAuthority(); - } - - void connectAds() { - if (xds_stream_ == nullptr) { - AssertionResult result = xds_upstream_->waitForHttpConnection(*dispatcher_, xds_connection_); - RELEASE_ASSERT(result, result.message()); - result = xds_connection_->waitForNewStream(*dispatcher_, xds_stream_); - RELEASE_ASSERT(result, result.message()); - xds_stream_->startGrpcStream(); - } - } - - void connectAuthority1() { - AssertionResult result = - authority1_upstream_->waitForHttpConnection(*dispatcher_, authority1_xds_connection_); - RELEASE_ASSERT(result, result.message()); - result = authority1_xds_connection_->waitForNewStream(*dispatcher_, authority1_xds_stream_); - RELEASE_ASSERT(result, result.message()); - authority1_xds_stream_->startGrpcStream(); - } - - void connectDefaultAuthority() { - AssertionResult result = default_authority_upstream_->waitForHttpConnection( - *dispatcher_, default_authority_xds_connection_); - RELEASE_ASSERT(result, result.message()); - result = default_authority_xds_connection_->waitForNewStream(*dispatcher_, - default_authority_xds_stream_); - RELEASE_ASSERT(result, result.message()); - default_authority_xds_stream_->startGrpcStream(); - } - - envoy::config::endpoint::v3::ClusterLoadAssignment - buildClusterLoadAssignment(const std::string& name) { - // The first fake upstream is the emulated backend server. - return ConfigHelper::buildClusterLoadAssignment( - name, Network::Test::getLoopbackAddressString(ipVersion()), - fake_upstreams_[0].get()->localAddress()->ip()->port()); - } - - void setupClustersFromOldAds() { - config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - bootstrap.mutable_dynamic_resources()->mutable_cds_config()->mutable_ads(); - }); - } - - void setupListenersFromOldAds() { - config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - bootstrap.mutable_dynamic_resources()->mutable_lds_config()->mutable_ads(); - }); - } - - envoy::config::listener::v3::Listener buildListener(const std::string& name, - const std::string& route_config) { - return ConfigHelper::buildListener( - name, route_config, Network::Test::getLoopbackAddressString(ipVersion()), "ads_test"); - } - - void makeSingleRequest() { - registerTestServerPorts({"http"}); - testRouterHeaderOnlyRequestAndResponse(); - cleanupUpstreamAndDownstream(); - } - - // Data members that emulate the authority1 server. - FakeUpstream* authority1_upstream_; - FakeHttpConnectionPtr authority1_xds_connection_; - FakeStreamPtr authority1_xds_stream_; - - // Data members that emulate the default_authority server. - FakeUpstream* default_authority_upstream_; - FakeHttpConnectionPtr default_authority_xds_connection_; - FakeStreamPtr default_authority_xds_stream_; -}; - INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, AdsXdsTpConfigsIntegrationTest, ADS_INTEGRATION_PARAMS); @@ -233,22 +16,22 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, CdsPointsToAuthorityEds) { // Wait for ADS clusters request and send a cluster that points to load // assignment in authority1.com. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); auto cluster1 = ConfigHelper::buildCluster("cluster_1"); cluster1.mutable_eds_cluster_config()->set_service_name( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"); cluster1.mutable_eds_cluster_config()->clear_eds_config(); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1}, {cluster1}, {}, "1"); // Authority1 receives an EDS request, and sends a response. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1")}, @@ -258,27 +41,27 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, CdsPointsToAuthorityEds) { {}, "1", {}, authority1_xds_stream_.get()); // Old ADS receives a CDS ACK. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // Authority1 receives an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "1", + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); // Send the Listener and route config using the old ADS. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {ConfigHelper::buildRouteConfig("route_config_0", "cluster_1")}, {ConfigHelper::buildRouteConfig("route_config_0", "cluster_1")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -293,49 +76,49 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityEds) { // Wait for ADS clusters request and send a cluster that points to load // assignment in authority1.com. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); auto cluster1 = ConfigHelper::buildCluster("cluster_1"); cluster1.mutable_eds_cluster_config()->set_service_name( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"); cluster1.mutable_eds_cluster_config()->clear_eds_config(); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1}, {cluster1}, {}, "1"); // Authority1 receives an EDS request, and sends a response. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); envoy::config::endpoint::v3::ClusterLoadAssignment cla = buildClusterLoadAssignment( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "1", {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "1", {}, authority1_xds_stream_.get()); // Old ADS receives a CDS ACK. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // Authority1 receives an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "1", + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); test_server_->waitForCounterGe("cluster.cluster_1.update_success", 1); // Send the Listener and route config using the old ADS. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {ConfigHelper::buildRouteConfig("route_config_0", "cluster_1")}, {ConfigHelper::buildRouteConfig("route_config_0", "cluster_1")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -343,12 +126,12 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityEds) { // Update the EDS config. cla.mutable_endpoints(0)->mutable_load_balancing_weight()->set_value(50); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "2", {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "2", {}, authority1_xds_stream_.get()); // Expect an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "2", + Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); @@ -367,22 +150,22 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityToFetchEds) { // Wait for ADS clusters request and send a cluster that points to load // assignment in authority1.com. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); auto cluster1 = ConfigHelper::buildCluster("cluster_1"); cluster1.mutable_eds_cluster_config()->set_service_name( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"); cluster1.mutable_eds_cluster_config()->clear_eds_config(); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1}, {cluster1}, {}, "1"); // Authority1 receives an EDS request, and sends a response. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1")}, @@ -392,28 +175,28 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityToFetchEds) { {}, "1", {}, authority1_xds_stream_.get()); // Old ADS receives a CDS ACK. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "1", {}, {}, {})); // Authority1 receives an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "1", + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); test_server_->waitForCounterGe("cluster.cluster_1.update_success", 1); // Send the Listener and route config using the old ADS. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, + Config::TestTypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"route_config_0"}, {"route_config_0"}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {ConfigHelper::buildRouteConfig("route_config_0", "cluster_1")}, {ConfigHelper::buildRouteConfig("route_config_0", "cluster_1")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "1", {"route_config_0"}, {}, {})); test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); @@ -424,19 +207,19 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityToFetchEds) { "xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1"); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {cluster1}, {cluster1}, {}, "2"); // Default-authority receives an EDS request, and sends a response. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1"}, {"xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", default_authority_xds_stream_.get())); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment( "xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1")}, @@ -446,11 +229,11 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityToFetchEds) { {}, "2", {}, default_authority_xds_stream_.get()); // Old ADS receives a CDS ACK. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "2", {}, {}, {})); // Default-authority receives an EDS ACK. EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "2", + compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"xdstp://default_authority.com/" "envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", @@ -459,7 +242,7 @@ TEST_P(AdsXdsTpConfigsIntegrationTest, UpdateAuthorityToFetchEds) { // Authority1 subscription is removed. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", {}, {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {}, {}, {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); diff --git a/test/integration/alpn_integration_test.cc b/test/integration/alpn_integration_test.cc index d15d590332df1..1254721306811 100644 --- a/test/integration/alpn_integration_test.cc +++ b/test/integration/alpn_integration_test.cc @@ -146,7 +146,7 @@ TEST_P(AlpnIntegrationTest, Http2RememberSettings) { test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); { - absl::MutexLock l(&fake_upstreams_[0]->lock()); + absl::MutexLock l(fake_upstreams_[0]->lock()); IntegrationCodecClientPtr codec_client1 = makeHttpConnection(lookupPort("http")); auto response1 = codec_client1->makeHeaderOnlyRequest(default_request_headers_); IntegrationCodecClientPtr codec_client2 = makeHttpConnection(lookupPort("http")); diff --git a/test/integration/api_listener_integration_test.cc b/test/integration/api_listener_integration_test.cc index 6e5bd89ceca40..44a0c14916454 100644 --- a/test/integration/api_listener_integration_test.cc +++ b/test/integration/api_listener_integration_test.cc @@ -162,7 +162,7 @@ TEST_P(ApiListenerIntegrationTest, FromWorkerThread) { ThreadLocal::TypedSlot<>::makeUnique(test_server_->server().threadLocal()); slot->set([&dispatchers_mutex, &dispatchers, &has_dispatcher]( Event::Dispatcher& dispatcher) -> std::shared_ptr { - absl::MutexLock ml(&dispatchers_mutex); + absl::MutexLock ml(dispatchers_mutex); // A string comparison on thread name seems to be the only way to // distinguish worker threads from the main thread with the slots interface. if (dispatcher.name() != "main_thread") { diff --git a/test/integration/autonomous_upstream.cc b/test/integration/autonomous_upstream.cc index 8b05a02b5cb70..e747d0a989445 100644 --- a/test/integration/autonomous_upstream.cc +++ b/test/integration/autonomous_upstream.cc @@ -44,7 +44,7 @@ void AutonomousStream::decodeHeaders(Http::RequestHeaderMapSharedPtr&& headers, FakeStream::decodeHeaders(std::move(headers), end_stream); if (send_response) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); sendResponse(); } } diff --git a/test/integration/base_integration_test.cc b/test/integration/base_integration_test.cc index 3741ae20c2efd..22ff417048e5e 100644 --- a/test/integration/base_integration_test.cc +++ b/test/integration/base_integration_test.cc @@ -223,7 +223,7 @@ std::string BaseIntegrationTest::finalizeConfigWithPorts(ConfigHelper& config_he envoy::service::discovery::v3::DiscoveryResponse lds; lds.set_version_info("0"); for (auto& listener : config_helper.bootstrap().static_resources().listeners()) { - ProtobufWkt::Any* resource = lds.add_resources(); + Protobuf::Any* resource = lds.add_resources(); resource->PackFrom(listener); } #ifdef ENVOY_ENABLE_YAML @@ -377,12 +377,12 @@ bool BaseIntegrationTest::getSocketOption(const std::string& listener_name, int std::vector> listeners; test_server_->server().dispatcher().post([&]() { listeners = test_server_->server().listenerManager().listeners(); - l.Lock(); + l.lock(); listeners_ready = true; - l.Unlock(); + l.unlock(); }); l.LockWhen(absl::Condition(&listeners_ready)); - l.Unlock(); + l.unlock(); for (auto& listener : listeners) { if (listener.get().name() == listener_name) { @@ -404,12 +404,12 @@ void BaseIntegrationTest::registerTestServerPorts(const std::vector std::vector> listeners; test_server->server().dispatcher().post([&listeners, &listeners_ready, &l, &test_server]() { listeners = test_server->server().listenerManager().listeners(); - l.Lock(); + l.lock(); listeners_ready = true; - l.Unlock(); + l.unlock(); }); l.LockWhen(absl::Condition(&listeners_ready)); - l.Unlock(); + l.unlock(); auto listener_it = listeners.cbegin(); auto port_it = port_names.cbegin(); diff --git a/test/integration/base_integration_test.h b/test/integration/base_integration_test.h index 64ba788d0dd8b..0d54d3b6cf090 100644 --- a/test/integration/base_integration_test.h +++ b/test/integration/base_integration_test.h @@ -190,12 +190,11 @@ class BaseIntegrationTest : protected Logger::Loggable { absl::nullopt); template - void - sendDiscoveryResponse(const std::string& type_url, const std::vector& state_of_the_world, - const std::vector& added_or_updated, - const std::vector& removed, const std::string& version, - const absl::flat_hash_map& metadata = {}, - FakeStream* stream = nullptr) { + void sendDiscoveryResponse(const std::string& type_url, const std::vector& state_of_the_world, + const std::vector& added_or_updated, + const std::vector& removed, const std::string& version, + const absl::flat_hash_map& metadata = {}, + FakeStream* stream = nullptr) { if (sotw_or_delta_ == Grpc::SotwOrDelta::Sotw || sotw_or_delta_ == Grpc::SotwOrDelta::UnifiedSotw) { sendSotwDiscoveryResponse(type_url, state_of_the_world, version, stream, metadata); @@ -237,10 +236,9 @@ class BaseIntegrationTest : protected Logger::Loggable { sendSotwDiscoveryResponse(type_url, messages, version, stream, {}); } template - void - sendSotwDiscoveryResponse(const std::string& type_url, const std::vector& messages, - const std::string& version, FakeStream* stream, - const absl::flat_hash_map& metadata) { + void sendSotwDiscoveryResponse(const std::string& type_url, const std::vector& messages, + const std::string& version, FakeStream* stream, + const absl::flat_hash_map& metadata) { if (stream == nullptr) { stream = xds_stream_.get(); } @@ -287,7 +285,7 @@ class BaseIntegrationTest : protected Logger::Loggable { void sendDeltaDiscoveryResponse(const std::string& type_url, const std::vector& added_or_updated, const std::vector& removed, const std::string& version, - const absl::flat_hash_map& metadata) { + const absl::flat_hash_map& metadata) { sendDeltaDiscoveryResponse(type_url, added_or_updated, removed, version, xds_stream_, {}, metadata); } @@ -297,7 +295,7 @@ class BaseIntegrationTest : protected Logger::Loggable { sendDeltaDiscoveryResponse(const std::string& type_url, const std::vector& added_or_updated, const std::vector& removed, const std::string& version, FakeStream* stream, const std::vector& aliases, - const absl::flat_hash_map& metadata) { + const absl::flat_hash_map& metadata) { auto response = createDeltaDiscoveryResponse(type_url, added_or_updated, removed, version, aliases, metadata); if (stream == nullptr) { @@ -327,7 +325,7 @@ class BaseIntegrationTest : protected Logger::Loggable { createDeltaDiscoveryResponse(const std::string& type_url, const std::vector& added_or_updated, const std::vector& removed, const std::string& version, const std::vector& aliases, - const absl::flat_hash_map& metadata) { + const absl::flat_hash_map& metadata) { std::vector resources; for (const auto& message : added_or_updated) { envoy::service::discovery::v3::Resource resource; diff --git a/test/integration/buffer_accounting_integration_test.cc b/test/integration/buffer_accounting_integration_test.cc index c2864879230a1..e7a4bcd042494 100644 --- a/test/integration/buffer_accounting_integration_test.cc +++ b/test/integration/buffer_accounting_integration_test.cc @@ -77,7 +77,7 @@ void runOnWorkerThreadsAndWaitforCompletion(Server::Instance& server, std::funct void waitForNumTurns(std::vector& turns, absl::Mutex& mu, uint32_t expected_size) { - absl::MutexLock l(&mu); + absl::MutexLock l(mu); auto check_data_in_connection_output_buffer = [&turns, &mu, expected_size]() { mu.AssertHeld(); return turns.size() == expected_size; @@ -365,7 +365,6 @@ TEST_P(Http2BufferWatermarksTest, ShouldTrackAllocatedBytesToShadowUpstream) { const uint32_t request_body_size = 4096; const uint32_t response_body_size = 4096; TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.streaming_shadow", "true"}}); autonomous_upstream_ = true; autonomous_allow_incomplete_streams_ = true; @@ -916,7 +915,7 @@ class Http2DeferredProcessingIntegrationTest : public Http2BufferWatermarksTest test_server_->waitForCounterEq("http.config_test.downstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.request_body_.length(), 1000); })); @@ -970,7 +969,7 @@ class Http2DeferredProcessingIntegrationTest : public Http2BufferWatermarksTest test_server_->waitForCounterEq("cluster.cluster_0.upstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 1000); })); @@ -1050,7 +1049,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, CanBufferInDownstreamCodec) { test_server_->waitForCounterEq("http.config_test.downstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.request_body_.length(), 1000); })); @@ -1093,7 +1092,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, CanBufferInUpstreamCodec) { test_server_->waitForCounterEq("cluster.cluster_0.upstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 1000); })); @@ -1135,7 +1134,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, CanDeferOnStreamCloseForUpstream) test_server_->waitForCounterEq("cluster.cluster_0.upstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 1000); })); @@ -1191,7 +1190,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, test_server_->waitForCounterEq("cluster.cluster_0.upstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 9000); })); @@ -1238,7 +1237,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, test_server_->waitForCounterEq("cluster.cluster_0.upstream_flow_control_resumed_reading_total", 0); EXPECT_TRUE(tee_filter_factory_.inspectStreamTee(1, [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 1000); })); @@ -1288,7 +1287,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, CanRoundRobinBetweenStreams) { [&turns, &mu](StreamTee& tee, Http::StreamDecoderFilterCallbacks* decoder_callbacks) ABSL_EXCLUSIVE_LOCKS_REQUIRED(tee.mutex_) -> Http::FilterDataStatus { (void)tee; // silence gcc unused warning (the absl annotation usage didn't mark it used.) - absl::MutexLock l(&mu); + absl::MutexLock l(mu); turns.push_back(decoder_callbacks->streamId()); return Http::FilterDataStatus::Continue; }; @@ -1339,7 +1338,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, CanRoundRobinBetweenStreams) { // Check that during deferred processing we round robin between the streams. // Turns in the sequence 0-3 and 8-11 should match. { - absl::MutexLock l(&mu); + absl::MutexLock l(mu); for (uint32_t i = 0; i < num_requests; ++i) { EXPECT_EQ(turns[i], turns[i + 8]); } @@ -1368,7 +1367,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, RoundRobinWithStreamsExiting) { auto record_turns_on_encode_and_stop_writes_on_endstream = [this, &turns, &mu](StreamTee& tee, Http::StreamEncoderFilterCallbacks* encoder_callbacks) ABSL_EXCLUSIVE_LOCKS_REQUIRED(tee.mutex_) -> Http::FilterDataStatus { - absl::MutexLock l(&mu); + absl::MutexLock l(mu); turns.push_back(encoder_callbacks->streamId()); if (tee.encode_end_stream_) { @@ -1412,26 +1411,26 @@ TEST_P(Http2DeferredProcessingIntegrationTest, RoundRobinWithStreamsExiting) { // a chance. waitForNumTurns(turns, mu, 5); { - absl::MutexLock l(&mu); + absl::MutexLock l(mu); // Check ordering as expected. EXPECT_EQ(turns[3], turns[0]); EXPECT_EQ(turns[4], turns[1]); } tee_filter_factory_.inspectStreamTee(tee_filter_factory_.computeClientStreamId(0), [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 8000); }); tee_filter_factory_.inspectStreamTee(tee_filter_factory_.computeClientStreamId(1), [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_TRUE(tee.encode_end_stream_); }); tee_filter_factory_.inspectStreamTee(tee_filter_factory_.computeClientStreamId(2), [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 4000); }); @@ -1447,12 +1446,12 @@ TEST_P(Http2DeferredProcessingIntegrationTest, RoundRobinWithStreamsExiting) { // buffer stopping the 3rd stream from flushing its buffered data. tee_filter_factory_.inspectStreamTee(tee_filter_factory_.computeClientStreamId(2), [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_TRUE(tee.encode_end_stream_); }); tee_filter_factory_.inspectStreamTee(tee_filter_factory_.computeClientStreamId(0), [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_EQ(tee.response_body_.length(), 8000); }); // The 1st stream will finish. @@ -1460,7 +1459,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, RoundRobinWithStreamsExiting) { waitForNumTurns(turns, mu, 7); tee_filter_factory_.inspectStreamTee(tee_filter_factory_.computeClientStreamId(0), [](const StreamTee& tee) { - absl::MutexLock l{&tee.mutex_}; + absl::MutexLock l{tee.mutex_}; EXPECT_TRUE(tee.encode_end_stream_); }); // All responses would have drained to client. @@ -1504,7 +1503,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, ChunkProcessesStreams) { auto record_on_decode = [this, &turns, &mu](StreamTee& tee, Http::StreamDecoderFilterCallbacks* decoder_callbacks) ABSL_EXCLUSIVE_LOCKS_REQUIRED(tee.mutex_) -> Http::FilterDataStatus { - absl::MutexLock l(&mu); + absl::MutexLock l(mu); turns.emplace_back(decoder_callbacks->streamId(), tee.request_body_.length()); // Allows us to build more than chunk size in a stream, as the @@ -1561,7 +1560,7 @@ TEST_P(Http2DeferredProcessingIntegrationTest, ChunkProcessesStreams) { EXPECT_TRUE(upstream_requests[2]->waitForData(*dispatcher_, 131000)); { - absl::MutexLock l(&mu); + absl::MutexLock l(mu); // The 3rd stream should have gone multiple times to drain out the 128KiB of // data. Each chunk drain is 10KB. ASSERT_GE(turns.size(), 3); diff --git a/test/integration/cds_integration_test.cc b/test/integration/cds_integration_test.cc index aa3fb252c749e..9e6b9996519a8 100644 --- a/test/integration/cds_integration_test.cc +++ b/test/integration/cds_integration_test.cc @@ -102,7 +102,7 @@ class CdsIntegrationTest : public Grpc::DeltaSotwDeferredClustersIntegrationPara acceptXdsConnection(); // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for cluster_1. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendClusterDiscoveryResponse({cluster1_}, {cluster1_}, {}, "55"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of @@ -121,7 +121,7 @@ class CdsIntegrationTest : public Grpc::DeltaSotwDeferredClustersIntegrationPara const std::vector& added_or_updated, const std::vector& removed, const std::string& version) { sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, state_of_the_world, added_or_updated, removed, version); + Config::TestTypeUrl::get().Cluster, state_of_the_world, added_or_updated, removed, version); } // Regression test to catch the code declaring a gRPC service method for {SotW,delta} @@ -191,7 +191,7 @@ TEST_P(CdsIntegrationTest, CdsClusterUpDownUp) { } // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendClusterDiscoveryResponse({}, {}, {ClusterName1}, "42"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. @@ -215,7 +215,7 @@ TEST_P(CdsIntegrationTest, CdsClusterUpDownUp) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is back. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "42", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "42", {}, {}, {})); sendClusterDiscoveryResponse({cluster1_}, {cluster1_}, {}, "413"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of @@ -255,7 +255,7 @@ TEST_P(CdsIntegrationTest, CdsClusterTeardownWhileConnecting) { {":method", "GET"}, {":path", "/cluster1"}, {":scheme", "http"}, {":authority", "host"}}); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendClusterDiscoveryResponse({}, {}, {ClusterName1}, "42"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. @@ -294,7 +294,7 @@ class DeferredCreationClusterStatsTest : public CdsIntegrationTest { envoy::config::cluster::v3::Cluster::ROUND_ROBIN); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_updated}, {cluster1_updated}, {}, "42"); + Config::TestTypeUrl::get().Cluster, {cluster1_updated}, {cluster1_updated}, {}, "42"); } void removeClusters(const std::vector& removed) { @@ -444,7 +444,7 @@ TEST_P(CdsIntegrationTest, CdsClusterWithThreadAwareLbCycleUpDownUp) { test_server_->waitForCounterGe("cluster_manager.cluster_added", 1); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendClusterDiscoveryResponse({}, {}, {ClusterName1}, "42"); // Make sure that Envoy's ClusterManager has made use of the DiscoveryResponse that says // cluster_1 is gone. @@ -459,13 +459,13 @@ TEST_P(CdsIntegrationTest, CdsClusterWithThreadAwareLbCycleUpDownUp) { // Cyclically add and remove cluster with ThreadAwareLb. for (int i = 42; i < 142; i += 2) { EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, absl::StrCat(i), {}, {}, {})); + compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, absl::StrCat(i), {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, absl::StrCat(i + 1)); - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, absl::StrCat(i + 1), {}, {}, {})); + Config::TestTypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, absl::StrCat(i + 1)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, absl::StrCat(i + 1), {}, + {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {}, {}, {ClusterName1}, absl::StrCat(i + 2)); + Config::TestTypeUrl::get().Cluster, {}, {}, {ClusterName1}, absl::StrCat(i + 2)); } cleanupUpstreamAndDownstream(); @@ -480,9 +480,9 @@ TEST_P(CdsIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_2 is here. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); // The '3' includes the fake CDS server. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); @@ -492,7 +492,7 @@ TEST_P(CdsIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is gone. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "42", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "42", {}, {}, {})); sendClusterDiscoveryResponse({cluster2_}, {}, {ClusterName1}, "43"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. @@ -504,9 +504,9 @@ TEST_P(CdsIntegrationTest, TwoClusters) { ASSERT_TRUE(codec_client_->waitForDisconnect()); // Tell Envoy that cluster_1 is back. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "43", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "43", {}, {}, {})); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_}, {}, "413"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_}, {}, "413"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse describing cluster_1 that we sent. Again, 3 includes CDS server. @@ -534,7 +534,7 @@ TEST_P(CdsIntegrationTest, TwoClustersAndRedirects) { // Tell Envoy that cluster_2 is here. initialize(); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster2_}, {}, "42"); // The '3' includes the fake CDS server. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); // Tell Envoy that cluster_1 is gone. @@ -594,8 +594,8 @@ TEST_P(CdsIntegrationTest, VersionsRememberedAfterReconnect) { // Tell Envoy that cluster_2 is here. This update does *not* need to include cluster_1, // which Envoy should already know about despite the disconnect. - sendDeltaDiscoveryResponse(Config::TypeUrl::get().Cluster, - {cluster2_}, {}, "42"); + sendDeltaDiscoveryResponse( + Config::TestTypeUrl::get().Cluster, {cluster2_}, {}, "42"); // The '3' includes the fake CDS server. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); @@ -635,6 +635,8 @@ TEST_P(CdsIntegrationTest, CdsClusterDownWithLotsOfIdleConnections) { ->mutable_idle_timeout() ->set_seconds(600); }); + config_helper_.setDownstreamHttp2MaxConcurrentStreams(2001); + initialize(); std::vector responses; std::vector upstream_connections; @@ -679,7 +681,7 @@ TEST_P(CdsIntegrationTest, CdsClusterDownWithLotsOfIdleConnections) { test_server_->waitForCounterGe("cluster_manager.cluster_added", 1); // Tell Envoy that cluster_1 is gone. Envoy will try to close all idle connections - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendClusterDiscoveryResponse({}, {}, {ClusterName1}, "42"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. @@ -750,7 +752,7 @@ TEST_P(CdsIntegrationTest, DISABLED_CdsClusterDownWithLotsOfConnectingConnection test_server_->waitForCounterEq("cluster.cluster_1.upstream_cx_total", num_requests); // Tell Envoy that cluster_1 is gone. Envoy will try to close all pending connections - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "55", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "55", {}, {}, {})); sendClusterDiscoveryResponse({}, {}, {ClusterName1}, "42"); // We can continue the test once we're sure that Envoy's ClusterManager has made use of // the DiscoveryResponse that says cluster_1 is gone. diff --git a/test/integration/circuit_breakers_integration_test.cc b/test/integration/circuit_breakers_integration_test.cc index 6d38b10776938..adc4a9885deaf 100644 --- a/test/integration/circuit_breakers_integration_test.cc +++ b/test/integration/circuit_breakers_integration_test.cc @@ -151,7 +151,7 @@ TEST_P(CircuitBreakersIntegrationTest, CircuitBreakerRuntimeProto) { auto* layer = bootstrap.mutable_layered_runtime()->add_layers(); layer->set_name("enable layer"); - ProtobufWkt::Struct& runtime = *layer->mutable_static_layer(); + Protobuf::Struct& runtime = *layer->mutable_static_layer(); (*runtime.mutable_fields())["circuit_breakers.cluster_0.default.max_requests"].set_number_value( 0); diff --git a/test/integration/cluster_filter_integration_test.cc b/test/integration/cluster_filter_integration_test.cc index a363d9f512c2d..e716b0a5195ff 100644 --- a/test/integration/cluster_filter_integration_test.cc +++ b/test/integration/cluster_filter_integration_test.cc @@ -23,7 +23,7 @@ class TestParent { class PoliteFilter : public Network::Filter, Logger::Loggable { public: - PoliteFilter(TestParent& parent, const ProtobufWkt::StringValue& value) + PoliteFilter(TestParent& parent, const Protobuf::StringValue& value) : parent_(parent), greeting_(value.value()) {} Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override { @@ -80,14 +80,14 @@ class PoliteFilterConfigFactory Network::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message& proto_config, Server::Configuration::UpstreamFactoryContext&) override { - auto config = dynamic_cast(proto_config); + auto config = dynamic_cast(proto_config); return [this, config](Network::FilterManager& filter_manager) -> void { filter_manager.addFilter(std::make_shared(test_parent_, config)); }; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.upstream.polite"; } @@ -139,7 +139,7 @@ class ClusterFilterTcpIntegrationTest : public ClusterFilterIntegrationTestBase, auto* cluster_0 = bootstrap.mutable_static_resources()->mutable_clusters(0); auto* filter = cluster_0->add_filters(); filter->set_name("envoy.upstream.polite"); - ProtobufWkt::StringValue config; + Protobuf::StringValue config; config.set_value("surely "); filter->mutable_typed_config()->PackFrom(config); }); @@ -199,7 +199,7 @@ class ClusterFilterHttpIntegrationTest : public ClusterFilterIntegrationTestBase auto* cluster_0 = bootstrap.mutable_static_resources()->mutable_clusters(0); auto* filter = cluster_0->add_filters(); filter->set_name("envoy.upstream.polite"); - ProtobufWkt::StringValue config; + Protobuf::StringValue config; config.set_value(""); filter->mutable_typed_config()->PackFrom(config); }); diff --git a/test/integration/cluster_upstream_extension_integration_test.cc b/test/integration/cluster_upstream_extension_integration_test.cc index 0853bedb76d52..da83a4d865d7f 100644 --- a/test/integration/cluster_upstream_extension_integration_test.cc +++ b/test/integration/cluster_upstream_extension_integration_test.cc @@ -26,7 +26,7 @@ class ClusterUpstreamExtensionIntegrationTest const std::string& key1, const std::string& key2, const std::string& value) { - ProtobufWkt::Struct struct_obj; + Protobuf::Struct struct_obj; (*struct_obj.mutable_fields())[key2] = ValueUtil::stringValue(value); (*metadata.mutable_filter_metadata())[key1] = struct_obj; } diff --git a/test/integration/command_formatter_extension_integration_test.cc b/test/integration/command_formatter_extension_integration_test.cc index e0fe588e61302..b8f3df1eccfc5 100644 --- a/test/integration/command_formatter_extension_integration_test.cc +++ b/test/integration/command_formatter_extension_integration_test.cc @@ -22,7 +22,7 @@ TEST_F(CommandFormatterExtensionIntegrationTest, BasicExtension) { Registry::InjectFactory command_register(factory); std::vector formatters; envoy::config::core::v3::TypedExtensionConfig typed_config; - ProtobufWkt::StringValue config; + Protobuf::StringValue config; typed_config.set_name("envoy.formatter.TestFormatter"); typed_config.mutable_typed_config()->PackFrom(config); diff --git a/test/integration/eds_integration_test.cc b/test/integration/eds_integration_test.cc index 4458b00694b61..e7484ab42aa81 100644 --- a/test/integration/eds_integration_test.cc +++ b/test/integration/eds_integration_test.cc @@ -17,15 +17,6 @@ namespace Envoy { namespace { -MATCHER_P2(SingleHeaderValueIs, key, value, - absl::StrFormat("Header \"%s\" equals \"%s\"", key, value)) { - const auto hdr = arg.get(::Envoy::Http::LowerCaseString(std::string(key))); - if (hdr.size() != 1) { - return false; - } - return hdr[0]->value() == value; -} - void validateClusters(const Upstream::ClusterManager::ClusterInfoMap& active_cluster_map, const std::string& cluster, size_t expected_active_clusters, size_t hosts_expected, size_t healthy_hosts, size_t degraded_hosts) { @@ -97,6 +88,7 @@ class EdsIntegrationTest uint32_t healthy_endpoints = 0; uint32_t degraded_endpoints = 0; uint32_t disable_active_hc_endpoints = 0; + absl::optional load_balancing_weight = absl::nullopt; absl::optional weighted_priority_health = absl::nullopt; absl::optional overprovisioning_factor = absl::nullopt; absl::optional drop_overload_numerator = absl::nullopt; @@ -147,6 +139,10 @@ class EdsIntegrationTest ->mutable_health_check_config() ->set_disable_active_health_check(true); } + if (endpoint_setting.load_balancing_weight.has_value()) { + endpoint->mutable_load_balancing_weight()->set_value( + endpoint_setting.load_balancing_weight.value()); + } } if (await_update) { @@ -235,7 +231,7 @@ class EdsIntegrationTest EXPECT_EQ(status, response->headers().getStatusValue()); if (numerator == 100) { EXPECT_THAT(response->headers(), - SingleHeaderValueIs("x-envoy-unconditional-drop-overload", "true")); + ContainsHeader("x-envoy-unconditional-drop-overload", "true")); } cleanupUpstreamAndDownstream(); } @@ -815,5 +811,44 @@ TEST_P(EdsIntegrationTest, DropOverloadTestForEdsClusterNoDrop) { dropOverloadTe TEST_P(EdsIntegrationTest, DropOverloadTestForEdsClusterAllDrop) { dropOverloadTest(100, "503"); } +TEST_P(EdsIntegrationTest, LoadBalancerRejectsEndpoints) { + autonomous_upstream_ = true; + initializeTest(false /* http_active_hc */, [](envoy::config::cluster::v3::Cluster& cluster) { + // Maglev (and all load balancers that inherit `ThreadAwareLoadBalancerBase`) has a + // constraint that the total endpoint weights must not exceed uint32_max. Use this to generate + // errors instead of writing a test load balancing extension. + cluster.set_lb_policy(::envoy::config::cluster::v3::Cluster::MAGLEV); + }); + EndpointSettingOptions options; + options.total_endpoints = 4; + options.healthy_endpoints = 4; + options.load_balancing_weight = UINT32_MAX - 1; + setEndpoints(options, true, false); + test_server_->waitForCounterGe("cluster.cluster_0.update_rejected", 1); +} + +TEST_P(EdsIntegrationTest, LoadBalancerRejectsEndpointsWithHealthcheck) { + cluster_.mutable_common_lb_config()->set_ignore_new_hosts_until_first_hc(true); + autonomous_upstream_ = true; + initializeTest(true /* http_active_hc */, [](envoy::config::cluster::v3::Cluster& cluster) { + // Disable the healthy panic threshold, which causes the initial update from the EDS update + // to not include any of the hosts so that there isn't an error detected for the weights + // until after a health check passes. + cluster.mutable_common_lb_config()->mutable_healthy_panic_threshold(); + + // Maglev (and all load balancers that inherit `ThreadAwareLoadBalancerBase`) has a + // constraint that the total endpoint weights must not exceed uint32_max. Use this to generate + // errors instead of writing a test load balancing extension. + cluster.set_lb_policy(::envoy::config::cluster::v3::Cluster::MAGLEV); + }); + + EndpointSettingOptions options; + options.total_endpoints = 4; + options.healthy_endpoints = 4; + options.load_balancing_weight = UINT32_MAX - 1; + setEndpoints(options, true, false); + test_server_->waitForCounterGe("cluster.cluster_0.update_rejected", 1); +} + } // namespace } // namespace Envoy diff --git a/test/integration/extension_discovery_integration_test.cc b/test/integration/extension_discovery_integration_test.cc index 1761424dc7faa..0766b32a9bbee 100644 --- a/test/integration/extension_discovery_integration_test.cc +++ b/test/integration/extension_discovery_integration_test.cc @@ -276,7 +276,7 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara void sendLdsResponse(const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); response.add_resources()->PackFrom(listener_config_); lds_stream_->sendGrpcMessage(response); } @@ -329,7 +329,7 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara void sendHttpFilterEcdsResponseWithFullYaml(const std::string& name, const std::string& version, const std::string& full_yaml) { - const auto configuration = TestUtility::parseYaml(full_yaml); + const auto configuration = TestUtility::parseYaml(full_yaml); envoy::config::core::v3::TypedExtensionConfig typed_config; typed_config.set_name(name); typed_config.mutable_typed_config()->MergeFrom(configuration); diff --git a/test/integration/fake_access_log.h b/test/integration/fake_access_log.h index e433fb50bd0a7..5ac0fafe59121 100644 --- a/test/integration/fake_access_log.h +++ b/test/integration/fake_access_log.h @@ -30,7 +30,7 @@ class FakeAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message&, AccessLog::FilterPtr&&, - Server::Configuration::FactoryContext&, + Server::Configuration::GenericFactoryContext&, std::vector&& = {}) override { std::lock_guard guard(log_callback_lock_); auto access_log_instance = std::make_shared(log_cb_); diff --git a/test/integration/fake_upstream.cc b/test/integration/fake_upstream.cc index c063165053db6..559484e147ec7 100644 --- a/test/integration/fake_upstream.cc +++ b/test/integration/fake_upstream.cc @@ -48,7 +48,7 @@ FakeStream::FakeStream(FakeHttpConnection& parent, Http::ResponseEncoder& encode } void FakeStream::decodeHeaders(Http::RequestHeaderMapSharedPtr&& headers, bool end_stream) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); headers_ = std::move(headers); if (header_validator_) { header_validator_->transformRequestHeaders(*headers_); @@ -58,13 +58,13 @@ void FakeStream::decodeHeaders(Http::RequestHeaderMapSharedPtr&& headers, bool e void FakeStream::decodeData(Buffer::Instance& data, bool end_stream) { received_data_ = true; - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); body_.add(data); setEndStream(end_stream); } void FakeStream::decodeTrailers(Http::RequestTrailerMapPtr&& trailers) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); setEndStream(true); trailers_ = std::move(trailers); } @@ -89,7 +89,7 @@ void FakeStream::encode1xxHeaders(const Http::ResponseHeaderMap& headers) { Http::createHeaderMap(headers)); postToConnectionThread([this, headers_copy]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -117,7 +117,7 @@ void FakeStream::encodeHeaders(const Http::HeaderMap& headers, bool end_stream) postToConnectionThread([this, headers_copy = std::move(headers_copy), end_stream]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -130,7 +130,7 @@ void FakeStream::encodeHeaders(const Http::HeaderMap& headers, bool end_stream) void FakeStream::encodeData(std::string data, bool end_stream) { postToConnectionThread([this, data, end_stream]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -144,7 +144,7 @@ void FakeStream::encodeData(std::string data, bool end_stream) { void FakeStream::encodeData(uint64_t size, bool end_stream) { postToConnectionThread([this, size, end_stream]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -159,7 +159,7 @@ void FakeStream::encodeData(Buffer::Instance& data, bool end_stream) { std::shared_ptr data_copy = std::make_shared(data); postToConnectionThread([this, data_copy, end_stream]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -174,7 +174,7 @@ void FakeStream::encodeTrailers(const Http::HeaderMap& trailers) { Http::createHeaderMap(trailers)); postToConnectionThread([this, trailers_copy]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -187,7 +187,7 @@ void FakeStream::encodeTrailers(const Http::HeaderMap& trailers) { void FakeStream::encodeResetStream() { postToConnectionThread([this]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -204,7 +204,7 @@ void FakeStream::encodeResetStream() { void FakeStream::encodeMetadata(const Http::MetadataMapVector& metadata_map_vector) { postToConnectionThread([this, &metadata_map_vector]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -217,7 +217,7 @@ void FakeStream::encodeMetadata(const Http::MetadataMapVector& metadata_map_vect void FakeStream::readDisable(bool disable) { postToConnectionThread([this, disable]() -> void { { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!parent_.connected() || saw_reset_) { // Encoded already deleted. return; @@ -228,12 +228,12 @@ void FakeStream::readDisable(bool disable) { } void FakeStream::onResetStream(Http::StreamResetReason, absl::string_view) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); saw_reset_ = true; } AssertionResult FakeStream::waitForHeadersComplete(milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return headers_ != nullptr; }; if (!time_system_.waitFor(lock_, absl::Condition(&reached), timeout)) { @@ -265,7 +265,7 @@ bool waitForWithDispatcherRun(Event::TestTimeSystem& time_system, absl::Mutex& l AssertionResult FakeStream::waitForData(Event::Dispatcher& client_dispatcher, uint64_t body_length, milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!waitForWithDispatcherRun( time_system_, lock_, [this, body_length]() @@ -278,7 +278,7 @@ AssertionResult FakeStream::waitForData(Event::Dispatcher& client_dispatcher, ui AssertionResult FakeStream::waitForData(Event::Dispatcher& client_dispatcher, absl::string_view data, milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!waitForWithDispatcherRun( time_system_, lock_, [this, &data]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { @@ -297,7 +297,7 @@ AssertionResult FakeStream::waitForData(Event::Dispatcher& client_dispatcher, AssertionResult FakeStream::waitForData(Event::Dispatcher& client_dispatcher, const FakeStream::ValidatorFunction& data_validator, std::chrono::milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!waitForWithDispatcherRun( time_system_, lock_, [this, data_validator]() @@ -310,7 +310,7 @@ AssertionResult FakeStream::waitForData(Event::Dispatcher& client_dispatcher, AssertionResult FakeStream::waitForEndStream(Event::Dispatcher& client_dispatcher, milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!waitForWithDispatcherRun( time_system_, lock_, [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return end_stream_; }, client_dispatcher, @@ -321,7 +321,7 @@ AssertionResult FakeStream::waitForEndStream(Event::Dispatcher& client_dispatche } AssertionResult FakeStream::waitForReset(milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!time_system_.waitFor(lock_, absl::Condition(&saw_reset_), timeout)) { return AssertionFailure() << "Timed out waiting for reset."; } @@ -330,7 +330,7 @@ AssertionResult FakeStream::waitForReset(milliseconds timeout) { AssertionResult FakeStream::waitForReset(Event::Dispatcher& client_dispatcher, std::chrono::milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!waitForWithDispatcherRun( time_system_, lock_, [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return saw_reset_; }, client_dispatcher, timeout)) { @@ -484,7 +484,7 @@ Http::ServerHeaderValidatorPtr FakeHttpConnection::makeHeaderValidator() { } Http::RequestDecoder& FakeHttpConnection::newStream(Http::ResponseEncoder& encoder, bool) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); new_streams_.emplace_back(new FakeStream(*this, encoder, time_system_)); return *new_streams_.back(); } @@ -538,7 +538,7 @@ void FakeHttpConnection::encodeProtocolError() { AssertionResult FakeConnectionBase::waitForDisconnect(milliseconds timeout) { ENVOY_LOG(trace, "FakeConnectionBase waiting for disconnect"); - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return !shared_connection_.connectedLockHeld(); }; @@ -555,7 +555,7 @@ AssertionResult FakeConnectionBase::waitForDisconnect(milliseconds timeout) { AssertionResult FakeConnectionBase::waitForRstDisconnect(std::chrono::milliseconds timeout) { ENVOY_LOG(trace, "FakeConnectionBase waiting for RST disconnect"); - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return shared_connection_.rstDisconnected(); }; @@ -572,7 +572,7 @@ AssertionResult FakeConnectionBase::waitForRstDisconnect(std::chrono::millisecon } AssertionResult FakeConnectionBase::waitForHalfClose(milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!time_system_.waitFor(lock_, absl::Condition(&half_closed_), timeout)) { return AssertionFailure() << "Timed out waiting for half close."; } @@ -580,7 +580,7 @@ AssertionResult FakeConnectionBase::waitForHalfClose(milliseconds timeout) { } AssertionResult FakeConnectionBase::waitForNoPost(milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!time_system_.waitFor( lock_, absl::Condition( @@ -600,7 +600,7 @@ void FakeConnectionBase::postToConnectionThread(std::function cb) { cb(); { // Snag this lock not because it's needed but so waitForNoPost doesn't stall - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); --pending_cbs_; } }); @@ -609,7 +609,7 @@ void FakeConnectionBase::postToConnectionThread(std::function cb) { AssertionResult FakeHttpConnection::waitForNewStream(Event::Dispatcher& client_dispatcher, FakeStreamPtr& stream, std::chrono::milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (!waitForWithDispatcherRun( time_system_, lock_, [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return !new_streams_.empty(); }, @@ -730,7 +730,7 @@ void FakeUpstream::cleanUp() { bool FakeUpstream::createNetworkFilterChain(Network::Connection& connection, const Filter::NetworkFilterFactoriesList&) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); if (read_disable_on_new_connection_ && http_type_ != Http::CodecType::HTTP3) { // Disable early close detection to avoid closing the network connection before full // initialization is complete. @@ -771,7 +771,7 @@ void FakeUpstream::threadRoutine() { dispatcher_->run(Event::Dispatcher::RunType::Block); handler_.reset(); { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); new_connections_.clear(); quic_connections_.clear(); consumed_connections_.clear(); @@ -787,7 +787,7 @@ AssertionResult FakeUpstream::waitForHttpConnection(Event::Dispatcher& client_di } { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); // As noted in createNetworkFilterChain, HTTP3 FakeHttpConnections are not // lazily created, so HTTP3 needs a different wait path here. @@ -818,7 +818,7 @@ AssertionResult FakeUpstream::waitForHttpConnection(Event::Dispatcher& client_di } } return runOnDispatcherThreadAndWait([&]() { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); connection = std::make_unique( *this, consumeConnection(), http_type_, time_system_, config_.max_request_headers_kb_, config_.max_request_headers_count_, config_.headers_with_underscores_action_); @@ -872,7 +872,7 @@ FakeUpstream::waitForHttpConnection(Event::Dispatcher& client_dispatcher, ABSL_MUST_USE_RESULT AssertionResult FakeUpstream::assertPendingConnectionsEmpty() { return runOnDispatcherThreadAndWait([&]() { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); return new_connections_.empty() ? AssertionSuccess() : AssertionFailure(); }); } @@ -885,7 +885,7 @@ AssertionResult FakeUpstream::waitForRawConnection(FakeRawConnectionPtr& connect } { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return !new_connections_.empty(); }; @@ -897,7 +897,7 @@ AssertionResult FakeUpstream::waitForRawConnection(FakeRawConnectionPtr& connect } return runOnDispatcherThreadAndWait([&]() { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); connection = makeRawConnection(consumeConnection(), timeSystem()); connection->initialize(); // Skip enableHalfClose if the connection is already disconnected. @@ -910,7 +910,7 @@ AssertionResult FakeUpstream::waitForRawConnection(FakeRawConnectionPtr& connect void FakeUpstream::convertFromRawToHttp(FakeRawConnectionPtr& raw_connection, FakeHttpConnectionPtr& connection) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); SharedConnectionWrapper& shared_connection = raw_connection->sharedConnection(); connection = std::make_unique( @@ -945,7 +945,7 @@ AssertionResult FakeUpstream::waitForUdpDatagram(Network::UdpRecvData& data_to_f << "Must initialize the FakeUpstream first by calling initializeServer()."; } - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return !received_datagrams_.empty(); }; @@ -960,7 +960,7 @@ AssertionResult FakeUpstream::waitForUdpDatagram(Network::UdpRecvData& data_to_f } Network::FilterStatus FakeUpstream::onRecvDatagram(Network::UdpRecvData& data) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); received_datagrams_.emplace_back(std::move(data)); return Network::FilterStatus::StopIteration; @@ -1002,7 +1002,7 @@ AssertionResult FakeUpstream::rawWriteConnection(uint32_t index, const std::stri << "Must initialize the FakeUpstream first by calling initializeServer()."; } - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); auto iter = consumed_connections_.begin(); std::advance(iter, index); return (*iter)->executeOnDispatcher( @@ -1049,7 +1049,7 @@ void FakeRawConnection::initialize() { AssertionResult FakeRawConnection::waitForData(uint64_t num_bytes, std::string* data, milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this, num_bytes]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return data_.size() == num_bytes; }; @@ -1068,7 +1068,7 @@ AssertionResult FakeRawConnection::waitForData(uint64_t num_bytes, std::string* AssertionResult FakeRawConnection::waitForData(const std::function& data_validator, std::string* data, milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this, &data_validator]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { return data_validator(data_); }; ENVOY_LOG(debug, "waiting for data"); @@ -1093,7 +1093,7 @@ AssertionResult FakeRawConnection::write(const std::string& data, bool end_strea Network::FilterStatus FakeRawConnection::ReadFilter::onData(Buffer::Instance& data, bool end_stream) { - absl::MutexLock lock(&parent_.lock_); + absl::MutexLock lock(parent_.lock_); ENVOY_LOG(debug, "got {} bytes, end_stream {}", data.length(), end_stream); parent_.data_.append(data.toString()); parent_.half_closed_ = end_stream; @@ -1104,7 +1104,7 @@ Network::FilterStatus FakeRawConnection::ReadFilter::onData(Buffer::Instance& da ABSL_MUST_USE_RESULT AssertionResult FakeHttpConnection::waitForInexactRawData(absl::string_view data, std::string& out, std::chrono::milliseconds timeout) { - absl::MutexLock lock(&lock_); + absl::MutexLock lock(lock_); const auto reached = [this, data, &out]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_) { char peek_buf[200]; auto result = dynamic_cast(&connection()) diff --git a/test/integration/filter_manager_integration_test.cc b/test/integration/filter_manager_integration_test.cc index 518daa2486e9c..a0738cd85e4eb 100644 --- a/test/integration/filter_manager_integration_test.cc +++ b/test/integration/filter_manager_integration_test.cc @@ -265,7 +265,7 @@ class DispenserFilterConfigFactory : public Server::Configuration::NamedNetworkF ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return name_; } diff --git a/test/integration/filters/address_restore_listener_filter.cc b/test/integration/filters/address_restore_listener_filter.cc index e623afbcae506..581ab56c47cdf 100644 --- a/test/integration/filters/address_restore_listener_filter.cc +++ b/test/integration/filters/address_restore_listener_filter.cc @@ -54,7 +54,7 @@ class FakeOriginalDstListenerFilterConfigFactory } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { diff --git a/test/integration/filters/decode_dynamic_metadata_filter.cc b/test/integration/filters/decode_dynamic_metadata_filter.cc index e5441bfa131c6..3624c4e52eaa6 100644 --- a/test/integration/filters/decode_dynamic_metadata_filter.cc +++ b/test/integration/filters/decode_dynamic_metadata_filter.cc @@ -16,13 +16,13 @@ class DecodeDynamicMetadataFilter : public Http::PassThroughFilter { for (auto const& [k, v] : kvs.fields()) { std::string value; switch (v.kind_case()) { - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: value = fmt::format("{:g}", v.number_value()); break; - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: value = v.string_value(); break; - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: value = v.bool_value() ? "true" : "false"; break; default: diff --git a/test/integration/filters/listener_typed_metadata_filter.cc b/test/integration/filters/listener_typed_metadata_filter.cc index 8150d36153371..ef85187a0e672 100644 --- a/test/integration/filters/listener_typed_metadata_filter.cc +++ b/test/integration/filters/listener_typed_metadata_filter.cc @@ -27,13 +27,13 @@ class BazTypedMetadataFactory : public Network::ListenerTypedMetadataFactory { std::string name() const override { return std::string(kMetadataKey); } std::unique_ptr - parse(const ProtobufWkt::Struct&) const override { + parse(const Protobuf::Struct&) const override { ADD_FAILURE() << "Filter should not parse struct-typed metadata."; return nullptr; } std::unique_ptr - parse(const ProtobufWkt::Any& d) const override { - ProtobufWkt::StringValue v; + parse(const Protobuf::Any& d) const override { + Protobuf::StringValue v; EXPECT_TRUE(d.UnpackTo(&v)); auto object = std::make_unique(); object->item_ = v.value(); diff --git a/test/integration/filters/stream_info_to_headers_filter.cc b/test/integration/filters/stream_info_to_headers_filter.cc index 3b792c495e65a..2036909af97af 100644 --- a/test/integration/filters/stream_info_to_headers_filter.cc +++ b/test/integration/filters/stream_info_to_headers_filter.cc @@ -18,28 +18,28 @@ std::string toUsec(MonotonicTime time) { return absl::StrCat(time.time_since_epo } // namespace void addValueHeaders(Http::ResponseHeaderMap& headers, std::string key_prefix, - const ProtobufWkt::Value& val) { + const Protobuf::Value& val) { switch (val.kind_case()) { - case ProtobufWkt::Value::kNullValue: + case Protobuf::Value::kNullValue: headers.addCopy(Http::LowerCaseString(key_prefix), "null"); break; - case ProtobufWkt::Value::kNumberValue: + case Protobuf::Value::kNumberValue: headers.addCopy(Http::LowerCaseString(key_prefix), std::to_string(val.number_value())); break; - case ProtobufWkt::Value::kStringValue: + case Protobuf::Value::kStringValue: headers.addCopy(Http::LowerCaseString(key_prefix), val.string_value()); break; - case ProtobufWkt::Value::kBoolValue: + case Protobuf::Value::kBoolValue: headers.addCopy(Http::LowerCaseString(key_prefix), val.bool_value() ? "true" : "false"); break; - case ProtobufWkt::Value::kListValue: { + case Protobuf::Value::kListValue: { const auto& vals = val.list_value().values(); for (auto i = 0; i < vals.size(); ++i) { addValueHeaders(headers, key_prefix + "." + std::to_string(i), vals[i]); } break; } - case ProtobufWkt::Value::kStructValue: + case Protobuf::Value::kStructValue: for (const auto& field : val.struct_value().fields()) { addValueHeaders(headers, key_prefix + "." + field.first, field.second); } diff --git a/test/integration/hds_integration_test.cc b/test/integration/hds_integration_test.cc index 7dc791d864fc7..5cf697c69ad82 100644 --- a/test/integration/hds_integration_test.cc +++ b/test/integration/hds_integration_test.cc @@ -268,6 +268,10 @@ class HdsIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, public H void waitForEndpointHealthResponse(envoy::config::core::v3::HealthStatus healthy) { ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, response_)); + if (!response_.has_endpoint_health_response() || + response_.endpoint_health_response().endpoints_health_size() == 0) { + return; + } while (!checkEndpointHealthResponse(response_.endpoint_health_response().endpoints_health(0), healthy, host_upstream_->localAddress())) { ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, response_)); @@ -1288,5 +1292,41 @@ TEST_P(HdsIntegrationTest, SingleEndpointHealthyHttpHdsReconnect) { cleanupHdsConnection(); } +TEST_P(HdsIntegrationTest, RemoveClusterDuringHealthCheck) { + initialize(); + + // Server <--> Envoy + waitForHdsStream(); + ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); + EXPECT_EQ(envoy_msg_.health_check_request().capability().health_check_protocols(0), + envoy::service::health::v3::Capability::HTTP); + + // Server asks for health checking + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); + hds_stream_->startGrpcStream(); + hds_stream_->sendGrpcMessage(server_health_check_specifier_); + test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); + + // Envoy sends a health check message to an endpoint + healthcheckEndpoints(); + + server_health_check_specifier_ = envoy::service::health::v3::HealthCheckSpecifier(); + server_health_check_specifier_.mutable_interval()->set_nanos(100000000); // 0.1 seconds + + hds_stream_->sendGrpcMessage(server_health_check_specifier_); + test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); + + // As the HDS cluster is destroyed, existing connections should be closed. + EXPECT_TRUE(host_fake_connection_->waitForDisconnect()); + + // Receive updates until the one we expect arrives + waitForEndpointHealthResponse(envoy::config::core::v3::UNHEALTHY); + + // Clean up connections + cleanupHostConnections(); + cleanupHdsConnection(); +} + } // namespace } // namespace Envoy diff --git a/test/integration/header_integration_test.cc b/test/integration/header_integration_test.cc index f3b316444dda0..5248983d35fd1 100644 --- a/test/integration/header_integration_test.cc +++ b/test/integration/header_integration_test.cc @@ -307,25 +307,24 @@ class HeaderIntegrationTest if (use_eds_) { addHeader(route_config->mutable_response_headers_to_add(), "x-routeconfig-dynamic", - R"(%UPSTREAM_METADATA(["test.namespace", "key"])%)", append); + R"(%UPSTREAM_METADATA(test.namespace:key)%)", append); // Iterate over VirtualHosts, nested Routes and WeightedClusters, adding a dynamic // response header. for (auto& vhost : *route_config->mutable_virtual_hosts()) { addHeader(vhost.mutable_response_headers_to_add(), "x-vhost-dynamic", - R"(vhost:%UPSTREAM_METADATA(["test.namespace", "key"])%)", append); + R"(vhost:%UPSTREAM_METADATA(test.namespace:key)%)", append); for (auto& route : *vhost.mutable_routes()) { addHeader(route.mutable_response_headers_to_add(), "x-route-dynamic", - R"(route:%UPSTREAM_METADATA(["test.namespace", "key"])%)", append); + R"(route:%UPSTREAM_METADATA(test.namespace:key)%)", append); if (route.has_route()) { auto* route_action = route.mutable_route(); if (route_action->has_weighted_clusters()) { for (auto& c : *route_action->mutable_weighted_clusters()->mutable_clusters()) { addHeader(c.mutable_response_headers_to_add(), "x-weighted-cluster-dynamic", - R"(weighted:%UPSTREAM_METADATA(["test.namespace", "key"])%)", - append); + R"(weighted:%UPSTREAM_METADATA(test.namespace:key)%)", append); } } } @@ -391,7 +390,7 @@ class HeaderIntegrationTest envoy::service::discovery::v3::DiscoveryResponse discovery_response; discovery_response.set_version_info("1"); - discovery_response.set_type_url(Config::TypeUrl::get().ClusterLoadAssignment); + discovery_response.set_type_url(Config::TestTypeUrl::get().ClusterLoadAssignment); auto cluster_load_assignment = TestUtility::parseYaml(fmt::format( @@ -1053,6 +1052,45 @@ TEST_P(HeaderIntegrationTest, TestDynamicHeaders) { }); } +TEST_P(HeaderIntegrationTest, TestResponseHeadersOnlyBeHandledOnce) { + initializeFilter(HeaderMode::Append, false); + registerTestServerPorts({"http"}); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + auto encoder_decoder = codec_client_->startRequest(Http::TestRequestHeaderMapImpl{ + {":method", "POST"}, + {":path", "/vhost-and-route"}, + {":scheme", "http"}, + {":authority", "vhost-headers.com"}, + }); + auto response = std::move(encoder_decoder.second); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + ASSERT_TRUE(fake_upstream_connection_->close()); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + ASSERT_TRUE(response->waitForEndStream()); + + if (downstream_protocol_ == Http::CodecType::HTTP1) { + ASSERT_TRUE(codec_client_->waitForDisconnect()); + } else { + codec_client_->close(); + } + + EXPECT_FALSE(upstream_request_->complete()); + EXPECT_EQ(0U, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + + Http::TestResponseHeaderMapImpl response_headers{response->headers()}; + EXPECT_EQ(1, response_headers.get(Http::LowerCaseString("x-route-response")).size()); + EXPECT_EQ("route", response_headers.get_("x-route-response")); + EXPECT_EQ(1, response_headers.get(Http::LowerCaseString("x-vhost-response")).size()); + EXPECT_EQ("vhost", response_headers.get_("x-vhost-response")); +} + // Validates that XFF gets properly parsed. TEST_P(HeaderIntegrationTest, TestXFFParsing) { initializeFilter(HeaderMode::Replace, false); @@ -1351,24 +1389,24 @@ TEST_P(EmptyHeaderIntegrationTest, AllProtocolsPassEmptyHeaders) { *vhost.add_request_headers_to_add() = TestUtility::parseYaml(R"EOF( header: key: "x-ds-add-empty" - value: "%PER_REQUEST_STATE(does.not.exist)%" + value: "%FILTER_STATE(does.not.exist:PLAIN)%" keep_empty_value: true )EOF"); *vhost.add_request_headers_to_add() = TestUtility::parseYaml(R"EOF( header: key: "x-ds-no-add-empty" - value: "%PER_REQUEST_STATE(does.not.exist)%" + value: "%FILTER_STATE(does.not.exist:PLAIN)%" )EOF"); *vhost.add_response_headers_to_add() = TestUtility::parseYaml(R"EOF( header: key: "x-us-add-empty" - value: "%PER_REQUEST_STATE(does.not.exist)%" + value: "%FILTER_STATE(does.not.exist:PLAIN)%" keep_empty_value: true )EOF"); *vhost.add_response_headers_to_add() = TestUtility::parseYaml(R"EOF( header: key: "x-us-no-add-empty" - value: "%PER_REQUEST_STATE(does.not.exist)%" + value: "%FILTER_STATE(does.not.exist:PLAIN)%" )EOF"); config_helper_.addVirtualHost(vhost); diff --git a/test/integration/health_check_integration_test.cc b/test/integration/health_check_integration_test.cc index bda97baab3ac8..03b13d1aaf19b 100644 --- a/test/integration/health_check_integration_test.cc +++ b/test/integration/health_check_integration_test.cc @@ -196,9 +196,10 @@ class HttpHealthCheckIntegrationTestBase } // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster_data.cluster_}, {cluster_data.cluster_}, {}, "55"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); // Wait for upstream to receive health check request. ASSERT_TRUE(cluster_data.host_upstream_->waitForHttpConnection( @@ -560,9 +561,10 @@ class TcpHealthCheckIntegrationTest : public Event::TestUsingSimulatedTime, health_check->mutable_tcp_health_check()->add_receive()->set_text("506F6E67"); // "Pong" // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster_data.cluster_}, {cluster_data.cluster_}, {}, "55"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); // Wait for upstream to receive TCP HC request. ASSERT_TRUE( @@ -582,9 +584,10 @@ class TcpHealthCheckIntegrationTest : public Event::TestUsingSimulatedTime, proxy_protocol_config); // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster_data.cluster_}, {cluster_data.cluster_}, {}, "55"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); // Wait for upstream to receive TCP HC request. ASSERT_TRUE( @@ -713,9 +716,10 @@ class GrpcHealthCheckIntegrationTest : public Event::TestUsingSimulatedTime, health_check->mutable_grpc_health_check(); // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster_data.cluster_}, {cluster_data.cluster_}, {}, "55"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); // Wait for upstream to receive HC request. grpc::health::v1::HealthCheckRequest request; @@ -886,9 +890,10 @@ class ExternalHealthCheckIntegrationTest cluster_data.external_host_upstream_->localAddress()->ip()->port()); // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster_data.cluster_}, {cluster_data.cluster_}, {}, "55"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); // Wait for upstream to receive EXTERNAL HC request. ASSERT_TRUE(cluster_data.external_host_upstream_->waitForRawConnection( @@ -950,5 +955,116 @@ TEST_P(ExternalHealthCheckIntegrationTest, SingleEndpointTimeoutExternal) { EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); } +// Test HTTP health check with POST method and payload +TEST_P(HttpHealthCheckIntegrationTest, SingleEndpointHealthyHttpWithPayload) { + const uint32_t cluster_idx = 0; + initialize(); + + // Setup HTTP health check with POST method and payload + const envoy::type::v3::CodecClientType codec_client_type = + (Http::CodecType::HTTP1 == upstream_protocol_) ? envoy::type::v3::CodecClientType::HTTP1 + : envoy::type::v3::CodecClientType::HTTP2; + + auto& cluster_data = clusters_[cluster_idx]; + auto* health_check = addHealthCheck(cluster_data.cluster_); + health_check->mutable_http_health_check()->set_path("/api/health"); + health_check->mutable_http_health_check()->set_method(envoy::config::core::v3::POST); + health_check->mutable_http_health_check()->set_codec_client_type(codec_client_type); + + // Set request payload + health_check->mutable_http_health_check()->mutable_send()->set_text( + "48656C6C6F20576F726C64"); // "Hello World" in hex + + health_check->mutable_unhealthy_threshold()->set_value(1); + + // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); + + // Wait for upstream to receive health check request. + ASSERT_TRUE(cluster_data.host_upstream_->waitForHttpConnection( + *dispatcher_, cluster_data.host_fake_connection_)); + ASSERT_TRUE(cluster_data.host_fake_connection_->waitForNewStream(*dispatcher_, + cluster_data.host_stream_)); + ASSERT_TRUE(cluster_data.host_stream_->waitForEndStream(*dispatcher_)); + + // Verify the health check request + EXPECT_EQ(cluster_data.host_stream_->headers().getPathValue(), "/api/health"); + EXPECT_EQ(cluster_data.host_stream_->headers().getMethodValue(), "POST"); + EXPECT_EQ(cluster_data.host_stream_->headers().getHostValue(), cluster_data.name_); + EXPECT_EQ(cluster_data.host_stream_->headers().getContentLengthValue(), + "11"); // "Hello World" is 11 bytes + + // Verify the request body + EXPECT_EQ(cluster_data.host_stream_->body().toString(), "Hello World"); + + // Endpoint responds with healthy status to the health check. + cluster_data.host_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, + false); + cluster_data.host_stream_->encodeData(1024, true); + + // Verify that Envoy detected the health check response. + test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 1); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.success")->value()); + EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); +} + +// Test HTTP health check with PUT method and binary payload +TEST_P(HttpHealthCheckIntegrationTest, SingleEndpointHealthyHttpWithBinaryPayload) { + const uint32_t cluster_idx = 0; + initialize(); + + const envoy::type::v3::CodecClientType codec_client_type = + (Http::CodecType::HTTP1 == upstream_protocol_) ? envoy::type::v3::CodecClientType::HTTP1 + : envoy::type::v3::CodecClientType::HTTP2; + + auto& cluster_data = clusters_[cluster_idx]; + auto* health_check = addHealthCheck(cluster_data.cluster_); + health_check->mutable_http_health_check()->set_path("/health"); + health_check->mutable_http_health_check()->set_method(envoy::config::core::v3::PUT); + health_check->mutable_http_health_check()->set_codec_client_type(codec_client_type); + + // Set hex payload - JSON + const std::string json_payload = "{\"check\":\"health\"}"; + health_check->mutable_http_health_check()->mutable_send()->set_text( + "7B22636865636B223A226865616C7468227D"); // {"check":"health"} in hex + + health_check->mutable_unhealthy_threshold()->set_value(1); + + // Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse. + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, + {cluster_data.cluster_}, + {cluster_data.cluster_}, {}, "55"); + + // Wait for upstream to receive health check request. + ASSERT_TRUE(cluster_data.host_upstream_->waitForHttpConnection( + *dispatcher_, cluster_data.host_fake_connection_)); + ASSERT_TRUE(cluster_data.host_fake_connection_->waitForNewStream(*dispatcher_, + cluster_data.host_stream_)); + ASSERT_TRUE(cluster_data.host_stream_->waitForEndStream(*dispatcher_)); + + // Verify the health check request + EXPECT_EQ(cluster_data.host_stream_->headers().getPathValue(), "/health"); + EXPECT_EQ(cluster_data.host_stream_->headers().getMethodValue(), "PUT"); + EXPECT_EQ(cluster_data.host_stream_->headers().getContentLengthValue(), + std::to_string(json_payload.length())); + + // Verify the request body + EXPECT_EQ(cluster_data.host_stream_->body().toString(), json_payload); + + // Endpoint responds with healthy status to the health check. + cluster_data.host_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, + false); + cluster_data.host_stream_->encodeData(1024, true); + + // Verify that Envoy detected the health check response. + test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 1); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.success")->value()); + EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); +} + } // namespace } // namespace Envoy diff --git a/test/integration/http2_flood_integration_test.cc b/test/integration/http2_flood_integration_test.cc index 52fb08c647399..27f8adf740e44 100644 --- a/test/integration/http2_flood_integration_test.cc +++ b/test/integration/http2_flood_integration_test.cc @@ -376,7 +376,7 @@ TEST_P(Http2FloodMitigationTest, Data) { EXPECT_GE(22000, buffer_factory->sumMaxBufferSizes()); // Verify that all buffers have watermarks set. EXPECT_THAT(buffer_factory->highWatermarkRange(), - testing::Pair(256 * 1024 * 1024, 1024 * 1024 * 1024)); + testing::Pair(16 * 1024 * 1024, 1024 * 1024 * 1024)); } // Verify that the server can detect flood triggered by a DATA frame from a decoder filter call diff --git a/test/integration/http_conn_pool_integration_test.cc b/test/integration/http_conn_pool_integration_test.cc index ffe975921d72e..a15d46b329889 100644 --- a/test/integration/http_conn_pool_integration_test.cc +++ b/test/integration/http_conn_pool_integration_test.cc @@ -210,8 +210,10 @@ TEST_P(HttpConnPoolIntegrationTest, PoolDrainAfterDrainApiAllClusters) { EXPECT_TRUE(response->complete()); // Drain connection pools via API. Need to post this to the server thread. - test_server_->server().dispatcher().post( - [this] { test_server_->server().clusterManager().drainConnections(nullptr); }); + test_server_->server().dispatcher().post([this] { + test_server_->server().clusterManager().drainConnections( + nullptr, ConnectionPool::DrainBehavior::DrainExistingConnections); + }); ASSERT_TRUE(first_connection->waitForDisconnect()); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 638feb2551d4e..8e18e60dd8d18 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -1888,18 +1888,21 @@ void HttpIntegrationTest::expectDownstreamBytesSentAndReceived(BytesCountExpecta } void Http2RawFrameIntegrationTest::startHttp2Session() { + startHttp2Session(Http2Frame::makeEmptySettingsFrame()); +} + +void Http2RawFrameIntegrationTest::startHttp2Session(const Http2Frame& settings) { ASSERT_TRUE(tcp_client_->write(Http2Frame::Preamble, false, false)); - // Send empty initial SETTINGS frame. - auto settings = Http2Frame::makeEmptySettingsFrame(); + // Send initial SETTINGS frame. ASSERT_TRUE(tcp_client_->write(std::string(settings), false, false)); // Read initial SETTINGS frame from the server. readFrame(); - // Send an SETTINGS ACK. - settings = Http2Frame::makeEmptySettingsFrame(Http2Frame::SettingsFlags::Ack); - ASSERT_TRUE(tcp_client_->write(std::string(settings), false, false)); + // Send a SETTINGS ACK. + auto settings_ack = Http2Frame::makeEmptySettingsFrame(Http2Frame::SettingsFlags::Ack); + ASSERT_TRUE(tcp_client_->write(std::string(settings_ack), false, false)); // read pending SETTINGS and WINDOW_UPDATE frames readFrame(); @@ -1907,6 +1910,10 @@ void Http2RawFrameIntegrationTest::startHttp2Session() { } void Http2RawFrameIntegrationTest::beginSession() { + beginSession(Http2Frame::makeEmptySettingsFrame()); +} + +void Http2RawFrameIntegrationTest::beginSession(const Http2Frame& settings) { setDownstreamProtocol(Http::CodecType::HTTP2); setUpstreamProtocol(Http::CodecType::HTTP2); // set lower outbound frame limits to make tests run faster @@ -1918,7 +1925,7 @@ void Http2RawFrameIntegrationTest::beginSession() { envoy::config::core::v3::SocketOption::STATE_PREBIND, ENVOY_MAKE_SOCKET_OPTION_NAME(SOL_SOCKET, SO_RCVBUF), 1024)); tcp_client_ = makeTcpConnection(lookupPort("http"), options); - startHttp2Session(); + startHttp2Session(settings); } Http2Frame Http2RawFrameIntegrationTest::readFrame() { diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 520949f035cbb..548228f97fa69 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -392,10 +392,12 @@ class Http2RawFrameIntegrationTest : public HttpIntegrationTest { : HttpIntegrationTest(Http::CodecType::HTTP2, version) {} protected: + void startHttp2Session(const Http2Frame& settings); void startHttp2Session(); Http2Frame readFrame(); void sendFrame(const Http2Frame& frame); virtual void beginSession(); + virtual void beginSession(const Http2Frame& settings); IntegrationTcpClientPtr tcp_client_; }; diff --git a/test/integration/http_subset_lb_integration_test.cc b/test/integration/http_subset_lb_integration_test.cc index 11707c624851b..da7eafe04c67b 100644 --- a/test/integration/http_subset_lb_integration_test.cc +++ b/test/integration/http_subset_lb_integration_test.cc @@ -98,8 +98,7 @@ class HttpSubsetLbIntegrationTest auto* resp_header = vhost->add_response_headers_to_add(); auto* header = resp_header->mutable_header(); header->set_key(host_type_header_); - header->set_value( - fmt::format(R"EOF(%UPSTREAM_METADATA(["envoy.lb", "{}"])%)EOF", type_key_)); + header->set_value(fmt::format(R"EOF(%UPSTREAM_METADATA(envoy.lb:{})%)EOF", type_key_)); resp_header = vhost->add_response_headers_to_add(); header = resp_header->mutable_header(); diff --git a/test/integration/http_typed_per_filter_config_test.cc b/test/integration/http_typed_per_filter_config_test.cc index 6c9ceaf468835..95b7bd7bf6515 100644 --- a/test/integration/http_typed_per_filter_config_test.cc +++ b/test/integration/http_typed_per_filter_config_test.cc @@ -41,7 +41,7 @@ TEST_F(HTTPTypedPerFilterConfigTest, RejectUnknownHttpFilterInTypedPerFilterConf hcm) { auto* virtual_host = hcm.mutable_route_config()->mutable_virtual_hosts(0); auto* config = virtual_host->mutable_typed_per_filter_config(); - (*config)["filter.unknown"].PackFrom(Envoy::ProtobufWkt::Struct()); + (*config)["filter.unknown"].PackFrom(Envoy::Protobuf::Struct()); }); EXPECT_DEATH(initialize(), "Didn't find a registered implementation for 'filter.unknown' with type URL: " @@ -57,7 +57,7 @@ TEST_F(HTTPTypedPerFilterConfigTest, IgnoreUnknownOptionalHttpFilterInTypedPerFi envoy::config::route::v3::FilterConfig filter_config; filter_config.set_is_optional(true); - filter_config.mutable_config()->PackFrom(Envoy::ProtobufWkt::Struct()); + filter_config.mutable_config()->PackFrom(Envoy::Protobuf::Struct()); (*config)["filter.unknown"].PackFrom(filter_config); auto* filter = hcm.mutable_http_filters()->Add(); diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index 290ad989645e2..b86ba24ba68d0 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -53,13 +53,13 @@ TEST_P(IntegrationAdminTest, AdminLogging) { EXPECT_EQ("200", request("admin", "POST", "/logging", response)); EXPECT_EQ("text/plain; charset=UTF-8", contentType(response)); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().XContentTypeOptions, "nosniff")); + ContainsHeader(Http::Headers::get().XContentTypeOptions, "nosniff")); // Bad level EXPECT_EQ("400", request("admin", "POST", "/logging?level=blah", response)); EXPECT_EQ("text/plain; charset=UTF-8", contentType(response)); EXPECT_THAT(response->headers(), - HeaderValueOf(Http::Headers::get().XContentTypeOptions, "nosniff")); + ContainsHeader(Http::Headers::get().XContentTypeOptions, "nosniff")); EXPECT_THAT(response->body(), HasSubstr("error: unknown logger level\n")); // Bad logger diff --git a/test/integration/integration_test.cc b/test/integration/integration_test.cc index aaa4e6a5582f9..b1e66223ebed6 100644 --- a/test/integration/integration_test.cc +++ b/test/integration/integration_test.cc @@ -28,7 +28,6 @@ #include "gtest/gtest.h" using Envoy::Http::Headers; -using Envoy::Http::HeaderValueOf; using Envoy::Http::HttpStatusIs; using testing::Combine; using testing::ContainsRegex; @@ -172,7 +171,7 @@ class TestConnectionBalanceFactory : public Network::ConnectionBalanceFactory { public: ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom empty config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } Network::ConnectionBalancerSharedPtr createConnectionBalancerFromProto(const Protobuf::Message&, @@ -408,7 +407,7 @@ TEST_P(IntegrationTest, ConnectionCloseHeader) { EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), HttpStatusIs("200")); - EXPECT_THAT(response->headers(), HeaderValueOf(Headers::get().Connection, "close")); + EXPECT_THAT(response->headers(), ContainsHeader(Headers::get().Connection, "close")); EXPECT_EQ(codec_client_->lastConnectionEvent(), Network::ConnectionEvent::RemoteClose); } @@ -1633,8 +1632,8 @@ TEST_P(IntegrationTest, TestHead) { EXPECT_THAT(response->headers(), HttpStatusIs("200")); EXPECT_EQ(response->headers().ContentLength(), nullptr); EXPECT_THAT(response->headers(), - HeaderValueOf(Headers::get().TransferEncoding, - Http::Headers::get().TransferEncodingValues.Chunked)); + ContainsHeader(Headers::get().TransferEncoding, + Http::Headers::get().TransferEncodingValues.Chunked)); EXPECT_EQ(0, response->body().size()); // Preserve explicit content length. @@ -1643,7 +1642,7 @@ TEST_P(IntegrationTest, TestHead) { response = sendRequestAndWaitForResponse(head_request, 0, content_length_response, 0); ASSERT_TRUE(response->complete()); EXPECT_THAT(response->headers(), HttpStatusIs("200")); - EXPECT_THAT(response->headers(), HeaderValueOf(Headers::get().ContentLength, "12")); + EXPECT_THAT(response->headers(), ContainsHeader(Headers::get().ContentLength, "12")); EXPECT_EQ(response->headers().TransferEncoding(), nullptr); EXPECT_EQ(0, response->body().size()); } @@ -1756,13 +1755,13 @@ TEST_P(IntegrationTest, ViaAppendHeaderOnly) { {"via", "foo"}, {"connection", "close"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Headers::get().Via, "foo, bar")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Headers::get().Via, "foo, bar")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(codec_client_->waitForDisconnect()); EXPECT_TRUE(response->complete()); EXPECT_THAT(response->headers(), HttpStatusIs("200")); - EXPECT_THAT(response->headers(), HeaderValueOf(Headers::get().Via, "bar")); + EXPECT_THAT(response->headers(), ContainsHeader(Headers::get().Via, "bar")); } // Validate that 100-continue works as expected with via header addition on both request and @@ -2471,7 +2470,7 @@ class TestRetryOptionsPredicateFactory : public Upstream::RetryOptionsPredicateF ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom empty config proto. This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "test_retry_options_predicate_factory"; } @@ -2652,7 +2651,7 @@ TEST_P(IntegrationTest, AppendXForwardedPort) { {":authority", "host"}, {"connection", "close"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), Not(HeaderValueOf(Headers::get().ForwardedPort, ""))); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader(Headers::get().ForwardedPort, ""))); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(codec_client_->waitForDisconnect()); @@ -2695,7 +2694,7 @@ TEST_P(IntegrationTest, IgnoreAppendingXForwardedPortIfHasBeenSet) { {"connection", "close"}, {"x-forwarded-port", "8080"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Headers::get().ForwardedPort, "8080")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Headers::get().ForwardedPort, "8080")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(codec_client_->waitForDisconnect()); @@ -2717,7 +2716,7 @@ TEST_P(IntegrationTest, PreserveXForwardedPortFromTrustedHop) { {"connection", "close"}, {"x-forwarded-port", "80"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Headers::get().ForwardedPort, "80")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Headers::get().ForwardedPort, "80")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(codec_client_->waitForDisconnect()); @@ -2739,7 +2738,8 @@ TEST_P(IntegrationTest, OverwriteXForwardedPortFromUntrustedHop) { {"connection", "close"}, {"x-forwarded-port", "80"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), Not(HeaderValueOf(Headers::get().ForwardedPort, "80"))); + EXPECT_THAT(upstream_request_->headers(), + Not(ContainsHeader(Headers::get().ForwardedPort, "80"))); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(codec_client_->waitForDisconnect()); @@ -2761,7 +2761,7 @@ TEST_P(IntegrationTest, DoNotOverwriteXForwardedPortFromUntrustedHop) { {"connection", "close"}, {"x-forwarded-port", "80"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Headers::get().ForwardedPort, "80")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Headers::get().ForwardedPort, "80")); upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(codec_client_->waitForDisconnect()); diff --git a/test/integration/leds_integration_test.cc b/test/integration/leds_integration_test.cc index 45a39644fbbf4..6e2fb32115e10 100644 --- a/test/integration/leds_integration_test.cc +++ b/test/integration/leds_integration_test.cc @@ -98,7 +98,7 @@ class LedsIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, if (await_update) { // Receive LEDS ack. EXPECT_TRUE(compareDeltaDiscoveryRequest( - Config::TypeUrl::get().LbEndpoint, {}, {}, + Config::TestTypeUrl::get().LbEndpoint, {}, {}, leds_upstream_info_.stream_by_resource_name_[localities_prefixes_[locality_idx]].get())); } } @@ -116,7 +116,7 @@ class LedsIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, ASSERT(locality_stream != nullptr); envoy::service::discovery::v3::DeltaDiscoveryResponse response; response.set_system_version_info(version); - response.set_type_url(Config::TypeUrl::get().LbEndpoint); + response.set_type_url(Config::TestTypeUrl::get().LbEndpoint); for (const auto& endpoint_name : to_delete_list) { *response.add_removed_resources() = endpoint_name; @@ -317,16 +317,16 @@ class LedsIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for cluster_'s localities // (ClusterLoadAssignment). - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, {"cluster_0"}, {}, eds_upstream_info_.defaultStream().get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cluster_load_assignment_}, {}, "2", + Config::TestTypeUrl::get().ClusterLoadAssignment, {cluster_load_assignment_}, {}, "2", eds_upstream_info_.defaultStream().get()); // Receive EDS ack. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, {}, {}, - eds_upstream_info_.defaultStream().get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, {}, + {}, eds_upstream_info_.defaultStream().get())); EXPECT_EQ(1, test_server_->gauge("cluster_manager.warming_clusters")->value()); EXPECT_EQ(2, test_server_->gauge("cluster_manager.active_clusters")->value()); @@ -797,7 +797,7 @@ TEST_P(LedsIntegrationTest, LedsSameAddressEndpoints) { // Await for update (LEDS Ack). EXPECT_TRUE(compareDeltaDiscoveryRequest( - Config::TypeUrl::get().LbEndpoint, {}, {}, + Config::TestTypeUrl::get().LbEndpoint, {}, {}, leds_upstream_info_.stream_by_resource_name_[localities_prefixes_[0]].get())); // Verify that the update is successful. diff --git a/test/integration/listener_lds_integration_test.cc b/test/integration/listener_lds_integration_test.cc index cc34e9c08224c..ceb4d5692e18c 100644 --- a/test/integration/listener_lds_integration_test.cc +++ b/test/integration/listener_lds_integration_test.cc @@ -172,7 +172,7 @@ class ListenerIntegrationTestBase : public HttpIntegrationTest { const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); for (const auto& listener_config : listener_configs) { response.add_resources()->PackFrom(listener_config); } @@ -194,7 +194,7 @@ class ListenerIntegrationTestBase : public HttpIntegrationTest { void sendRdsResponse(const std::string& route_config, const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().RouteConfiguration); + response.set_type_url(Config::TestTypeUrl::get().RouteConfiguration); const auto route_configuration = TestUtility::parseYaml(route_config); response.add_resources()->PackFrom(route_configuration); @@ -1223,7 +1223,7 @@ class ListenerFilterIntegrationTest : public BaseIntegrationTest, const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); for (const auto& listener_config : listener_configs) { response.add_resources()->PackFrom(listener_config); } @@ -1739,7 +1739,7 @@ class RebalancerTest : public testing::TestWithParamPackFrom(ProtobufWkt::Struct()); + filter.mutable_typed_config()->PackFrom(Protobuf::Struct()); auto& virtual_listener_config = *bootstrap.mutable_static_resources()->add_listeners(); virtual_listener_config = src_listener_config; virtual_listener_config.mutable_use_original_dst()->set_value(false); diff --git a/test/integration/load_stats_integration_test.cc b/test/integration/load_stats_integration_test.cc index 9f9fad52f677a..942cb92f03b60 100644 --- a/test/integration/load_stats_integration_test.cc +++ b/test/integration/load_stats_integration_test.cc @@ -354,7 +354,8 @@ class LoadStatsIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, } void requestLoadStatsResponse(const std::vector& clusters, - bool send_all_clusters = false) { + bool send_all_clusters = false, + bool report_endpoint_granularity = false) { envoy::service::load_stats::v3::LoadStatsResponse loadstats_response; loadstats_response.mutable_load_reporting_interval()->MergeFrom( Protobuf::util::TimeUtil::MillisecondsToDuration(load_report_interval_ms_)); @@ -364,6 +365,7 @@ class LoadStatsIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, if (send_all_clusters) { loadstats_response.set_send_all_clusters(true); } + loadstats_response.set_report_endpoint_granularity(report_endpoint_granularity); loadstats_stream_->sendGrpcMessage(loadstats_response); // Wait until the request has been received by Envoy. test_server_->waitForCounterGe("load_reporter.requests", ++load_requests_); @@ -907,5 +909,50 @@ TEST_P(LoadStatsIntegrationTest, SuccessWithCustomMetricsNotSent) { cleanupLoadStatsConnection(); } +// Validate basic endpoint-level load stats reporting with successful and failing requests. +TEST_P(LoadStatsIntegrationTest, EndpointLevelStatsReportingSuccessAndFailure) { + initialize(); + + waitForLoadStatsStream(); + ASSERT_TRUE(waitForLoadStatsRequest({})); + loadstats_stream_->startGrpcStream(); + + // Tell Envoy to report for cluster_0 and enable endpoint granularity. + requestLoadStatsResponse({"cluster_0"}, false /*send_all_clusters*/, + true /*report_endpoint_granularity*/); + + // Configure cluster_0 with one endpoint (service_upstream_[0], which is fake_upstreams_[1]) + // in the "winter" locality. + updateClusterLoadAssignment({{0}}, {}, {}, {}); + test_server_->waitForGaugeEq("cluster.cluster_0.membership_total", 1); + + sendAndReceiveUpstream(0, 200, false /*send_orca_load_report*/); + sendAndReceiveUpstream(0, 503, false /*send_orca_load_report*/); + + // Construct the expected UpstreamLocalityStats with one UpstreamEndpointStats. + // Total: 1 success, 1 error, 2 issued. + envoy::config::endpoint::v3::UpstreamLocalityStats uls = + localityStats("winter", 1 /*success*/, 1 /*error*/, 0 /*active*/, 2 /*issued*/); + + auto* eps = uls.add_upstream_endpoint_stats(); + + const auto& endpoint_address = fake_upstreams_[1]->localAddress(); + eps->mutable_address()->mutable_socket_address()->set_address( + endpoint_address->ip()->addressAsString()); + eps->mutable_address()->mutable_socket_address()->set_port_value(endpoint_address->ip()->port()); + eps->set_total_successful_requests(1); + eps->set_total_error_requests(1); + eps->set_total_issued_requests(2); + + std::vector expected_uls_vector = {uls}; + ASSERT_TRUE(waitForLoadStatsRequest(expected_uls_vector)); + + EXPECT_EQ(1, test_server_->counter("load_reporter.requests")->value()); + EXPECT_EQ(2, test_server_->counter("load_reporter.responses")->value()); + EXPECT_EQ(0, test_server_->counter("load_reporter.errors")->value()); + + cleanupLoadStatsConnection(); +} + } // namespace } // namespace Envoy diff --git a/test/integration/multiplexed_integration_test.cc b/test/integration/multiplexed_integration_test.cc index 1c6db190d84fe..e1161c9eae798 100644 --- a/test/integration/multiplexed_integration_test.cc +++ b/test/integration/multiplexed_integration_test.cc @@ -104,6 +104,14 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, MultiplexedIntegrationTestWithSimulatedTime {Http::CodecType::HTTP1})), HttpProtocolIntegrationTest::protocolTestParamsToString); +class MultiplexedIntegrationTestWithSimulatedTimeHttp2Only : public Event::TestUsingSimulatedTime, + public MultiplexedIntegrationTest {}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, MultiplexedIntegrationTestWithSimulatedTimeHttp2Only, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( + {Http::CodecType::HTTP2}, {Http::CodecType::HTTP2})), + HttpProtocolIntegrationTest::protocolTestParamsToString); + TEST_P(MultiplexedIntegrationTest, RouterRequestAndResponseWithBodyNoBuffer) { testRouterRequestAndResponseWithBody(1024, 512, false, false); } @@ -246,6 +254,51 @@ TEST_P(MultiplexedIntegrationTest, CodecStreamIdleTimeout) { ASSERT_TRUE(response->waitForReset()); } +// Test that the codec stream flush timeout can be overridden independently from +// the connection manager stream idle timeout. +TEST_P(MultiplexedIntegrationTest, CodecStreamIdleTimeoutOverride) { + config_helper_.setBufferLimits(1024, 1024); + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + // Disable the generic stream idle timeout. This will be overridden by the + // stream_flush_timeout and the test should work exactly the same as the + // CodecStreamIdleTimeout test. + hcm.mutable_stream_idle_timeout()->set_seconds(0); + hcm.mutable_stream_idle_timeout()->set_nanos(0); + + hcm.mutable_stream_flush_timeout()->set_seconds(0); + constexpr uint64_t FlushTimeoutMs = 400; + hcm.mutable_stream_flush_timeout()->set_nanos(FlushTimeoutMs * 1000 * 1000); + }); + initialize(); + const size_t stream_flow_control_window = + downstream_protocol_ == Http::CodecType::HTTP3 ? 32 * 1024 : 65535; + envoy::config::core::v3::Http2ProtocolOptions http2_options = + ::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions()) + .value(); + http2_options.mutable_initial_stream_window_size()->set_value(stream_flow_control_window); +#ifdef ENVOY_ENABLE_QUIC + if (downstream_protocol_ == Http::CodecType::HTTP3) { + dynamic_cast(*quic_connection_persistent_info_) + .quic_config_.SetInitialStreamFlowControlWindowToSend(stream_flow_control_window); + dynamic_cast(*quic_connection_persistent_info_) + .quic_config_.SetInitialSessionFlowControlWindowToSend(stream_flow_control_window); + } +#endif + codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), http2_options); + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(stream_flow_control_window + 2000, true); + std::string flush_timeout_counter(downstreamProtocol() == Http::CodecType::HTTP3 + ? "http3.tx_flush_timeout" + : "http2.tx_flush_timeout"); + test_server_->waitForCounterEq(flush_timeout_counter, 1); + ASSERT_TRUE(response->waitForReset()); +} + TEST_P(MultiplexedIntegrationTest, Http2DownstreamKeepalive) { EXCLUDE_DOWNSTREAM_HTTP3; // Http3 keepalive doesn't timeout and close connection. constexpr uint64_t interval_ms = 1; @@ -1261,7 +1314,7 @@ TEST_P(MultiplexedIntegrationTestWithSimulatedTime, GoAwayAfterTooManyResets) { test_server_->waitForCounterEq("http.config_test.downstream_rq_too_many_premature_resets", 1); } -TEST_P(MultiplexedIntegrationTestWithSimulatedTime, GoAwayQuicklyAfterTooManyResets) { +TEST_P(MultiplexedIntegrationTestWithSimulatedTimeHttp2Only, GoAwayQuicklyAfterTooManyResets) { EXCLUDE_DOWNSTREAM_HTTP3; // Need to wait for the server to reset the stream // before opening new one. const int total_streams = 100; @@ -1287,6 +1340,70 @@ TEST_P(MultiplexedIntegrationTestWithSimulatedTime, GoAwayQuicklyAfterTooManyRes test_server_->waitForCounterEq("http.config_test.downstream_rq_too_many_premature_resets", 1); } +TEST_P(MultiplexedIntegrationTestWithSimulatedTimeHttp2Only, TooManyRequestResetAndNoRecursion) { + if (downstreamProtocol() != Http::CodecType::HTTP2 || + upstreamProtocol() != Http::CodecType::HTTP2) { + // This test is only valid for HTTP/2 and HTTP/3. + return; + } + + config_helper_.setDownstreamHttp2MaxConcurrentStreams(60000); + config_helper_.setUpstreamHttp2MaxConcurrentStreams(60000); + + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + bootstrap.mutable_static_resources() + ->mutable_clusters(0) + ->mutable_circuit_breakers() + ->add_thresholds() + ->mutable_max_requests() + ->set_value(60000); + }); + + config_helper_.addRuntimeOverride("overload.premature_reset_total_stream_count", + absl::StrCat(100)); + + autonomous_upstream_ = true; + autonomous_allow_incomplete_streams_ = true; + initialize(); + + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/healthcheck"}, {":scheme", "http"}, {":authority", "host"}}; + codec_client_ = makeHttpConnection(lookupPort("http")); + + const int pending_streams = 1800; // 18000 in local or this consume too much resource. + std::vector> encoder_decoders; + encoder_decoders.reserve(pending_streams); + + const int pending_streams_per_iteration = pending_streams / 4; + for (size_t i = 0; i < 4; i++) { + for (size_t j = 0; j < pending_streams_per_iteration; ++j) { + // Send and wait + encoder_decoders.emplace_back(codec_client_->startRequest(headers)); + } + test_server_->waitForCounterEq("http.config_test.downstream_rq_total", + pending_streams_per_iteration * (i + 1), + TestUtility::DefaultTimeout * 5); + } + + // Reset 50 streams and then the connection should be closed because too much premature resets. + // All streams should be reset correctly without recursion. + for (int i = 0; i < 50; ++i) { + // Send and reset + auto encoder_decoder = codec_client_->startRequest(headers); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + codec_client_->sendReset(*request_encoder_); + ASSERT_TRUE(response->waitForReset()); + } + + // Envoy should disconnect client due to premature reset check + ASSERT_TRUE(codec_client_->waitForDisconnect()); + test_server_->waitForCounterEq("http.config_test.downstream_rq_rx_reset", pending_streams + 50, + TestUtility::DefaultTimeout * 5); + // If there is recursion, this result won't be 1. + test_server_->waitForCounterEq("http.config_test.downstream_rq_too_many_premature_resets", 1); +} + TEST_P(MultiplexedIntegrationTestWithSimulatedTime, DontGoAwayAfterTooManyResetsForLongStreams) { EXCLUDE_DOWNSTREAM_HTTP3; // Need to wait for the server to reset the stream // before opening new one. @@ -2998,6 +3115,9 @@ TEST_P(Http2FrameIntegrationTest, CloseConnectionWithDeferredStreams) { ->mutable_timeout() ->set_seconds(0); }); + config_helper_.setDownstreamHttp2MaxConcurrentStreams(20001); + config_helper_.setUpstreamHttp2MaxConcurrentStreams(20001); + beginSession(); std::string buffer; diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index a73f3b56d617d..a4848e9a179da 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -701,7 +701,7 @@ TEST_P(MultiplexedUpstreamIntegrationTest, AutoRetrySafeRequestUponTooEarlyRespo waitForNextUpstreamRequest(0); // If the request already has Early-Data header, no additional Early-Data header should be added // and the header should be forwarded as is. - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Http::Headers::get().EarlyData, "1")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Http::Headers::get().EarlyData, "1")); upstream_request_->encodeHeaders(too_early_response_headers, true); ASSERT_TRUE(response2->waitForEndStream()); // 425 response should be forwarded back to the client. @@ -862,7 +862,7 @@ class QuicFailHandshakeCryptoServerStreamFactory : public Quic::EnvoyQuicCryptoServerStreamFactoryInterface { public: Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "envoy.quic.crypto_stream.server.fail_handshake"; } @@ -901,7 +901,7 @@ TEST_P(MultiplexedUpstreamIntegrationTest, UpstreamDisconnectDuringEarlyData) { envoy::config::listener::v3::QuicProtocolOptions options; auto* crypto_stream_config = options.mutable_crypto_stream_config(); crypto_stream_config->set_name("envoy.quic.crypto_stream.server.fail_handshake"); - crypto_stream_config->mutable_typed_config()->PackFrom(ProtobufWkt::Struct()); + crypto_stream_config->mutable_typed_config()->PackFrom(Protobuf::Struct()); mergeOptions(options); initialize(); diff --git a/test/integration/network_extension_discovery_integration_test.cc b/test/integration/network_extension_discovery_integration_test.cc index 2ea3f018d93d2..d1df64e133baa 100644 --- a/test/integration/network_extension_discovery_integration_test.cc +++ b/test/integration/network_extension_discovery_integration_test.cc @@ -212,7 +212,7 @@ class NetworkExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrat void sendLdsResponse(const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); response.add_resources()->PackFrom(listener_config_); lds_stream_->sendGrpcMessage(response); } diff --git a/test/integration/overload_integration_test.cc b/test/integration/overload_integration_test.cc index 37454d3b4e0da..8887f2155e0a2 100644 --- a/test/integration/overload_integration_test.cc +++ b/test/integration/overload_integration_test.cc @@ -297,6 +297,35 @@ TEST_P(OverloadIntegrationTest, BypassOverloadManagerTest) { codec_client_->close(); } +class Http2RawFrameOverloadIntegrationTest : public BaseOverloadIntegrationTest, + public Http2RawFrameIntegrationTest, + public testing::Test { +public: + Http2RawFrameOverloadIntegrationTest() + : Http2RawFrameIntegrationTest(Envoy::Network::Address::IpVersion::v4) { + setupHttp2ImplOverrides(Envoy::Http2Impl::Oghttp2); + } + +protected: + void initializeOverloadManager( + const envoy::config::overload::v3::ScaleTimersOverloadActionConfig& config) { + envoy::config::overload::v3::OverloadAction overload_action = + TestUtility::parseYaml(R"EOF( + name: "envoy.overload_actions.reduce_timeouts" + triggers: + - name: "envoy.resource_monitors.testonly.fake_resource_monitor" + scaled: + scaling_threshold: 0.5 + saturation_threshold: 0.9 + )EOF"); + overload_action.mutable_typed_config()->PackFrom(config); + setupOverloadManagerConfig(overload_action); + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + *bootstrap.mutable_overload_manager() = this->overload_manager_config_; + }); + } +}; + class OverloadScaledTimerIntegrationTest : public OverloadIntegrationTest { protected: void initializeOverloadManager( @@ -679,6 +708,57 @@ TEST_P(OverloadScaledTimerIntegrationTest, CloseIdleHttpStream) { EXPECT_THAT(response->body(), HasSubstr("stream timeout")); } +TEST_F(Http2RawFrameOverloadIntegrationTest, FlushTimeoutWhenDownstreamBlocked) { + initializeOverloadManager( + TestUtility::parseYaml(R"EOF( + timer_scale_factors: + - timer: HTTP_DOWNSTREAM_STREAM_FLUSH + min_timeout: 1s + )EOF")); + + // Create a downstream connection with an initial stream window size of 1 rather than the default + // 65535. + beginSession(Http2Frame::makeSettingsFrame( + Http2Frame::SettingsFlags::None, + {{static_cast(Http2Frame::Setting::InitialWindowSize), 1}})); + + // Simulate increased load so the timer is reduced to the minimum value. + updateResource(0.9); + test_server_->waitForGaugeEq("overload.envoy.overload_actions.reduce_timeouts.scale_percent", + 100); + + // Send a headers-only request. + sendFrame(Http2Frame::makeRequest(1, /*host=*/"sni.lyft.com", /*path=*/"/test/long/url")); + + // Respond from upstream with more data than the downstream window will allow. + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(2, true); + + // Read the response headers. + Http2Frame response_headers = readFrame(); + EXPECT_EQ(response_headers.streamId(), 1); + EXPECT_EQ(response_headers.type(), Http2Frame::Type::Headers); + + // Downstream receive window is 1, so the Envoy will encode 1 byte and buffer 1 byte. + Http2Frame response_data = readFrame(); + EXPECT_EQ(response_data.streamId(), 1); + EXPECT_EQ(response_data.type(), Http2Frame::Type::Data); + EXPECT_EQ(response_data.payloadSize(), 1); + + // The client DOES NOT send a window update, so eventually Envoy's flush timer will fire... + timeSystem().advanceTimeWait(std::chrono::seconds(2)); + test_server_->waitForCounterGe("http2.tx_flush_timeout", 1); + + // ... Which will cause the stream to be reset. + Http2Frame reset_frame = readFrame(); + EXPECT_EQ(reset_frame.streamId(), 1); + EXPECT_EQ(reset_frame.type(), Http2Frame::Type::RstStream); + + tcp_client_->close(); + test_server_->waitForGaugeEq("http.config_test.downstream_rq_active", 0); +} + TEST_P(OverloadScaledTimerIntegrationTest, TlsHandshakeTimeout) { if (downstreamProtocol() == Http::CodecClient::Type::HTTP3 || upstreamProtocol() == Http::CodecClient::Type::HTTP3) { @@ -1094,6 +1174,45 @@ TEST_P(LoadShedPointIntegrationTest, Http2ServerDispatchSendsGoAwayCompletingPen "overload.envoy.load_shed_points.http2_server_go_away_on_dispatch.scale_percent", 0); } +TEST_P(LoadShedPointIntegrationTest, Http2ServerDispatchSendsGoAwayAndClosesConnection) { + // Test only applies to HTTP2. + if (downstreamProtocol() != Http::CodecClient::Type::HTTP2) { + return; + } + autonomous_upstream_ = true; + autonomous_allow_incomplete_streams_ = true; + initializeOverloadManager( + TestUtility::parseYaml(R"EOF( + name: "envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch" + triggers: + - name: "envoy.resource_monitors.testonly.fake_resource_monitor" + threshold: + value: 0.90 + )EOF")); + + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + auto [first_request_encoder, first_request_decoder] = + codec_client_->startRequest(default_request_headers_); + test_server_->waitForCounterEq("http.config_test.downstream_rq_http2_total", 1); + + // Put envoy in overloaded state to send GOAWAY frames and close the connection. + updateResource(0.95); + test_server_->waitForGaugeEq( + "overload.envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch.scale_percent", + 100); + + auto second_request_decoder = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + + // The downstream should receive the GOAWAY and the connection should be closed. + ASSERT_TRUE(codec_client_->waitForDisconnect()); + EXPECT_TRUE(codec_client_->sawGoAway()); + test_server_->waitForCounterEq("http2.goaway_sent", 1); + test_server_->waitForCounterEq("http.config_test.downstream_rq_overload_close", 1); + + // The second request will not complete. + EXPECT_FALSE(second_request_decoder->complete()); +} + TEST_P(LoadShedPointIntegrationTest, HttpConnectionMnagerCloseConnectionCreatingCodec) { if (downstreamProtocol() == Http::CodecClient::Type::HTTP3) { return; diff --git a/test/integration/protocol_integration_test.cc b/test/integration/protocol_integration_test.cc index adefc8e4e8ca5..8f7fedbe5cdf7 100644 --- a/test/integration/protocol_integration_test.cc +++ b/test/integration/protocol_integration_test.cc @@ -26,6 +26,7 @@ #include "source/common/protobuf/utility.h" #include "source/common/runtime/runtime_impl.h" #include "source/common/upstream/upstream_impl.h" +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" #include "test/common/http/http2/http2_frame.h" #include "test/common/upstream/utility.h" @@ -75,6 +76,84 @@ TEST_P(ProtocolIntegrationTest, ShutdownWithActiveConnPoolConnections) { checkSimpleRequestSuccess(0U, 0U, response.get()); } +// Test upstream_rq_per_cx metric tracks requests per connection +TEST_P(ProtocolIntegrationTest, UpstreamRequestsPerConnectionMetric) { + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Send 3 requests on the same connection + for (int i = 0; i < 3; ++i) { + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } + + // Use the proper cleanup pattern that triggers histogram recording + cleanupUpstreamAndDownstream(); + + // Wait for the histogram to actually have samples using the proper integration test pattern + test_server_->waitUntilHistogramHasSamples("cluster.cluster_0.upstream_rq_per_cx"); + + // Get the histogram and read values using the proper pattern + auto histogram = test_server_->histogram("cluster.cluster_0.upstream_rq_per_cx"); + + uint64_t sample_count = + TestUtility::readSampleCount(test_server_->server().dispatcher(), *histogram); + uint64_t sample_sum = TestUtility::readSampleSum(test_server_->server().dispatcher(), *histogram); + + // Should have 1 sample with value 3 (3 requests on 1 connection) + EXPECT_EQ(sample_count, 1); + EXPECT_EQ(sample_sum, 3); +} + +// Test that upstream_rq_per_cx metric is NOT recorded when handshake fails +TEST_P(ProtocolIntegrationTest, UpstreamRequestsPerConnectionMetricHandshakeFailure) { + // This test intentionally causes upstream connection failures, so bypass the upstream validation + testing_upstream_intentionally_ = true; + + // Configure upstream with invalid port to force connection failure before handshake completion + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + auto* lb_endpoint = + cluster->mutable_load_assignment()->mutable_endpoints(0)->mutable_lb_endpoints(0); + // Use port 1 which is invalid/inaccessible to force connection establishment failure + lb_endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_port_value(1); + }); + config_helper_.skipPortUsageValidation(); + + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Send request that will fail due to upstream connection failure + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + + // Wait for response (should fail with 503 Service Unavailable) + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("503", response->headers().getStatusValue()); + + // Clean up + codec_client_->close(); + + // Wait for connection failure to be recorded + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_connect_fail", 1); + + // Verify that NO upstream_rq_per_cx histogram samples were recorded + // because hasHandshakeCompleted() returned false (connection never established) + auto histogram = test_server_->histogram("cluster.cluster_0.upstream_rq_per_cx"); + uint64_t sample_count = + TestUtility::readSampleCount(test_server_->server().dispatcher(), *histogram); + + // Key assertion: No histogram samples should be recorded for failed connections + EXPECT_EQ(sample_count, 0); + + // Also verify connection failure was recorded (proving connection attempt was made) + EXPECT_GE(test_server_->counter("cluster.cluster_0.upstream_cx_connect_fail")->value(), 1); +} + TEST_P(ProtocolIntegrationTest, LogicalDns) { if (use_universal_header_validator_) { // TODO(#27132): auto_host_rewrite is broken for IPv6 and is failing UHV validation @@ -85,6 +164,11 @@ TEST_P(ProtocolIntegrationTest, LogicalDns) { auto& cluster = *bootstrap.mutable_static_resources()->mutable_clusters(0); cluster.set_type(envoy::config::cluster::v3::Cluster::LOGICAL_DNS); cluster.set_dns_lookup_family(envoy::config::cluster::v3::Cluster::ALL); + auto* typed_dns_resolver_config = cluster.mutable_typed_dns_resolver_config(); + typed_dns_resolver_config->set_name("envoy.network.dns_resolver.getaddrinfo"); + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + getaddrinfo_config; + typed_dns_resolver_config->mutable_typed_config()->PackFrom(getaddrinfo_config); }); config_helper_.addConfigModifier( [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& @@ -107,6 +191,11 @@ TEST_P(ProtocolIntegrationTest, StrictDns) { auto& cluster = *bootstrap.mutable_static_resources()->mutable_clusters(0); cluster.set_type(envoy::config::cluster::v3::Cluster::STRICT_DNS); cluster.set_dns_lookup_family(envoy::config::cluster::v3::Cluster::ALL); + auto* typed_dns_resolver_config = cluster.mutable_typed_dns_resolver_config(); + typed_dns_resolver_config->set_name("envoy.network.dns_resolver.getaddrinfo"); + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + getaddrinfo_config; + typed_dns_resolver_config->mutable_typed_config()->PackFrom(getaddrinfo_config); }); initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -988,6 +1077,62 @@ TEST_P(ProtocolIntegrationTest, Retry) { BytesCountExpectation(2204, 520, 150, 6)); } +TEST_P(ProtocolIntegrationTest, RetryWithBodyLargerThanNetworkBuffer) { + // Set the network buffer to be 16KB. + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + RELEASE_ASSERT(bootstrap.mutable_static_resources()->listeners_size() >= 1, ""); + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + + listener->mutable_per_connection_buffer_limit_bytes()->set_value(16 * 1024); + }); + // Set the request body buffer limit to be 1MB so it can retry requests up to 1MB. + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_request_body_buffer_limit() + ->set_value(1024 * 1024); + }); + + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + // Send a request with 64Kb body so it is larger than the network 16Kb buffer. + constexpr uint32_t kRequestBodySize = 64 * 1024; + auto response = codec_client_->makeRequestWithBody( + Http::TestRequestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "sni.lyft.com"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + kRequestBodySize); + waitForNextUpstreamRequest(); + // Note that 503 is sent with end_stream=false (response with body). This will cause Envoy to + // reset the response because it knows it is going to retry the request and it does not need the + // response body. + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "503"}}, false); + if (fake_upstreams_[0]->httpType() == Http::CodecType::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_, + std::chrono::milliseconds(500))); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ(kRequestBodySize, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + EXPECT_EQ(512U, response->body().size()); +} + // Regression test to guarantee that buffering for retries and shadows doesn't double the body size. // This test is actually irrelevant for QUIC, as this issue only shows up with header-only requests. // QUIC will always send an empty data frame with FIN. @@ -1137,7 +1282,7 @@ TEST_P(ProtocolIntegrationTest, RetryStreamingCancelDueToBufferOverflow) { hcm) { auto* route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); - route->mutable_per_request_buffer_limit_bytes()->set_value(1024); + route->mutable_request_body_buffer_limit()->set_value(1024); route->mutable_route() ->mutable_retry_policy() ->mutable_retry_back_off() @@ -1401,7 +1546,7 @@ TEST_P(ProtocolIntegrationTest, RetryHittingBufferLimit) { // Very similar set-up to RetryHittingBufferLimits but using the route specific cap. TEST_P(ProtocolIntegrationTest, RetryHittingRouteLimits) { auto host = config_helper_.createVirtualHost("routelimit.lyft.com", "/"); - host.mutable_per_request_buffer_limit_bytes()->set_value(0); + host.mutable_request_body_buffer_limit()->set_value(0); config_helper_.addVirtualHost(host); initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -1586,8 +1731,6 @@ TEST_P(DownstreamProtocolIntegrationTest, EnvoyProxying102DelayBalsaReset) { config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { hcm.set_proxy_100_continue(true); }); - config_helper_.addRuntimeOverride( - "envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done", "false"); config_helper_.addRuntimeOverride("envoy.reloadable_features.http1_balsa_delay_reset", "true"); initialize(); @@ -1617,8 +1760,6 @@ TEST_P(DownstreamProtocolIntegrationTest, EnvoyProxying102DelayBalsaResetWaitFor config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { hcm.set_proxy_100_continue(true); }); - config_helper_.addRuntimeOverride( - "envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done", "true"); config_helper_.addRuntimeOverride("envoy.reloadable_features.http1_balsa_delay_reset", "true"); initialize(); @@ -1878,9 +2019,9 @@ TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresDropped) { Http::TestRequestTrailerMapImpl{{"trailer1", "value1"}, {"trailer_2", "value2"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), Not(HeaderHasValueRef("foo_bar", "baz"))); + EXPECT_THAT(upstream_request_->headers(), Not(ContainsHeader("foo_bar", "baz"))); // Headers with underscores should be dropped from request headers and trailers. - EXPECT_THAT(*upstream_request_->trailers(), Not(HeaderHasValueRef("trailer_2", "value2"))); + EXPECT_THAT(*upstream_request_->trailers(), Not(ContainsHeader("trailer_2", "value2"))); upstream_request_->encodeHeaders( Http::TestResponseHeaderMapImpl{{":status", "200"}, {"bar_baz", "fooz"}}, false); upstream_request_->encodeData("b", false); @@ -1890,8 +2031,8 @@ TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresDropped) { EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); // Both response headers and trailers must retain headers with underscores. - EXPECT_THAT(response->headers(), HeaderHasValueRef("bar_baz", "fooz")); - EXPECT_THAT(*response->trailers(), HeaderHasValueRef("response_trailer", "ok")); + EXPECT_THAT(response->headers(), ContainsHeader("bar_baz", "fooz")); + EXPECT_THAT(*response->trailers(), ContainsHeader("response_trailer", "ok")); Stats::Store& stats = test_server_->server().stats(); std::string stat_name; switch (downstreamProtocol()) { @@ -1924,13 +2065,13 @@ TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresRemainByDefault) { {"foo_bar", "baz"}}); waitForNextUpstreamRequest(); - EXPECT_THAT(upstream_request_->headers(), HeaderHasValueRef("foo_bar", "baz")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("foo_bar", "baz")); upstream_request_->encodeHeaders( Http::TestResponseHeaderMapImpl{{":status", "200"}, {"bar_baz", "fooz"}}, true); ASSERT_TRUE(response->waitForEndStream()); EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); - EXPECT_THAT(response->headers(), HeaderHasValueRef("bar_baz", "fooz")); + EXPECT_THAT(response->headers(), ContainsHeader("bar_baz", "fooz")); } // Verify that request with headers containing underscores is rejected when configured. @@ -2036,8 +2177,8 @@ TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresInResponseAllowRequest) { EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); // Both response headers and trailers must retain headers with underscores. - EXPECT_THAT(response->headers(), HeaderHasValueRef("bar_baz", "fooz")); - EXPECT_THAT(*response->trailers(), HeaderHasValueRef("response_trailer", "ok")); + EXPECT_THAT(response->headers(), ContainsHeader("bar_baz", "fooz")); + EXPECT_THAT(*response->trailers(), ContainsHeader("response_trailer", "ok")); } TEST_P(DownstreamProtocolIntegrationTest, ValidZeroLengthContent) { @@ -5185,8 +5326,13 @@ TEST_P(ProtocolIntegrationTest, InvalidResponseHeaderNameStreamError) { TEST_P(ProtocolIntegrationTest, ServerHalfCloseBeforeClientWithBufferedResponseData) { config_helper_.addRuntimeOverride( "envoy.reloadable_features.allow_multiplexed_upstream_half_close", "true"); - useAccessLog("%DURATION% %REQUEST_DURATION% %REQUEST_TX_DURATION% %RESPONSE_DURATION% " - "%RESPONSE_TX_DURATION%"); + config_helper_.addRuntimeOverride("envoy.reloadable_features.quic_defer_logging_to_ack_listener", + "true"); + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.quic_fix_defer_logging_miss_for_half_closed_stream", "true"); + + useAccessLog("%DURATION% %ROUNDTRIP_DURATION% %REQUEST_DURATION% %REQUEST_TX_DURATION% " + "%RESPONSE_DURATION% %RESPONSE_TX_DURATION%"); constexpr uint32_t kStreamWindowSize = 64 * 1024; // Set buffer limit large enough to accommodate H/2 stream window, so we can cause downstream // codec to buffer data without pushing back on upstream. @@ -5262,15 +5408,29 @@ TEST_P(ProtocolIntegrationTest, ServerHalfCloseBeforeClientWithBufferedResponseD } } - std::string timing = waitForAccessLog(access_log_name_); + std::string log = waitForAccessLog(access_log_name_); + std::vector timings = absl::StrSplit(log, ' '); + ASSERT_EQ(timings.size(), 6); if (fake_upstreams_[0]->httpType() != Http::CodecType::HTTP1 && downstreamProtocol() != Http::CodecType::HTTP1) { - // All duration values should be present (no '-' in the access log) when neither upstream nor - // downstream is H/1 - ASSERT_FALSE(absl::StrContains(timing, '-')); + // All duration values except for ROUNDTRIP_DURATION should be present (no '-' in the access + // log) when neither upstream nor downstream is H/1 + EXPECT_GE(/* DURATION */ std::stoi(timings.at(0)), 0); + if (downstreamProtocol() == Http::CodecType::HTTP3) { + // Only H/3 populate this metric. + EXPECT_GT(/* ROUNDTRIP_DURATION */ std::stoi(timings.at(1)), 0); + } + EXPECT_GE(/* REQUEST_DURATION */ std::stoi(timings.at(2)), 0); + EXPECT_GE(/* REQUEST_TX_DURATION */ std::stoi(timings.at(3)), 0); + EXPECT_GE(/* RESPONSE_DURATION */ std::stoi(timings.at(4)), 0); + EXPECT_GE(/* RESPONSE_TX_DURATION */ std::stoi(timings.at(5)), 0); } else { // When one the peers is H/1 the stream is reset and request duration values will be unset - ASSERT_TRUE(absl::StrContains(timing, " - - ")); + EXPECT_GE(/* DURATION */ std::stoi(timings.at(0)), 0); + EXPECT_EQ(/* ROUNDTRIP_DURATION */ timings.at(1), "-"); + EXPECT_EQ(/* REQUEST_DURATION */ timings.at(2), "-"); + EXPECT_EQ(/* REQUEST_TX_DURATION */ timings.at(3), "-"); + EXPECT_GE(/* RESPONSE_DURATION */ std::stoi(timings.at(4)), 0); } } @@ -5537,7 +5697,7 @@ TEST_P(DownstreamProtocolIntegrationTest, InvalidSchemeHeaderWithWhitespace) { // The scheme header is not conveyed in HTTP/1. EXPECT_EQ(nullptr, upstream_request_->headers().Scheme()); } else { - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Http::Headers::get().Scheme, "http")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Http::Headers::get().Scheme, "http")); } upstream_request_->encodeHeaders(default_response_headers_, true); ASSERT_TRUE(response->waitForEndStream()); diff --git a/test/integration/quic_http_integration_test.cc b/test/integration/quic_http_integration_test.cc index df35b341aaae1..3882cd9665c07 100644 --- a/test/integration/quic_http_integration_test.cc +++ b/test/integration/quic_http_integration_test.cc @@ -126,7 +126,7 @@ TEST_P(QuicHttpIntegrationTest, ZeroRtt) { // Send a complete request on the second connection. auto response2 = codec_client_->makeHeaderOnlyRequest(default_request_headers_); waitForNextUpstreamRequest(0); - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Http::Headers::get().EarlyData, "1")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Http::Headers::get().EarlyData, "1")); upstream_request_->encodeHeaders(default_response_headers_, true); ASSERT_TRUE(response2->waitForEndStream()); // Ensure 0-RTT was used by second connection. @@ -157,7 +157,7 @@ TEST_P(QuicHttpIntegrationTest, ZeroRtt) { /*wait_for_1rtt_key*/ false); auto response3 = codec_client_->makeHeaderOnlyRequest(default_request_headers_); waitForNextUpstreamRequest(0); - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Http::Headers::get().EarlyData, "1")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Http::Headers::get().EarlyData, "1")); upstream_request_->encodeHeaders(too_early_response_headers, true); ASSERT_TRUE(response3->waitForEndStream()); // This is downstream sending early data, so the 425 response should be forwarded back to the @@ -177,7 +177,7 @@ TEST_P(QuicHttpIntegrationTest, ZeroRtt) { waitForNextUpstreamRequest(0); // If the request already has Early-Data header, no additional Early-Data header should be added // and the header should be forwarded as is. - EXPECT_THAT(upstream_request_->headers(), HeaderValueOf(Http::Headers::get().EarlyData, "2")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader(Http::Headers::get().EarlyData, "2")); upstream_request_->encodeHeaders(too_early_response_headers, true); ASSERT_TRUE(response4->waitForEndStream()); // 425 response should be forwarded back to the client. @@ -309,7 +309,7 @@ TEST_P(QuicHttpIntegrationTest, PortMigration) { Network::Address::InstanceConstSharedPtr local_addr = Network::Test::getCanonicalLoopbackAddress(version_); quic_connection_->switchConnectionSocket( - createConnectionSocket(server_addr_, local_addr, nullptr, /*prefer_gro=*/true)); + createConnectionSocket(server_addr_, local_addr, nullptr)); EXPECT_NE(old_port, local_addr->ip()->port()); // Send the rest data. codec_client_->sendData(*request_encoder_, 1024u, true); @@ -340,7 +340,7 @@ TEST_P(QuicHttpIntegrationTest, PortMigration) { auto options = std::make_shared(); options->push_back(option); quic_connection_->switchConnectionSocket( - createConnectionSocket(server_addr_, local_addr, options, /*prefer_gro=*/true)); + createConnectionSocket(server_addr_, local_addr, options)); EXPECT_TRUE(codec_client_->disconnected()); cleanupUpstreamAndDownstream(); } @@ -1211,7 +1211,7 @@ TEST_P(QuicHttpIntegrationTest, DisableQpack) { auto response = codec_client_->makeHeaderOnlyRequest(headers); waitForNextUpstreamRequest(0); // Cookie crumbling is disabled along with QPACK. - EXPECT_THAT(upstream_request_->headers(), HeaderHasValueRef("cookie", "x;y")); + EXPECT_THAT(upstream_request_->headers(), ContainsHeader("cookie", "x;y")); upstream_request_->encodeHeaders(default_response_headers_, true); ASSERT_TRUE(response->waitForEndStream()); codec_client_->close(); diff --git a/test/integration/quic_http_integration_test.h b/test/integration/quic_http_integration_test.h index 21319319d0349..c67fc2d50b7d2 100644 --- a/test/integration/quic_http_integration_test.h +++ b/test/integration/quic_http_integration_test.h @@ -66,8 +66,7 @@ class TestEnvoyQuicClientConnection : public EnvoyQuicClientConnection { bool validation_failure_on_path_response, quic::ConnectionIdGeneratorInterface& generator) : EnvoyQuicClientConnection(server_connection_id, initial_peer_address, helper, alarm_factory, - supported_versions, local_addr, dispatcher, options, generator, - /*prefer_gro=*/true), + supported_versions, local_addr, dispatcher, options, generator), dispatcher_(dispatcher), validation_failure_on_path_response_(validation_failure_on_path_response) {} diff --git a/test/integration/redirect_integration_test.cc b/test/integration/redirect_integration_test.cc index 03375ca855b9f..947f5d815142a 100644 --- a/test/integration/redirect_integration_test.cc +++ b/test/integration/redirect_integration_test.cc @@ -267,8 +267,7 @@ TEST_P(RedirectIntegrationTest, ConnectionCloseHeaderHonoredInInternalRedirect) ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); // "Connection: close" should be sent back in response. - EXPECT_THAT(response->headers(), - Envoy::Http::HeaderValueOf(Envoy::Http::Headers::get().Connection, "close")); + EXPECT_THAT(response->headers(), ContainsHeader(Envoy::Http::Headers::get().Connection, "close")); // Envoy should close the connection immediately. ASSERT_TRUE(codec_client_->waitForDisconnect(std::chrono::milliseconds(2000))); @@ -566,7 +565,7 @@ TEST_P(RedirectIntegrationTest, InternalRedirectCancelledDueToBufferOverflow) { [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) { auto* route = hcm.mutable_route_config()->mutable_virtual_hosts(2)->mutable_routes(0); - route->mutable_per_request_buffer_limit_bytes()->set_value(1024); + route->mutable_request_body_buffer_limit()->set_value(1024); }); initialize(); diff --git a/test/integration/rtds_integration_test.cc b/test/integration/rtds_integration_test.cc index 39846e7c92194..c245b15f68843 100644 --- a/test/integration/rtds_integration_test.cc +++ b/test/integration/rtds_integration_test.cc @@ -223,7 +223,7 @@ TEST_P(RtdsIntegrationTest, RtdsReload) { EXPECT_EQ("yar", getRuntimeKey("bar")); EXPECT_EQ("", getRuntimeKey("baz")); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "", {"some_rtds_layer"}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "", {"some_rtds_layer"}, {"some_rtds_layer"}, {}, true)); auto some_rtds_layer = TestUtility::parseYaml(R"EOF( name: some_rtds_layer @@ -232,7 +232,7 @@ TEST_P(RtdsIntegrationTest, RtdsReload) { baz: meh )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", initial_load_success_ + 1); EXPECT_EQ("bar", getRuntimeKey("foo")); @@ -244,15 +244,15 @@ TEST_P(RtdsIntegrationTest, RtdsReload) { EXPECT_EQ(initial_keys_ + 1, test_server_->gauge("runtime.num_keys")->value()); EXPECT_EQ(3, test_server_->gauge("runtime.num_layers")->value()); - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"some_rtds_layer"}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "1", {"some_rtds_layer"}, + {}, {})); some_rtds_layer = TestUtility::parseYaml(R"EOF( name: some_rtds_layer layer: baz: saz )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "2"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "2"); test_server_->waitForCounterGe("runtime.load_success", initial_load_success_ + 2); EXPECT_EQ("whatevs", getRuntimeKey("foo")); @@ -276,7 +276,7 @@ TEST_P(RtdsIntegrationTest, RtdsUpdate) { EXPECT_EQ("yar", getRuntimeKey("bar")); EXPECT_EQ("", getRuntimeKey("baz")); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "", {"some_rtds_layer"}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "", {"some_rtds_layer"}, {"some_rtds_layer"}, {}, true)); auto some_rtds_layer = TestUtility::parseYaml(R"EOF( name: some_rtds_layer @@ -286,9 +286,9 @@ TEST_P(RtdsIntegrationTest, RtdsUpdate) { )EOF"); // Use the Resource wrapper no matter if it is Sotw or Delta. - sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1", - {{"test", ProtobufWkt::Any()}}); + sendDiscoveryResponse(Config::TestTypeUrl::get().Runtime, + {some_rtds_layer}, {some_rtds_layer}, + {}, "1", {{"test", Protobuf::Any()}}); test_server_->waitForCounterGe("runtime.load_success", initial_load_success_ + 1); EXPECT_EQ("bar", getRuntimeKey("foo")); @@ -337,7 +337,7 @@ TEST_P(RtdsIntegrationTest, RtdsAfterAsyncPrimaryClusterInitialization) { // After this xDS connection should be established. Verify that dynamic runtime values are loaded. acceptXdsConnection(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "", {"some_rtds_layer"}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "", {"some_rtds_layer"}, {"some_rtds_layer"}, {}, true)); auto some_rtds_layer = TestUtility::parseYaml(R"EOF( name: some_rtds_layer @@ -346,7 +346,7 @@ TEST_P(RtdsIntegrationTest, RtdsAfterAsyncPrimaryClusterInitialization) { baz: meh )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", initial_load_success_ + 1); EXPECT_EQ("bar", getRuntimeKey("foo")); diff --git a/test/integration/sds_dynamic_integration_test.cc b/test/integration/sds_dynamic_integration_test.cc index 8bd076a522765..881e09be4d4be 100644 --- a/test/integration/sds_dynamic_integration_test.cc +++ b/test/integration/sds_dynamic_integration_test.cc @@ -189,7 +189,7 @@ class SdsDynamicIntegrationBaseTest : public Grpc::BaseGrpcClientIntegrationPara void sendSdsResponse(const envoy::extensions::transport_sockets::tls::v3::Secret& secret) { envoy::service::discovery::v3::DiscoveryResponse discovery_response; discovery_response.set_version_info("1"); - discovery_response.set_type_url(Config::TypeUrl::get().Secret); + discovery_response.set_type_url(Config::TestTypeUrl::get().Secret); discovery_response.add_resources()->PackFrom(secret); xds_stream_->sendGrpcMessage(discovery_response); @@ -1063,16 +1063,16 @@ class SdsCdsIntegrationTest : public SdsDynamicIntegrationBaseTest { std::unique_ptr& sdsUpstream() override { return fake_upstreams_[1]; } void sendCdsResponse() { - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {dynamic_cluster_}, {dynamic_cluster_}, {}, "55"); + Config::TestTypeUrl::get().Cluster, {dynamic_cluster_}, {dynamic_cluster_}, {}, "55"); } void sendSdsResponse2(const envoy::extensions::transport_sockets::tls::v3::Secret& secret, FakeStream& sds_stream) { envoy::service::discovery::v3::DiscoveryResponse discovery_response; discovery_response.set_version_info("1"); - discovery_response.set_type_url(Config::TypeUrl::get().Secret); + discovery_response.set_type_url(Config::TestTypeUrl::get().Secret); discovery_response.add_resources()->PackFrom(secret); sds_stream.sendGrpcMessage(discovery_response); } @@ -1114,8 +1114,8 @@ TEST_P(SdsCdsIntegrationTest, BasicSuccess) { // The 4 clusters are CDS,SDS,static and dynamic cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {}, "42"); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {}, "42"); // Successfully removed the dynamic cluster. test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); } @@ -1233,14 +1233,14 @@ class SdsDynamicClusterIntegrationTest : public SdsDynamicIntegrationBaseTest { std::unique_ptr& cdsUpstream() { return fake_upstreams_[0]; } void sendCdsResponse(bool do_not_send_sds_cluster = false) { - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); if (!do_not_send_sds_cluster) { sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {sds_cluster_, dynamic_cluster_}, + Config::TestTypeUrl::get().Cluster, {sds_cluster_, dynamic_cluster_}, {sds_cluster_, dynamic_cluster_}, {}, "55"); } else { sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {dynamic_cluster_}, {dynamic_cluster_}, {}, "55"); + Config::TestTypeUrl::get().Cluster, {dynamic_cluster_}, {dynamic_cluster_}, {}, "55"); } } @@ -1248,7 +1248,7 @@ class SdsDynamicClusterIntegrationTest : public SdsDynamicIntegrationBaseTest { FakeStream& sds_stream) { envoy::service::discovery::v3::DiscoveryResponse discovery_response; discovery_response.set_version_info("1"); - discovery_response.set_type_url(Config::TypeUrl::get().Secret); + discovery_response.set_type_url(Config::TestTypeUrl::get().Secret); discovery_response.add_resources()->PackFrom(secret); sds_stream.sendGrpcMessage(discovery_response); } @@ -1303,8 +1303,8 @@ TEST_P(SdsDynamicClusterIntegrationTest, BasicSuccess) { // The 4 clusters are CDS,SDS,static and dynamic cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {}, "42"); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {}, "42"); // Successfully removed the dynamic cluster. test_server_->waitForGaugeEq("cluster_manager.active_clusters", 2); } @@ -1347,8 +1347,8 @@ TEST_P(SdsDynamicClusterIntegrationTest, EdsBootStrapCluster) { // The 4 clusters are CDS,SDS,static and dynamic cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {}, "42"); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {}, "42"); // Successfully removed the dynamic cluster. test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); } @@ -1410,8 +1410,8 @@ TEST_P(SdsDynamicClusterIntegrationTest, ClusterRefersNonExistentSdsCluster) { // The 3 clusters are CDS, static and dynamic cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {}, "42"); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {}, "42"); // Successfully removed the dynamic cluster. test_server_->waitForGaugeEq("cluster_manager.active_clusters", 2); } @@ -1442,8 +1442,8 @@ TEST_P(SdsDynamicClusterIntegrationTest, CdsSdsCyclicDependency) { // The 3 clusters are CDS, static and dynamic cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {}, "42"); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {}, "42"); // Successfully removed the dynamic cluster. test_server_->waitForGaugeEq("cluster_manager.active_clusters", 2); } @@ -1580,8 +1580,8 @@ TEST_P(SdsCdsPrivateKeyIntegrationTest, BasicSdsCdsPrivateKeyProvider) { // The 4 clusters are CDS,SDS,static and dynamic cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 4); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, {}, - {}, "42"); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {}, + {}, {}, "42"); // Successfully removed the dynamic cluster. test_server_->waitForGaugeEq("cluster_manager.active_clusters", 3); } diff --git a/test/integration/sds_generic_secret_integration_test.cc b/test/integration/sds_generic_secret_integration_test.cc index 539157d499d40..3b3629bc14924 100644 --- a/test/integration/sds_generic_secret_integration_test.cc +++ b/test/integration/sds_generic_secret_integration_test.cc @@ -134,7 +134,7 @@ class SdsGenericSecretApiConfigSourceIntegrationTest : public Grpc::GrpcClientIn generic_secret->mutable_secret()->set_inline_string("DUMMY_AES_128_KEY"); envoy::service::discovery::v3::DiscoveryResponse discovery_response; discovery_response.set_version_info("0"); - discovery_response.set_type_url(Config::TypeUrl::get().Secret); + discovery_response.set_type_url(Config::TestTypeUrl::get().Secret); discovery_response.add_resources()->PackFrom(secret); xds_stream_->sendGrpcMessage(discovery_response); } diff --git a/test/integration/server.h b/test/integration/server.h index 335d55fa4431b..a78a8b4eb4ef2 100644 --- a/test/integration/server.h +++ b/test/integration/server.h @@ -77,12 +77,12 @@ class TestScopeWrapper : public Scope { TestScopeWrapper(Thread::MutexBasicLockable& lock, ScopeSharedPtr wrapped_scope, Store& store) : lock_(lock), wrapped_scope_(wrapped_scope), store_(store) {} - ScopeSharedPtr createScope(const std::string& name) override { + ScopeSharedPtr createScope(const std::string& name, bool) override { Thread::LockGuard lock(lock_); return std::make_shared(lock_, wrapped_scope_->createScope(name), store_); } - ScopeSharedPtr scopeFromStatName(StatName name) override { + ScopeSharedPtr scopeFromStatName(StatName name, bool) override { Thread::LockGuard lock(lock_); return std::make_shared(lock_, wrapped_scope_->scopeFromStatName(name), store_); @@ -196,6 +196,7 @@ class NotifyingCounter : public Stats::Counter { uint32_t use_count() const override { return counter_->use_count(); } StatName tagExtractedStatName() const override { return counter_->tagExtractedStatName(); } bool used() const override { return counter_->used(); } + void markUnused() override { counter_->markUnused(); } bool hidden() const override { return counter_->hidden(); } SymbolTable& symbolTable() override { return counter_->symbolTable(); } const SymbolTable& constSymbolTable() const override { return counter_->constSymbolTable(); } @@ -365,6 +366,16 @@ class TestIsolatedStoreImpl : public StoreRoot { void extractAndAppendTags(absl::string_view, StatNamePool&, StatNameTagVector&) override {}; const Stats::TagVector& fixedTags() override { CONSTRUCT_ON_FIRST_USE(Stats::TagVector); } + void evictUnused() override { + Thread::LockGuard lock(lock_); + eviction_count_++; + } + + uint32_t evictionCount() const { + Thread::LockGuard lock(lock_); + return eviction_count_; + } + // Stats::StoreRoot void addSink(Sink&) override {} void setTagProducer(TagProducerPtr&&) override {} @@ -381,6 +392,7 @@ class TestIsolatedStoreImpl : public StoreRoot { IsolatedStoreImpl store_; PostMergeCb merge_cb_; ScopeSharedPtr lazy_default_scope_; + uint32_t eviction_count_{0}; }; } // namespace Stats diff --git a/test/integration/shadow_policy_integration_test.cc b/test/integration/shadow_policy_integration_test.cc index 72b52059f31f1..7963fc7e354d4 100644 --- a/test/integration/shadow_policy_integration_test.cc +++ b/test/integration/shadow_policy_integration_test.cc @@ -22,8 +22,6 @@ class ShadowPolicyIntegrationTest ShadowPolicyIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP2, std::get<0>(GetParam())), SocketInterfaceSwap(Network::Socket::Type::Stream) { - scoped_runtime_.mergeValues( - {{"envoy.reloadable_features.streaming_shadow", streaming_shadow_ ? "true" : "false"}}); setUpstreamProtocol(Http::CodecType::HTTP2); autonomous_upstream_ = true; setUpstreamCount(2); @@ -598,7 +596,7 @@ TEST_P(ShadowPolicyIntegrationTest, ShadowRequestOverRouteBufferLimit) { config_helper_.addConfigModifier([](ConfigHelper::HttpConnectionManager& hcm) { hcm.mutable_route_config() ->mutable_virtual_hosts(0) - ->mutable_per_request_buffer_limit_bytes() + ->mutable_request_body_buffer_limit() ->set_value(0); }); config_helper_.disableDelayClose(); diff --git a/test/integration/stats_integration_test.cc b/test/integration/stats_integration_test.cc index abae5c6bd24c7..e92e4d3357c11 100644 --- a/test/integration/stats_integration_test.cc +++ b/test/integration/stats_integration_test.cc @@ -400,6 +400,7 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSize) { // 2021/08/18 13176 40577 40700 Support slow start mode // 2022/03/14 42000 Fix test flakes // 2022/10/27 44000 Update tcmalloc + // 2025/07/28 40266 44299 44500 Add request_count_ field to ActiveClient // Note: when adjusting this value: EXPECT_MEMORY_EQ is active only in CI // 'release' builds, where we control the platform and tool-chain. So you @@ -420,7 +421,7 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSize) { // https://github.com/envoyproxy/envoy/issues/12209 // EXPECT_MEMORY_EQ(m_per_cluster, 37061); } - EXPECT_MEMORY_LE(m_per_cluster, 44000); // Round up to allow platform variations. + EXPECT_MEMORY_LE(m_per_cluster, 44500); // Round up to allow platform variations. } TEST_P(ClusterMemoryTestRunner, MemoryLargeHostSizeWithStats) { diff --git a/test/integration/tcp_conn_pool_integration_test.cc b/test/integration/tcp_conn_pool_integration_test.cc index 0aa2e61343555..5bf016e8a888e 100644 --- a/test/integration/tcp_conn_pool_integration_test.cc +++ b/test/integration/tcp_conn_pool_integration_test.cc @@ -98,7 +98,7 @@ class TestFilterConfigFactory : public Server::Configuration::NamedNetworkFilter ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { CONSTRUCT_ON_FIRST_USE(std::string, "envoy.test.router"); } diff --git a/test/integration/tcp_proxy_integration_test.cc b/test/integration/tcp_proxy_integration_test.cc index a8cfd0790aef6..bc8de62ae98ee 100644 --- a/test/integration/tcp_proxy_integration_test.cc +++ b/test/integration/tcp_proxy_integration_test.cc @@ -1058,9 +1058,9 @@ class TcpProxyMetadataMatchIntegrationTest : public TcpProxyIntegrationTest { envoy::config::core::v3::Metadata TcpProxyMetadataMatchIntegrationTest::lbMetadata(std::map values) { - ProtobufWkt::Struct map; + Protobuf::Struct map; auto* mutable_fields = map.mutable_fields(); - ProtobufWkt::Value value; + Protobuf::Value value; std::map::iterator it; for (it = values.begin(); it != values.end(); it++) { @@ -1314,10 +1314,10 @@ class InjectDynamicMetadata : public Network::ReadFilter { return Network::FilterStatus::StopIteration; } - ProtobufWkt::Value val; + Protobuf::Value val; val.set_string_value(data.toString()); - ProtobufWkt::Struct& map = + Protobuf::Struct& map = (*read_callbacks_->connection() .streamInfo() .dynamicMetadata() diff --git a/test/integration/tcp_proxy_odcds_integration_test.cc b/test/integration/tcp_proxy_odcds_integration_test.cc index c36feede0ff2b..5fa53b90b8606 100644 --- a/test/integration/tcp_proxy_odcds_integration_test.cc +++ b/test/integration/tcp_proxy_odcds_integration_test.cc @@ -139,15 +139,15 @@ TEST_P(TcpProxyOdcdsIntegrationTest, SingleTcpClient) { odcds_stream_->startGrpcStream(); test_server_->waitForCounterEq("tcp.tcpproxy_stats.on_demand_cluster_attempt", 1); // Verify the on-demand CDS request and respond with the prepared `new_cluster`. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); // The on demand cluster request is received and the response is not sent. The tcp proxy must not ASSERT_TRUE(fake_upstreams_.back()->assertPendingConnectionsEmpty()); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); // This upstream is listening on the endpoint of `new_cluster`. It starts to serve tcp_proxy. FakeRawConnectionPtr fake_upstream_connection; @@ -184,12 +184,12 @@ TEST_P(TcpProxyOdcdsIntegrationTest, RepeatedRequest) { odcds_stream_->startGrpcStream(); // Verify the on-demand CDS request and respond without providing the cluster. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {new_cluster_}, {}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); test_server_->waitForCounterEq("tcp.tcpproxy_stats.on_demand_cluster_attempt", expected_upstream_connections); @@ -243,7 +243,7 @@ TEST_P(TcpProxyOdcdsIntegrationTest, ShutdownConnectionOnTimeout) { odcds_stream_->startGrpcStream(); // Verify the on-demand CDS request and respond without providing the cluster. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); EXPECT_EQ(1, test_server_->counter("tcp.tcpproxy_stats.on_demand_cluster_attempt")->value()); @@ -266,12 +266,12 @@ TEST_P(TcpProxyOdcdsIntegrationTest, ShutdownConnectionOnClusterMissing) { odcds_stream_->startGrpcStream(); // Verify the on-demand CDS request and respond the required cluster is missing. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().Cluster, {}, {"new_cluster"}, "1", odcds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {}, {}, odcds_stream_.get())); + Config::TestTypeUrl::get().Cluster, {}, {"new_cluster"}, "1", odcds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {}, {}, + odcds_stream_.get())); EXPECT_EQ(1, test_server_->counter("tcp.tcpproxy_stats.on_demand_cluster_attempt")->value()); @@ -295,7 +295,7 @@ TEST_P(TcpProxyOdcdsIntegrationTest, ShutdownAllConnectionsOnClusterLookupTimeou odcds_stream_->startGrpcStream(); // Verify the on-demand CDS request and respond without providing the cluster. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); EXPECT_EQ(1, test_server_->counter("tcp.tcpproxy_stats.on_demand_cluster_attempt")->value()); @@ -326,7 +326,7 @@ TEST_P(TcpProxyOdcdsIntegrationTest, ShutdownTcpClientBeforeOdcdsResponse) { odcds_stream_->startGrpcStream(); // Verify the on-demand CDS request and stall the response before tcp client close. - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().Cluster, {"new_cluster"}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().Cluster, {"new_cluster"}, {}, odcds_stream_.get())); EXPECT_EQ(1, test_server_->counter("tcp.tcpproxy_stats.on_demand_cluster_attempt")->value()); // Client disconnect when the tcp proxy is waiting for the on demand response. diff --git a/test/integration/tcp_tunneling_integration_test.cc b/test/integration/tcp_tunneling_integration_test.cc index 72f2c6bf240c2..afdca7a30f4a3 100644 --- a/test/integration/tcp_tunneling_integration_test.cc +++ b/test/integration/tcp_tunneling_integration_test.cc @@ -5,6 +5,7 @@ #include "envoy/config/core/v3/proxy_protocol.pb.h" #include "envoy/extensions/access_loggers/file/v3/file.pb.h" #include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.h" +#include "envoy/extensions/request_id/uuid/v3/uuid.pb.h" #include "envoy/extensions/upstreams/http/tcp/v3/tcp_connection_pool.pb.h" #include "test/integration/filters/add_header_filter.pb.h" @@ -12,6 +13,7 @@ #include "test/integration/http_integration.h" #include "test/integration/http_protocol_integration.h" #include "test/integration/tcp_tunneling_integration.h" +#include "test/test_common/simulated_time_system.h" #include "gtest/gtest.h" @@ -827,6 +829,42 @@ class TcpTunnelingIntegrationTest : public BaseTcpTunnelingIntegrationTest { BaseTcpTunnelingIntegrationTest::SetUp(); } + void enableRequestIdGeneration() { + config_helper_.addConfigModifier( + [&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy proxy_config; + proxy_config.set_stat_prefix("tcp_stats"); + proxy_config.set_cluster("cluster_0"); + proxy_config.mutable_tunneling_config()->set_hostname("foo.lyft.com:80"); + // Configure request ID generation for tunneling using the UUID request ID extension. + envoy::extensions::filters::network::http_connection_manager::v3::RequestIDExtension + request_id_extension; + envoy::extensions::request_id::uuid::v3::UuidRequestIdConfig uuid_config; + request_id_extension.mutable_typed_config()->PackFrom(uuid_config); + proxy_config.mutable_tunneling_config()->mutable_request_id_extension()->CopyFrom( + request_id_extension); + + // Add a file access log to capture the dynamic metadata request id before packing. + tunnel_access_log_path_ = TestEnvironment::temporaryPath(TestUtility::uniqueFilename()); + envoy::extensions::access_loggers::file::v3::FileAccessLog fal; + fal.set_path(tunnel_access_log_path_); + fal.mutable_log_format()->mutable_text_format_source()->set_inline_string( + "%DYNAMIC_METADATA(envoy.filters.network.tcp_proxy:tunnel_request_id)%\n"); + proxy_config.add_access_log()->mutable_typed_config()->PackFrom(fal); + + auto* listeners = bootstrap.mutable_static_resources()->mutable_listeners(); + for (auto& listener : *listeners) { + if (listener.name() != "tcp_proxy") { + continue; + } + auto* filter_chain = listener.mutable_filter_chains(0); + auto* filter = filter_chain->mutable_filters(0); + filter->mutable_typed_config()->PackFrom(proxy_config); + break; + } + }); + } + const HttpFilterProto getAddHeaderFilterConfig(const std::string& name, const std::string& key, const std::string& value) { HttpFilterProto filter_config; @@ -872,6 +910,7 @@ class TcpTunnelingIntegrationTest : public BaseTcpTunnelingIntegrationTest { } } int downstream_buffer_limit_{0}; + std::string tunnel_access_log_path_; }; TEST_P(TcpTunnelingIntegrationTest, Basic) { @@ -986,6 +1025,30 @@ TEST_P(TcpTunnelingIntegrationTest, FlowControlOnAndGiantBody) { testGiantRequestAndResponse(10 * 1024 * 1024, 10 * 1024 * 1024); } +TEST_P(TcpTunnelingIntegrationTest, GeneratesRequestIdHeaderWhenEnabled) { + enableRequestIdGeneration(); + initialize(); + + // Start a connection, and verify the upgrade headers are received upstream. + tcp_client_ = makeTcpConnection(lookupPort("tcp_proxy")); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + + // x-request-id should be present. + const auto& hdrs = upstream_request_->headers(); + EXPECT_FALSE(hdrs.getRequestIdValue().empty()); + + // Complete handshake and basic bidi flow to ensure no regressions. + upstream_request_->encodeHeaders(default_response_headers_, false); + sendBidiData(fake_upstream_connection_); + closeConnection(fake_upstream_connection_); + + // Verify access log contains a non-empty request id line for filter-state key. + const std::string log_content = waitForAccessLog(tunnel_access_log_path_); + EXPECT_FALSE(log_content.empty()); +} + TEST_P(TcpTunnelingIntegrationTest, SendDataUpstreamAfterUpstreamClose) { if (upstreamProtocol() == Http::CodecType::HTTP1) { // HTTP/1.1 can't frame with FIN bits. @@ -1764,9 +1827,11 @@ TEST_P(TcpTunnelingIntegrationTest, UpstreamConnectingDownstreamDisconnect) { ASSERT_TRUE(fake_upstream_connection_->close()); } -TEST_P(TcpTunnelingIntegrationTest, TestIdletimeoutWithLargeOutstandingData) { - enableHalfClose(false); - config_helper_.setBufferLimits(1024, 1024); +// Test idle timeout when connection establishment is prevented by not sending upstream response +TEST_P(TcpTunnelingIntegrationTest, + IdleTimeoutNoUpstreamConnectionNoIdleTimeoutSetOnNewConnection) { + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.tcp_proxy_set_idle_timer_immediately_on_new_connection", "false"); config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(1); auto* filter_chain = listener->mutable_filter_chains(0); @@ -1776,27 +1841,36 @@ TEST_P(TcpTunnelingIntegrationTest, TestIdletimeoutWithLargeOutstandingData) { auto tcp_proxy_config = MessageUtil::anyConvert( *config_blob); - tcp_proxy_config.mutable_idle_timeout()->set_nanos( - std::chrono::duration_cast(std::chrono::milliseconds(500)) - .count()); + + tcp_proxy_config.mutable_tunneling_config()->set_hostname("foo.lyft.com:80"); + tcp_proxy_config.mutable_idle_timeout()->CopyFrom( + ProtobufUtil::TimeUtil::MillisecondsToDuration(1)); + config_blob->PackFrom(tcp_proxy_config); }); initialize(); - setUpConnection(fake_upstream_connection_); + // Start downstream TCP connection (CONNECT will be sent upstream). + tcp_client_ = makeTcpConnection(lookupPort("tcp_proxy")); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + EXPECT_EQ(upstream_request_->headers().getMethodValue(), "CONNECT"); - std::string data(1024 * 16, 'a'); - ASSERT_TRUE(tcp_client_->write(data)); - upstream_request_->encodeData(data, false); + // Don't send response headers - this prevents the tunnel from being fully established. + // The TCP proxy will wait for the response, and the idle timeout will not trigger as + // the idle timeout is not set immediately on new connection. - tcp_client_->waitForDisconnect(); + // Verify the stream wasn't reset due to timeout if (upstreamProtocol() == Http::CodecType::HTTP1) { - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - tcp_client_->close(); + ASSERT_FALSE(fake_upstream_connection_->waitForDisconnect(std::chrono::milliseconds(10))); } else { - ASSERT_TRUE(upstream_request_->waitForReset()); + ASSERT_FALSE(upstream_request_->waitForReset(std::chrono::milliseconds(10))); } + + // Clean up the TCP client + tcp_client_->close(); } // Test that a downstream flush works correctly (all data is flushed) @@ -1847,6 +1921,12 @@ TEST_P(TcpTunnelingIntegrationTest, TcpProxyUpstreamFlush) { // Use a very large size to make sure it is larger than the kernel socket read buffer. const uint32_t size = 50 * 1024 * 1024; config_helper_.setBufferLimits(size, size); + + // Ensure this HTTP2 flow control window is enough. + if (upstreamProtocol() == Http::CodecType::HTTP2) { + config_helper_.setUpstreamHttp2WindowSize(size, size); + } + initialize(); setUpConnection(fake_upstream_connection_); @@ -2375,5 +2455,134 @@ INSTANTIATE_TEST_SUITE_P( {Http::CodecType::HTTP1, Http::CodecType::HTTP2, Http::CodecType::HTTP3}, {Http::CodecType::HTTP1, Http::CodecType::HTTP2, Http::CodecType::HTTP3})), BaseTcpTunnelingIntegrationTest::protocolTestParamsToString); + +/** + * Simulated time fixture only for deterministic idle-timeout test. + */ +class TcpTunnelingIntegrationTestSimTime : public Event::TestUsingSimulatedTime, + public BaseTcpTunnelingIntegrationTest { +public: + void SetUp() override { + enableHalfClose(true); + + config_helper_.addConfigModifier( + [&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy proxy_config; + proxy_config.set_stat_prefix("tcp_stats"); + proxy_config.set_cluster("cluster_0"); + proxy_config.mutable_tunneling_config()->set_hostname("foo.lyft.com:80"); + + auto* listener = bootstrap.mutable_static_resources()->add_listeners(); + listener->set_name("tcp_proxy"); + + auto* socket_address = listener->mutable_address()->mutable_socket_address(); + socket_address->set_address(Network::Test::getLoopbackAddressString(version_)); + socket_address->set_port_value(0); + + auto* filter_chain = listener->add_filter_chains(); + auto* filter = filter_chain->add_filters(); + filter->mutable_typed_config()->PackFrom(proxy_config); + filter->set_name("envoy.filters.network.tcp_proxy"); + }); + BaseTcpTunnelingIntegrationTest::SetUp(); + } +}; + +TEST_P(TcpTunnelingIntegrationTestSimTime, TestIdletimeoutWithLargeOutstandingData) { + const auto idle_timeout = 5; // 5 seconds + + enableHalfClose(false); + config_helper_.setBufferLimits(1024, 1024); + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(1); + auto* filter_chain = listener->mutable_filter_chains(0); + auto* config_blob = filter_chain->mutable_filters(0)->mutable_typed_config(); + + ASSERT_TRUE(config_blob->Is()); + auto tcp_proxy_config = + MessageUtil::anyConvert( + *config_blob); + tcp_proxy_config.mutable_idle_timeout()->CopyFrom( + ProtobufUtil::TimeUtil::SecondsToDuration(idle_timeout)); + config_blob->PackFrom(tcp_proxy_config); + }); + + initialize(); + + setUpConnection(fake_upstream_connection_); + + std::string data(1024 * 16, 'a'); + ASSERT_TRUE(tcp_client_->write(data)); + upstream_request_->encodeData(data, false); + + // Advance simulated time to trigger the idle timeout deterministically. + timeSystem().advanceTimeAndRun(std::chrono::seconds(idle_timeout), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + test_server_->waitForCounterGe("tcp.tcp_stats.idle_timeout", 1); + + tcp_client_->waitForDisconnect(); + if (upstreamProtocol() == Http::CodecType::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + tcp_client_->close(); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } +} + +// Test idle timeout when connection establishment is prevented by not sending upstream response +TEST_P(TcpTunnelingIntegrationTestSimTime, + IdleTimeoutNoUpstreamConnectionWithIdleTimeoutSetOnNewConnection) { + const auto idle_timeout = 5; // 5 seconds + + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(1); + auto* filter_chain = listener->mutable_filter_chains(0); + auto* config_blob = filter_chain->mutable_filters(0)->mutable_typed_config(); + + ASSERT_TRUE(config_blob->Is()); + auto tcp_proxy_config = + MessageUtil::anyConvert( + *config_blob); + + tcp_proxy_config.mutable_tunneling_config()->set_hostname("foo.lyft.com:80"); + tcp_proxy_config.mutable_idle_timeout()->CopyFrom( + ProtobufUtil::TimeUtil::SecondsToDuration(idle_timeout)); + + config_blob->PackFrom(tcp_proxy_config); + }); + + initialize(); + + // Start downstream TCP connection (CONNECT will be sent upstream). + tcp_client_ = makeTcpConnection(lookupPort("tcp_proxy")); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + EXPECT_EQ(upstream_request_->headers().getMethodValue(), "CONNECT"); + + // Don't send response headers - this prevents the tunnel from being fully established. + // The TCP proxy will wait for the response, and the idle timeout will trigger. + + // Advance simulated time to fire idle timer. + timeSystem().advanceTimeAndRun(std::chrono::seconds(idle_timeout), *dispatcher_, + Event::Dispatcher::RunType::NonBlock); + + test_server_->waitForCounterGe("tcp.tcp_stats.idle_timeout", 1); + tcp_client_->waitForHalfClose(); + + if (upstreamProtocol() == Http::CodecType::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } + tcp_client_->close(); +} + +// Excluded HTTP/3 protocol tests as they are crashing under simulated time. +INSTANTIATE_TEST_SUITE_P(IpAndHttpVersionsSimTime, TcpTunnelingIntegrationTestSimTime, + testing::ValuesIn(BaseTcpTunnelingIntegrationTest::getProtocolTestParams( + {Http::CodecType::HTTP1, Http::CodecType::HTTP2}, + {Http::CodecType::HTTP1, Http::CodecType::HTTP2})), + BaseTcpTunnelingIntegrationTest::protocolTestParamsToString); } // namespace } // namespace Envoy diff --git a/test/integration/tracked_watermark_buffer.cc b/test/integration/tracked_watermark_buffer.cc index 664036e279235..175cf83c5a44c 100644 --- a/test/integration/tracked_watermark_buffer.cc +++ b/test/integration/tracked_watermark_buffer.cc @@ -141,14 +141,14 @@ uint64_t TrackedWatermarkBufferFactory::sumMaxBufferSizes() const { return val; } -std::pair TrackedWatermarkBufferFactory::highWatermarkRange() const { +std::pair TrackedWatermarkBufferFactory::highWatermarkRange() const { absl::MutexLock lock(&mutex_); - uint32_t min_watermark = 0; - uint32_t max_watermark = 0; + uint64_t min_watermark = 0; + uint64_t max_watermark = 0; bool watermarks_set = false; for (auto& item : buffer_infos_) { - uint32_t watermark = item.second.watermark_; + uint64_t watermark = item.second.watermark_; if (watermark == 0) { max_watermark = 0; watermarks_set = true; diff --git a/test/integration/tracked_watermark_buffer.h b/test/integration/tracked_watermark_buffer.h index 8ede6a903f476..4af467b3f5fc8 100644 --- a/test/integration/tracked_watermark_buffer.h +++ b/test/integration/tracked_watermark_buffer.h @@ -20,7 +20,7 @@ class TrackedWatermarkBuffer : public Buffer::WatermarkBuffer { public: TrackedWatermarkBuffer( std::function update_size, - std::function update_high_watermark, + std::function update_high_watermark, std::function on_delete, std::function on_bind, std::function below_low_watermark, std::function above_high_watermark, @@ -30,7 +30,7 @@ class TrackedWatermarkBuffer : public Buffer::WatermarkBuffer { on_delete_(on_delete), on_bind_(on_bind) {} ~TrackedWatermarkBuffer() override { on_delete_(this); } - void setWatermarks(uint32_t watermark, uint32_t overload) override { + void setWatermarks(uint64_t watermark, uint32_t overload) override { update_high_watermark_(watermark); WatermarkBuffer::setWatermarks(watermark, overload); } @@ -53,7 +53,7 @@ class TrackedWatermarkBuffer : public Buffer::WatermarkBuffer { private: std::function update_size_; - std::function update_high_watermark_; + std::function update_high_watermark_; std::function on_delete_; std::function on_bind_; }; @@ -85,7 +85,7 @@ class TrackedWatermarkBufferFactory : public WatermarkBufferFactory { uint64_t sumMaxBufferSizes() const; // Get lower and upper bound on buffer high watermarks. A watermark of 0 indicates that watermark // functionality is disabled. - std::pair highWatermarkRange() const; + std::pair highWatermarkRange() const; // Number of accounts created. uint64_t numAccountsCreated() const; @@ -152,7 +152,7 @@ class TrackedWatermarkBufferFactory : public WatermarkBufferFactory { void checkIfExpectedBalancesMet() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); struct BufferInfo { - uint32_t watermark_ = 0; + uint64_t watermark_ = 0; uint64_t current_size_ = 0; uint64_t max_size_ = 0; }; diff --git a/test/integration/typed_metadata_integration_test.cc b/test/integration/typed_metadata_integration_test.cc index d49d318c7f50a..734083691f0bb 100644 --- a/test/integration/typed_metadata_integration_test.cc +++ b/test/integration/typed_metadata_integration_test.cc @@ -23,9 +23,9 @@ INSTANTIATE_TEST_SUITE_P(Protocols, ListenerTypedMetadataIntegrationTest, TEST_P(ListenerTypedMetadataIntegrationTest, Hello) { // Add some typed metadata to the listener. - ProtobufWkt::StringValue value; + Protobuf::StringValue value; value.set_value("hello world"); - ProtobufWkt::Any packed_value; + Protobuf::Any packed_value; packed_value.PackFrom(value); config_helper_.addListenerTypedMetadata("test.listener.typed.metadata", packed_value); @@ -54,14 +54,21 @@ class TestAccessLogFactory : public AccessLog::AccessLogInstanceFactory { public: AccessLog::InstanceSharedPtr createAccessLogInstance(const Protobuf::Message&, AccessLog::FilterPtr&&, - Server::Configuration::FactoryContext& context, + Server::Configuration::GenericFactoryContext&, std::vector&& = {}) override { - // Check that expected listener metadata is present - EXPECT_EQ(1, context.listenerInfo().metadata().typed_filter_metadata().size()); - const auto iter = context.listenerInfo().metadata().typed_filter_metadata().find( - "test.listener.typed.metadata"); - EXPECT_NE(iter, context.listenerInfo().metadata().typed_filter_metadata().end()); - return std::make_shared>(); + auto out = std::make_shared>(); + EXPECT_CALL(*out, log(_, _)) + .WillRepeatedly(testing::Invoke( + [](const Formatter::HttpFormatterContext&, const StreamInfo::StreamInfo& info) -> void { + // Check that expected listener metadata is present + auto listener_info = info.downstreamAddressProvider().listenerInfo(); + ASSERT_TRUE(listener_info.has_value()); + EXPECT_EQ(1, listener_info->metadata().typed_filter_metadata().size()); + const auto iter = listener_info->metadata().typed_filter_metadata().find( + "test.listener.typed.metadata"); + EXPECT_NE(iter, listener_info->metadata().typed_filter_metadata().end()); + })); + return out; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { @@ -77,9 +84,9 @@ TEST_P(ListenerTypedMetadataIntegrationTest, ListenerMetadataPlumbingToAccessLog Registry::InjectFactory factory_register(factory); // Add some typed metadata to the listener. - ProtobufWkt::StringValue value; + Protobuf::StringValue value; value.set_value("hello world"); - ProtobufWkt::Any packed_value; + Protobuf::Any packed_value; packed_value.PackFrom(value); config_helper_.addListenerTypedMetadata("test.listener.typed.metadata", packed_value); diff --git a/test/integration/udp_tunneling_integration_test.cc b/test/integration/udp_tunneling_integration_test.cc index 2223279b21751..635c84c91a5f7 100644 --- a/test/integration/udp_tunneling_integration_test.cc +++ b/test/integration/udp_tunneling_integration_test.cc @@ -225,7 +225,7 @@ TEST_P(ConnectUdpTerminationIntegrationTest, DropUnknownCapsules) { setUpConnection(); Network::UdpRecvData request_datagram; const std::string unknown_capsule_fragment = - absl::HexStringToBytes("01" // DATAGRAM Capsule Type + absl::HexStringToBytes("17" // Reserved UNKNOWN Capsule Type "08" // Capsule Length "00" // Context ID "a1a2a3a4a5a6a7" // UDP Proxying Payload @@ -538,6 +538,11 @@ name: udp_proxy return deflated_size; } + void drainListeners() { + test_server_->server().dispatcher().post([this]() { test_server_->server().drainListeners(); }); + test_server_->waitForCounterEq("listener_manager.listener_stopped", 1); + } + TestConfig config_; Http::TestResponseHeaderMapImpl response_headers_{{":status", "200"}, {"capsule-protocol", "?1"}}; Network::Address::InstanceConstSharedPtr listener_address_; @@ -1553,6 +1558,25 @@ TEST_P(UdpTunnelingIntegrationTest, BytesMeterAccessLog) { test_server_->waitForGaugeEq("udp.foo.downstream_sess_active", 0); } +TEST_P(UdpTunnelingIntegrationTest, DrainListenersWhileTunnelingActiveSessionIsStillActive) { + TestConfig config{"host.com", "target.com", 1, 30, false, "", + BufferOptions{1, 30}, absl::nullopt}; + setup(config); + + const std::string datagram = "hello"; + establishConnection(datagram); + // Wait for buffered datagram. + ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, expectedCapsules({datagram}))); + + // Send a response and keep the session alive. + sendCapsuleDownstream("response", false); + test_server_->waitForGaugeEq("udp.foo.downstream_sess_active", 1); + + // Drain listeners while udp session is still active. + drainListeners(); + test_server_->waitForGaugeEq("udp.foo.downstream_sess_active", 0); +} + INSTANTIATE_TEST_SUITE_P(IpAndHttpVersions, UdpTunnelingIntegrationTest, testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( {Http::CodecType::HTTP2}, {Http::CodecType::HTTP2})), diff --git a/test/integration/upstream_http_filter_integration_test.cc b/test/integration/upstream_http_filter_integration_test.cc index 1ce734a75b03f..14aeb1b364ba2 100644 --- a/test/integration/upstream_http_filter_integration_test.cc +++ b/test/integration/upstream_http_filter_integration_test.cc @@ -31,7 +31,6 @@ constexpr absl::string_view expected_types[] = { using HttpFilterProto = envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter; -using Http::HeaderValueOf; using testing::Not; class UpstreamHttpFilterIntegrationTestBase : public HttpIntegrationTest { @@ -174,8 +173,8 @@ TEST_P(StaticRouterOrClusterFiltersIntegrationTest, initialize(); auto headers = sendRequestAndGetHeaders(); - EXPECT_THAT(*headers, Not(HeaderValueOf("x-test-router", "aa"))); - EXPECT_THAT(*headers, HeaderValueOf("x-test-cluster", "bb")); + EXPECT_THAT(*headers, Not(ContainsHeader("x-test-router", "aa"))); + EXPECT_THAT(*headers, ContainsHeader("x-test-cluster", "bb")); } TEST_P(StaticRouterOrClusterFiltersIntegrationTest, ClusterUpstreamFiltersDisabled) { @@ -185,7 +184,7 @@ TEST_P(StaticRouterOrClusterFiltersIntegrationTest, ClusterUpstreamFiltersDisabl initialize(); auto headers = sendRequestAndGetHeaders(); - EXPECT_THAT(*headers, Not(HeaderValueOf("x-test-router", "aa"))); + EXPECT_THAT(*headers, Not(ContainsHeader("x-test-router", "aa"))); } TEST_P(StaticRouterOrClusterFiltersIntegrationTest, RouterUpstreamFiltersDisabled) { @@ -195,7 +194,7 @@ TEST_P(StaticRouterOrClusterFiltersIntegrationTest, RouterUpstreamFiltersDisable initialize(); auto headers = sendRequestAndGetHeaders(); - EXPECT_THAT(*headers, Not(HeaderValueOf("x-test-cluster", "bb"))); + EXPECT_THAT(*headers, Not(ContainsHeader("x-test-cluster", "bb"))); } TEST_P(StaticRouterOrClusterFiltersIntegrationTest, @@ -213,9 +212,9 @@ TEST_P(StaticRouterOrClusterFiltersIntegrationTest, auto headers = sendRequestAndGetHeaders(); if (useRouterFilters()) { - EXPECT_THAT(*headers, Not(HeaderValueOf(default_header_key_, default_header_value_))); + EXPECT_THAT(*headers, Not(ContainsHeader(default_header_key_, default_header_value_))); } else { - EXPECT_THAT(*headers, HeaderValueOf(default_header_key_, default_header_value_)); + EXPECT_THAT(*headers, ContainsHeader(default_header_key_, default_header_value_)); } } @@ -475,7 +474,7 @@ class UpstreamHttpExtensionDiscoveryIntegrationTestBase void sendLdsResponse(const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); response.add_resources()->PackFrom(listener_config_); lds_stream_->sendGrpcMessage(response); } diff --git a/test/integration/upstream_network_filter_integration_test.cc b/test/integration/upstream_network_filter_integration_test.cc index 2dbb46ad52972..399ce5be2acdb 100644 --- a/test/integration/upstream_network_filter_integration_test.cc +++ b/test/integration/upstream_network_filter_integration_test.cc @@ -282,7 +282,7 @@ class UpstreamNetworkExtensionDiscoveryIntegrationTest void sendLdsResponse(const std::string& version) { envoy::service::discovery::v3::DiscoveryResponse response; response.set_version_info(version); - response.set_type_url(Config::TypeUrl::get().Listener); + response.set_type_url(Config::TestTypeUrl::get().Listener); response.add_resources()->PackFrom(listener_config_); lds_stream_->sendGrpcMessage(response); } diff --git a/test/integration/upstreams/per_host_upstream_config.h b/test/integration/upstreams/per_host_upstream_config.h index 8730d689d1acc..ed3ed2372d8e1 100644 --- a/test/integration/upstreams/per_host_upstream_config.h +++ b/test/integration/upstreams/per_host_upstream_config.h @@ -30,11 +30,11 @@ void addHeader(Envoy::Http::RequestHeaderMap& header_map, absl::string_view head absl::string_view key2) { if (auto filter_metadata = metadata.filter_metadata().find(std::string(key1)); filter_metadata != metadata.filter_metadata().end()) { - const ProtobufWkt::Struct& data_struct = filter_metadata->second; + const Protobuf::Struct& data_struct = filter_metadata->second; const auto& fields = data_struct.fields(); if (auto iter = fields.find(toStdStringView(key2)); // NOLINT(std::string_view) iter != fields.end()) { - if (iter->second.kind_case() == ProtobufWkt::Value::kStringValue) { + if (iter->second.kind_case() == Protobuf::Value::kStringValue) { header_map.setCopy(Envoy::Http::LowerCaseString(std::string(header_name)), iter->second.string_value()); } @@ -111,7 +111,7 @@ class PerHostGenericConnPoolFactory : public Router::GenericConnPoolFactory { } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; diff --git a/test/integration/vhds.h b/test/integration/vhds.h index d09d5abfe513e..ccef3b90cef5d 100644 --- a/test/integration/vhds.h +++ b/test/integration/vhds.h @@ -201,20 +201,20 @@ class VhdsIntegrationTest : public HttpIntegrationTest, RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"my_route"}, true)); sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, {rdsConfig()}, "1"); + Config::TestTypeUrl::get().RouteConfiguration, {rdsConfig()}, "1"); result = xds_connection_->waitForNewStream(*dispatcher_, vhds_stream_); RELEASE_ASSERT(result, result.message()); vhds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, {buildVirtualHost()}, {}, "1", vhds_stream_.get()); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, + Config::TestTypeUrl::get().VirtualHost, {buildVirtualHost()}, {}, "1", vhds_stream_.get()); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); // Wait for our statically specified listener to become ready, and register its port in the @@ -233,7 +233,7 @@ class VhdsIntegrationTest : public HttpIntegrationTest, const std::vector& aliases = {}) { envoy::service::discovery::v3::DeltaDiscoveryResponse response; response.set_system_version_info("system_version_info_this_is_a_test"); - response.set_type_url(Config::TypeUrl::get().VirtualHost); + response.set_type_url(Config::TestTypeUrl::get().VirtualHost); auto* resource = response.add_resources(); resource->set_name("my_route/cannot-resolve-alias"); resource->set_version(version); @@ -249,7 +249,7 @@ class VhdsIntegrationTest : public HttpIntegrationTest, const std::vector& removed, const std::string& version, FakeStreamPtr& stream, const std::vector& aliases, const std::vector& unresolved_aliases) { auto response = createDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, added_or_updated, removed, version, aliases, {}); + Config::TestTypeUrl::get().VirtualHost, added_or_updated, removed, version, aliases, {}); for (const auto& unresolved_alias : unresolved_aliases) { auto* resource = response.add_resources(); resource->set_name(unresolved_alias); @@ -266,7 +266,7 @@ class VhdsIntegrationTest : public HttpIntegrationTest, createDeltaDiscoveryResponseWithResourceNameUsedAsAlias() { envoy::service::discovery::v3::DeltaDiscoveryResponse ret; ret.set_system_version_info("system_version_info_this_is_a_test"); - ret.set_type_url(Config::TypeUrl::get().VirtualHost); + ret.set_type_url(Config::TestTypeUrl::get().VirtualHost); auto* resource = ret.add_resources(); resource->set_name("my_route/vhost_1"); diff --git a/test/integration/vhds_integration_test.cc b/test/integration/vhds_integration_test.cc index 959f627ca9b15..dc27c6d7f19fb 100644 --- a/test/integration/vhds_integration_test.cc +++ b/test/integration/vhds_integration_test.cc @@ -78,10 +78,10 @@ class VhdsInitializationTest : public HttpIntegrationTest, RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", + EXPECT_TRUE(compareSotwDiscoveryRequest(Config::TestTypeUrl::get().RouteConfiguration, "", {"my_route"}, true)); sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml( RdsWithoutVhdsConfig)}, "1"); @@ -110,7 +110,7 @@ TEST_P(VhdsInitializationTest, InitializeVhdsAfterRdsHasBeenInitialized) { // Update RouteConfig, this time include VHDS config sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml(RdsConfigWithVhosts)}, "2"); @@ -118,15 +118,15 @@ TEST_P(VhdsInitializationTest, InitializeVhdsAfterRdsHasBeenInitialized) { RELEASE_ASSERT(result, result.message()); vhds_stream_->startGrpcStream(); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); sendDeltaDiscoveryResponse( - Config::TypeUrl::get().VirtualHost, + Config::TestTypeUrl::get().VirtualHost, {TestUtility::parseYaml( fmt::format(VhostTemplate, "my_route/vhost_0", "vhost.first"))}, {}, "1", vhds_stream_.get()); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_.get())); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TestTypeUrl::get().VirtualHost, {}, {}, + vhds_stream_.get())); // Confirm vhost.first that was configured via VHDS is reachable testRouterHeaderOnlyRequestAndResponse(nullptr, 1, "/", "vhost.first"); @@ -144,7 +144,7 @@ TEST_P(VhdsIntegrationTest, RdsUpdateWithoutVHDSChangesDoesNotRestartVHDS) { // Update RouteConfig, but don't change VHDS config sendSotwDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, + Config::TestTypeUrl::get().RouteConfiguration, {TestUtility::parseYaml(RdsConfigWithVhosts)}, "2"); diff --git a/test/integration/websocket_integration_test.cc b/test/integration/websocket_integration_test.cc index 3e3aa2d2e1aaa..c303ea2a23c98 100644 --- a/test/integration/websocket_integration_test.cc +++ b/test/integration/websocket_integration_test.cc @@ -36,6 +36,10 @@ Http::TestResponseHeaderMapImpl upgradeResponseHeaders(const char* upgrade_type {":status", "101"}, {"connection", "upgrade"}, {"upgrade", upgrade_type}}; } +Http::TestResponseHeaderMapImpl upgradeFailedResponseHeaders() { + return Http::TestResponseHeaderMapImpl{{":status", "500"}}; +} + template void commonValidate(ProxiedHeaders& proxied_headers, const OriginalHeaders& original_headers) { // If no content length is specified, the HTTP1 codec will add a chunked encoding header. @@ -118,6 +122,18 @@ ConfigHelper::HttpModifierFunction setRouteUsingWebsocket() { hcm) { hcm.add_upgrade_configs()->set_upgrade_type("websocket"); }; } +ConfigHelper::HttpModifierFunction setRouteRetryOn5xxPolicy() { + return [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + auto* route_config = hcm.mutable_route_config(); + auto* virtual_host = route_config->mutable_virtual_hosts(0); + auto* route = virtual_host->mutable_routes(0)->mutable_route(); + auto* retry_policy = route->mutable_retry_policy(); + retry_policy->set_retry_on("5xx"); + retry_policy->mutable_num_retries()->set_value(1); + }; +} + void WebsocketIntegrationTest::initialize() { if (upstreamProtocol() == Http::CodecType::HTTP2) { config_helper_.addConfigModifier( @@ -657,6 +673,10 @@ TEST_P(WebsocketIntegrationTest, Http1UpgradeStatusCodeUpgradeRequired) { return; } + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_allow_4xx_5xx_through_filter_chain", "false"}}); + useAccessLog("%RESPONSE_CODE_DETAILS%"); config_helper_.addConfigModifier(setRouteUsingWebsocket()); initialize(); @@ -676,6 +696,103 @@ TEST_P(WebsocketIntegrationTest, Http1UpgradeStatusCodeUpgradeRequired) { ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); } +// Test Websocket Upgrade in HTTP1 with 500 response code. +// Upgrade is a HTTP1 header. +TEST_P(WebsocketIntegrationTest, Http1UpgradeStatus5OOWithFilterChain) { + if (downstreamProtocol() != Http::CodecType::HTTP1 || + upstreamProtocol() != Http::CodecType::HTTP1) { + return; + } + + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_allow_4xx_5xx_through_filter_chain", "true"}}); + + useAccessLog("%RESPONSE_CODE_DETAILS%"); + config_helper_.addConfigModifier(setRouteUsingWebsocket()); + initialize(); + + auto in_correct_status_response_headers = upgradeFailedResponseHeaders(); + + // The upgrade should be paused, but the response header is proxied back to downstream. + performUpgrade(upgradeRequestHeaders(), in_correct_status_response_headers, true); + EXPECT_EQ("500", response_->headers().Status()->value().getStringView()); + + test_server_->waitForCounterEq("cluster.cluster_0.upstream_cx_destroy", 0); + test_server_->waitForGaugeEq("http.config_test.downstream_cx_upgrades_active", 1); + codec_client_->close(); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); +} + +// Test Websocket Upgrade in HTTP1 with a retry policy. +TEST_P(WebsocketIntegrationTest, Http1UpgradeRetryWithFilterChain) { + if (downstreamProtocol() != Http::CodecType::HTTP1 || + upstreamProtocol() != Http::CodecType::HTTP1) { + return; + } + + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_allow_4xx_5xx_through_filter_chain", "true"}}); + + useAccessLog("%RESPONSE_CODE_DETAILS%"); + config_helper_.addConfigModifier(setRouteUsingWebsocket()); + config_helper_.addConfigModifier(setRouteRetryOn5xxPolicy()); + + initialize(); + + // Establish the initial connection. + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Send websocket upgrade request + auto encoder_decoder = codec_client_->startRequest(upgradeRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + test_server_->waitForCounterGe("http.config_test.downstream_cx_upgrades_total", 1); + test_server_->waitForGaugeGe("http.config_test.downstream_cx_upgrades_active", 1); + + // Verify the first upgrade was received upstream. + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + validateUpgradeRequestHeaders(upstream_request_->headers(), upgradeRequestHeaders()); + + // Send a 500 response to trigger retry + auto failed_response_headers = upgradeFailedResponseHeaders(); + upstream_request_->encodeHeaders(failed_response_headers, false); + + // For HTTP/1, the connection will be closed and a new one established for retry + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + + // Wait for the retry - new upstream connection and request + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + validateUpgradeRequestHeaders(upstream_request_->headers(), upgradeRequestHeaders()); + test_server_->waitForCounterEq("cluster.cluster_0.upstream_rq_retry", 1); + + // Send the successful 101 upgrade response on the second try + auto success_response_headers = upgradeResponseHeaders(); + upstream_request_->encodeHeaders(success_response_headers, false); + + // Verify the successful upgrade response was received downstream. + response_->waitForHeaders(); + EXPECT_EQ("101", response_->headers().Status()->value().getStringView()); + EXPECT_EQ("upgrade", response_->headers().Connection()->value().getStringView()); + EXPECT_EQ("websocket", response_->headers().Upgrade()->value().getStringView()); + validateUpgradeResponseHeaders(response_->headers(), success_response_headers); + test_server_->waitForCounterEq("cluster.cluster_0.upstream_rq_retry_success", 1); + + // Verify successful websocket connection by sending bidirectional data + sendBidirectionalData(); + + // Clean up + codec_client_->sendData(*request_encoder_, "bye!", false); + codec_client_->close(); + ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, "hellobye!")); + ASSERT_TRUE(waitForUpstreamDisconnectOrReset()); +} + // Test data flow when websocket handshake failed. TEST_P(WebsocketIntegrationTest, BidirectionalUpgradeFailedWithPrePayload) { if (downstreamProtocol() != Http::CodecType::HTTP1 || @@ -683,6 +800,10 @@ TEST_P(WebsocketIntegrationTest, BidirectionalUpgradeFailedWithPrePayload) { return; } + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_allow_4xx_5xx_through_filter_chain", "true"}}); + config_helper_.addConfigModifier(setRouteUsingWebsocket()); initialize(); @@ -712,10 +833,136 @@ TEST_P(WebsocketIntegrationTest, BidirectionalUpgradeFailedWithPrePayload) { ASSERT_FALSE(fake_upstream_connection->waitForData( FakeRawConnection::waitForInexactMatch("foo boo"), nullptr, std::chrono::milliseconds(10))); - tcp_client->waitForDisconnect(); + tcp_client->close(); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); } +// Test websocket upgrade per-try timeout +TEST_P(WebsocketIntegrationTest, WebSocketUpgradePerTryTimeout) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_enable_timeout_on_upgrade_response", "true"}}); + + config_helper_.addConfigModifier(setRouteUsingWebsocket()); + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + auto* route_config = hcm.mutable_route_config(); + auto* virtual_host = route_config->mutable_virtual_hosts(0); + auto* route = virtual_host->mutable_routes(0)->mutable_route(); + route->mutable_retry_policy()->mutable_per_try_timeout()->set_nanos( + 200 * 1000 * 1000); // 200ms per-try timeout + }); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = codec_client_->startRequest(upgradeRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + test_server_->waitForCounterGe("http.config_test.downstream_cx_upgrades_total", 1); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_per_try_timeout", 1); + ASSERT_TRUE(response_->waitForEndStream()); + EXPECT_EQ("504", response_->headers().getStatusValue()); + + codec_client_->close(); + ASSERT_TRUE(waitForUpstreamDisconnectOrReset()); +} + +// Test websocket upgrade route timeout +TEST_P(WebsocketIntegrationTest, WebSocketUpgradeRouteTimeout) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_enable_timeout_on_upgrade_response", "true"}}); + + config_helper_.addConfigModifier(setRouteUsingWebsocket()); + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + auto* route_config = hcm.mutable_route_config(); + auto* virtual_host = route_config->mutable_virtual_hosts(0); + auto* route = virtual_host->mutable_routes(0)->mutable_route(); + route->mutable_timeout()->set_nanos(200 * 1000 * 1000); // 200ms route timeout + }); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = codec_client_->startRequest(upgradeRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + test_server_->waitForCounterGe("http.config_test.downstream_cx_upgrades_total", 1); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_timeout", 1); + ASSERT_TRUE(response_->waitForEndStream()); + EXPECT_EQ("504", response_->headers().getStatusValue()); + + codec_client_->close(); + ASSERT_TRUE(waitForUpstreamDisconnectOrReset()); +} + +// Test websocket upgrade route timeout is maintained with retries +TEST_P(WebsocketIntegrationTest, WebSocketUpgradeRouteTimeoutWithRetries) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.websocket_enable_timeout_on_upgrade_response", "true"}}); + + config_helper_.addConfigModifier(setRouteUsingWebsocket()); + config_helper_.addConfigModifier(setRouteRetryOn5xxPolicy()); + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + auto* route_config = hcm.mutable_route_config(); + auto* virtual_host = route_config->mutable_virtual_hosts(0); + auto* route = virtual_host->mutable_routes(0)->mutable_route(); + route->mutable_timeout()->set_nanos(200 * 1000 * 1000); // 200ms route timeout + }); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = codec_client_->startRequest(upgradeRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + test_server_->waitForCounterGe("http.config_test.downstream_cx_upgrades_total", 1); + + // First attempt - send 500 to trigger retry + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + upstream_request_->encodeHeaders(upgradeFailedResponseHeaders(), false); + + // Wait for the first request to be reset or disconnected + ASSERT_TRUE(waitForUpstreamDisconnectOrReset()); + test_server_->waitForCounterEq("cluster.cluster_0.upstream_rq_retry", 1); + + // Second attempt - wait for new connection or reuse existing one + FakeHttpConnectionPtr fake_upstream_connection2; + FakeStreamPtr upstream_request2; + if (upstreamProtocol() == Http::CodecType::HTTP1) { + // HTTP/1 creates a new connection for retry + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection2)); + ASSERT_TRUE(fake_upstream_connection2->waitForNewStream(*dispatcher_, upstream_request2)); + } else { + // HTTP/2 and HTTP/3 can reuse the existing connection + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request2)); + } + ASSERT_TRUE(upstream_request2->waitForHeadersComplete()); + + // Route timeout should still fire after retry + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_timeout", 1); + ASSERT_TRUE(response_->waitForEndStream()); + EXPECT_EQ("504", response_->headers().getStatusValue()); + + cleanupUpstreamAndDownstream(); +} + TEST_P(WebsocketIntegrationTest, WebsocketUpgradeWithDisabledSmallBufferFilter) { if (downstreamProtocol() != Http::CodecType::HTTP1 || upstreamProtocol() != Http::CodecType::HTTP1) { diff --git a/test/integration/weighted_cluster_integration_test.cc b/test/integration/weighted_cluster_integration_test.cc index df4676f4c1089..fb405b30119df 100644 --- a/test/integration/weighted_cluster_integration_test.cc +++ b/test/integration/weighted_cluster_integration_test.cc @@ -4,6 +4,7 @@ #include "envoy/config/bootstrap/v3/bootstrap.pb.h" #include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/extensions/filters/http/header_mutation/v3/header_mutation.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "test/integration/filters/repick_cluster_filter.h" @@ -28,7 +29,7 @@ class WeightedClusterIntegrationTest : public testing::TestWithParam& weights) { + void initializeConfig(const std::vector& weights, bool test_header_mutation = false) { // Set the cluster configuration for `cluster_1` config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* cluster = bootstrap.mutable_static_resources()->add_clusters(); @@ -39,10 +40,13 @@ class WeightedClusterIntegrationTest : public testing::TestWithParamset_name("cluster_0"); cluster->mutable_weight()->set_value(weights[0]); + if (test_header_mutation) { + envoy::config::core::v3::HeaderValueOption* header_value_option = + cluster->mutable_request_headers_to_add()->Add(); + auto* mutable_header = header_value_option->mutable_header(); + mutable_header->set_key("x-cluster-name-test"); + mutable_header->set_value("cluster_0"); + + envoy::extensions::filters::http::header_mutation::v3::HeaderMutationPerRoute + header_mutation; + std::string header_mutation_config = R"EOF( + mutations: + response_mutations: + - append: + header: + key: "x-cluster-header-mutation-test" + value: "cluster-0" + )EOF"; + TestUtility::loadFromYaml(header_mutation_config, header_mutation); + (*cluster->mutable_typed_per_filter_config())["envoy.filters.http.header_mutation"] + .PackFrom(header_mutation); + } + // Add a cluster with `cluster_header` specified. cluster = weighted_clusters->add_clusters(); cluster->set_cluster_header(std::string(Envoy::RepickClusterFilter::ClusterHeaderName)); @@ -67,7 +93,8 @@ class WeightedClusterIntegrationTest : public testing::TestWithParam& getDefaultWeights() { return default_weights_; } - void sendRequestAndValidateResponse(const std::vector& upstream_indices) { + void sendRequestAndValidateResponse(const std::vector& upstream_indices, + bool check_header_mutation = false) { // Create a client aimed at Envoy’s default HTTP port. codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); @@ -86,6 +113,14 @@ class WeightedClusterIntegrationTest : public testing::TestWithParamcomplete()); EXPECT_EQ(0U, upstream_request_->bodyLength()); + if (check_header_mutation) { + EXPECT_EQ( + upstream_request_->headers().get(Http::LowerCaseString("x-cluster-name-test")).size(), 1); + EXPECT_EQ(result.response->headers() + .get(Http::LowerCaseString("x-cluster-header-mutation-test")) + .size(), + 1); + } // Verify the proxied response was received downstream, as expected. EXPECT_TRUE(result.response->complete()); EXPECT_EQ("200", result.response->headers().getStatusValue()); @@ -121,6 +156,19 @@ TEST_P(WeightedClusterIntegrationTest, SteerTrafficToOneClusterWithName) { EXPECT_EQ(test_server_->counter("cluster.cluster_0.upstream_cx_total")->value(), 1); } +// Test the header mutations in weighted clusters. +TEST_P(WeightedClusterIntegrationTest, SteerTrafficToOneClusterWithHeaderMutation) { + setDeterministicValue(); + initializeConfig(getDefaultWeights(), true); + + // The expected destination cluster upstream is index 0 since the selected + // value is set to 0 indirectly via `setDeterministicValue()` above to set the weight to 0. + sendRequestAndValidateResponse({0}, true); + + // Check that the expected upstream cluster has incoming request. + EXPECT_EQ(test_server_->counter("cluster.cluster_0.upstream_cx_total")->value(), 1); +} + // Steer the traffic (i.e. send the request) to the weighted cluster with `cluster_header` // specified. TEST_P(WeightedClusterIntegrationTest, SteerTrafficToOneClusterWithHeader) { diff --git a/test/integration/xds_config_tracker_integration_test.cc b/test/integration/xds_config_tracker_integration_test.cc index b80eae8717fcf..069da0d2037dc 100644 --- a/test/integration/xds_config_tracker_integration_test.cc +++ b/test/integration/xds_config_tracker_integration_test.cc @@ -113,7 +113,7 @@ class TestXdsConfigTrackerFactory : public Config::XdsConfigTrackerFactory { std::string name() const override { return "envoy.config.xds.test_xds_tracker"; }; - Config::XdsConfigTrackerPtr createXdsConfigTracker(const ProtobufWkt::Any&, + Config::XdsConfigTrackerPtr createXdsConfigTracker(const Protobuf::Any&, ProtobufMessage::ValidationVisitor&, Api::Api& api, Event::Dispatcher&) override { return std::make_unique(api.rootScope()); @@ -197,10 +197,10 @@ TEST_P(XdsConfigTrackerIntegrationTest, XdsConfigTrackerSuccessCount) { Registry::InjectFactory registered(factory); initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_, cluster2_}, {}, "1"); + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_, cluster2_}, {}, "1"); // 3 because the statically specified CDS server itself counts as a cluster. test_server_->waitForGaugeGe("cluster_manager.active_clusters", 3); @@ -217,14 +217,14 @@ TEST_P(XdsConfigTrackerIntegrationTest, XdsConfigTrackerSuccessCountWithWrapper) Registry::InjectFactory registered(factory); initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); // Add a typed metadata to the Resource wrapper. test::envoy::config::xds::TestTrackerMetadata test_metadata; - ProtobufWkt::Any packed_value; + Protobuf::Any packed_value; packed_value.PackFrom(test_metadata); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_, cluster2_}, {}, "1", + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster2_}, {cluster1_, cluster2_}, {}, "1", {{kTestKey, packed_value}}); // 3 because the statically specified CDS server itself counts as a cluster. @@ -241,7 +241,7 @@ TEST_P(XdsConfigTrackerIntegrationTest, XdsConfigTrackerFailureCount) { Registry::InjectFactory registered(factory); initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); const auto route_config = TestUtility::parseYaml(R"EOF( @@ -256,7 +256,7 @@ TEST_P(XdsConfigTrackerIntegrationTest, XdsConfigTrackerFailureCount) { )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {route_config}, {route_config}, {}, "3"); + Config::TestTypeUrl::get().Cluster, {route_config}, {route_config}, {}, "3"); // Resources are rejected because Message's TypeUrl != Resource's test_server_->waitForCounterEq("test_xds_tracker.on_config_rejected", 1); @@ -270,9 +270,9 @@ TEST_P(XdsConfigTrackerIntegrationTest, XdsConfigTrackerPartialUpdate) { initialize(); // The first of duplicates has already been successfully applied, // and a duplicate exception should be threw. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( - Config::TypeUrl::get().Cluster, {cluster1_, cluster1_, cluster2_}, + Config::TestTypeUrl::get().Cluster, {cluster1_, cluster1_, cluster2_}, {cluster1_, cluster1_, cluster2_}, {}, "5"); // For Delta, the response will be rejected when checking the message due to the duplication. diff --git a/test/integration/xds_delegate_extension_integration_test.cc b/test/integration/xds_delegate_extension_integration_test.cc index 2b021b854c1ab..75e19a2bf5b59 100644 --- a/test/integration/xds_delegate_extension_integration_test.cc +++ b/test/integration/xds_delegate_extension_integration_test.cc @@ -84,7 +84,7 @@ class TestXdsResourcesDelegateFactory : public Config::XdsResourcesDelegateFacto std::string name() const override { return "envoy.config.xds.test_delegate"; }; - Config::XdsResourcesDelegatePtr createXdsResourcesDelegate(const ProtobufWkt::Any&, + Config::XdsResourcesDelegatePtr createXdsResourcesDelegate(const Protobuf::Any&, ProtobufMessage::ValidationVisitor&, Api::Api&, Event::Dispatcher&) override { @@ -209,7 +209,7 @@ TEST_P(XdsDelegateExtensionIntegrationTest, XdsResourcesDelegateOnConfigUpdated) acceptXdsConnection(); int current_on_config_updated_count = TestXdsResourcesDelegate::OnConfigUpdatedCount; - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "", {"some_rtds_layer"}, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "", {"some_rtds_layer"}, {"some_rtds_layer"}, {}, true)); auto some_rtds_layer = TestUtility::parseYaml(R"EOF( name: some_rtds_layer @@ -218,7 +218,7 @@ TEST_P(XdsDelegateExtensionIntegrationTest, XdsResourcesDelegateOnConfigUpdated) baz: meh )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", initial_load_success_ + 1); int expected_on_config_updated_count = ++current_on_config_updated_count; waitforOnConfigUpdatedCount(expected_on_config_updated_count); @@ -227,15 +227,15 @@ TEST_P(XdsDelegateExtensionIntegrationTest, XdsResourcesDelegateOnConfigUpdated) EXPECT_EQ("bar", getRuntimeKey("foo")); EXPECT_EQ("meh", getRuntimeKey("baz")); - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"some_rtds_layer"}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Runtime, "1", {"some_rtds_layer"}, + {}, {})); some_rtds_layer = TestUtility::parseYaml(R"EOF( name: some_rtds_layer layer: baz: saz )EOF"); sendDiscoveryResponse( - Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "2"); + Config::TestTypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "2"); test_server_->waitForCounterGe("runtime.load_success", initial_load_success_ + 2); expected_on_config_updated_count = ++current_on_config_updated_count; waitforOnConfigUpdatedCount(expected_on_config_updated_count); diff --git a/test/integration/xds_integration_test.cc b/test/integration/xds_integration_test.cc index afb04e806a062..7064e8f157e9c 100644 --- a/test/integration/xds_integration_test.cc +++ b/test/integration/xds_integration_test.cc @@ -941,27 +941,27 @@ TEST_P(XdsSotwMultipleAuthoritiesTest, SameResourceNameAndTypeFromMultipleAuthor // SDS for the first cluster. initXdsStream(getXdsUpstream1(), xds_connection_1_, xds_stream_1_); EXPECT_TRUE(compareSotwDiscoveryRequest( - /*expected_type_url=*/Config::TypeUrl::get().Secret, + /*expected_type_url=*/Config::TestTypeUrl::get().Secret, /*expected_version=*/"", /*expected_resource_names=*/{cert_name}, /*expect_node=*/true, Grpc::Status::WellKnownGrpcStatus::Ok, /*expected_error_message=*/"", xds_stream_1_.get())); auto sds_resource = getClientSecret(cert_name); sendSotwDiscoveryResponse( - Config::TypeUrl::get().Secret, {sds_resource}, "1", xds_stream_1_.get()); + Config::TestTypeUrl::get().Secret, {sds_resource}, "1", xds_stream_1_.get()); } { // SDS for the second cluster. initXdsStream(getXdsUpstream2(), xds_connection_2_, xds_stream_2_); EXPECT_TRUE(compareSotwDiscoveryRequest( - /*expected_type_url=*/Config::TypeUrl::get().Secret, + /*expected_type_url=*/Config::TestTypeUrl::get().Secret, /*expected_version=*/"", /*expected_resource_names=*/{cert_name}, /*expect_node=*/true, Grpc::Status::WellKnownGrpcStatus::Ok, /*expected_error_message=*/"", xds_stream_2_.get())); auto sds_resource = getClientSecret(cert_name); sendSotwDiscoveryResponse( - Config::TypeUrl::get().Secret, {sds_resource}, "1", xds_stream_2_.get()); + Config::TestTypeUrl::get().Secret, {sds_resource}, "1", xds_stream_2_.get()); } }; diff --git a/test/integration/xdstp_config_sources_integration.h b/test/integration/xdstp_config_sources_integration.h new file mode 100644 index 0000000000000..88ac3ee53b885 --- /dev/null +++ b/test/integration/xdstp_config_sources_integration.h @@ -0,0 +1,174 @@ +#pragma once + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/endpoint/v3/endpoint.pb.h" +#include "envoy/config/listener/v3/listener.pb.h" +#include "envoy/config/route/v3/route.pb.h" +#include "envoy/grpc/status.h" + +#include "source/common/config/protobuf_link_hacks.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" +#include "source/common/tls/server_context_config_impl.h" +#include "source/common/tls/server_ssl_socket.h" +#include "source/common/version/version.h" + +#include "test/common/grpc/grpc_client_integration.h" +#include "test/integration/ads_integration.h" +#include "test/integration/http_integration.h" +#include "test/integration/utility.h" +#include "test/test_common/network_utility.h" +#include "test/test_common/resources.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using testing::AssertionResult; + +namespace Envoy { + +// A base class for the xDS-TP based config sources (defined in the bootstrap) tests, without the +// ads_config definition. +class XdsTpConfigsIntegration : public AdsDeltaSotwIntegrationSubStateParamTest, + public HttpIntegrationTest { +public: + XdsTpConfigsIntegration() + : HttpIntegrationTest(Http::CodecType::HTTP2, ipVersion(), + ConfigHelper::httpProxyConfig(false)) { + config_helper_.addRuntimeOverride("envoy.reloadable_features.unified_mux", + (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw || + sotwOrDelta() == Grpc::SotwOrDelta::UnifiedDelta) + ? "true" + : "false"); + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions", "true"); + // Not using the normal xds upstream, but the + // authority1_upstream_/default_upstream. + create_xds_upstream_ = false; + // Not testing TLS in this case. + tls_xds_upstream_ = false; + sotw_or_delta_ = sotwOrDelta(); + setUpstreamProtocol(Http::CodecType::HTTP2); + } + + FakeUpstream* createAdsUpstream() { + ASSERT(!tls_xds_upstream_); + addFakeUpstream(Http::CodecType::HTTP2); + return fake_upstreams_.back().get(); + } + + void TearDown() override { + cleanupXdsConnection(authority1_xds_connection_); + cleanupXdsConnection(default_authority_xds_connection_); + } + + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + // An upstream for authority1 (H/2), an upstream for the default_authority (H/2), and an + // upstream for a backend (H/1). + authority1_upstream_ = createAdsUpstream(); + default_authority_upstream_ = createAdsUpstream(); + if (test_requires_additional_upstream_) { + addFakeUpstream(Http::CodecType::HTTP1); + } + } + + bool isSotw() const { + return sotwOrDelta() == Grpc::SotwOrDelta::Sotw || + sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw; + } + + // Adds config_source for authority1.com and a default_config_source for + // default_authority.com. + void initialize() override { + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // Add the first config_source. + { + auto* config_source1 = bootstrap.mutable_config_sources()->Add(); + config_source1->mutable_authorities()->Add()->set_name("authority1.com"); + auto* api_config_source = config_source1->mutable_api_config_source(); + api_config_source->set_api_type( + isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC + : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::V3); + api_config_source->set_set_node_on_first_message_only(true); + auto* grpc_service = api_config_source->add_grpc_services(); + setGrpcService(*grpc_service, "authority1_cluster", authority1_upstream_->localAddress()); + auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); + xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + xds_cluster->set_name("authority1_cluster"); + } + // Add the default config source. + { + auto* default_config_source = bootstrap.mutable_default_config_source(); + default_config_source->mutable_authorities()->Add()->set_name("default_authority.com"); + auto* api_config_source = default_config_source->mutable_api_config_source(); + api_config_source->set_api_type( + isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC + : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::V3); + api_config_source->set_set_node_on_first_message_only(true); + auto* grpc_service = api_config_source->add_grpc_services(); + setGrpcService(*grpc_service, "default_authority_cluster", + default_authority_upstream_->localAddress()); + auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); + xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + xds_cluster->set_name("default_authority_cluster"); + } + }); + HttpIntegrationTest::initialize(); + } + + void connectAuthority1() { + AssertionResult result = + authority1_upstream_->waitForHttpConnection(*dispatcher_, authority1_xds_connection_); + RELEASE_ASSERT(result, result.message()); + result = authority1_xds_connection_->waitForNewStream(*dispatcher_, authority1_xds_stream_); + RELEASE_ASSERT(result, result.message()); + authority1_xds_stream_->startGrpcStream(); + } + + void connectDefaultAuthority() { + AssertionResult result = default_authority_upstream_->waitForHttpConnection( + *dispatcher_, default_authority_xds_connection_); + RELEASE_ASSERT(result, result.message()); + result = default_authority_xds_connection_->waitForNewStream(*dispatcher_, + default_authority_xds_stream_); + RELEASE_ASSERT(result, result.message()); + default_authority_xds_stream_->startGrpcStream(); + } + + void cleanupXdsConnection(FakeHttpConnectionPtr& connection) { + if (connection != nullptr) { + AssertionResult result = connection->close(); + RELEASE_ASSERT(result, result.message()); + result = connection->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + connection.reset(); + } + } + + envoy::config::endpoint::v3::ClusterLoadAssignment + buildClusterLoadAssignment(const std::string& name) { + // The last fake upstream is the emulated server. + return ConfigHelper::buildClusterLoadAssignment( + name, Network::Test::getLoopbackAddressString(ipVersion()), + fake_upstreams_.back().get()->localAddress()->ip()->port()); + } + + bool test_requires_additional_upstream_{true}; + + // Data members that emulate the authority1 server. + FakeUpstream* authority1_upstream_; + FakeHttpConnectionPtr authority1_xds_connection_; + FakeStreamPtr authority1_xds_stream_; + + // Data members that emulate the default_authority server. + FakeUpstream* default_authority_upstream_; + FakeHttpConnectionPtr default_authority_xds_connection_; + FakeStreamPtr default_authority_xds_stream_; +}; + +} // namespace Envoy diff --git a/test/integration/xdstp_config_sources_integration_test.cc b/test/integration/xdstp_config_sources_integration_test.cc index 8ff2f3d6529e7..b17eb0713287b 100644 --- a/test/integration/xdstp_config_sources_integration_test.cc +++ b/test/integration/xdstp_config_sources_integration_test.cc @@ -14,10 +14,9 @@ #include "source/common/version/version.h" #include "test/common/grpc/grpc_client_integration.h" -#include "test/config/v2_link_hacks.h" -#include "test/integration/ads_integration.h" #include "test/integration/http_integration.h" #include "test/integration/utility.h" +#include "test/integration/xdstp_config_sources_integration.h" #include "test/test_common/network_utility.h" #include "test/test_common/resources.h" #include "test/test_common/utility.h" @@ -30,140 +29,9 @@ namespace Envoy { // Tests for xDS-TP based config sources (defined in the bootstrap), without the // ads_config definition. -class XdsTpConfigsIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, - public HttpIntegrationTest { +class XdsTpConfigsIntegrationTest : public XdsTpConfigsIntegration { public: - XdsTpConfigsIntegrationTest() - : HttpIntegrationTest(Http::CodecType::HTTP2, ipVersion(), - ConfigHelper::httpProxyConfig(false)) { - config_helper_.addRuntimeOverride("envoy.reloadable_features.unified_mux", - (sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw || - sotwOrDelta() == Grpc::SotwOrDelta::UnifiedDelta) - ? "true" - : "false"); - // Not using the normal xds upstream, but the - // authority1_upstream_/default_upstream. - create_xds_upstream_ = false; - // Not testing TLS in this case. - tls_xds_upstream_ = false; - sotw_or_delta_ = sotwOrDelta(); - setUpstreamProtocol(Http::CodecType::HTTP2); - } - - FakeUpstream* createAdsUpstream() { - ASSERT(!tls_xds_upstream_); - addFakeUpstream(Http::CodecType::HTTP2); - return fake_upstreams_.back().get(); - } - - void TearDown() override { - cleanupXdsConnection(authority1_xds_connection_); - cleanupXdsConnection(default_authority_xds_connection_); - } - - void createUpstreams() override { - HttpIntegrationTest::createUpstreams(); - // An upstream for authority1 (H/2), an upstream for the default_authority (H/2), and an - // upstream for a backend (H/1). - authority1_upstream_ = createAdsUpstream(); - default_authority_upstream_ = createAdsUpstream(); - addFakeUpstream(Http::CodecType::HTTP1); - } - - bool isSotw() const { - return sotwOrDelta() == Grpc::SotwOrDelta::Sotw || - sotwOrDelta() == Grpc::SotwOrDelta::UnifiedSotw; - } - - // Adds config_source for authority1.com and a default_config_source for - // default_authority.com. - void initialize() override { - config_helper_.addRuntimeOverride( - "envoy.reloadable_features.xdstp_based_config_singleton_subscriptions", "true"); - config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - // Add the first config_source. - { - auto* config_source1 = bootstrap.mutable_config_sources()->Add(); - config_source1->mutable_authorities()->Add()->set_name("authority1.com"); - auto* api_config_source = config_source1->mutable_api_config_source(); - api_config_source->set_api_type( - isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC - : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); - api_config_source->set_transport_api_version(envoy::config::core::v3::V3); - api_config_source->set_set_node_on_first_message_only(true); - auto* grpc_service = api_config_source->add_grpc_services(); - setGrpcService(*grpc_service, "authority1_cluster", authority1_upstream_->localAddress()); - auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); - xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - xds_cluster->set_name("authority1_cluster"); - } - // Add the default config source. - { - auto* default_config_source = bootstrap.mutable_default_config_source(); - default_config_source->mutable_authorities()->Add()->set_name("default_authority.com"); - auto* api_config_source = default_config_source->mutable_api_config_source(); - api_config_source->set_api_type( - isSotw() ? envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC - : envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); - api_config_source->set_transport_api_version(envoy::config::core::v3::V3); - api_config_source->set_set_node_on_first_message_only(true); - auto* grpc_service = api_config_source->add_grpc_services(); - setGrpcService(*grpc_service, "default_authority_cluster", - default_authority_upstream_->localAddress()); - auto* xds_cluster = bootstrap.mutable_static_resources()->add_clusters(); - xds_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); - xds_cluster->set_name("default_authority_cluster"); - } - }); - HttpIntegrationTest::initialize(); - } - - void connectAuthority1() { - AssertionResult result = - authority1_upstream_->waitForHttpConnection(*dispatcher_, authority1_xds_connection_); - RELEASE_ASSERT(result, result.message()); - result = authority1_xds_connection_->waitForNewStream(*dispatcher_, authority1_xds_stream_); - RELEASE_ASSERT(result, result.message()); - authority1_xds_stream_->startGrpcStream(); - } - - void connectDefaultAuthority() { - AssertionResult result = default_authority_upstream_->waitForHttpConnection( - *dispatcher_, default_authority_xds_connection_); - RELEASE_ASSERT(result, result.message()); - result = default_authority_xds_connection_->waitForNewStream(*dispatcher_, - default_authority_xds_stream_); - RELEASE_ASSERT(result, result.message()); - default_authority_xds_stream_->startGrpcStream(); - } - - void cleanupXdsConnection(FakeHttpConnectionPtr& connection) { - if (connection != nullptr) { - AssertionResult result = connection->close(); - RELEASE_ASSERT(result, result.message()); - result = connection->waitForDisconnect(); - RELEASE_ASSERT(result, result.message()); - connection.reset(); - } - } - - envoy::config::endpoint::v3::ClusterLoadAssignment - buildClusterLoadAssignment(const std::string& name) { - // The last fake upstream is the emulated server. - return ConfigHelper::buildClusterLoadAssignment( - name, Network::Test::getLoopbackAddressString(ipVersion()), - fake_upstreams_.back().get()->localAddress()->ip()->port()); - } - - // Data members that emulate the authority1 server. - FakeUpstream* authority1_upstream_; - FakeHttpConnectionPtr authority1_xds_connection_; - FakeStreamPtr authority1_xds_stream_; - - // Data members that emulate the default_authority server. - FakeUpstream* default_authority_upstream_; - FakeHttpConnectionPtr default_authority_xds_connection_; - FakeStreamPtr default_authority_xds_stream_; + XdsTpConfigsIntegrationTest() = default; }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, XdsTpConfigsIntegrationTest, @@ -207,12 +75,12 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigAuthority1) { // Authority1 should receive the EDS request. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1")}, @@ -223,7 +91,7 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigAuthority1) { // Expect an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "1", + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); }; @@ -274,7 +142,7 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigAuthority1Update) { // Authority1 should receive the EDS request. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); @@ -282,12 +150,12 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigAuthority1Update) { auto cla = buildClusterLoadAssignment( "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "1", {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "1", {}, authority1_xds_stream_.get()); // Expect an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "1", + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); }; @@ -312,12 +180,12 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigAuthority1Update) { // Send an update to the load-assignment. cla.mutable_endpoints(0)->mutable_locality()->set_sub_zone("new_sub_zone"); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "2", {}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {cla}, {cla}, {}, "2", {}, authority1_xds_stream_.get()); // Expect an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "2", + Config::TestTypeUrl::get().ClusterLoadAssignment, "2", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); @@ -365,14 +233,14 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigDefaultSource) { // Default Authority should receive the EDS request. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "", + Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1"}, {"xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1"}, {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", default_authority_xds_stream_.get())); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment( "xdstp://default_authority.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" "cluster1")}, @@ -383,7 +251,7 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigDefaultSource) { // Expect an EDS ACK. EXPECT_TRUE(compareDiscoveryRequest( - Config::TypeUrl::get().ClusterLoadAssignment, "1", + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1"}, {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", default_authority_xds_stream_.get())); @@ -397,6 +265,132 @@ TEST_P(XdsTpConfigsIntegrationTest, EdsOnlyConfigDefaultSource) { cleanupUpstreamAndDownstream(); } -// TODO(adisuissa): add a test that validates that two clusters with the same -// config source multiplex the request on the same stream. +// Validate that two clusters with the same source multiplex the request on the +// same stream. +TEST_P(XdsTpConfigsIntegrationTest, TwoClustersWithEdsOnlyConfigAuthority1) { + // Setup a static cluster that requires EDS from authority1. + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* static_resources = bootstrap.mutable_static_resources(); + + // Add 2 EDS clusters that will fetch endpoints from authority1. + static_resources->mutable_clusters()->Add()->CopyFrom( + TestUtility::parseYaml( + R"EOF( + name: xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1 + type: EDS + eds_cluster_config: {} + )EOF")); + static_resources->mutable_clusters()->Add()->CopyFrom( + TestUtility::parseYaml( + R"EOF( + name: xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster2 + type: EDS + eds_cluster_config: {} + )EOF")); + }); + + // Update the route to the xdstp-based cluster. + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->set_cluster("xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/" + "clusters/cluster1"); + }); + + // Envoy will request the endpoints of the cluster in the bootstrap during the + // initialization phase. This will make sure the xDS server answers with the + // correct assignment. + on_server_init_function_ = [this]() { + connectAuthority1(); + connectDefaultAuthority(); + + // Authority1 should receive the EDS request containing the 2 resources. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().ClusterLoadAssignment, "", + {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1", + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster2"}, + {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1", + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster2"}, + {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse( + Config::TestTypeUrl::get().ClusterLoadAssignment, + {buildClusterLoadAssignment( + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" + "cluster1"), + buildClusterLoadAssignment( + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" + "cluster2")}, + {buildClusterLoadAssignment( + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" + "cluster1"), + buildClusterLoadAssignment( + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/" + "cluster2")}, + {}, "1", {}, authority1_xds_stream_.get()); + + // Expect an EDS ACK. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().ClusterLoadAssignment, "1", + {"xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster1", + "xdstp://authority1.com/envoy.config.endpoint.v3.ClusterLoadAssignment/clusters/cluster2"}, + {}, {}, false, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + }; + + initialize(); + + test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); + EXPECT_EQ(5, test_server_->gauge("cluster_manager.active_clusters")->value()); + // Try to send a request and see that it reaches the backend (backend 3). + testRouterHeaderOnlyRequestAndResponse(nullptr, 3); + cleanupUpstreamAndDownstream(); +} + +// Validate that a bootstrap cluster that has an xds-tp based config RDS source +// works. +TEST_P(XdsTpConfigsIntegrationTest, RdsOnlyConfigAuthority1) { + test_requires_additional_upstream_ = false; + const std::string route_config_name = + "xdstp://authority1.com/envoy.config.route.v3.RouteConfiguration/my_routes/route1"; + // Set up the listener to point to an RDS resource (that will route to the + // the default cluster_0). + // Update the route to the xdstp-based cluster. + config_helper_.addConfigModifier( + [&route_config_name]( + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { hcm.mutable_rds()->set_route_config_name(route_config_name); }); + + // Envoy will request the routes of the listener in the bootstrap during the + // initialization phase. This will make sure the xDS server answers with the + // correct assignment. + on_server_init_function_ = [this, &route_config_name]() { + connectAuthority1(); + connectDefaultAuthority(); + + // Authority1 should receive the RDS request. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().RouteConfiguration, "", {route_config_name}, {route_config_name}, + {}, true, Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + sendDiscoveryResponse( + Config::TestTypeUrl::get().RouteConfiguration, + {ConfigHelper::buildRouteConfig(route_config_name, "cluster_0")}, + {ConfigHelper::buildRouteConfig(route_config_name, "cluster_0")}, {}, "1", {}, + authority1_xds_stream_.get()); + + // Expect an RDS ACK. + EXPECT_TRUE(compareDiscoveryRequest( + Config::TestTypeUrl::get().RouteConfiguration, "1", {route_config_name}, {}, {}, false, + Grpc::Status::WellKnownGrpcStatus::Ok, "", authority1_xds_stream_.get())); + }; + initialize(); + + test_server_->waitForCounterGe("listener_manager.listener_create_success", 1); + // Try to send a request and see that it reaches the backend (backend 0). + testRouterHeaderOnlyRequestAndResponse(nullptr); + cleanupUpstreamAndDownstream(); +} + } // namespace Envoy diff --git a/test/mocks/config/mocks.h b/test/mocks/config/mocks.h index f4cde563ec4aa..f4d3e60608de0 100644 --- a/test/mocks/config/mocks.h +++ b/test/mocks/config/mocks.h @@ -40,7 +40,7 @@ class MockOpaqueResourceDecoder : public OpaqueResourceDecoder { MockOpaqueResourceDecoder(); ~MockOpaqueResourceDecoder() override; - MOCK_METHOD(ProtobufTypes::MessagePtr, decodeResource, (const ProtobufWkt::Any& resource)); + MOCK_METHOD(ProtobufTypes::MessagePtr, decodeResource, (const Protobuf::Any& resource)); MOCK_METHOD(std::string, resourceName, (const Protobuf::Message& resource)); }; @@ -50,7 +50,7 @@ class MockUntypedConfigUpdateCallbacks : public UntypedConfigUpdateCallbacks { ~MockUntypedConfigUpdateCallbacks() override; MOCK_METHOD(void, onConfigUpdate, - (const Protobuf::RepeatedPtrField& resources, + (const Protobuf::RepeatedPtrField& resources, const std::string& version_info)); MOCK_METHOD(void, onConfigUpdate, @@ -137,8 +137,8 @@ class MockGrpcMux : public GrpcMux { MOCK_METHOD(EdsResourcesCacheOptRef, edsResourcesCache, ()); MOCK_METHOD(absl::Status, updateMuxSource, - (Grpc::RawAsyncClientPtr && primary_async_client, - Grpc::RawAsyncClientPtr&& failover_async_client, Stats::Scope& scope, + (Grpc::RawAsyncClientSharedPtr && primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, BackOffStrategyPtr&& backoff_strategy, const envoy::config::core::v3::ApiConfigSource& ads_config_source)); }; @@ -199,7 +199,7 @@ class MockContextProvider : public ContextProvider { MOCK_METHOD(Common::CallbackHandlePtr, addDynamicContextUpdateCallback, (UpdateNotificationCb callback), (const)); - Common::CallbackManager update_cb_handler_; + Common::CallbackManager update_cb_handler_; }; template diff --git a/test/mocks/config/xds_manager.h b/test/mocks/config/xds_manager.h index 11caa68147fd1..f38f96410f696 100644 --- a/test/mocks/config/xds_manager.h +++ b/test/mocks/config/xds_manager.h @@ -18,6 +18,8 @@ class MockXdsManager : public XdsManager { (const envoy::config::bootstrap::v3::Bootstrap& bootstrap, Upstream::ClusterManager* cm)); MOCK_METHOD(void, startXdstpAdsMuxes, ()); + MOCK_METHOD(ScopedResume, pause, (const std::string& type_url), (override)); + MOCK_METHOD(ScopedResume, pause, (const std::vector& type_urls), (override)); MOCK_METHOD(absl::StatusOr, subscribeToSingletonResource, (absl::string_view resource_name, OptRef config, diff --git a/test/mocks/filesystem/mocks.cc b/test/mocks/filesystem/mocks.cc index bd396f2aae2d9..085d0a5a09746 100644 --- a/test/mocks/filesystem/mocks.cc +++ b/test/mocks/filesystem/mocks.cc @@ -10,7 +10,7 @@ MockFile::MockFile() = default; MockFile::~MockFile() = default; Api::IoCallBoolResult MockFile::open(FlagSet flag) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); Api::IoCallBoolResult result = open_(flag); is_open_ = result.return_value_; @@ -20,7 +20,7 @@ Api::IoCallBoolResult MockFile::open(FlagSet flag) { } Api::IoCallSizeResult MockFile::write(absl::string_view buffer) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); if (!is_open_) { return {-1, Api::IoErrorPtr(nullptr, [](Api::IoError*) { PANIC("reached unexpected code"); })}; } @@ -32,7 +32,7 @@ Api::IoCallSizeResult MockFile::write(absl::string_view buffer) { } Api::IoCallSizeResult MockFile::pread(void* buf, uint64_t count, uint64_t offset) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); if (!is_open_) { return {-1, Api::IoErrorPtr(nullptr, [](Api::IoError*) { PANIC("reached unexpected code"); })}; } @@ -44,7 +44,7 @@ Api::IoCallSizeResult MockFile::pread(void* buf, uint64_t count, uint64_t offset } Api::IoCallSizeResult MockFile::pwrite(const void* buf, uint64_t count, uint64_t offset) { - absl::MutexLock lock(&mutex_); + absl::MutexLock lock(mutex_); if (!is_open_) { return {-1, Api::IoErrorPtr(nullptr, [](Api::IoError*) { PANIC("reached unexpected code"); })}; } diff --git a/test/mocks/grpc/mocks.cc b/test/mocks/grpc/mocks.cc index c375a51226c7f..4cd28643d1903 100644 --- a/test/mocks/grpc/mocks.cc +++ b/test/mocks/grpc/mocks.cc @@ -44,6 +44,9 @@ MockAsyncClientManager::MockAsyncClientManager() { .WillByDefault(Invoke([](const envoy::config::core::v3::GrpcService&, Stats::Scope&, bool) { return std::make_unique>(); })); + ON_CALL(*this, getOrCreateRawAsyncClientWithHashKey(_, _, _)).WillByDefault(Invoke([] { + return std::make_shared>(); + })); } MockAsyncClientManager::~MockAsyncClientManager() = default; diff --git a/test/mocks/grpc/mocks.h b/test/mocks/grpc/mocks.h index 2fa79082f5d86..fbfcfb44b1470 100644 --- a/test/mocks/grpc/mocks.h +++ b/test/mocks/grpc/mocks.h @@ -26,6 +26,7 @@ class MockAsyncRequest : public AsyncRequest { MOCK_METHOD(void, cancel, ()); MOCK_METHOD(const StreamInfo::StreamInfo&, streamInfo, (), (const)); + MOCK_METHOD(void, detach, ()); }; class MockAsyncStream : public RawAsyncStream { diff --git a/test/mocks/http/mocks.h b/test/mocks/http/mocks.h index b419480bd4c25..51ab250d9e6a6 100644 --- a/test/mocks/http/mocks.h +++ b/test/mocks/http/mocks.h @@ -141,7 +141,7 @@ class MockStreamCallbacks : public StreamCallbacks { class MockCodecEventCallbacks : public CodecEventCallbacks { public: MockCodecEventCallbacks(); - ~MockCodecEventCallbacks(); + ~MockCodecEventCallbacks() override; MOCK_METHOD(void, onCodecEncodeComplete, ()); MOCK_METHOD(void, onCodecLowLevelReset, ()); @@ -277,8 +277,8 @@ class MockStreamDecoderFilterCallbacks : public StreamDecoderFilterCallbacks, MOCK_METHOD(void, onDecoderFilterBelowWriteBufferLowWatermark, ()); MOCK_METHOD(void, addDownstreamWatermarkCallbacks, (DownstreamWatermarkCallbacks&)); MOCK_METHOD(void, removeDownstreamWatermarkCallbacks, (DownstreamWatermarkCallbacks&)); - MOCK_METHOD(void, setDecoderBufferLimit, (uint32_t)); - MOCK_METHOD(uint32_t, decoderBufferLimit, ()); + MOCK_METHOD(void, setDecoderBufferLimit, (uint64_t)); + MOCK_METHOD(uint64_t, decoderBufferLimit, ()); MOCK_METHOD(bool, recreateStream, (const ResponseHeaderMap* headers)); MOCK_METHOD(void, sendGoAwayAndClose, ()); MOCK_METHOD(void, addUpstreamSocketOptions, (const Network::Socket::OptionsSharedPtr& options)); @@ -343,7 +343,7 @@ class MockStreamDecoderFilterCallbacks : public StreamDecoderFilterCallbacks, MOCK_METHOD(bool, shouldLoadShed, (), (const)); Buffer::InstancePtr buffer_; - std::list callbacks_{}; + std::list callbacks_; testing::NiceMock downstream_callbacks_; testing::NiceMock active_span_; testing::NiceMock tracing_config_; @@ -652,6 +652,7 @@ class MockConnectionManagerConfig : public ConnectionManagerConfig { MOCK_METHOD(bool, http1SafeMaxConnectionDuration, (), (const)); MOCK_METHOD(absl::optional, maxStreamDuration, (), (const)); MOCK_METHOD(std::chrono::milliseconds, streamIdleTimeout, (), (const)); + MOCK_METHOD(absl::optional, streamFlushTimeout, (), (const)); MOCK_METHOD(std::chrono::milliseconds, requestTimeout, (), (const)); MOCK_METHOD(std::chrono::milliseconds, requestHeadersTimeout, (), (const)); MOCK_METHOD(std::chrono::milliseconds, delayedCloseTimeout, (), (const)); @@ -800,14 +801,6 @@ class HeaderValueOfMatcher { const testing::Matcher matcher_; }; -// Test that a HeaderMap argument contains exactly one header with the given -// key, whose value satisfies the given expectation. The expectation can be a -// matcher, or a string that the value should equal. -template HeaderValueOfMatcher HeaderValueOf(K key, const T& matcher) { - return HeaderValueOfMatcher(LowerCaseString(key), - testing::SafeMatcherCast(matcher)); -} - // Tests the provided Envoy HeaderMap for the provided HTTP status code. MATCHER_P(HttpStatusIs, expected_code, "") { const HeaderEntry* status = arg.Status(); @@ -987,16 +980,13 @@ MATCHER_P(HeaderMapEqualRef, rhs, "") { return equal; } -// Test that a HeaderMapPtr argument includes a given key-value pair, e.g., -// HeaderHasValue("Upgrade", "WebSocket") -template -testing::Matcher HeaderHasValue(K key, V value) { - return testing::Pointee(Http::HeaderValueOf(key, value)); -} - -// Like HeaderHasValue, but matches against a HeaderMap& argument. -template Http::HeaderValueOfMatcher HeaderHasValueRef(K key, V value) { - return Http::HeaderValueOf(key, value); +// Test that a HeaderMap& argument includes a given key-value pair, e.g., +// ContainsHeader("Upgrade", "WebSocket"). Key is case-insensitive. +// Value can be a matcher, e.g. +// ContainsHeader("Upgrade", HasSubstr("Socket")) +template Http::HeaderValueOfMatcher ContainsHeader(K key, V value) { + return Http::HeaderValueOfMatcher(Http::LowerCaseString(key), + testing::SafeMatcherCast(value)); } } // namespace Envoy diff --git a/test/mocks/http/mocks_test.cc b/test/mocks/http/mocks_test.cc index 9ed2d1b227a22..78209bb33a293 100644 --- a/test/mocks/http/mocks_test.cc +++ b/test/mocks/http/mocks_test.cc @@ -8,48 +8,6 @@ using ::testing::_; using ::testing::Not; namespace Http { -TEST(HeaderValueOfTest, ConstHeaderMap) { - const TestRequestHeaderMapImpl header_map{{"key", "expected value"}}; - - // Positive checks. - EXPECT_THAT(header_map, HeaderValueOf("key", "expected value")); - EXPECT_THAT(header_map, HeaderValueOf("key", _)); - - // Negative checks. - EXPECT_THAT(header_map, Not(HeaderValueOf("key", "other value"))); - EXPECT_THAT(header_map, Not(HeaderValueOf("other key", _))); -} - -TEST(HeaderValueOfTest, MutableHeaderMap) { - TestRequestHeaderMapImpl header_map; - - // Negative checks. - EXPECT_THAT(header_map, Not(HeaderValueOf("key", "other value"))); - EXPECT_THAT(header_map, Not(HeaderValueOf("other key", _))); - - header_map.addCopy("key", "expected value"); - - // Positive checks. - EXPECT_THAT(header_map, HeaderValueOf("key", "expected value")); - EXPECT_THAT(header_map, HeaderValueOf("key", _)); -} - -TEST(HeaderValueOfTest, LowerCaseString) { - TestRequestHeaderMapImpl header_map; - LowerCaseString key("key"); - LowerCaseString other_key("other_key"); - - // Negative checks. - EXPECT_THAT(header_map, Not(HeaderValueOf(key, "other value"))); - EXPECT_THAT(header_map, Not(HeaderValueOf(other_key, _))); - - header_map.addCopy(key, "expected value"); - header_map.addCopy(other_key, "ValUe"); - - // Positive checks. - EXPECT_THAT(header_map, HeaderValueOf(key, "expected value")); - EXPECT_THAT(header_map, HeaderValueOf(other_key, _)); -} TEST(HttpStatusIsTest, CheckStatus) { TestResponseHeaderMapImpl header_map; @@ -106,36 +64,47 @@ TEST(IsSupersetOfHeadersTest, MutableHeaderMap) { } } // namespace Http -TEST(HeaderHasValueRefTest, MutableValueRef) { - Http::TestRequestHeaderMapImpl header_map; - - EXPECT_THAT(header_map, Not(HeaderHasValueRef("key", "value"))); - EXPECT_THAT(header_map, Not(HeaderHasValueRef("other key", "value"))); +TEST(ContainsHeaderTest, ConstHeaderMap) { + const Http::TestRequestHeaderMapImpl header_map{{"key", "expected value"}}; - header_map.addCopy("key", "value"); + // Positive checks. + EXPECT_THAT(header_map, ContainsHeader("key", "expected value")); + EXPECT_THAT(header_map, ContainsHeader("key", _)); - EXPECT_THAT(header_map, HeaderHasValueRef("key", "value")); - EXPECT_THAT(header_map, Not(HeaderHasValueRef("key", "wrong value"))); + // Negative checks. + EXPECT_THAT(header_map, Not(ContainsHeader("key", "other value"))); + EXPECT_THAT(header_map, Not(ContainsHeader("other key", _))); } -TEST(HeaderHasValueRefTest, ConstValueRef) { - const Http::TestRequestHeaderMapImpl header_map{{"key", "expected value"}}; +TEST(ContainsHeaderTest, MutableHeaderMap) { + Http::TestRequestHeaderMapImpl header_map; + + // Negative checks. + EXPECT_THAT(header_map, Not(ContainsHeader("key", "other value"))); + EXPECT_THAT(header_map, Not(ContainsHeader("other key", _))); + + header_map.addCopy("key", "expected value"); - EXPECT_THAT(header_map, Not(HeaderHasValueRef("key", "other value"))); - EXPECT_THAT(header_map, HeaderHasValueRef("key", "expected value")); + // Positive checks. + EXPECT_THAT(header_map, ContainsHeader("key", "expected value")); + EXPECT_THAT(header_map, ContainsHeader("key", _)); } -TEST(HeaderHasValueRefTest, LowerCaseStringArguments) { - Http::LowerCaseString key("key"), other_key("other key"); +TEST(ContainsHeaderTest, LowerCaseStringArguments) { Http::TestRequestHeaderMapImpl header_map; + Http::LowerCaseString key("key"); + Http::LowerCaseString other_key("other_key"); - EXPECT_THAT(header_map, Not(HeaderHasValueRef(key, "value"))); - EXPECT_THAT(header_map, Not(HeaderHasValueRef(other_key, "value"))); + // Negative checks. + EXPECT_THAT(header_map, Not(ContainsHeader(key, "other value"))); + EXPECT_THAT(header_map, Not(ContainsHeader(other_key, _))); - header_map.addCopy(key, "value"); + header_map.addCopy(key, "expected value"); + header_map.addCopy(other_key, "ValUe"); - EXPECT_THAT(header_map, HeaderHasValueRef(key, "value")); - EXPECT_THAT(header_map, Not(HeaderHasValueRef(other_key, "wrong value"))); + // Positive checks. + EXPECT_THAT(header_map, ContainsHeader(key, "expected value")); + EXPECT_THAT(header_map, ContainsHeader(other_key, _)); } TEST(HeaderMatcherTest, OutputsActualHeadersOnMatchFailure) { diff --git a/test/mocks/http/stateful_session.h b/test/mocks/http/stateful_session.h index 362b34c2db304..846341f34137e 100644 --- a/test/mocks/http/stateful_session.h +++ b/test/mocks/http/stateful_session.h @@ -27,7 +27,7 @@ class MockSessionStateFactoryConfig : public Http::SessionStateFactoryConfig { MockSessionStateFactoryConfig(); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } MOCK_METHOD(SessionStateFactorySharedPtr, createSessionStateFactory, diff --git a/test/mocks/network/connection.h b/test/mocks/network/connection.h index 31ac1c806bf8f..0ae16ae6c83e7 100644 --- a/test/mocks/network/connection.h +++ b/test/mocks/network/connection.h @@ -85,6 +85,7 @@ class MockConnectionBase { MOCK_METHOD(void, setBufferLimits, (uint32_t limit)); \ MOCK_METHOD(uint32_t, bufferLimit, (), (const)); \ MOCK_METHOD(bool, aboveHighWatermark, (), (const)); \ + MOCK_METHOD(const ConnectionSocketPtr&, getSocket, (), (const)); \ MOCK_METHOD(const Network::ConnectionSocket::OptionsSharedPtr&, socketOptions, (), (const)); \ MOCK_METHOD(StreamInfo::StreamInfo&, streamInfo, ()); \ MOCK_METHOD(const StreamInfo::StreamInfo&, streamInfo, (), (const)); \ diff --git a/test/mocks/network/mocks.h b/test/mocks/network/mocks.h index 3674c6fe3a4e7..93ba6f2169ebb 100644 --- a/test/mocks/network/mocks.h +++ b/test/mocks/network/mocks.h @@ -273,6 +273,7 @@ class MockListenerFilter : public ListenerFilter { MOCK_METHOD(void, destroy_, ()); MOCK_METHOD(Network::FilterStatus, onAccept, (ListenerFilterCallbacks&)); MOCK_METHOD(Network::FilterStatus, onData, (Network::ListenerFilterBuffer&)); + MOCK_METHOD(void, onClose, ()); size_t listener_filter_max_read_bytes_{0}; }; @@ -435,8 +436,8 @@ class MockListenerFilterCallbacks : public ListenerFilterCallbacks { MOCK_METHOD(ConnectionSocket&, socket, ()); MOCK_METHOD(Event::Dispatcher&, dispatcher, ()); MOCK_METHOD(void, continueFilterChain, (bool)); - MOCK_METHOD(void, setDynamicMetadata, (const std::string&, const ProtobufWkt::Struct&)); - MOCK_METHOD(void, setDynamicTypedMetadata, (const std::string&, const ProtobufWkt::Any& value)); + MOCK_METHOD(void, setDynamicMetadata, (const std::string&, const Protobuf::Struct&)); + MOCK_METHOD(void, setDynamicTypedMetadata, (const std::string&, const Protobuf::Any& value)); MOCK_METHOD(envoy::config::core::v3::Metadata&, dynamicMetadata, ()); MOCK_METHOD(const envoy::config::core::v3::Metadata&, dynamicMetadata, (), (const)); MOCK_METHOD(StreamInfo::FilterState&, filterState, (), ()); @@ -587,6 +588,10 @@ class MockIp : public Address::Ip { MOCK_METHOD(const std::string&, addressAsString, (), (const)); MOCK_METHOD(bool, isAnyAddress, (), (const)); MOCK_METHOD(bool, isUnicastAddress, (), (const)); + MOCK_METHOD(bool, isLinkLocalAddress, (), (const)); + MOCK_METHOD(bool, isUniqueLocalAddress, (), (const)); + MOCK_METHOD(bool, isSiteLocalAddress, (), (const)); + MOCK_METHOD(bool, isTeredoAddress, (), (const)); MOCK_METHOD(const Address::Ipv4*, ipv4, (), (const)); MOCK_METHOD(const Address::Ipv6*, ipv6, (), (const)); MOCK_METHOD(uint32_t, port, (), (const)); diff --git a/test/mocks/router/mocks.cc b/test/mocks/router/mocks.cc index 53e44e30dde00..b976bc8ccf382 100644 --- a/test/mocks/router/mocks.cc +++ b/test/mocks/router/mocks.cc @@ -103,10 +103,9 @@ MockRouteEntry::MockRouteEntry() ON_CALL(*this, clusterName()).WillByDefault(ReturnRef(cluster_name_)); ON_CALL(*this, opaqueConfig()).WillByDefault(ReturnRef(opaque_config_)); ON_CALL(*this, rateLimitPolicy()).WillByDefault(ReturnRef(rate_limit_policy_)); - ON_CALL(*this, retryPolicy()).WillByDefault(ReturnRef(retry_policy_)); + ON_CALL(*this, retryPolicy()).WillByDefault(ReturnRef(base_retry_policy_)); ON_CALL(*this, internalRedirectPolicy()).WillByDefault(ReturnRef(internal_redirect_policy_)); - ON_CALL(*this, retryShadowBufferLimit()) - .WillByDefault(Return(std::numeric_limits::max())); + ON_CALL(*this, shadowPolicies()).WillByDefault(ReturnRef(shadow_policies_)); ON_CALL(*this, timeout()).WillByDefault(Return(std::chrono::milliseconds(10))); ON_CALL(*this, includeVirtualHostRateLimits()).WillByDefault(Return(true)); @@ -144,7 +143,12 @@ MockDecorator::MockDecorator() { } MockDecorator::~MockDecorator() = default; -MockRouteTracing::MockRouteTracing() = default; +MockRouteTracing::MockRouteTracing() { + ON_CALL(*this, getCustomTags()).WillByDefault(ReturnRef(custom_tags_)); + ON_CALL(*this, getClientSampling()).WillByDefault(ReturnRef(client_sampling_)); + ON_CALL(*this, getRandomSampling()).WillByDefault(ReturnRef(random_sampling_)); + ON_CALL(*this, getOverallSampling()).WillByDefault(ReturnRef(overall_sampling_)); +} MockRouteTracing::~MockRouteTracing() = default; MockRoute::MockRoute() { @@ -161,11 +165,10 @@ MockRoute::MockRoute() { ON_CALL(*this, clusterName()).WillByDefault(ReturnRef(route_entry_.cluster_name_)); ON_CALL(*this, opaqueConfig()).WillByDefault(ReturnRef(route_entry_.opaque_config_)); ON_CALL(*this, rateLimitPolicy()).WillByDefault(ReturnRef(route_entry_.rate_limit_policy_)); - ON_CALL(*this, retryPolicy()).WillByDefault(ReturnRef(route_entry_.retry_policy_)); + ON_CALL(*this, retryPolicy()).WillByDefault(ReturnRef(route_entry_.base_retry_policy_)); ON_CALL(*this, internalRedirectPolicy()) .WillByDefault(ReturnRef(route_entry_.internal_redirect_policy_)); - ON_CALL(*this, retryShadowBufferLimit()) - .WillByDefault(Return(std::numeric_limits::max())); + ON_CALL(*this, shadowPolicies()).WillByDefault(ReturnRef(route_entry_.shadow_policies_)); ON_CALL(*this, timeout()).WillByDefault(Return(std::chrono::milliseconds(10))); ON_CALL(*this, includeVirtualHostRateLimits()).WillByDefault(Return(true)); diff --git a/test/mocks/router/mocks.h b/test/mocks/router/mocks.h index ed0741ad25a82..47584ab4b4571 100644 --- a/test/mocks/router/mocks.h +++ b/test/mocks/router/mocks.h @@ -60,7 +60,8 @@ class MockDirectResponseEntry : public DirectResponseEntry { // DirectResponseEntry MOCK_METHOD(void, finalizeResponseHeaders, - (Http::ResponseHeaderMap & headers, const StreamInfo::StreamInfo& stream_info), + (Http::ResponseHeaderMap & headers, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info), (const)); MOCK_METHOD(Http::HeaderTransforms, responseHeaderTransforms, (const StreamInfo::StreamInfo& stream_info, bool do_formatting), (const)); @@ -119,6 +120,10 @@ class TestHedgePolicy : public HedgePolicy { class TestRetryPolicy : public RetryPolicy { public: + static std::shared_ptr create() { return std::make_shared(); } + static std::shared_ptr> createMock() { + return std::make_shared>(); + } TestRetryPolicy(); ~TestRetryPolicy() override; @@ -326,7 +331,7 @@ class MockVirtualHost : public VirtualHost { MOCK_METHOD(bool, includeIsTimeoutRetryHeader, (), (const)); MOCK_METHOD(Upstream::RetryPrioritySharedPtr, retryPriority, ()); MOCK_METHOD(Upstream::RetryHostPredicateSharedPtr, retryHostPredicate, ()); - MOCK_METHOD(uint32_t, retryShadowBufferLimit, (), (const)); + MOCK_METHOD(uint64_t, requestBodyBufferLimit, (), (const)); MOCK_METHOD(RouteSpecificFilterConfigs, perFilterConfigs, (absl::string_view), (const)); MOCK_METHOD(const envoy::config::core::v3::Metadata&, metadata, (), (const)); MOCK_METHOD(const Envoy::Config::TypedMetadata&, typedMetadata, (), (const)); @@ -368,7 +373,7 @@ class MockMetadataMatchCriteria : public MetadataMatchCriteria { // Router::MetadataMatchCriteria MOCK_METHOD(const std::vector&, metadataMatchCriteria, (), (const)); - MOCK_METHOD(MetadataMatchCriteriaConstPtr, mergeMatchCriteria, (const ProtobufWkt::Struct&), + MOCK_METHOD(MetadataMatchCriteriaConstPtr, mergeMatchCriteria, (const Protobuf::Struct&), (const)); MOCK_METHOD(MetadataMatchCriteriaConstPtr, filterMatchCriteria, (const std::set&), (const)); @@ -412,13 +417,14 @@ class MockRouteEntry : public RouteEntry { MOCK_METHOD(const std::string&, clusterName, (), (const)); MOCK_METHOD(Http::Code, clusterNotFoundResponseCode, (), (const)); MOCK_METHOD(void, finalizeRequestHeaders, - (Http::RequestHeaderMap & headers, const StreamInfo::StreamInfo& stream_info, - bool insert_envoy_original_path), + (Http::RequestHeaderMap & headers, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info, bool insert_envoy_original_path), (const)); MOCK_METHOD(Http::HeaderTransforms, requestHeaderTransforms, (const StreamInfo::StreamInfo& stream_info, bool do_formatting), (const)); MOCK_METHOD(void, finalizeResponseHeaders, - (Http::ResponseHeaderMap & headers, const StreamInfo::StreamInfo& stream_info), + (Http::ResponseHeaderMap & headers, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info), (const)); MOCK_METHOD(Http::HeaderTransforms, responseHeaderTransforms, (const StreamInfo::StreamInfo& stream_info, bool do_formatting), (const)); @@ -428,14 +434,15 @@ class MockRouteEntry : public RouteEntry { MOCK_METHOD(const Router::TlsContextMatchCriteria*, tlsContextMatchCriteria, (), (const)); MOCK_METHOD(Upstream::ResourcePriority, priority, (), (const)); MOCK_METHOD(const RateLimitPolicy&, rateLimitPolicy, (), (const)); - MOCK_METHOD(const RetryPolicy&, retryPolicy, (), (const)); + MOCK_METHOD(const RetryPolicyConstSharedPtr&, retryPolicy, (), (const)); MOCK_METHOD(const InternalRedirectPolicy&, internalRedirectPolicy, (), (const)); MOCK_METHOD(const PathMatcherSharedPtr&, pathMatcher, (), (const)); MOCK_METHOD(const PathRewriterSharedPtr&, pathRewriter, (), (const)); - MOCK_METHOD(uint32_t, retryShadowBufferLimit, (), (const)); + MOCK_METHOD(uint64_t, requestBodyBufferLimit, (), (const)); MOCK_METHOD(const std::vector&, shadowPolicies, (), (const)); MOCK_METHOD(std::chrono::milliseconds, timeout, (), (const)); MOCK_METHOD(absl::optional, idleTimeout, (), (const)); + MOCK_METHOD(absl::optional, flushTimeout, (), (const)); MOCK_METHOD(bool, usingNewTimeouts, (), (const)); MOCK_METHOD(absl::optional, maxStreamDuration, (), (const)); MOCK_METHOD(absl::optional, grpcTimeoutHeaderMax, (), (const)); @@ -461,7 +468,8 @@ class MockRouteEntry : public RouteEntry { std::string cluster_name_{"fake_cluster"}; std::multimap opaque_config_; - TestRetryPolicy retry_policy_; + std::shared_ptr retry_policy_ = TestRetryPolicy::create(); + RetryPolicyConstSharedPtr base_retry_policy_ = retry_policy_; testing::NiceMock internal_redirect_policy_; PathMatcherSharedPtr path_matcher_; PathRewriterSharedPtr path_rewriter_; @@ -501,6 +509,11 @@ class MockRouteTracing : public RouteTracing { MOCK_METHOD(const envoy::type::v3::FractionalPercent&, getRandomSampling, (), (const)); MOCK_METHOD(const envoy::type::v3::FractionalPercent&, getOverallSampling, (), (const)); MOCK_METHOD(const Tracing::CustomTagMap&, getCustomTags, (), (const)); + + envoy::type::v3::FractionalPercent client_sampling_; + envoy::type::v3::FractionalPercent random_sampling_; + envoy::type::v3::FractionalPercent overall_sampling_; + Tracing::CustomTagMap custom_tags_; }; class MockRoute : public RouteEntryAndRoute { @@ -527,13 +540,14 @@ class MockRoute : public RouteEntryAndRoute { MOCK_METHOD(const std::string&, clusterName, (), (const)); MOCK_METHOD(Http::Code, clusterNotFoundResponseCode, (), (const)); MOCK_METHOD(void, finalizeRequestHeaders, - (Http::RequestHeaderMap & headers, const StreamInfo::StreamInfo& stream_info, - bool insert_envoy_original_path), + (Http::RequestHeaderMap & headers, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info, bool insert_envoy_original_path), (const)); MOCK_METHOD(Http::HeaderTransforms, requestHeaderTransforms, (const StreamInfo::StreamInfo& stream_info, bool do_formatting), (const)); MOCK_METHOD(void, finalizeResponseHeaders, - (Http::ResponseHeaderMap & headers, const StreamInfo::StreamInfo& stream_info), + (Http::ResponseHeaderMap & headers, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info), (const)); MOCK_METHOD(Http::HeaderTransforms, responseHeaderTransforms, (const StreamInfo::StreamInfo& stream_info, bool do_formatting), (const)); @@ -543,14 +557,15 @@ class MockRoute : public RouteEntryAndRoute { MOCK_METHOD(const Router::TlsContextMatchCriteria*, tlsContextMatchCriteria, (), (const)); MOCK_METHOD(Upstream::ResourcePriority, priority, (), (const)); MOCK_METHOD(const RateLimitPolicy&, rateLimitPolicy, (), (const)); - MOCK_METHOD(const RetryPolicy&, retryPolicy, (), (const)); + MOCK_METHOD(const RetryPolicyConstSharedPtr&, retryPolicy, (), (const)); MOCK_METHOD(const InternalRedirectPolicy&, internalRedirectPolicy, (), (const)); MOCK_METHOD(const PathMatcherSharedPtr&, pathMatcher, (), (const)); MOCK_METHOD(const PathRewriterSharedPtr&, pathRewriter, (), (const)); - MOCK_METHOD(uint32_t, retryShadowBufferLimit, (), (const)); + MOCK_METHOD(uint64_t, requestBodyBufferLimit, (), (const)); MOCK_METHOD(const std::vector&, shadowPolicies, (), (const)); MOCK_METHOD(std::chrono::milliseconds, timeout, (), (const)); MOCK_METHOD(absl::optional, idleTimeout, (), (const)); + MOCK_METHOD(absl::optional, flushTimeout, (), (const)); MOCK_METHOD(bool, usingNewTimeouts, (), (const)); MOCK_METHOD(absl::optional, maxStreamDuration, (), (const)); MOCK_METHOD(absl::optional, grpcTimeoutHeaderMax, (), (const)); @@ -754,7 +769,7 @@ class MockClusterSpecifierPluginFactoryConfig : public ClusterSpecifierPluginFac Server::Configuration::ServerFactoryContext& context)); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.router.cluster_specifier_plugin.mock"; } diff --git a/test/mocks/router/router_filter_interface.h b/test/mocks/router/router_filter_interface.h index 1dffa1177ef9a..92be0f8c83a20 100644 --- a/test/mocks/router/router_filter_interface.h +++ b/test/mocks/router/router_filter_interface.h @@ -35,6 +35,8 @@ class MockRouterFilterInterface : public RouterFilterInterface { MOCK_METHOD(void, onPerTryTimeout, (UpstreamRequest & upstream_request)); MOCK_METHOD(void, onPerTryIdleTimeout, (UpstreamRequest & upstream_request)); MOCK_METHOD(void, onStreamMaxDurationReached, (UpstreamRequest & upstream_request)); + MOCK_METHOD(void, setupRouteTimeoutForWebsocketUpgrade, ()); + MOCK_METHOD(void, disableRouteTimeoutForWebsocketUpgrade, ()); MOCK_METHOD(Envoy::Http::StreamDecoderFilterCallbacks*, callbacks, ()); MOCK_METHOD(Upstream::ClusterInfoConstSharedPtr, cluster, ()); diff --git a/test/mocks/server/server_factory_context.h b/test/mocks/server/server_factory_context.h index 7221103be6359..059a50aaa9032 100644 --- a/test/mocks/server/server_factory_context.h +++ b/test/mocks/server/server_factory_context.h @@ -51,6 +51,7 @@ class MockStatsConfig : public virtual StatsConfig { MOCK_METHOD(bool, flushOnAdmin, (), (const)); MOCK_METHOD(const Stats::SinkPredicates*, sinkPredicates, (), (const)); MOCK_METHOD(bool, enableDeferredCreationStats, (), (const)); + MOCK_METHOD(uint32_t, evictOnFlush, (), (const)); }; class MockServerFactoryContext : public virtual ServerFactoryContext { diff --git a/test/mocks/server/tracer_factory.cc b/test/mocks/server/tracer_factory.cc index 7dcaa39f7ae18..99525eabed8ca 100644 --- a/test/mocks/server/tracer_factory.cc +++ b/test/mocks/server/tracer_factory.cc @@ -13,7 +13,7 @@ using ::testing::Invoke; MockTracerFactory::MockTracerFactory(const std::string& name) : name_(name) { ON_CALL(*this, createEmptyConfigProto()).WillByDefault(Invoke([] { - return std::make_unique(); + return std::make_unique(); })); } diff --git a/test/mocks/stats/mocks.h b/test/mocks/stats/mocks.h index 2b0d25435ca9c..e476b6bf75776 100644 --- a/test/mocks/stats/mocks.h +++ b/test/mocks/stats/mocks.h @@ -142,6 +142,7 @@ class MockCounter : public MockStatWithRefcount { MOCK_METHOD(uint64_t, latch, ()); MOCK_METHOD(void, reset, ()); MOCK_METHOD(bool, used, (), (const)); + MOCK_METHOD(void, markUnused, ()); MOCK_METHOD(bool, hidden, (), (const)); MOCK_METHOD(uint64_t, value, (), (const)); @@ -164,6 +165,7 @@ class MockGauge : public MockStatWithRefcount { MOCK_METHOD(void, sub, (uint64_t amount)); MOCK_METHOD(void, mergeImportMode, (ImportMode)); MOCK_METHOD(bool, used, (), (const)); + MOCK_METHOD(void, markUnused, ()); MOCK_METHOD(bool, hidden, (), (const)); MOCK_METHOD(uint64_t, value, (), (const)); MOCK_METHOD(absl::optional, cachedShouldImport, (), (const)); @@ -181,6 +183,7 @@ class MockHistogram : public MockMetric { ~MockHistogram() override; MOCK_METHOD(bool, used, (), (const)); + MOCK_METHOD(void, markUnused, ()); MOCK_METHOD(bool, hidden, (), (const)); MOCK_METHOD(Histogram::Unit, unit, (), (const)); MOCK_METHOD(void, recordValue, (uint64_t value)); @@ -206,6 +209,7 @@ class MockParentHistogram : public MockMetric { std::string quantileSummary() const override { return ""; }; std::string bucketSummary() const override { return ""; }; MOCK_METHOD(bool, used, (), (const)); + MOCK_METHOD(void, markUnused, ()); MOCK_METHOD(bool, hidden, (), (const)); MOCK_METHOD(Histogram::Unit, unit, (), (const)); MOCK_METHOD(void, recordValue, (uint64_t value)); @@ -292,10 +296,10 @@ class MockScope : public TestUtil::TestScope { public: MockScope(StatName prefix, MockStore& store); - ScopeSharedPtr createScope(const std::string& name) override { + ScopeSharedPtr createScope(const std::string& name, bool) override { return ScopeSharedPtr(createScope_(name)); } - ScopeSharedPtr scopeFromStatName(StatName name) override { + ScopeSharedPtr scopeFromStatName(StatName name, bool) override { return createScope_(symbolTable().toString(name)); } diff --git a/test/mocks/stream_info/mocks.h b/test/mocks/stream_info/mocks.h index 93927e4e74974..67bc08f87eb12 100644 --- a/test/mocks/stream_info/mocks.h +++ b/test/mocks/stream_info/mocks.h @@ -142,10 +142,10 @@ class MockStreamInfo : public StreamInfo { MOCK_METHOD(const Router::VirtualHostConstSharedPtr&, virtualHost, (), (const)); MOCK_METHOD(envoy::config::core::v3::Metadata&, dynamicMetadata, ()); MOCK_METHOD(const envoy::config::core::v3::Metadata&, dynamicMetadata, (), (const)); - MOCK_METHOD(void, setDynamicMetadata, (const std::string&, const ProtobufWkt::Struct&)); + MOCK_METHOD(void, setDynamicMetadata, (const std::string&, const Protobuf::Struct&)); MOCK_METHOD(void, setDynamicMetadata, (const std::string&, const std::string&, const std::string&)); - MOCK_METHOD(void, setDynamicTypedMetadata, (const std::string&, const ProtobufWkt::Any& value)); + MOCK_METHOD(void, setDynamicTypedMetadata, (const std::string&, const Protobuf::Any& value)); MOCK_METHOD(const FilterStateSharedPtr&, filterState, ()); MOCK_METHOD(const FilterState&, filterState, (), (const)); MOCK_METHOD(void, setRequestHeaders, (const Http::RequestHeaderMap&)); diff --git a/test/mocks/tracing/mocks.cc b/test/mocks/tracing/mocks.cc index 22987c53152d2..02c8b7f26680d 100644 --- a/test/mocks/tracing/mocks.cc +++ b/test/mocks/tracing/mocks.cc @@ -14,7 +14,6 @@ MockSpan::~MockSpan() = default; MockConfig::MockConfig() { ON_CALL(*this, operationName()).WillByDefault(ReturnPointee(&operation_name_)); - ON_CALL(*this, customTags()).WillByDefault(Return(&custom_tags_)); ON_CALL(*this, verbose()).WillByDefault(ReturnPointee(&verbose_)); ON_CALL(*this, maxPathTagLength()).WillByDefault(Return(uint32_t(256))); ON_CALL(*this, spawnUpstreamSpan()).WillByDefault(ReturnPointee(&spawn_upstream_span_)); diff --git a/test/mocks/tracing/mocks.h b/test/mocks/tracing/mocks.h index 51b0fbc6c83e0..b2bc492a1bb29 100644 --- a/test/mocks/tracing/mocks.h +++ b/test/mocks/tracing/mocks.h @@ -18,13 +18,12 @@ class MockConfig : public Config { ~MockConfig() override; MOCK_METHOD(OperationName, operationName, (), (const)); - MOCK_METHOD(const CustomTagMap*, customTags, (), (const)); + MOCK_METHOD(void, modifySpan, (Span&), (const)); MOCK_METHOD(bool, verbose, (), (const)); MOCK_METHOD(uint32_t, maxPathTagLength, (), (const)); MOCK_METHOD(bool, spawnUpstreamSpan, (), (const)); OperationName operation_name_{OperationName::Ingress}; - CustomTagMap custom_tags_; bool verbose_{false}; bool spawn_upstream_span_{false}; }; @@ -40,7 +39,8 @@ class MockSpan : public Span { MOCK_METHOD(void, finishSpan, ()); MOCK_METHOD(void, injectContext, (Tracing::TraceContext & request_headers, const Tracing::UpstreamContext& upstream)); - MOCK_METHOD(void, setSampled, (const bool sampled)); + MOCK_METHOD(void, setSampled, (bool sampled)); + MOCK_METHOD(bool, useLocalDecision, (), (const)); MOCK_METHOD(void, setBaggage, (absl::string_view key, absl::string_view value)); MOCK_METHOD(std::string, getBaggage, (absl::string_view key)); MOCK_METHOD(std::string, getTraceId, (), (const)); diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index bf422346aedd2..1971ebe9d2756 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -71,7 +71,7 @@ class MockUpstreamLocalAddressSelectorFactory : public UpstreamLocalAddressSelec (const)); ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "mock.upstream.local.address.selector"; } diff --git a/test/mocks/upstream/cluster_manager.h b/test/mocks/upstream/cluster_manager.h index 92db8e71ad696..6dcb21b32a723 100644 --- a/test/mocks/upstream/cluster_manager.h +++ b/test/mocks/upstream/cluster_manager.h @@ -84,7 +84,9 @@ class MockClusterManager : public ClusterManager { } MOCK_METHOD(void, drainConnections, (const std::string& cluster, DrainConnectionsHostPredicate predicate)); - MOCK_METHOD(void, drainConnections, (DrainConnectionsHostPredicate predicate)); + MOCK_METHOD(void, drainConnections, + (DrainConnectionsHostPredicate predicate, + ConnectionPool::DrainBehavior drain_behavior)); MOCK_METHOD(absl::Status, checkActiveStaticCluster, (const std::string& cluster)); MOCK_METHOD(absl::StatusOr, allocateOdCdsApi, (OdCdsCreationFunction creation_function, diff --git a/test/mocks/upstream/cluster_manager_factory.h b/test/mocks/upstream/cluster_manager_factory.h index fc8aaec38f74b..a11f5101b3a8d 100644 --- a/test/mocks/upstream/cluster_manager_factory.h +++ b/test/mocks/upstream/cluster_manager_factory.h @@ -46,7 +46,8 @@ class MockClusterManagerFactory : public ClusterManagerFactory { MOCK_METHOD(absl::StatusOr, createCds, (const envoy::config::core::v3::ConfigSource& cds_config, - const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm)); + const xds::core::v3::ResourceLocator* cds_resources_locator, ClusterManager& cm, + bool support_multi_ads_sources)); }; } // namespace Upstream diff --git a/test/mocks/upstream/host_set.h b/test/mocks/upstream/host_set.h index 86c313d22f882..4e1d72dfe40b0 100644 --- a/test/mocks/upstream/host_set.h +++ b/test/mocks/upstream/host_set.h @@ -17,7 +17,7 @@ class MockHostSet : public HostSet { ~MockHostSet() override; void runCallbacks(const HostVector added, const HostVector removed) { - THROW_IF_NOT_OK(member_update_cb_helper_.runCallbacks(priority(), added, removed)); + member_update_cb_helper_.runCallbacks(priority(), added, removed); } ABSL_MUST_USE_RESULT Common::CallbackHandlePtr @@ -43,8 +43,6 @@ class MockHostSet : public HostSet { MOCK_METHOD(const HostsPerLocality&, excludedHostsPerLocality, (), (const)); MOCK_METHOD(HostsPerLocalityConstSharedPtr, excludedHostsPerLocalityPtr, (), (const)); MOCK_METHOD(LocalityWeightsConstSharedPtr, localityWeights, (), (const)); - MOCK_METHOD(absl::optional, chooseHealthyLocality, ()); - MOCK_METHOD(absl::optional, chooseDegradedLocality, ()); MOCK_METHOD(uint32_t, priority, (), (const)); uint32_t overprovisioningFactor() const override { return overprovisioning_factor_; } void setOverprovisioningFactor(const uint32_t overprovisioning_factor) { @@ -61,7 +59,8 @@ class MockHostSet : public HostSet { HostsPerLocalitySharedPtr degraded_hosts_per_locality_{new HostsPerLocalityImpl()}; HostsPerLocalitySharedPtr excluded_hosts_per_locality_{new HostsPerLocalityImpl()}; LocalityWeightsConstSharedPtr locality_weights_{{}}; - Common::CallbackManager member_update_cb_helper_; + Common::CallbackManager + member_update_cb_helper_; uint32_t priority_{}; uint32_t overprovisioning_factor_{}; bool weighted_priority_health_{false}; diff --git a/test/mocks/upstream/priority_set.cc b/test/mocks/upstream/priority_set.cc index 4da1d13f3bae3..2182a7a3c80c2 100644 --- a/test/mocks/upstream/priority_set.cc +++ b/test/mocks/upstream/priority_set.cc @@ -49,8 +49,8 @@ HostSet& MockPrioritySet::getHostSet(uint32_t priority) { void MockPrioritySet::runUpdateCallbacks(uint32_t priority, const HostVector& hosts_added, const HostVector& hosts_removed) { - THROW_IF_NOT_OK(member_update_cb_helper_.runCallbacks(hosts_added, hosts_removed)); - THROW_IF_NOT_OK(priority_update_cb_helper_.runCallbacks(priority, hosts_added, hosts_removed)); + member_update_cb_helper_.runCallbacks(hosts_added, hosts_removed); + priority_update_cb_helper_.runCallbacks(priority, hosts_added, hosts_removed); } } // namespace Upstream diff --git a/test/mocks/upstream/priority_set.h b/test/mocks/upstream/priority_set.h index b7ba1d2019706..9b2b3100d524a 100644 --- a/test/mocks/upstream/priority_set.h +++ b/test/mocks/upstream/priority_set.h @@ -25,8 +25,7 @@ class MockPrioritySet : public PrioritySet { MOCK_METHOD(void, updateHosts, (uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, - const HostVector& hosts_removed, uint64_t seed, - absl::optional weighted_priority_health, + const HostVector& hosts_removed, absl::optional weighted_priority_health, absl::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map)); MOCK_METHOD(void, batchHostUpdate, (BatchUpdateCb&)); @@ -39,8 +38,8 @@ class MockPrioritySet : public PrioritySet { std::vector host_sets_; std::vector member_update_cbs_; - Common::CallbackManager member_update_cb_helper_; - Common::CallbackManager + Common::CallbackManager member_update_cb_helper_; + Common::CallbackManager priority_update_cb_helper_; HostMapConstSharedPtr cross_priority_host_map_{std::make_shared()}; diff --git a/test/mocks/upstream/retry_priority_factory.h b/test/mocks/upstream/retry_priority_factory.h index 158359c22a482..097bd23642a3b 100644 --- a/test/mocks/upstream/retry_priority_factory.h +++ b/test/mocks/upstream/retry_priority_factory.h @@ -23,7 +23,7 @@ class MockRetryPriorityFactory : public RetryPriorityFactory { ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } private: diff --git a/test/mocks/upstream/test_retry_host_predicate_factory.h b/test/mocks/upstream/test_retry_host_predicate_factory.h index b436ae01bcacd..f3ab3228b4f09 100644 --- a/test/mocks/upstream/test_retry_host_predicate_factory.h +++ b/test/mocks/upstream/test_retry_host_predicate_factory.h @@ -19,7 +19,7 @@ class TestRetryHostPredicateFactory : public RetryHostPredicateFactory { ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } }; } // namespace Upstream diff --git a/test/mocks/upstream/typed_load_balancer_factory.h b/test/mocks/upstream/typed_load_balancer_factory.h index a0b9b2b4daed4..1c2c75dd8855c 100644 --- a/test/mocks/upstream/typed_load_balancer_factory.h +++ b/test/mocks/upstream/typed_load_balancer_factory.h @@ -34,7 +34,7 @@ class MockTypedLoadBalancerFactory : public TypedLoadBalancerFactory { ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } }; } // namespace Upstream diff --git a/test/server/BUILD b/test/server/BUILD index b597bb661b0be..b26802f61a59d 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -69,7 +69,6 @@ envoy_cc_test( "//source/extensions/transport_sockets/raw_buffer:config", "//source/server:configuration_lib", "//test/common/upstream:utility_lib", - "//test/mocks:common_lib", "//test/mocks/network:network_mocks", "//test/mocks/server:factory_context_mocks", "//test/mocks/server:instance_mocks", @@ -361,9 +360,10 @@ envoy_cc_test( ], rbe_pool = "6gig", deps = [ + "//source/common/common:notification_lib", "//source/common/version:version_lib", "//source/extensions/access_loggers/file:config", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/filters/http/buffer:config", "//source/extensions/filters/http/grpc_http1_bridge:config", "//source/extensions/filters/http/health_check:config", @@ -378,6 +378,7 @@ envoy_cc_test( "//test/config:v2_link_hacks", "//test/integration:integration_lib", "//test/mocks/api:api_mocks", + "//test/mocks/config:xds_manager_mocks", "//test/mocks/server:bootstrap_extension_factory_mocks", "//test/mocks/server:fatal_action_factory_mocks", "//test/mocks/server:hot_restart_mocks", diff --git a/test/server/admin/admin_test.cc b/test/server/admin/admin_test.cc index d36dbd2b96556..572b30d17a431 100644 --- a/test/server/admin/admin_test.cc +++ b/test/server/admin/admin_test.cc @@ -148,7 +148,7 @@ TEST_P(AdminInstanceTest, Help) { /clusters: upstream cluster status /config_dump: dump current Envoy configs (experimental) resource: The resource to dump - mask: The mask to apply. When both resource and mask are specified, the mask is applied to every element in the desired repeated field so that only a subset of fields are returned. The mask is parsed as a ProtobufWkt::FieldMask + mask: The mask to apply. When both resource and mask are specified, the mask is applied to every element in the desired repeated field so that only a subset of fields are returned. The mask is parsed as a Protobuf::FieldMask name_regex: Dump only the currently loaded configurations whose names match the specified regex. Can be used with both resource and mask query parameters. include_eds: Dump currently loaded configuration including EDS. See the response definition for more information /contention: dump current Envoy mutex contention stats (if enabled) @@ -166,7 +166,7 @@ TEST_P(AdminInstanceTest, Help) { /help: print out list of admin commands /hot_restart_version: print the hot restart compatibility version /init_dump: dump current Envoy init manager information (experimental) - mask: The desired component to dump unready targets. The mask is parsed as a ProtobufWkt::FieldMask. For example, get the unready targets of all listeners with /init_dump?mask=listener` + mask: The desired component to dump unready targets. The mask is parsed as a Protobuf::FieldMask. For example, get the unready targets of all listeners with /init_dump?mask=listener` /listeners: print listener info format: File format to use; One of (text, json) /logging (POST): query/change logging levels diff --git a/test/server/admin/config_dump_handler_test.cc b/test/server/admin/config_dump_handler_test.cc index ecf381ea28124..85fe1e3636328 100644 --- a/test/server/admin/config_dump_handler_test.cc +++ b/test/server/admin/config_dump_handler_test.cc @@ -53,7 +53,7 @@ TEST_P(AdminInstanceTest, ConfigDump) { Buffer::OwnedImpl response2; Http::TestResponseHeaderMapImpl header_map; auto entry = admin_.getConfigTracker().add("foo", [](const Matchers::StringMatcher&) { - auto msg = std::make_unique(); + auto msg = std::make_unique(); msg->set_value("bar"); return msg; }); @@ -77,24 +77,24 @@ TEST_P(AdminInstanceTest, ConfigDumpMaintainsOrder) { // Add configs in random order and validate config_dump dumps in the order. auto bootstrap_entry = admin_.getConfigTracker().add("bootstrap", [](const Matchers::StringMatcher&) { - auto msg = std::make_unique(); + auto msg = std::make_unique(); msg->set_value("bootstrap_config"); return msg; }); auto route_entry = admin_.getConfigTracker().add("routes", [](const Matchers::StringMatcher&) { - auto msg = std::make_unique(); + auto msg = std::make_unique(); msg->set_value("routes_config"); return msg; }); auto listener_entry = admin_.getConfigTracker().add("listeners", [](const Matchers::StringMatcher&) { - auto msg = std::make_unique(); + auto msg = std::make_unique(); msg->set_value("listeners_config"); return msg; }); auto cluster_entry = admin_.getConfigTracker().add("clusters", [](const Matchers::StringMatcher&) { - auto msg = std::make_unique(); + auto msg = std::make_unique(); msg->set_value("clusters_config"); return msg; }); @@ -723,7 +723,7 @@ TEST_P(AdminInstanceTest, ConfigDumpNonExistentResource) { Buffer::OwnedImpl response; Http::TestResponseHeaderMapImpl header_map; auto listeners = admin_.getConfigTracker().add("listeners", [](const Matchers::StringMatcher&) { - auto msg = std::make_unique(); + auto msg = std::make_unique(); msg->set_value("listeners_config"); return msg; }); diff --git a/test/server/admin/config_tracker_impl_test.cc b/test/server/admin/config_tracker_impl_test.cc index 3f0f994fe12f4..a664ddb704419 100644 --- a/test/server/admin/config_tracker_impl_test.cc +++ b/test/server/admin/config_tracker_impl_test.cc @@ -18,7 +18,7 @@ class ConfigTrackerImplTest : public testing::Test { }; } - ProtobufTypes::MessagePtr testMsg() { return std::make_unique(); } + ProtobufTypes::MessagePtr testMsg() { return std::make_unique(); } ~ConfigTrackerImplTest() override = default; diff --git a/test/server/admin/stats_handler_test.cc b/test/server/admin/stats_handler_test.cc index 5383c3d54b943..2968850167803 100644 --- a/test/server/admin/stats_handler_test.cc +++ b/test/server/admin/stats_handler_test.cc @@ -43,13 +43,17 @@ class StatsHandlerTest { } // Set buckets for tests. - void setHistogramBucketSettings(const std::string& prefix, const std::vector& buckets) { + void setHistogramBucketSettings(const std::string& prefix, const std::vector& buckets, + absl::optional bins) { envoy::config::metrics::v3::StatsConfig config; auto& bucket_settings = *config.mutable_histogram_bucket_settings(); envoy::config::metrics::v3::HistogramBucketSettings setting; setting.mutable_match()->set_prefix(prefix); setting.mutable_buckets()->Add(buckets.begin(), buckets.end()); + if (bins) { + setting.mutable_bins()->set_value(*bins); + } bucket_settings.Add(std::move(setting)); store_->setHistogramSettings(std::make_unique(config, context_)); @@ -332,7 +336,7 @@ TEST_F(AdminStatsTest, HandlerStatsJsonNoHistograms) { TEST_F(AdminStatsFilterTest, HandlerStatsJsonHistogramBucketsCumulative) { const std::string url = "/stats?histogram_buckets=cumulative&format=json"; // Set h as prefix to match both histograms. - setHistogramBucketSettings("h", {1, 2, 3, 4}); + setHistogramBucketSettings("h", {1, 2, 3, 4}, {}); Stats::Counter& c1 = store_->counterFromString("c1"); @@ -488,7 +492,7 @@ TEST_F(AdminStatsFilterTest, HandlerStatsHiddenInvalid) { TEST_F(AdminStatsFilterTest, HandlerStatsJsonHistogramBucketsDisjoint) { const std::string url = "/stats?histogram_buckets=disjoint&format=json"; // Set h as prefix to match both histograms. - setHistogramBucketSettings("h", {1, 2, 3, 4}); + setHistogramBucketSettings("h", {1, 2, 3, 4}, 1); Stats::Counter& c1 = store_->counterFromString("c1"); @@ -1252,7 +1256,7 @@ class ThreadedTest : public testing::Test { for (uint32_t s = 0; s < NumScopes; ++s) { Stats::ScopeSharedPtr scope = store_->rootScope()->scopeFromStatName(scope_names_[s]); { - absl::MutexLock lock(&scope_mutexes_[s]); + absl::MutexLock lock(scope_mutexes_[s]); scopes_[s] = scope; } for (Stats::StatName counter_name : counter_names_) { diff --git a/test/server/api_listener_test.cc b/test/server/api_listener_test.cc index 9d1bd32235687..fc2b19b42c556 100644 --- a/test/server/api_listener_test.cc +++ b/test/server/api_listener_test.cc @@ -6,6 +6,7 @@ #include "source/extensions/api_listeners/default_api_listener/api_listener_impl.h" +#include "test/mocks/http/stream_encoder.h" #include "test/mocks/network/mocks.h" #include "test/mocks/server/instance.h" #include "test/mocks/server/listener_component_factory.h" @@ -122,7 +123,7 @@ name: test_api_listener const envoy::config::listener::v3::Listener config = parseListenerFromV3Yaml(yaml); - ProtobufWkt::Any expected_any_proto; + Protobuf::Any expected_any_proto; envoy::config::cluster::v3::Cluster expected_cluster_proto; expected_cluster_proto.set_name("cluster1"); expected_cluster_proto.set_type(envoy::config::cluster::v3::Cluster::EDS); @@ -238,6 +239,242 @@ name: test_api_listener EXPECT_ENVOY_BUG(connection.enableHalfClose(true), "Unexpected function call"); EXPECT_ENVOY_BUG(connection.isHalfCloseEnabled(), "Unexpected function call"); + + // Validate methods updated in SyntheticConnection. + EXPECT_DEATH(connection.getSocket(), "not implemented"); +} + +// Exercise SyntheticReadCallbacks unimplemented methods and PANIC behavior for socket(). +TEST_F(ApiListenerTest, SyntheticReadCallbacksUnimplementedMethods) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV3Yaml(yaml); + server_.server_factory_context_->cluster_manager_.initializeClusters( + {"dynamic_forward_proxy_cluster"}, {}); + HttpApiListenerFactory factory; + auto http_api_listener = factory.create(config, server_, config.name()).value(); + auto api_listener = http_api_listener->createHttpApiListener(server_.dispatcher()); + ASSERT_NE(api_listener, nullptr); + + // Access SyntheticReadCallbacks through the wrapper. + auto* wrapper = dynamic_cast(api_listener.get()); + ASSERT_NE(wrapper, nullptr); + auto& read_callbacks = wrapper->readCallbacks(); + + Buffer::OwnedImpl dummy_buffer("x"); + EXPECT_ENVOY_BUG(read_callbacks.continueReading(), "Unexpected call to continueReading"); + EXPECT_ENVOY_BUG(read_callbacks.injectReadDataToFilterChain(dummy_buffer, false), + "Unexpected call to injectReadDataToFilterChain"); + EXPECT_ENVOY_BUG(read_callbacks.disableClose(true), "Unexpected call to disableClose"); + EXPECT_ENVOY_BUG(read_callbacks.startUpstreamSecureTransport(), + "Unexpected call to startUpstreamSecureTransport"); + EXPECT_EQ(read_callbacks.upstreamHost(), nullptr); + EXPECT_ENVOY_BUG(read_callbacks.upstreamHost(nullptr), "Unexpected call to upstreamHost"); + EXPECT_DEATH(read_callbacks.socket(), "not implemented"); +} + +// Test the new socket management methods added to Network::Connection interface +TEST_F(ApiListenerTest, SyntheticConnectionSocketMethods) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV3Yaml(yaml); + server_.server_factory_context_->cluster_manager_.initializeClusters( + {"dynamic_forward_proxy_cluster"}, {}); + HttpApiListenerFactory factory; + auto http_api_listener = factory.create(config, server_, config.name()).value(); + + auto api_listener = http_api_listener->createHttpApiListener(server_.dispatcher()); + ASSERT_NE(api_listener, nullptr); + auto& connection = dynamic_cast(api_listener.get()) + ->readCallbacks() + .connection(); + + // Test getSocket() - should PANIC for SyntheticConnection + EXPECT_DEATH(connection.getSocket(), "not implemented"); +} + +// Verify base address access and drain decision behavior. +TEST_F(ApiListenerTest, NewStreamHandleReturnsDecoderHandle) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV3Yaml(yaml); + server_.server_factory_context_->cluster_manager_.initializeClusters( + {"dynamic_forward_proxy_cluster"}, {}); + + HttpApiListenerFactory factory; + auto http_api_listener = factory.create(config, server_, config.name()).value(); + auto api_listener = http_api_listener->createHttpApiListener(server_.dispatcher()); + ASSERT_NE(api_listener, nullptr); + + testing::NiceMock response_encoder; + ON_CALL(response_encoder, getStream()) + .WillByDefault(testing::ReturnRef(response_encoder.stream_)); + ON_CALL(response_encoder, setRequestDecoder(testing::_)).WillByDefault(testing::Return()); + + auto decoder_handle = api_listener->newStreamHandle(response_encoder); + ASSERT_NE(decoder_handle, nullptr); + + // Tear down in safe order. + decoder_handle.reset(); + api_listener.reset(); +} + +// Removing callbacks prevents receiving RemoteClose on ApiListenerWrapper destruction. +TEST_F(ApiListenerTest, NoEventAfterCallbackRemovalOnShutdown) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV3Yaml(yaml); + server_.server_factory_context_->cluster_manager_.initializeClusters( + {"dynamic_forward_proxy_cluster"}, {}); + + HttpApiListenerFactory factory; + auto http_api_listener = factory.create(config, server_, config.name()).value(); + auto api_listener = http_api_listener->createHttpApiListener(server_.dispatcher()); + ASSERT_NE(api_listener, nullptr); + + Network::MockConnectionCallbacks network_connection_callbacks; + auto& connection = dynamic_cast(api_listener.get()) + ->readCallbacks() + .connection(); + connection.addConnectionCallbacks(network_connection_callbacks); + connection.removeConnectionCallbacks(network_connection_callbacks); + + EXPECT_CALL(network_connection_callbacks, onEvent(testing::_)).Times(0); + api_listener.reset(); +} + +// Test the new socket management methods added to Network::Connection interface +TEST_F(ApiListenerTest, SyntheticConnectionSocketMethods) { + const std::string yaml = R"EOF( +name: test_api_listener +address: + socket_address: + address: 127.0.0.1 + port_value: 1234 +api_listener: + api_listener: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + )EOF"; + + const envoy::config::listener::v3::Listener config = parseListenerFromV3Yaml(yaml); + server_.server_factory_context_->cluster_manager_.initializeClusters( + {"dynamic_forward_proxy_cluster"}, {}); + HttpApiListenerFactory factory; + auto http_api_listener = factory.create(config, server_, config.name()).value(); + + auto api_listener = http_api_listener->createHttpApiListener(server_.dispatcher()); + ASSERT_NE(api_listener, nullptr); + auto& connection = dynamic_cast(api_listener.get()) + ->readCallbacks() + .connection(); + + // Test getSocket() - should PANIC for SyntheticConnection + EXPECT_DEATH(connection.getSocket(), "not implemented"); + + // Test setSocketReused() - should be a no-op for SyntheticConnection + EXPECT_NO_THROW(connection.setSocketReused(true)); + EXPECT_NO_THROW(connection.setSocketReused(false)); + + // Test isSocketReused() - should always return false for SyntheticConnection + EXPECT_FALSE(connection.isSocketReused()); + connection.setSocketReused(true); + EXPECT_FALSE(connection.isSocketReused()); // Should still return false } } // namespace Server diff --git a/test/server/config_validation/BUILD b/test/server/config_validation/BUILD index 3c9c1a5d048fd..d6fd1c8f27a13 100644 --- a/test/server/config_validation/BUILD +++ b/test/server/config_validation/BUILD @@ -53,13 +53,13 @@ envoy_cc_test( rbe_pool = "6gig", deps = [ "//source/extensions/access_loggers/stream:config", - "//source/extensions/clusters/logical_dns:logical_dns_cluster_lib", + "//source/extensions/clusters/dns:dns_cluster_lib", "//source/extensions/clusters/original_dst:original_dst_cluster_lib", - "//source/extensions/clusters/strict_dns:strict_dns_cluster_lib", "//source/extensions/filters/http/router:config", "//source/extensions/filters/listener/original_dst:config", "//source/extensions/filters/network/http_connection_manager:config", "//source/extensions/listener_managers/validation_listener_manager:validation_listener_manager_lib", + "//source/extensions/network/dns_resolver/cares:config", "//source/extensions/transport_sockets/tls:config", "//source/server/admin:admin_filter_lib", "//source/server/config_validation:server_lib", diff --git a/test/server/config_validation/cluster_manager_test.cc b/test/server/config_validation/cluster_manager_test.cc index 9c88b384fa86b..3063c33deacdd 100644 --- a/test/server/config_validation/cluster_manager_test.cc +++ b/test/server/config_validation/cluster_manager_test.cc @@ -29,27 +29,22 @@ namespace Upstream { namespace { TEST(ValidationClusterManagerTest, MockedMethods) { - NiceMock server; + testing::NiceMock server_context; - Stats::TestUtil::TestStore& stats_store = server.server_factory_context_->store_; - Event::GlobalTimeSystem& time_system = server.server_factory_context_->time_system_; + Stats::TestUtil::TestStore& stats_store = server_context.store_; + Event::GlobalTimeSystem& time_system = server_context.time_system_; Api::ApiPtr api(Api::createApiForTest(stats_store, time_system)); - ON_CALL(*server.server_factory_context_, api()).WillByDefault(testing::ReturnRef(*api)); + ON_CALL(server_context, api()).WillByDefault(testing::ReturnRef(*api)); + Extensions::TransportSockets::Tls::ContextManagerImpl ssl_context_manager{server_context}; + ON_CALL(server_context, sslContextManager()) + .WillByDefault(testing::ReturnRef(ssl_context_manager)); - NiceMock& tls = server.server_factory_context_->thread_local_; - - testing::NiceMock secret_manager; auto dns_resolver = std::make_shared>(); - Extensions::TransportSockets::Tls::ContextManagerImpl ssl_context_manager{ - *server.server_factory_context_}; - - Http::ContextImpl http_context(stats_store.symbolTable()); Quic::QuicStatNames quic_stat_names(stats_store.symbolTable()); ValidationClusterManagerFactory factory( - *server.server_factory_context_, stats_store, tls, http_context, - [dns_resolver]() -> Network::DnsResolverSharedPtr { return dns_resolver; }, - ssl_context_manager, quic_stat_names, server); + server_context, [dns_resolver]() -> Network::DnsResolverSharedPtr { return dns_resolver; }, + quic_stat_names); const envoy::config::bootstrap::v3::Bootstrap bootstrap; ClusterManagerPtr cluster_manager = *factory.clusterManagerFromProto(bootstrap); diff --git a/test/server/config_validation/xds_fuzz.cc b/test/server/config_validation/xds_fuzz.cc index d05ba8e0048e4..6f949486a7c14 100644 --- a/test/server/config_validation/xds_fuzz.cc +++ b/test/server/config_validation/xds_fuzz.cc @@ -39,7 +39,7 @@ void XdsFuzzTest::updateListener( const std::vector& added_or_updated, const std::vector& removed) { ENVOY_LOG_MISC(debug, "Sending Listener DiscoveryResponse version {}", version_); - sendDiscoveryResponse(Config::TypeUrl::get().Listener, + sendDiscoveryResponse(Config::TestTypeUrl::get().Listener, listeners, added_or_updated, removed, std::to_string(version_)); } @@ -50,7 +50,7 @@ void XdsFuzzTest::updateRoute( const std::vector& removed) { ENVOY_LOG_MISC(debug, "Sending Route DiscoveryResponse version {}", version_); sendDiscoveryResponse( - Config::TypeUrl::get().RouteConfiguration, routes, added_or_updated, removed, + Config::TestTypeUrl::get().RouteConfiguration, routes, added_or_updated, removed, std::to_string(version_)); } @@ -161,7 +161,7 @@ void XdsFuzzTest::addListener(const std::string& listener_name, const std::strin // Use waitForAck instead of compareDiscoveryRequest as the client makes additional // DiscoveryRequests at launch that we might not want to respond to yet. - EXPECT_TRUE(waitForAck(Config::TypeUrl::get().Listener, std::to_string(version_))); + EXPECT_TRUE(waitForAck(Config::TestTypeUrl::get().Listener, std::to_string(version_))); if (removed) { verifier_.listenerUpdated(listener); } else { @@ -179,7 +179,7 @@ void XdsFuzzTest::removeListener(const std::string& listener_name) { if (removed) { lds_update_success_++; updateListener(listeners_, {}, {listener_name}); - EXPECT_TRUE(waitForAck(Config::TypeUrl::get().Listener, std::to_string(version_))); + EXPECT_TRUE(waitForAck(Config::TestTypeUrl::get().Listener, std::to_string(version_))); verifier_.listenerRemoved(listener_name); } } @@ -198,7 +198,7 @@ void XdsFuzzTest::addRoute(const std::string& route_name) { updateRoute(routes_, {route}, {}); verifier_.routeAdded(route); - EXPECT_TRUE(waitForAck(Config::TypeUrl::get().RouteConfiguration, std::to_string(version_))); + EXPECT_TRUE(waitForAck(Config::TestTypeUrl::get().RouteConfiguration, std::to_string(version_))); } /** @@ -237,17 +237,17 @@ void XdsFuzzTest::replay() { initialize(); // Set up cluster. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); - sendDiscoveryResponse(Config::TypeUrl::get().Cluster, + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TestTypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "0"); // TODO (dmitri-d) legacy delta sends node with every DiscoveryRequest, other mux implementations // follow set_node_on_first_message_only config flag - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", + EXPECT_TRUE(compareDiscoveryRequest(Config::TestTypeUrl::get().ClusterLoadAssignment, "", {"cluster_0"}, {"cluster_0"}, {}, sotw_or_delta_ == Grpc::SotwOrDelta::Delta)); sendDiscoveryResponse( - Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, + Config::TestTypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "0"); // The client will not subscribe to the RouteConfiguration type URL until it receives a listener, diff --git a/test/server/configuration_impl_test.cc b/test/server/configuration_impl_test.cc index 3e051873ce700..c916ebd031643 100644 --- a/test/server/configuration_impl_test.cc +++ b/test/server/configuration_impl_test.cc @@ -63,9 +63,9 @@ class ConfigurationImplTest : public testing::Test { ConfigurationImplTest() : api_(Api::createApiForTest()), ads_mux_(std::make_shared>()), cluster_manager_factory_( - server_context_, server_.stats(), server_.threadLocal(), server_.httpContext(), + server_context_, [this]() -> Network::DnsResolverSharedPtr { return this->server_.dnsResolver(); }, - server_.sslContextManager(), server_.quic_stat_names_, server_) { + server_.quic_stat_names_) { ON_CALL(server_context_.api_, threadFactory()) .WillByDefault( Invoke([this]() -> Thread::ThreadFactory& { return api_->threadFactory(); })); @@ -94,6 +94,7 @@ TEST_F(ConfigurationImplTest, DefaultStatsFlushInterval) { EXPECT_EQ(std::chrono::milliseconds(5000), config.statsConfig().flushInterval()); EXPECT_FALSE(config.statsConfig().flushOnAdmin()); + EXPECT_EQ(0, config.statsConfig().evictOnFlush()); } TEST_F(ConfigurationImplTest, CustomStatsFlushInterval) { @@ -224,6 +225,34 @@ TEST_F(ConfigurationImplTest, IntervalAndAdminFlush) { "Only one of stats_flush_interval or stats_flush_on_admin should be set!"); } +TEST_F(ConfigurationImplTest, Eviction) { + std::string json = R"EOF( + { + "stats_flush_interval": "0.500s", + "stats_eviction_interval": "1.5s" + } + )EOF"; + + auto bootstrap = Upstream::parseBootstrapFromV3Json(json); + MainImpl config; + EXPECT_TRUE(config.initialize(bootstrap, server_, cluster_manager_factory_).ok()); + EXPECT_EQ(3, config.statsConfig().evictOnFlush()); +} + +TEST_F(ConfigurationImplTest, EvictionNotMultiple) { + std::string json = R"EOF( + { + "stats_flush_interval": "0.500s", + "stats_eviction_interval": "0.750s" + } + )EOF"; + + auto bootstrap = Upstream::parseBootstrapFromV3Json(json); + MainImpl config; + EXPECT_THAT(config.initialize(bootstrap, server_, cluster_manager_factory_).message(), + testing::HasSubstr("must be a multiple")); +} + TEST_F(ConfigurationImplTest, SetUpstreamClusterPerConnectionBufferLimit) { const std::string json = R"EOF( { diff --git a/test/server/filter_config_test.cc b/test/server/filter_config_test.cc index 3161a547348ea..f89d2cfb878cd 100644 --- a/test/server/filter_config_test.cc +++ b/test/server/filter_config_test.cc @@ -46,7 +46,7 @@ TEST(NamedHttpFilterConfigFactoryTest, CreateFilterFactory) { TestHttpFilterConfigFactory factory; const std::string stats_prefix = "foo"; Server::Configuration::MockFactoryContext context; - ProtobufTypes::MessagePtr message{new Envoy::ProtobufWkt::Struct()}; + ProtobufTypes::MessagePtr message{new Envoy::Protobuf::Struct()}; EXPECT_TRUE(factory.createFilterFactoryFromProto(*message, stats_prefix, context).status().ok()); } @@ -55,7 +55,7 @@ TEST(NamedHttpFilterConfigFactoryTest, Dependencies) { TestHttpFilterConfigFactory factory; const std::string stats_prefix = "foo"; Server::Configuration::MockFactoryContext context; - ProtobufTypes::MessagePtr message{new Envoy::ProtobufWkt::Struct()}; + ProtobufTypes::MessagePtr message{new Envoy::Protobuf::Struct()}; EXPECT_TRUE(factory.createFilterFactoryFromProto(*message, stats_prefix, context).status().ok()); diff --git a/test/server/guarddog_impl_test.cc b/test/server/guarddog_impl_test.cc index 2f12ae1d8aeca..bbee03fac2536 100644 --- a/test/server/guarddog_impl_test.cc +++ b/test/server/guarddog_impl_test.cc @@ -579,9 +579,9 @@ class GuardDogActionsTest : public GuardDogTestBase { std::vector actions_; std::vector events_; - RecordGuardDogActionFactory log_factory_; + RecordGuardDogActionFactory log_factory_; Registry::InjectFactory register_log_factory_; - AssertGuardDogActionFactory assert_factory_; + AssertGuardDogActionFactory assert_factory_; Registry::InjectFactory register_assert_factory_; NiceMock fake_stats_; WatchDogSharedPtr first_dog_; diff --git a/test/server/options_impl_test.cc b/test/server/options_impl_test.cc index d6ed5184d9542..7e75a755738aa 100644 --- a/test/server/options_impl_test.cc +++ b/test/server/options_impl_test.cc @@ -666,7 +666,7 @@ class TestFactory : public Config::TypedFactory { ~TestFactory() override = default; std::string category() const override { return "test"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; @@ -680,7 +680,7 @@ class TestingFactory : public Config::TypedFactory { ~TestingFactory() override = default; std::string category() const override { return "testing"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; diff --git a/test/server/overload_manager_impl_test.cc b/test/server/overload_manager_impl_test.cc index 663751cfed182..a86f224c0e269 100644 --- a/test/server/overload_manager_impl_test.cc +++ b/test/server/overload_manager_impl_test.cc @@ -231,11 +231,11 @@ class OverloadManagerImplTest : public testing::Test { options_, creation_status); } - FakeResourceMonitorFactory factory1_; - FakeResourceMonitorFactory factory2_; - FakeResourceMonitorFactory factory3_; - FakeResourceMonitorFactory factory4_; - FakeProactiveResourceMonitorFactory factory5_; + FakeResourceMonitorFactory factory1_; + FakeResourceMonitorFactory factory2_; + FakeResourceMonitorFactory factory3_; + FakeResourceMonitorFactory factory4_; + FakeProactiveResourceMonitorFactory factory5_; Registry::InjectFactory register_factory1_; Registry::InjectFactory register_factory2_; Registry::InjectFactory register_factory3_; @@ -607,6 +607,35 @@ constexpr char kReducedTimeoutsConfig[] = R"YAML( saturation_threshold: 1.0 )YAML"; +constexpr char kReducedTimeoutsConfigWithFlush[] = R"YAML( + refresh_interval: + seconds: 1 + resource_monitors: + - name: envoy.resource_monitors.fake_resource1 + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + actions: + - name: envoy.overload_actions.reduce_timeouts + typed_config: + "@type": type.googleapis.com/envoy.config.overload.v3.ScaleTimersOverloadActionConfig + timer_scale_factors: + - timer: HTTP_DOWNSTREAM_CONNECTION_IDLE + min_timeout: 2s + - timer: HTTP_DOWNSTREAM_STREAM_IDLE + min_scale: { value: 10 } # percent + - timer: TRANSPORT_SOCKET_CONNECT + min_scale: { value: 40 } # percent + - timer: HTTP_DOWNSTREAM_CONNECTION_MAX + min_scale: { value: 20 } # percent + - timer: HTTP_DOWNSTREAM_STREAM_FLUSH + min_scale: { value: 30 } # percent + triggers: + - name: "envoy.resource_monitors.fake_resource1" + scaled: + scaling_threshold: 0.5 + saturation_threshold: 1.0 + )YAML"; + // These are the timer types according to the reduced timeouts config above. constexpr std::pair kReducedTimeoutsMinimums[]{ {TimerType::HttpDownstreamIdleConnectionTimeout, @@ -615,6 +644,16 @@ constexpr std::pair kReducedTimeoutsMinimu {TimerType::TransportSocketConnectTimeout, Event::ScaledMinimum(UnitFloat(0.4))}, {TimerType::HttpDownstreamMaxConnectionTimeout, Event::ScaledMinimum(UnitFloat(0.2))}, }; + +constexpr std::pair kReducedTimeoutsMinimumsWithFlush[]{ + {TimerType::HttpDownstreamIdleConnectionTimeout, + Event::AbsoluteMinimum(std::chrono::seconds(2))}, + {TimerType::HttpDownstreamIdleStreamTimeout, Event::ScaledMinimum(UnitFloat(0.1))}, + {TimerType::TransportSocketConnectTimeout, Event::ScaledMinimum(UnitFloat(0.4))}, + {TimerType::HttpDownstreamMaxConnectionTimeout, Event::ScaledMinimum(UnitFloat(0.2))}, + {TimerType::HttpDownstreamStreamFlush, Event::ScaledMinimum(UnitFloat(0.3))}, +}; + TEST_F(OverloadManagerImplTest, CreateScaledTimerManager) { auto manager(createOverloadManager(kReducedTimeoutsConfig)); @@ -633,6 +672,25 @@ TEST_F(OverloadManagerImplTest, CreateScaledTimerManager) { EXPECT_THAT(timer_minimums, Pointee(UnorderedElementsAreArray(kReducedTimeoutsMinimums))); } +TEST_F(OverloadManagerImplTest, CreateScaledTimerManagerWithFlush) { + auto manager(createOverloadManager(kReducedTimeoutsConfigWithFlush)); + + auto* mock_scaled_timer_manager = new Event::MockScaledRangeTimerManager(); + + Event::ScaledTimerTypeMapConstSharedPtr timer_minimums; + EXPECT_CALL(*manager, createScaledRangeTimerManager) + .WillOnce( + DoAll(SaveArg<1>(&timer_minimums), + Return(ByMove(Event::ScaledRangeTimerManagerPtr{mock_scaled_timer_manager})))); + + Event::MockDispatcher mock_dispatcher; + auto scaled_timer_manager = manager->scaledTimerFactory()(mock_dispatcher); + + EXPECT_EQ(scaled_timer_manager.get(), mock_scaled_timer_manager); + EXPECT_THAT(timer_minimums, + Pointee(UnorderedElementsAreArray(kReducedTimeoutsMinimumsWithFlush))); +} + TEST_F(OverloadManagerImplTest, AdjustScaleFactor) { setDispatcherExpectation(); auto manager(createOverloadManager(kReducedTimeoutsConfig)); diff --git a/test/server/server_corpus/clusterfuzz-testcase-minimized-server_fuzz_test-5393862409650176 b/test/server/server_corpus/clusterfuzz-testcase-minimized-server_fuzz_test-5393862409650176 new file mode 100644 index 0000000000000..63e62a37c5a14 --- /dev/null +++ b/test/server/server_corpus/clusterfuzz-testcase-minimized-server_fuzz_test-5393862409650176 @@ -0,0 +1,23 @@ +static_resources { + listeners { + name: "\'" + address { + socket_address { + protocol: UDP + address: "127.0.0.1" + port_value: 0 + network_namespace_filepath: ":" + } + } + socket_options { + int_value: 59 + type { + datagram { + } + } + } + fcds_config { + name: " " + } + } +} diff --git a/test/server/server_test.cc b/test/server/server_test.cc index 7307ae5cc861a..c942b8e899b0f 100644 --- a/test/server/server_test.cc +++ b/test/server/server_test.cc @@ -9,6 +9,7 @@ #include "envoy/server/fatal_action_config.h" #include "source/common/common/assert.h" +#include "source/common/common/notification.h" #include "source/common/network/address_impl.h" #include "source/common/network/listen_socket_impl.h" #include "source/common/network/socket_option_impl.h" @@ -23,7 +24,7 @@ #include "test/config/v2_link_hacks.h" #include "test/integration/server.h" #include "test/mocks/api/mocks.h" -#include "test/mocks/common.h" +#include "test/mocks/config/xds_manager.h" #include "test/mocks/server/bootstrap_extension_factory.h" #include "test/mocks/server/fatal_action_factory.h" #include "test/mocks/server/hot_restart.h" @@ -230,20 +231,21 @@ class RunHelperTest : public testing::Test { EXPECT_CALL(cm_, setInitializedCb(_)).WillOnce(SaveArg<0>(&cm_init_callback_)); ON_CALL(server_, shutdown()).WillByDefault(Assign(&shutdown_, true)); - helper_ = std::make_unique(server_, options_, dispatcher_, cm_, access_log_manager_, - init_manager_, overload_manager_, null_overload_manager_, - [this] { start_workers_.ready(); }); + helper_ = std::make_unique( + server_, options_, dispatcher_, xds_manager_, cm_, access_log_manager_, init_manager_, + overload_manager_, null_overload_manager_, mock_workers_start_cb_.AsStdFunction()); } NiceMock server_; testing::NiceMock options_; NiceMock dispatcher_; + NiceMock xds_manager_; NiceMock cm_; NiceMock access_log_manager_; NiceMock overload_manager_; NiceMock null_overload_manager_; Init::ManagerImpl init_manager_{""}; - ReadyWatcher start_workers_; + testing::MockFunction mock_workers_start_cb_; std::unique_ptr helper_; std::function cm_init_callback_; #ifndef WIN32 @@ -256,14 +258,14 @@ class RunHelperTest : public testing::Test { }; TEST_F(RunHelperTest, Normal) { - EXPECT_CALL(start_workers_, ready()); + EXPECT_CALL(mock_workers_start_cb_, Call); cm_init_callback_(); } // no signals on Windows #ifndef WIN32 TEST_F(RunHelperTest, ShutdownBeforeCmInitialize) { - EXPECT_CALL(start_workers_, ready()).Times(0); + EXPECT_CALL(mock_workers_start_cb_, Call).Times(0); sigterm_->callback_(); EXPECT_CALL(server_, isShutdown()).WillOnce(Return(shutdown_)); cm_init_callback_(); @@ -273,7 +275,7 @@ TEST_F(RunHelperTest, ShutdownBeforeCmInitialize) { // no signals on Windows #ifndef WIN32 TEST_F(RunHelperTest, ShutdownBeforeInitManagerInit) { - EXPECT_CALL(start_workers_, ready()).Times(0); + EXPECT_CALL(mock_workers_start_cb_, Call).Times(0); Init::ExpectableTargetImpl target; init_manager_.add(target); EXPECT_CALL(target, initialize()); @@ -429,7 +431,7 @@ class CustomStatsSinkFactory : public Server::Configuration::StatsSinkFactory { ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "envoy.custom_stats_sink"; } @@ -846,6 +848,10 @@ TEST_P(ServerInstanceImplTest, Stats) { EXPECT_EQ(2L, TestUtility::findGauge(stats_store_, "server.concurrency")->value()); EXPECT_EQ(3L, TestUtility::findGauge(stats_store_, "server.hot_restart_epoch")->value()); + ENVOY_NOTIFICATION("name", "stuff"); + ENVOY_NOTIFICATION("name1", "stuff1"); + ENVOY_NOTIFICATION("name3", "stuff3"); + EXPECT_EQ(3L, TestUtility::findCounter(stats_store_, "server.envoy_notifications")->value()); // The ENVOY_BUG stat works in release mode. #if defined(NDEBUG) // Test exponential back-off on a fixed line ENVOY_BUG. @@ -957,6 +963,38 @@ TEST_P(ServerInstanceImplTest, FlushStatsOnAdmin) { server_thread->join(); } +TEST_P(ServerInstanceImplTest, EvictStats) { + CustomStatsSinkFactory factory; + Registry::InjectFactory registered(factory); + auto server_thread = + startTestServer("test/server/test_data/server/stats_evict_bootstrap.yaml", true); + EXPECT_EQ(2, server_->statsConfig().evictOnFlush()); + EXPECT_EQ(std::chrono::seconds(5), server_->statsConfig().flushInterval()); + + auto counter = TestUtility::findCounter(stats_store_, "stats.flushed"); + + time_system_.advanceTimeWait(std::chrono::seconds(6)); + EXPECT_EQ(1L, counter->value()); + EXPECT_EQ(0, stats_store_.evictionCount()); + + // Eviction applied here: side-effect is that c1 is now marked as unused. + time_system_.advanceTimeWait(std::chrono::seconds(6)); + EXPECT_EQ(2L, counter->value()); + EXPECT_EQ(1, stats_store_.evictionCount()); + + time_system_.advanceTimeWait(std::chrono::seconds(6)); + EXPECT_EQ(3L, counter->value()); + EXPECT_EQ(1, stats_store_.evictionCount()); + + // Second pass of eviction deletes the counter. + time_system_.advanceTimeWait(std::chrono::seconds(6)); + EXPECT_EQ(4L, counter->value()); + EXPECT_EQ(2, stats_store_.evictionCount()); + + server_->dispatcher().post([&] { server_->shutdown(); }); + server_thread->join(); +} + TEST_P(ServerInstanceImplTest, ConcurrentFlushes) { CustomStatsSinkFactory factory; Registry::InjectFactory registered(factory); @@ -1591,7 +1629,7 @@ TEST_P(ServerInstanceImplTest, WithFatalActions) { // Inject Unsafe Factory NiceMock mock_unsafe_factory; EXPECT_CALL(mock_unsafe_factory, createEmptyConfigProto()).WillRepeatedly(Invoke([]() { - return std::make_unique(); + return std::make_unique(); })); EXPECT_CALL(mock_unsafe_factory, name()).WillRepeatedly(Return("envoy_test.fatal_action.unsafe")); @@ -1717,7 +1755,7 @@ class CallbacksStatsSinkFactory : public Server::Configuration::StatsSinkFactory ProtobufTypes::MessagePtr createEmptyConfigProto() override { // Using Struct instead of a custom per-filter empty config proto // This is only allowed in tests. - return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; + return ProtobufTypes::MessagePtr{new Envoy::Protobuf::Struct()}; } std::string name() const override { return "envoy.callbacks_stats_sink"; } diff --git a/test/server/test_data/server/stats_evict_bootstrap.yaml b/test/server/test_data/server/stats_evict_bootstrap.yaml new file mode 100644 index 0000000000000..b295c2c0c471c --- /dev/null +++ b/test/server/test_data/server/stats_evict_bootstrap.yaml @@ -0,0 +1,11 @@ +node: + id: bootstrap_id + cluster: bootstrap_cluster + locality: + zone: bootstrap_zone + sub_zone: bootstrap_sub_zone +stats_sinks: +- name: envoy.custom_stats_sink + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct +stats_eviction_interval: 10s diff --git a/test/test_common/resources.h b/test/test_common/resources.h index 323bbd9971a36..c83998242c1cf 100644 --- a/test/test_common/resources.h +++ b/test/test_common/resources.h @@ -26,7 +26,7 @@ class TypeUrlValues { const std::string Runtime{"type.googleapis.com/envoy.service.runtime.v3.Runtime"}; }; -using TypeUrl = ConstSingleton; +using TestTypeUrl = ConstSingleton; } // namespace Config } // namespace Envoy diff --git a/test/test_common/test_runtime.h b/test/test_common/test_runtime.h index 189c341d43adc..98e39503af454 100644 --- a/test/test_common/test_runtime.h +++ b/test/test_common/test_runtime.h @@ -76,10 +76,10 @@ class TestScopedStaticReloadableFeaturesRuntime { // Set up runtime. auto* runtime = config.add_layers(); runtime->set_name("test_static_layer_test_runtime"); - ProtobufWkt::Struct envoy_layer; - ProtobufWkt::Struct& runtime_values = + Protobuf::Struct envoy_layer; + Protobuf::Struct& runtime_values = *(*envoy_layer.mutable_fields())["envoy"].mutable_struct_value(); - ProtobufWkt::Struct& flags = + Protobuf::Struct& flags = *(*runtime_values.mutable_fields())["reloadable_features"].mutable_struct_value(); for (const auto& [key, value] : values) { (*flags.mutable_fields())[key].set_bool_value(value); diff --git a/test/test_common/utility.h b/test/test_common/utility.h index b4816a58a320e..8192ae549d94d 100644 --- a/test/test_common/utility.h +++ b/test/test_common/utility.h @@ -645,8 +645,7 @@ class TestUtility { */ static std::string nonZeroedGauges(const std::vector& gauges); - template - static inline MessageType anyConvert(const ProtobufWkt::Any& message) { + template static inline MessageType anyConvert(const Protobuf::Any& message) { return MessageUtil::anyConvert(message); } @@ -702,7 +701,7 @@ class TestUtility { template static Config::DecodedResourcesWrapper - decodeResources(const Protobuf::RepeatedPtrField& resources, + decodeResources(const Protobuf::RepeatedPtrField& resources, const std::string& version, const std::string& name_field = "name") { TestOpaqueResourceDecoderImpl resource_decoder(name_field); std::unique_ptr tmp_wrapper = @@ -765,8 +764,8 @@ class TestUtility { #ifdef ENVOY_ENABLE_YAML /** - * Compare two JSON strings serialized from ProtobufWkt::Struct for equality. When two identical - * ProtobufWkt::Struct are serialized into JSON strings, the results have the same set of + * Compare two JSON strings serialized from Protobuf::Struct for equality. When two identical + * Protobuf::Struct are serialized into JSON strings, the results have the same set of * properties (values), but the positions may be different. * * @param lhs JSON string on LHS. @@ -799,7 +798,7 @@ class TestUtility { MessageUtil::loadFromJson(json, message, ProtobufMessage::getStrictValidationVisitor()); } - static void loadFromJson(const std::string& json, ProtobufWkt::Struct& message) { + static void loadFromJson(const std::string& json, Protobuf::Struct& message) { MessageUtil::loadFromJson(json, message); } @@ -815,22 +814,22 @@ class TestUtility { static void jsonConvert(const Protobuf::Message& source, Protobuf::Message& dest) { // Explicit round-tripping to support conversions inside tests between arbitrary messages as a // convenience. - ProtobufWkt::Struct tmp; + Protobuf::Struct tmp; MessageUtil::jsonConvert(source, tmp); MessageUtil::jsonConvert(tmp, ProtobufMessage::getStrictValidationVisitor(), dest); } - static ProtobufWkt::Struct jsonToStruct(const std::string& json) { - ProtobufWkt::Struct message; + static Protobuf::Struct jsonToStruct(const std::string& json) { + Protobuf::Struct message; MessageUtil::loadFromJson(json, message); return message; } - static ProtobufWkt::Struct jsonArrayToStruct(const std::string& json) { + static Protobuf::Struct jsonArrayToStruct(const std::string& json) { // Hacky: add a surrounding root message, allowing JSON to be parsed into a struct. std::string root_message = absl::StrCat("{ \"testOnlyArrayRoot\": ", json, "}"); - ProtobufWkt::Struct message; + Protobuf::Struct message; MessageUtil::loadFromJson(root_message, message); return message; } @@ -886,6 +885,7 @@ namespace Tracing { class TestTraceContextImpl : public Tracing::TraceContext { public: + TestTraceContextImpl() = default; TestTraceContextImpl(const std::initializer_list>& values) { for (const auto& value : values) { context_map_[value.first] = value.second; diff --git a/test/test_common/wasm_base.h b/test/test_common/wasm_base.h index d066de36d46e1..2269d83d56329 100644 --- a/test/test_common/wasm_base.h +++ b/test/test_common/wasm_base.h @@ -76,7 +76,7 @@ template class WasmTestBase : public Base { auto vm_config = plugin_config.mutable_vm_config(); vm_config->set_vm_id("vm_id"); vm_config->set_runtime(absl::StrCat("envoy.wasm.runtime.", runtime)); - ProtobufWkt::StringValue vm_configuration_string; + Protobuf::StringValue vm_configuration_string; vm_configuration_string.set_value(vm_configuration_); vm_config->mutable_configuration()->PackFrom(vm_configuration_string); vm_config->mutable_code()->mutable_local()->set_inline_bytes(code); diff --git a/test/tools/router_check/BUILD b/test/tools/router_check/BUILD index b50a1feac0787..07a5c7178d4c0 100644 --- a/test/tools/router_check/BUILD +++ b/test/tools/router_check/BUILD @@ -58,6 +58,7 @@ envoy_cc_test_library( "@com_github_mirror_tclap//:tclap", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/set_metadata/v3:pkg_cc_proto", "@envoy_api//envoy/type/v3:pkg_cc_proto", ], ) @@ -68,5 +69,6 @@ envoy_proto_library( deps = [ "@envoy_api//envoy/config/core/v3:pkg", "@envoy_api//envoy/config/route/v3:pkg", + "@envoy_api//envoy/extensions/filters/http/set_metadata/v3:pkg", ], ) diff --git a/test/tools/router_check/router.cc b/test/tools/router_check/router.cc index db27f4ad97c5f..869617ea87b5d 100644 --- a/test/tools/router_check/router.cc +++ b/test/tools/router_check/router.cc @@ -7,6 +7,7 @@ #include "envoy/config/core/v3/base.pb.h" #include "envoy/config/route/v3/route.pb.h" +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.h" #include "envoy/type/v3/percent.pb.h" #include "source/common/common/enum_to_int.h" @@ -176,17 +177,21 @@ void RouterCheckTool::assignRuntimeFraction( void RouterCheckTool::finalizeHeaders(ToolConfig& tool_config, Envoy::StreamInfo::StreamInfoImpl stream_info) { if (!headers_finalized_ && tool_config.route_ != nullptr) { + const Formatter::HttpFormatterContext formatter_context(tool_config.request_headers_.get(), + tool_config.response_headers_.get(), + nullptr, {}, {}, nullptr); + if (tool_config.route_->directResponseEntry() != nullptr) { tool_config.route_->directResponseEntry()->rewritePathHeader(*tool_config.request_headers_, true); sendLocalReply(tool_config, *tool_config.route_->directResponseEntry()); tool_config.route_->directResponseEntry()->finalizeResponseHeaders( - *tool_config.response_headers_, stream_info); + *tool_config.response_headers_, formatter_context, stream_info); } else if (tool_config.route_->routeEntry() != nullptr) { - tool_config.route_->routeEntry()->finalizeRequestHeaders(*tool_config.request_headers_, - stream_info, true); + tool_config.route_->routeEntry()->finalizeRequestHeaders( + *tool_config.request_headers_, formatter_context, stream_info, true); tool_config.route_->routeEntry()->finalizeResponseHeaders(*tool_config.response_headers_, - stream_info); + formatter_context, stream_info); } } @@ -234,6 +239,47 @@ Json::ObjectSharedPtr loadFromFile(const std::string& file_path, Api::Api& api) return Json::Factory::loadFromString(contents).value(); } +void RouterCheckTool::applyDynamicMetadata( + Envoy::StreamInfo::StreamInfoImpl& stream_info, + const Envoy::Protobuf::RepeatedPtrField< + envoy::extensions::filters::http::set_metadata::v3::Metadata>& dynamic_metadata) { + if (dynamic_metadata.empty()) { + return; + } + + for (const auto& metadata : dynamic_metadata) { + if (metadata.has_value()) { + auto& mut_untyped_metadata = *stream_info.dynamicMetadata().mutable_filter_metadata(); + const std::string& metadata_namespace = metadata.metadata_namespace(); + + if (!mut_untyped_metadata.contains(metadata_namespace)) { + // Insert the new entry. + mut_untyped_metadata[metadata_namespace] = metadata.value(); + } else if (metadata.allow_overwrite()) { + // Get the existing metadata at this key for merging. + Protobuf::Struct& orig_fields = mut_untyped_metadata[metadata_namespace]; + const auto& to_merge = metadata.value(); + + // Merge the new metadata into the existing metadata. + StructUtil::update(orig_fields, to_merge); + } + // If allow_overwrite is false and entry exists, we skip it + } else if (metadata.has_typed_value()) { + auto& mut_typed_metadata = *stream_info.dynamicMetadata().mutable_typed_filter_metadata(); + const std::string& metadata_namespace = metadata.metadata_namespace(); + + if (!mut_typed_metadata.contains(metadata_namespace)) { + // Insert the new entry. + mut_typed_metadata[metadata_namespace] = metadata.typed_value(); + } else if (metadata.allow_overwrite()) { + // Overwrite the existing typed metadata at this key. + mut_typed_metadata[metadata_namespace] = metadata.typed_value(); + } + // If allow_overwrite is false and entry exists, we skip it + } + } +} + std::vector RouterCheckTool::compareEntries(const std::string& expected_routes) { envoy::RouterCheckToolSchema::Validation validation_config; @@ -254,6 +300,10 @@ RouterCheckTool::compareEntries(const std::string& expected_routes) { Envoy::Http::Protocol::Http11, factory_context_->mainThreadDispatcher().timeSource(), connection_info_provider, StreamInfo::FilterState::LifeSpan::FilterChain); ToolConfig tool_config = ToolConfig::create(check_config); + + // Apply dynamic metadata to stream_info before routing + applyDynamicMetadata(stream_info, check_config.input().dynamic_metadata()); + tool_config.route_ = config_->route(*tool_config.request_headers_, stream_info, tool_config.random_value_); diff --git a/test/tools/router_check/router.h b/test/tools/router_check/router.h index 6bab804167fc5..2bf32757a99cd 100644 --- a/test/tools/router_check/router.h +++ b/test/tools/router_check/router.h @@ -123,6 +123,14 @@ class RouterCheckTool : Logger::Loggable { */ void sendLocalReply(ToolConfig& tool_config, const Router::DirectResponseEntry& entry); + /** + * Apply dynamic metadata to stream_info, similar to how the set_metadata filter works. + */ + void applyDynamicMetadata( + Envoy::StreamInfo::StreamInfoImpl& stream_info, + const Envoy::Protobuf::RepeatedPtrField< + envoy::extensions::filters::http::set_metadata::v3::Metadata>& dynamic_metadata); + bool compareCluster(ToolConfig& tool_config, const envoy::RouterCheckToolSchema::ValidationAssert& expected, envoy::RouterCheckToolSchema::ValidationFailure& failure); diff --git a/test/tools/router_check/test/config/DynamicMetadata.golden.proto.json b/test/tools/router_check/test/config/DynamicMetadata.golden.proto.json new file mode 100644 index 0000000000000..0e8bb562b02e6 --- /dev/null +++ b/test/tools/router_check/test/config/DynamicMetadata.golden.proto.json @@ -0,0 +1,218 @@ +{ + "tests": [ + { + "test_name": "dynamic_metadata_basic_match", + "input": { + "authority": "edge.example.net", + "path": "/example", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "bar" + } + } + ] + }, + "validate": { + "cluster_name": "cluster2", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_fallback_no_metadata", + "input": { + "authority": "edge.example.net", + "path": "/example", + "method": "GET" + }, + "validate": { + "cluster_name": "cluster1", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_different_value", + "input": { + "authority": "edge.example.net", + "path": "/example", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "baz" + } + } + ] + }, + "validate": { + "cluster_name": "cluster3", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_nested_structure", + "input": { + "authority": "edge.example.net", + "path": "/nested", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "nested.meta", + "value": { + "level1": { + "level2": "value" + } + } + } + ] + }, + "validate": { + "cluster_name": "cluster4", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_inverted_match", + "input": { + "authority": "edge.example.net", + "path": "/inverted", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "different" + } + } + ] + }, + "validate": { + "cluster_name": "cluster5", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_inverted_no_match", + "input": { + "authority": "edge.example.net", + "path": "/inverted", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "bar" + } + } + ] + }, + "validate": { + "cluster_name": "cluster1", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_multiple_matchers_match", + "input": { + "authority": "edge.example.net", + "path": "/multiple", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "bar" + } + }, + { + "metadata_namespace": "other.meta", + "value": { + "status": "active" + } + } + ] + }, + "validate": { + "cluster_name": "cluster6", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_multiple_matchers_partial_match", + "input": { + "authority": "edge.example.net", + "path": "/multiple", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "bar" + } + } + ] + }, + "validate": { + "cluster_name": "cluster1", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_allow_overwrite", + "input": { + "authority": "edge.example.net", + "path": "/example", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "initial" + } + }, + { + "metadata_namespace": "example.meta", + "value": { + "foo": "bar" + }, + "allow_overwrite": true + } + ] + }, + "validate": { + "cluster_name": "cluster2", + "virtual_host_name": "default" + } + }, + { + "test_name": "dynamic_metadata_no_overwrite", + "input": { + "authority": "edge.example.net", + "path": "/example", + "method": "GET", + "dynamic_metadata": [ + { + "metadata_namespace": "example.meta", + "value": { + "foo": "bar" + } + }, + { + "metadata_namespace": "example.meta", + "value": { + "foo": "different" + }, + "allow_overwrite": false + } + ] + }, + "validate": { + "cluster_name": "cluster2", + "virtual_host_name": "default" + } + } + ] +} diff --git a/test/tools/router_check/test/config/DynamicMetadata.yaml b/test/tools/router_check/test/config/DynamicMetadata.yaml new file mode 100644 index 0000000000000..2173c4b55bb6a --- /dev/null +++ b/test/tools/router_check/test/config/DynamicMetadata.yaml @@ -0,0 +1,88 @@ +virtual_hosts: +- name: default + domains: + - 'edge.example.net' + routes: + # Route with different dynamic metadata value - more specific path + - route: + cluster: cluster3 + match: + path: /example + dynamic_metadata: + - filter: example.meta + path: + - key: foo + value: + string_match: + exact: baz + # More specific route with dynamic metadata - should match first for foo=bar + - route: + cluster: cluster2 + match: + path: /example + dynamic_metadata: + - filter: example.meta + path: + - key: foo + value: + string_match: + exact: bar + # Route with nested metadata structure + - route: + cluster: cluster4 + match: + path: /nested + dynamic_metadata: + - filter: nested.meta + path: + - key: level1 + - key: level2 + value: + string_match: + exact: value + # Route with inverted dynamic metadata matcher - more specific path + - route: + cluster: cluster5 + match: + path: /inverted + dynamic_metadata: + - filter: example.meta + path: + - key: foo + value: + string_match: + exact: bar + invert: true + # Fallback route for /inverted path + - route: + cluster: cluster1 + match: + path: /inverted + # Route with multiple dynamic metadata matchers - more specific path + - route: + cluster: cluster6 + match: + path: /multiple + dynamic_metadata: + - filter: example.meta + path: + - key: foo + value: + string_match: + exact: bar + - filter: other.meta + path: + - key: status + value: + string_match: + exact: active + # Fallback route for /multiple path + - route: + cluster: cluster1 + match: + path: /multiple + # Fallback route without dynamic metadata - should be last + - route: + cluster: cluster1 + match: + path: /example diff --git a/test/tools/router_check/test/router_test.cc b/test/tools/router_check/test/router_test.cc index 4905ed78cca8c..8fb592c927dc9 100644 --- a/test/tools/router_check/test/router_test.cc +++ b/test/tools/router_check/test/router_test.cc @@ -117,5 +117,20 @@ TEST(RouterCheckTest, RouterCheckTestRoutesFailuresTest) { EXPECT_TRUE(TestUtility::protoEqual(expected_result_proto, test_result, true)); } +TEST(RouterCheckTest, DynamicMetadataTest) { + const std::string config_filename_ = + TestEnvironment::runfilesPath(absl::StrCat(kDir, "DynamicMetadata.yaml")); + const std::string tests_filename_ = + TestEnvironment::runfilesPath(absl::StrCat(kDir, "DynamicMetadata.golden.proto.json")); + RouterCheckTool checktool = RouterCheckTool::create(config_filename_, false); + const std::vector test_results = + checktool.compareEntries(tests_filename_); + EXPECT_EQ(test_results.size(), 10); + for (const auto& test_result : test_results) { + EXPECT_TRUE(test_result.test_passed()) << "Test " << test_result.test_name() << " failed"; + EXPECT_FALSE(test_result.has_failure()) << "Test " << test_result.test_name() << " has failure"; + } +} + } // namespace } // namespace Envoy diff --git a/test/tools/router_check/validation.proto b/test/tools/router_check/validation.proto index 1d90144d6c976..e7fb0704607be 100644 --- a/test/tools/router_check/validation.proto +++ b/test/tools/router_check/validation.proto @@ -4,6 +4,7 @@ package envoy.RouterCheckToolSchema; import "envoy/config/core/v3/base.proto"; import "envoy/config/route/v3/route_components.proto"; +import "envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto"; import "google/protobuf/wrappers.proto"; import "validate/validate.proto"; @@ -70,6 +71,9 @@ message ValidationInput { repeated envoy.config.core.v3.HeaderValue additional_request_headers = 10; repeated envoy.config.core.v3.HeaderValue additional_response_headers = 11; + // Metadata to be added to the request as input for route determination. + repeated envoy.extensions.filters.http.set_metadata.v3.Metadata dynamic_metadata = 12; + // Runtime setting key to enable for the test case. // If a route depends on the runtime, the route will be enabled based on the random_value defined // in the test. Only a random_value less than the fractional percentage will enable the route. diff --git a/tools/api_proto_plugin/plugin.bzl b/tools/api_proto_plugin/plugin.bzl index 014749a7d1a5d..8c12b167ab2f0 100644 --- a/tools/api_proto_plugin/plugin.bzl +++ b/tools/api_proto_plugin/plugin.bzl @@ -1,5 +1,5 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load("@rules_proto//proto:defs.bzl", "ProtoInfo") +load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") # Borrowed from https://github.com/grpc/grpc-java/blob/v1.24.1/java_grpc_library.bzl#L61 def _path_ignoring_repository(f): diff --git a/tools/base/requirements.txt b/tools/base/requirements.txt index 17f74c31cf82b..52a28b6abe7b6 100644 --- a/tools/base/requirements.txt +++ b/tools/base/requirements.txt @@ -95,93 +95,93 @@ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp -aiohttp==3.12.14 \ - --hash=sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3 \ - --hash=sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a \ - --hash=sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179 \ - --hash=sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3 \ - --hash=sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d \ - --hash=sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086 \ - --hash=sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced \ - --hash=sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729 \ - --hash=sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0 \ - --hash=sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f \ - --hash=sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5 \ - --hash=sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245 \ - --hash=sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda \ - --hash=sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3 \ - --hash=sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0 \ - --hash=sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d \ - --hash=sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088 \ - --hash=sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767 \ - --hash=sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3 \ - --hash=sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c \ - --hash=sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9 \ - --hash=sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338 \ - --hash=sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e \ - --hash=sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa \ - --hash=sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641 \ - --hash=sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b \ - --hash=sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63 \ - --hash=sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7 \ - --hash=sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288 \ - --hash=sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da \ - --hash=sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b \ - --hash=sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656 \ - --hash=sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922 \ - --hash=sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4 \ - --hash=sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2 \ - --hash=sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0 \ - --hash=sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8 \ - --hash=sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419 \ - --hash=sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425 \ - --hash=sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635 \ - --hash=sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140 \ - --hash=sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393 \ - --hash=sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb \ - --hash=sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88 \ - --hash=sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf \ - --hash=sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28 \ - --hash=sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248 \ - --hash=sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84 \ - --hash=sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660 \ - --hash=sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933 \ - --hash=sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c \ - --hash=sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22 \ - --hash=sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab \ - --hash=sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b \ - --hash=sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0 \ - --hash=sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5 \ - --hash=sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc \ - --hash=sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff \ - --hash=sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee \ - --hash=sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7 \ - --hash=sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95 \ - --hash=sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61 \ - --hash=sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe \ - --hash=sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb \ - --hash=sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170 \ - --hash=sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae \ - --hash=sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3 \ - --hash=sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb \ - --hash=sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869 \ - --hash=sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd \ - --hash=sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db \ - --hash=sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b \ - --hash=sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab \ - --hash=sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8 \ - --hash=sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc \ - --hash=sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151 \ - --hash=sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663 \ - --hash=sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1 \ - --hash=sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c \ - --hash=sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026 \ - --hash=sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7 \ - --hash=sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2 \ - --hash=sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597 \ - --hash=sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3 \ - --hash=sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758 \ - --hash=sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd +aiohttp==3.12.15 \ + --hash=sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe \ + --hash=sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645 \ + --hash=sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af \ + --hash=sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263 \ + --hash=sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142 \ + --hash=sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6 \ + --hash=sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6 \ + --hash=sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09 \ + --hash=sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84 \ + --hash=sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1 \ + --hash=sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50 \ + --hash=sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a \ + --hash=sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79 \ + --hash=sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c \ + --hash=sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd \ + --hash=sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0 \ + --hash=sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75 \ + --hash=sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77 \ + --hash=sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c \ + --hash=sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab \ + --hash=sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4 \ + --hash=sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9 \ + --hash=sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421 \ + --hash=sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685 \ + --hash=sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b \ + --hash=sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf \ + --hash=sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693 \ + --hash=sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c \ + --hash=sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2 \ + --hash=sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519 \ + --hash=sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d \ + --hash=sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02 \ + --hash=sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea \ + --hash=sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05 \ + --hash=sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b \ + --hash=sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0 \ + --hash=sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd \ + --hash=sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98 \ + --hash=sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb \ + --hash=sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8 \ + --hash=sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f \ + --hash=sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89 \ + --hash=sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16 \ + --hash=sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64 \ + --hash=sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb \ + --hash=sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7 \ + --hash=sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728 \ + --hash=sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7 \ + --hash=sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830 \ + --hash=sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d \ + --hash=sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8 \ + --hash=sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d \ + --hash=sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406 \ + --hash=sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2 \ + --hash=sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9 \ + --hash=sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315 \ + --hash=sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d \ + --hash=sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd \ + --hash=sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d \ + --hash=sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51 \ + --hash=sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3 \ + --hash=sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34 \ + --hash=sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461 \ + --hash=sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b \ + --hash=sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc \ + --hash=sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530 \ + --hash=sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5 \ + --hash=sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d \ + --hash=sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7 \ + --hash=sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5 \ + --hash=sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54 \ + --hash=sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d \ + --hash=sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7 \ + --hash=sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117 \ + --hash=sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4 \ + --hash=sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1 \ + --hash=sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676 \ + --hash=sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b \ + --hash=sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d \ + --hash=sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0 \ + --hash=sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d \ + --hash=sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444 \ + --hash=sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0 \ + --hash=sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065 \ + --hash=sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545 \ + --hash=sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d # via # -r requirements.in # aio-api-github @@ -243,74 +243,91 @@ certifi==2024.8.30 \ # via # aioquic # requests -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b +cffi==2.0.0 \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ + --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ + --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ + --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ + --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ + --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ + --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ + --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ + --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ + --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ + --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ + --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ + --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ + --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ + --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ + --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ + --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ + --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ + --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via # -r requirements.in # cryptography @@ -447,10 +464,11 @@ coloredlogs==15.0.1 \ # via # -r requirements.in # aio-run-runner -cryptography==44.0.1 \ - --hash=sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7 \ - --hash=sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c \ - --hash=sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008 +cryptography==46.0.1 \ + --hash=sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6 \ + --hash=sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28 \ + --hash=sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8 \ + --hash=sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9 # via # -r requirements.in # aioquic @@ -461,10 +479,6 @@ dependatool==0.2.3 \ --hash=sha256:04bf88d01302eec697a69e8301d14668a89d676dbd2a3914e91c610a531e9db7 \ --hash=sha256:113a6641889d3dae7c81cb0a0483c31a2657f179474e11f4731b285963475ade # via -r requirements.in -deprecated==1.2.14 \ - --hash=sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c \ - --hash=sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3 - # via pygithub docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 @@ -688,9 +702,9 @@ gitdb==4.0.11 \ --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b # via gitpython -gitpython==3.1.44 \ - --hash=sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110 \ - --hash=sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269 +gitpython==3.1.45 \ + --hash=sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c \ + --hash=sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77 # via -r requirements.in humanfriendly==10.0 \ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ @@ -903,79 +917,90 @@ multidict==6.6.3 \ # -r requirements.in # aiohttp # yarl -orjson==3.11.0 \ - --hash=sha256:02dd4f0a1a2be943a104ce5f3ec092631ee3e9f0b4bb9eeee3400430bd94ddef \ - --hash=sha256:05f094edd2b782650b0761fd78858d9254de1c1286f5af43145b3d08cdacfd51 \ - --hash=sha256:0759b36428067dc777b202dd286fbdd33d7f261c6455c4238ea4e8474358b1e6 \ - --hash=sha256:0846e13abe79daece94a00b92574f294acad1d362be766c04245b9b4dd0e47e1 \ - --hash=sha256:08e191f8a55ac2c00be48e98a5d10dca004cbe8abe73392c55951bfda60fc123 \ - --hash=sha256:105bca887532dc71ce4b05a5de95dea447a310409d7a8cf0cb1c4a120469e9ad \ - --hash=sha256:1235fe7bbc37164f69302199d46f29cfb874018738714dccc5a5a44042c79c77 \ - --hash=sha256:1785df7ada75c18411ff7e20ac822af904a40161ea9dfe8c55b3f6b66939add6 \ - --hash=sha256:2560b740604751854be146169c1de7e7ee1e6120b00c1788ec3f3a012c6a243f \ - --hash=sha256:28acd19822987c5163b9e03a6e60853a52acfee384af2b394d11cb413b889246 \ - --hash=sha256:2a585042104e90a61eda2564d11317b6a304eb4e71cd33e839f5af6be56c34d3 \ - --hash=sha256:2e4c129da624f291bcc607016a99e7f04a353f6874f3bd8d9b47b88597d5f700 \ - --hash=sha256:2fb8ca8f0b4e31b8aaec674c7540649b64ef02809410506a44dc68d31bd5647b \ - --hash=sha256:325be41a8d7c227d460a9795a181511ba0e731cf3fee088c63eb47e706ea7559 \ - --hash=sha256:359cbe11bc940c64cb3848cf22000d2aef36aff7bfd09ca2c0b9cb309c387132 \ - --hash=sha256:41b38a894520b8cb5344a35ffafdf6ae8042f56d16771b2c5eb107798cee85ee \ - --hash=sha256:4305a638f4cf9bed3746ca3b7c242f14e05177d5baec2527026e0f9ee6c24fb7 \ - --hash=sha256:4430ec6ff1a1f4595dd7e0fad991bdb2fed65401ed294984c490ffa025926325 \ - --hash=sha256:475491bb78af2a0170f49e90013f1a0f1286527f3617491f8940d7e5da862da7 \ - --hash=sha256:47a54e660414baacd71ebf41a69bb17ea25abb3c5b69ce9e13e43be7ac20e342 \ - --hash=sha256:4a8ba9698655e16746fdf5266939427da0f9553305152aeb1a1cc14974a19cfb \ - --hash=sha256:4bfcfe498484161e011f8190a400591c52b026de96b3b3cbd3f21e8999b9dc0e \ - --hash=sha256:51646f6d995df37b6e1b628f092f41c0feccf1d47e3452c6e95e2474b547d842 \ - --hash=sha256:51cdca2f36e923126d0734efaf72ddbb5d6da01dbd20eab898bdc50de80d7b5a \ - --hash=sha256:5579acd235dd134467340b2f8a670c1c36023b5a69c6a3174c4792af7502bd92 \ - --hash=sha256:5587c85ae02f608a3f377b6af9eb04829606f518257cbffa8f5081c1aacf2e2f \ - --hash=sha256:57e8e7198a679ab21241ab3f355a7990c7447559e35940595e628c107ef23736 \ - --hash=sha256:5f797d57814975b78f5f5423acb003db6f9be5186b72d48bd97a1000e89d331d \ - --hash=sha256:613e54a2b10b51b656305c11235a9c4a5c5491ef5c283f86483d4e9e123ed5e4 \ - --hash=sha256:63c1c9772dafc811d16d6a7efa3369a739da15d1720d6e58ebe7562f54d6f4a2 \ - --hash=sha256:64a6a3e94a44856c3f6557e6aa56a6686544fed9816ae0afa8df9077f5759791 \ - --hash=sha256:67133847f9a35a5ef5acfa3325d4a2f7fe05c11f1505c4117bb086fc06f2a58f \ - --hash=sha256:6d09176a4a9e04a5394a4a0edd758f645d53d903b306d02f2691b97d5c736a9e \ - --hash=sha256:6d750b97d22d5566955e50b02c622f3a1d32744d7a578c878b29a873190ccb7a \ - --hash=sha256:720b4bb5e1b971960a62c2fa254c2d2a14e7eb791e350d05df8583025aa59d15 \ - --hash=sha256:7cf728cb3a013bdf9f4132575404bf885aa773d8bb4205656575e1890fc91990 \ - --hash=sha256:8335a0ba1c26359fb5c82d643b4c1abbee2bc62875e0f2b5bde6c8e9e25eb68c \ - --hash=sha256:84ae3d329360cf18fb61b67c505c00dedb61b0ee23abfd50f377a58e7d7bed06 \ - --hash=sha256:8514f9f9c667ce7d7ef709ab1a73e7fcab78c297270e90b1963df7126d2b0e23 \ - --hash=sha256:894635df36c0be32f1c8c8607e853b8865edb58e7618e57892e85d06418723eb \ - --hash=sha256:8bf058105a8aed144e0d1cfe7ac4174748c3fc7203f225abaeac7f4121abccb0 \ - --hash=sha256:923301f33ea866b18f8836cf41d9c6d33e3b5cab8577d20fed34ec29f0e13a0d \ - --hash=sha256:93b64b254414e2be55ac5257124b5602c5f0b4d06b80bd27d1165efe8f36e836 \ - --hash=sha256:9457ccbd8b241fb4ba516417a4c5b95ba0059df4ac801309bcb4ec3870f45ad9 \ - --hash=sha256:99d17aab984f4d029b8f3c307e6be3c63d9ee5ef55e30d761caf05e883009949 \ - --hash=sha256:9b6fbc2fc825aff1456dd358c11a0ad7912a4cb4537d3db92e5334af7463a967 \ - --hash=sha256:9d4d86910554de5c9c87bc560b3bdd315cc3988adbdc2acf5dda3797079407ed \ - --hash=sha256:9dac7fbf3b8b05965986c5cfae051eb9a30fced7f15f1d13a5adc608436eb486 \ - --hash=sha256:a2788f741e5a0e885e5eaf1d91d0c9106e03cb9575b0c55ba36fd3d48b0b1e9b \ - --hash=sha256:a57899bebbcea146616a2426d20b51b3562b4bc9f8039a3bd14fae361c23053d \ - --hash=sha256:a640e3954e7b4fcb160097551e54cafbde9966be3991932155b71071077881aa \ - --hash=sha256:aa1120607ec8fc98acf8c54aac6fb0b7b003ba883401fa2d261833111e2fa071 \ - --hash=sha256:acf5a63ae9cdb88274126af85913ceae554d8fd71122effa24a53227abbeee16 \ - --hash=sha256:b4089f940c638bb1947d54e46c1cd58f4259072fcc97bc833ea9c78903150ac9 \ - --hash=sha256:b5a4214ea59c8a3b56f8d484b28114af74e9fba0956f9be5c3ce388ae143bf1f \ - --hash=sha256:b5a8243e73690cc6e9151c9e1dd046a8f21778d775f7d478fa1eb4daa4897c61 \ - --hash=sha256:b8913baba9751f7400f8fa4ec18a8b618ff01177490842e39e47b66c1b04bc79 \ - --hash=sha256:c27de273320294121200440cd5002b6aeb922d3cb9dab3357087c69f04ca6934 \ - --hash=sha256:c4b48d9775b0cf1f0aca734f4c6b272cbfacfac38e6a455e6520662f9434afb7 \ - --hash=sha256:c60c99fe1e15894367b0340b2ff16c7c69f9c3f3a54aa3961a58c102b292ad94 \ - --hash=sha256:c7a1964a71c1567b4570c932a0084ac24ad52c8cf6253d1881400936565ed438 \ - --hash=sha256:d2218629dbfdeeb5c9e0573d59f809d42f9d49ae6464d2f479e667aee14c3ef4 \ - --hash=sha256:d69f95d484938d8fab5963e09131bcf9fbbb81fa4ec132e316eb2fb9adb8ce78 \ - --hash=sha256:d79c180cfb3ae68f13245d0ff551dca03d96258aa560830bf8a223bd68d8272c \ - --hash=sha256:d9760217b84d1aee393b4436fbe9c639e963ec7bc0f2c074581ce5fb3777e466 \ - --hash=sha256:dd7f9cd995da9e46fbac0a371f0ff6e89a21d8ecb7a8a113c0acb147b0a32f73 \ - --hash=sha256:e8d38d9e1e2cf9729658e35956cf01e13e89148beb4cb9e794c9c10c5cb252f8 \ - --hash=sha256:e98f02e23611763c9e5dfcb83bd33219231091589f0d1691e721aea9c52bf329 \ - --hash=sha256:ebeecd5d5511b3ca9dc4e7db0ab95266afd41baf424cc2fad8c2d3a3cdae650a \ - --hash=sha256:f018ed1986d79434ac712ff19f951cd00b4dfcb767444410fbb834ebec160abf \ - --hash=sha256:fe36e5012f886ff91c68b87a499c227fa220e9668cea96335219874c8be5fab5 \ - --hash=sha256:feaed3ed43a1d2df75c039798eb5ec92c350c7d86be53369bafc4f3700ce7df2 +orjson==3.11.3 \ + --hash=sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904 \ + --hash=sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873 \ + --hash=sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d \ + --hash=sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710 \ + --hash=sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f \ + --hash=sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7 \ + --hash=sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce \ + --hash=sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a \ + --hash=sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077 \ + --hash=sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b \ + --hash=sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f \ + --hash=sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb \ + --hash=sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225 \ + --hash=sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae \ + --hash=sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7 \ + --hash=sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451 \ + --hash=sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55 \ + --hash=sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804 \ + --hash=sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca \ + --hash=sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f \ + --hash=sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f \ + --hash=sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667 \ + --hash=sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c \ + --hash=sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467 \ + --hash=sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a \ + --hash=sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27 \ + --hash=sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb \ + --hash=sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b \ + --hash=sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204 \ + --hash=sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de \ + --hash=sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167 \ + --hash=sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1 \ + --hash=sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee \ + --hash=sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f \ + --hash=sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d \ + --hash=sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7 \ + --hash=sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a \ + --hash=sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b \ + --hash=sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a \ + --hash=sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc \ + --hash=sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa \ + --hash=sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770 \ + --hash=sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120 \ + --hash=sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2 \ + --hash=sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f \ + --hash=sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2 \ + --hash=sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc \ + --hash=sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43 \ + --hash=sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872 \ + --hash=sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e \ + --hash=sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be \ + --hash=sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810 \ + --hash=sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6 \ + --hash=sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2 \ + --hash=sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91 \ + --hash=sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc \ + --hash=sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424 \ + --hash=sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e \ + --hash=sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23 \ + --hash=sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1 \ + --hash=sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f \ + --hash=sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d \ + --hash=sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4 \ + --hash=sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f \ + --hash=sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229 \ + --hash=sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d \ + --hash=sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf \ + --hash=sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4 \ + --hash=sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038 \ + --hash=sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045 \ + --hash=sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633 \ + --hash=sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064 \ + --hash=sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc \ + --hash=sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049 \ + --hash=sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b \ + --hash=sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824 \ + --hash=sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c \ + --hash=sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6 \ + --hash=sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2 \ + --hash=sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e \ + --hash=sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1 \ + --hash=sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569 \ + --hash=sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c # via # -r requirements.in # envoy-base-utils @@ -1148,9 +1173,9 @@ pyflakes==3.4.0 \ --hash=sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58 \ --hash=sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f # via flake8 -pygithub==2.6.1 \ - --hash=sha256:6f2fa6d076ccae475f9fc392cc6cdbd54db985d4f69b8833a28397de75ed6ca3 \ - --hash=sha256:b5c035392991cca63959e9453286b41b54d83bf2de2daa7d7ff7e4312cebf3bf +pygithub==2.8.1 \ + --hash=sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0 \ + --hash=sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9 # via -r requirements.in pygments==2.18.0 \ --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ @@ -1196,9 +1221,9 @@ pynacl==1.5.0 \ --hash=sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b \ --hash=sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543 # via pygithub -pyopenssl==25.1.0 \ - --hash=sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab \ - --hash=sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b +pyopenssl==25.3.0 \ + --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ + --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 # via # -r requirements.in # aioquic @@ -1488,78 +1513,6 @@ verboselogs==1.7 \ # envoy-distribution-repo # envoy-github-abstract # envoy-github-release -wrapt==1.16.0 \ - --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \ - --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \ - --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \ - --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \ - --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \ - --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \ - --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \ - --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \ - --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \ - --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \ - --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \ - --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \ - --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \ - --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \ - --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \ - --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \ - --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \ - --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \ - --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \ - --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \ - --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \ - --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \ - --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \ - --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \ - --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \ - --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \ - --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \ - --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \ - --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \ - --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \ - --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \ - --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \ - --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \ - --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \ - --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \ - --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \ - --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \ - --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \ - --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \ - --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \ - --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \ - --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \ - --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \ - --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \ - --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \ - --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \ - --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \ - --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \ - --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \ - --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \ - --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \ - --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \ - --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \ - --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \ - --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \ - --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \ - --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \ - --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \ - --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \ - --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \ - --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \ - --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \ - --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \ - --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \ - --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \ - --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \ - --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \ - --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \ - --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \ - --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4 - # via deprecated yamllint==1.35.1 \ --hash=sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3 \ --hash=sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd diff --git a/tools/code_format/config.yaml b/tools/code_format/config.yaml index 72850f3e73c77..bd4915bb4ad60 100644 --- a/tools/code_format/config.yaml +++ b/tools/code_format/config.yaml @@ -90,7 +90,7 @@ paths: - source/common/filter/config_discovery_impl.h - source/common/config/utility.h - source/common/matcher/matcher.h - - source/extensions/common/matcher/trie_matcher.h + - source/extensions/common/matcher/ip_range_matcher.h - envoy/common/exception.h - source/common/protobuf/utility.h # legacy core files which throw exceptions. We can add to this list but strongly prefer @@ -324,6 +324,8 @@ paths: - test/extensions/filters/http/common/fuzz/uber_filter.h - test/extensions/http/cache/file_system_http_cache/cache_file_header_proto_util_test.cc - test/tools/router_check/router_check.cc + - source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper.cc + - test/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/rc_connection_wrapper_test.cc # Files in these paths can use std::regex std_regex: @@ -414,17 +416,6 @@ replacements: "absl::make_unique<": "std::make_unique<" protobuf_type_errors: - # Well-known types should be referenced from the ProtobufWkt namespace. - "Protobuf::Any": "ProtobufWkt::Any" - "Protobuf::Empty": "ProtobufWkt::Empty" - "Protobuf::ListValue": "ProtobufWkt::ListValue" - "Protobuf::NULL_VALUE": "ProtobufWkt::NULL_VALUE" - "Protobuf::StringValue": "ProtobufWkt::StringValue" - "Protobuf::Struct": "ProtobufWkt::Struct" - "Protobuf::Value": "ProtobufWkt::Value" - # Other common mis-namespacing of protobuf types. - "ProtobufWkt::Map": "Protobuf::Map" - "ProtobufWkt::MapPair": "Protobuf::MapPair" "ProtobufUtil::MessageDifferencer": "Protobuf::util::MessageDifferencer" include_angle: "#include <" diff --git a/tools/extensions/extensions_schema.yaml b/tools/extensions/extensions_schema.yaml index 03e48628f01c8..4f701c040a1b7 100644 --- a/tools/extensions/extensions_schema.yaml +++ b/tools/extensions/extensions_schema.yaml @@ -19,6 +19,7 @@ builtin: - envoy.matching.inputs.direct_source_ip - envoy.matching.inputs.source_type - envoy.matching.inputs.server_name +- envoy.matching.inputs.network_namespace - envoy.matching.inputs.transport_protocol - envoy.matching.inputs.application_protocol - envoy.matching.inputs.filter_state @@ -64,6 +65,7 @@ security_postures: categories: - envoy.access_loggers - envoy.bootstrap +- envoy.built_in_formatters - envoy.clusters - envoy.compression.compressor - envoy.compression.decompressor @@ -120,6 +122,7 @@ categories: - envoy.common.key_value - envoy.network.dns_resolver - envoy.network.connection_balance +- envoy.resolvers - envoy.rbac.matchers - envoy.rbac.principals - envoy.rbac.audit_loggers diff --git a/tools/proto_format/format_api.py b/tools/proto_format/format_api.py index 1d4bbd72eec96..d21d9901aa861 100644 --- a/tools/proto_format/format_api.py +++ b/tools/proto_format/format_api.py @@ -53,7 +53,7 @@ VERSIONING_BUILD_FILE_TEMPLATE = string.Template( """# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") licenses(["notice"]) # Apache 2 diff --git a/tools/protodoc/BUILD b/tools/protodoc/BUILD index 590013b8d3943..3befe9e2b63d6 100644 --- a/tools/protodoc/BUILD +++ b/tools/protodoc/BUILD @@ -1,6 +1,6 @@ load("@base_pip3//:requirements.bzl", "requirement") load("@com_github_grpc_grpc//bazel:python_rules.bzl", "py_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("//tools/base:envoy_python.bzl", "envoy_genjson", "envoy_jinja_env", "envoy_py_data", "envoy_pytool_binary", "envoy_pytool_library") load("//tools/protodoc:protodoc.bzl", "protodoc_rule") load("//tools/python:namespace.bzl", "envoy_py_namespace") diff --git a/tools/protojsonschema_with_aspects/protojsonschema.bzl b/tools/protojsonschema_with_aspects/protojsonschema.bzl index a1a5e38742ce7..786951f343489 100644 --- a/tools/protojsonschema_with_aspects/protojsonschema.bzl +++ b/tools/protojsonschema_with_aspects/protojsonschema.bzl @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "ProtoInfo") +load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") load("//tools/api_proto_plugin:plugin.bzl", "api_proto_plugin_aspect", "api_proto_plugin_impl") def _protojsonschema_impl(target, ctx): diff --git a/tools/repo/notify.py b/tools/repo/notify.py index a939ac9ac84a6..e61efeeeb0e11 100644 --- a/tools/repo/notify.py +++ b/tools/repo/notify.py @@ -30,7 +30,20 @@ ENVOY_REPO = "envoyproxy/envoy" # Oncall calendar -CALENDAR = "https://calendar.google.com/calendar/ical/d6glc0l5rc3v235q9l2j29dgovh3dn48%40import.calendar.google.com/public/basic.ics" +# This calendar is currently in the Google calndar account of @adisuissa. Once +# his account is closed, the calendar will not be available. In order to create +# a new calendar link, please do the following: +# 1. Find the link on the opsgenie page -> "Who is on-call" -> "Envoy maintainer +# rotation" -> calendar icon on the top-right of the screen. This will point to +# a webcall link, similar to: +# webcal://kubernetes.app.opsgenie.com/webapi/webcal/getRecentSchedule?webcalToken=&scheduleId=a3505963-c064-4c97-8865-947dfcb06060 +# 2. Go to your personal Google calendar, and add a new one (press '+' next to +# "Other calendars") -> then press "From URL". +# 3. Paste the webcal link to the "URL of calendar", check the "Make the +# calendar publicly accessible", and press "Add calendar". +# 4. In the calendar settings you can now change the calendar's name, and copy +# paste the "public address in iCal format" link here. +CALENDAR = "https://calendar.google.com/calendar/ical/jlcv20uad0arnm7g69ip9iu956vvnrf6%40import.calendar.google.com/public/basic.ics" ISSUE_LINK = "https://github.com/envoyproxy/envoy/issues?q=is%3Aissue+is%3Aopen+label%3Atriage" SLACK_EXPORT_URL = "https://api.slack.com/apps/A023NPQQ33K/oauth?" @@ -130,16 +143,19 @@ async def oncall_string(self): # Handle the event being created before today. date = priorweek.strftime("%Y%m%d") - response = await self.session.get(f"{CALENDAR}?getdate={date}") - content = await response.read() - parsed_calendar = icalendar.Calendar.from_ical(content) - - for component in parsed_calendar.walk(): - if component.name == "VEVENT": - if (sunday.date() == component.decoded("dtstart").date()): - return component.get("summary") - if (monday.date() == component.decoded("dtstart").date()): - return component.get("summary") + try: + response = await self.session.get(f"{CALENDAR}?getdate={date}") + content = await response.read() + parsed_calendar = icalendar.Calendar.from_ical(content) + + for component in parsed_calendar.walk(): + if component.name == "VEVENT": + if (sunday.date() == component.decoded("dtstart").date()): + return component.get("summary") + if (monday.date() == component.decoded("dtstart").date()): + return component.get("summary") + except Exception as e: + print("Error while fetching and parsing the on-call calendar: {e}") print("unable to find this week's oncall") return "unable to find this week's oncall" diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 4d9fa4cf18fc6..330e33fecad2e 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -2,9 +2,15 @@ # lower case and title case (e.g. HTTP will accept http and Http). Entries in all lower case # will accept title case (e.g. lyft matches Lyft). Prefixes (e.g., un-) or suffixes (e.g. -ing) # are allowed for any otherwise correctly spelled word. + +addrlen +itr +STREQ +htonl ABI ACK ACL +ACCEPTOR AES AJAX AllMuxes @@ -37,6 +43,7 @@ Millicores NID NIST NORMALISATION +Radix SQLSTATE bm BSON @@ -47,6 +54,7 @@ CIO cbegin cend constness +cstr deadcode DFP Dynatrace @@ -113,6 +121,7 @@ STX SkyWalking TIDs Timedout +ULA WRSQ WASI ceil @@ -1004,6 +1013,7 @@ metaprogramming metatable microbenchmarks midp +migratable milli mimics misconfiguration @@ -1184,6 +1194,8 @@ querydetails quiesce quitquitquit qvalue +radix +radixtree rapidjson ratelimit ratelimited @@ -1240,6 +1252,7 @@ reperform repicked replayable repo +representable reproducibility requirepass reselecting @@ -1414,6 +1427,7 @@ templating templatize templatized templatizing +teredo testability testcase testcases @@ -1570,4 +1584,4 @@ NAT NXDOMAIN DNAT RSP -EWMA +EWMA \ No newline at end of file diff --git a/tools/testdata/check_format/proto_style.cc b/tools/testdata/check_format/proto_style.cc index 670bcbe6d2254..52fc3d8b60ed2 100644 --- a/tools/testdata/check_format/proto_style.cc +++ b/tools/testdata/check_format/proto_style.cc @@ -8,8 +8,8 @@ Protobuf::StringValue stringvalue; Protobuf::Struct struct; Protobuf::Value value; Protobuf::MapPair mappair; -ProtobufWkt::Map map; -ProtobufWkt::MapPair mappair; +Protobuf::Map map; +Protobuf::MapPair mappair; ProtobufUtil::MessageDifferencer messagedifferencer; } // namespace Envoy diff --git a/tools/testdata/check_format/proto_style.cc.gold b/tools/testdata/check_format/proto_style.cc.gold index 0f1dc985d72b9..67935d4d9bd5f 100644 --- a/tools/testdata/check_format/proto_style.cc.gold +++ b/tools/testdata/check_format/proto_style.cc.gold @@ -1,12 +1,12 @@ namespace Envoy { -ProtobufWkt::Any any; -ProtobufWkt::Empty empty; -ProtobufWkt::ListValue list_value; -ProtobufWkt::Value value = ProtobufWkt::NULL_VALUE; -ProtobufWkt::StringValue stringvalue; -ProtobufWkt::Struct struct; -ProtobufWkt::Value value; +Protobuf::Any any; +Protobuf::Empty empty; +Protobuf::ListValue list_value; +Protobuf::Value value = Protobuf::NULL_VALUE; +Protobuf::StringValue stringvalue; +Protobuf::Struct struct; +Protobuf::Value value; Protobuf::MapPair mappair; Protobuf::Map map; Protobuf::MapPair mappair; diff --git a/tools/testdata/check_format/serialize_as_string.cc b/tools/testdata/check_format/serialize_as_string.cc index 705bf5dae77ff..ed2eeee7503d5 100644 --- a/tools/testdata/check_format/serialize_as_string.cc +++ b/tools/testdata/check_format/serialize_as_string.cc @@ -1,7 +1,7 @@ namespace Envoy { void use_serialize_as_string() { - google::protobuf::FieldMask mask; + Protobuf::FieldMask mask; const std::string key = mask.SerializeAsString(); } diff --git a/tools/testdata/protoxform/BUILD b/tools/testdata/protoxform/BUILD index b9e228605acd6..1cda001159e2e 100644 --- a/tools/testdata/protoxform/BUILD +++ b/tools/testdata/protoxform/BUILD @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") licenses(["notice"]) # Apache 2 diff --git a/tools/testdata/protoxform/envoy/v2/BUILD b/tools/testdata/protoxform/envoy/v2/BUILD index 2c6796578946d..23a0e4bc26dd3 100644 --- a/tools/testdata/protoxform/envoy/v2/BUILD +++ b/tools/testdata/protoxform/envoy/v2/BUILD @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") licenses(["notice"]) # Apache 2 diff --git a/tools/testdata/protoxform/external/BUILD b/tools/testdata/protoxform/external/BUILD index 3908c1ec3a49f..d08642ef346cc 100644 --- a/tools/testdata/protoxform/external/BUILD +++ b/tools/testdata/protoxform/external/BUILD @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") licenses(["notice"]) # Apache 2 diff --git a/tools/type_whisperer/file_descriptor_set_text.bzl b/tools/type_whisperer/file_descriptor_set_text.bzl index 5e1a858b4563e..ef16e9077037b 100644 --- a/tools/type_whisperer/file_descriptor_set_text.bzl +++ b/tools/type_whisperer/file_descriptor_set_text.bzl @@ -1,4 +1,4 @@ -load("@rules_proto//proto:defs.bzl", "ProtoInfo") +load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") def _file_descriptor_set_text(ctx): file_descriptor_sets = depset( diff --git a/tools/type_whisperer/proto_build_targets_gen.py b/tools/type_whisperer/proto_build_targets_gen.py index 947ea220154fd..2f005b52b6448 100644 --- a/tools/type_whisperer/proto_build_targets_gen.py +++ b/tools/type_whisperer/proto_build_targets_gen.py @@ -38,7 +38,8 @@ API_BUILD_FILE_TEMPLATE = string.Template( """# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. -load("@rules_proto//proto:defs.bzl", "proto_descriptor_set", "proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") licenses(["notice"]) # Apache 2 diff --git a/tools/vscode/generate_debug_config.py b/tools/vscode/generate_debug_config.py index 24fec97e7e9f3..111d45d2ba25f 100755 --- a/tools/vscode/generate_debug_config.py +++ b/tools/vscode/generate_debug_config.py @@ -38,11 +38,20 @@ def binary_path(bazel_bin, target): *[s for s in target.replace('@', 'external/').replace(':', '/').split('/') if s != '']) -def build_binary_with_debug_info(target): - subprocess.check_call(["bazel", *BAZEL_STARTUP_OPTIONS, "build", "-c", "dbg", target] - + BAZEL_OPTIONS) +def build_binary_with_debug_info(target, config=None): + build_args = ["bazel", *BAZEL_STARTUP_OPTIONS, "build"] + info_args = [] - bazel_bin = bazel_info("bazel-bin", ["-c", "dbg"]) + if config: + build_args.extend([f"--config={config}"]) + info_args.extend([f"--config={config}"]) + + build_args.extend(["-c", "dbg", target]) + build_args.extend(BAZEL_OPTIONS) + + subprocess.check_call(build_args) + + bazel_bin = bazel_info("bazel-bin", info_args + ["-c", "dbg"]) return binary_path(bazel_bin, target) @@ -131,10 +140,28 @@ def add_to_launch_json(target, binary, workspace, execroot, arguments, debugger_ write_launch_json(workspace, launch) +def auto_detect_config(): + """Auto-detect the best compiler configuration based on platform and availability.""" + try: + # Check if we're on ARM64 architecture + if platform.machine() in ['aarch64', 'arm64']: + # On ARM64, prefer clang for better compatibility + return "clang" + # On other architectures, try to detect available compilers + # This is a simple heuristic - could be made more sophisticated + return None # Let Bazel use its default + except: + return None + + if __name__ == "__main__": parser = argparse.ArgumentParser(description='Build and generate launch config for VSCode') parser.add_argument('--debugger', default="gdb", help="debugger type, one of [gdb, lldb]") parser.add_argument('--args', default='', help="command line arguments if target binary") + parser.add_argument( + '--config', + default=None, + help="bazel build config (e.g., clang, gcc). Auto-detected if not specified") parser.add_argument( '--overwrite', action="store_true", @@ -142,9 +169,16 @@ def add_to_launch_json(target, binary, workspace, execroot, arguments, debugger_ parser.add_argument('target', help="target binary which you want to build") args = parser.parse_args() + # Auto-detect config if not specified + config = args.config if args.config else auto_detect_config() + if config: + print(f"Using build config: {config}") + else: + print("Using default Bazel configuration") + workspace = get_workspace() execution_root = get_execution_root(workspace) - debug_binary = build_binary_with_debug_info(args.target) + debug_binary = build_binary_with_debug_info(args.target, config) add_to_launch_json( args.target, debug_binary, workspace, execution_root, args.args, args.debugger, args.overwrite)