Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
691 changes: 691 additions & 0 deletions dynamic-modules-jq/Cargo.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions dynamic-modules-jq/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "envoy-dynamic-module-jq"
version = "0.1.0"
edition = "2021"

[lib]
name = "envoy_dynamic_module_jq"
crate-type = ["cdylib"]

[dependencies]
# The SDK rev must exactly match the Envoy binary version due to strict ABI compatibility.
# This rev corresponds to Envoy v1.37.0. Update together with the Dockerfile ENVOY_VARIANT.
envoy-proxy-dynamic-modules-rust-sdk = { git = "https://github.com/envoyproxy/envoy", rev = "6d9bb7d9a85d616b220d1f8fe67b61f82bbdb8d3" }
jaq-core = "2.2.1"
jaq-std = "2.1.2"
jaq-json = { version = "1.1.3", features = ["serde_json"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
36 changes: 36 additions & 0 deletions dynamic-modules-jq/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
ARG ENVOY_IMAGE="${ENVOY_IMAGE:-envoyproxy/envoy}"
ARG ENVOY_VARIANT="${ENVOY_VARIANT:-v1.37-latest}"

# Stage 1: Build the Rust dynamic module
FROM rust:1.83-slim AS builder
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get -qq update -y \
&& apt-get -qq install --no-install-recommends -y \
clang \
libclang-dev \
pkg-config \
libssl-dev
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/root/.cargo/git \
--mount=type=cache,target=/build/target \
cargo build --release \
&& cp target/release/libenvoy_dynamic_module_jq.so /libenvoy_dynamic_module_jq.so

# Stage 2: Final Envoy image with the dynamic module
FROM ${ENVOY_IMAGE}:${ENVOY_VARIANT}
ENV DEBIAN_FRONTEND=noninteractive
RUN echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
&& apt-get -qq update -y \
&& apt-get -qq install --no-install-recommends -y curl
COPY --from=builder --chmod=755 /libenvoy_dynamic_module_jq.so /lib/libenvoy_dynamic_module_jq.so
COPY --chmod=644 jq-libs/ /jq-libs/
COPY --chmod=644 envoy.yaml /etc/envoy.yaml
CMD ["/usr/local/bin/envoy", "-c", "/etc/envoy.yaml"]
2 changes: 2 additions & 0 deletions dynamic-modules-jq/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
To learn about this sandbox and for instructions on how to run it please head over
to the [Envoy docs](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/dynamic-modules-jq.html).
14 changes: 14 additions & 0 deletions dynamic-modules-jq/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:

envoy:
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT_PROXY:-10000}:10000"
depends_on:
- upstream_service

upstream_service:
build:
context: ../shared/echo
56 changes: 56 additions & 0 deletions dynamic-modules-jq/envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
static_resources:
listeners:
- name: main
address:
socket_address:
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
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: upstream_service
http_filters:
- name: envoy.filters.http.dynamic_modules.jq_transform
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter
dynamic_module_config:
name: envoy_dynamic_module_jq
filter_name: jq_transform
filter_config:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"request_program": "del(.password, .secret)",
"response_program": "del(.hostname)"
}
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

clusters:
- name: upstream_service
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: upstream_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: upstream_service
port_value: 8080
189 changes: 189 additions & 0 deletions dynamic-modules-jq/example.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
.. _install_sandboxes_dynamic_modules_jq:

Dynamic modules jq filter
==========================

.. sidebar:: Requirements

.. include:: _include/docker-env-setup-link.rst

:ref:`curl <start_sandboxes_setup_curl>`
Used to make HTTP requests.

:ref:`jq <start_sandboxes_setup_jq>`
Used to parse and pretty-print JSON responses.

`Dynamic modules <https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/dynamic_modules>`_
let you extend Envoy by loading native shared libraries (``.so`` files) at runtime without recompiling
Envoy itself. This example ships a Rust module that embeds the
`jaq <https://github.com/01mf02/jaq>`_ engine — a pure-Rust implementation of the
`jq <https://jqlang.github.io/jq/>`_ JSON query language — to transform HTTP request and response
bodies inline, inside the Envoy filter chain.

Two independent ``jq`` programs are compiled once at filter-configuration time and then applied to
every matching HTTP body:

``request_program``
Transforms the **request body** before it is forwarded to the upstream service. In this example it
removes ``password`` and ``secret`` fields so they never reach the backend.

``response_program``
Transforms the **response body** before it is returned to the client. In this example it removes
the ``hostname`` field that the upstream echo service exposes, keeping backend topology private.

Step 1: Build the sandbox
*************************

Change to the ``dynamic-modules-jq`` directory. The first ``docker compose up`` compiles the Rust
module inside a builder container and then bakes the resulting ``.so`` file into the Envoy image.

.. code-block:: console

$ pwd
examples/dynamic-modules-jq
$ docker compose pull
$ docker compose up --build -d
$ docker compose ps

NAME SERVICE STATUS PORTS
dynamic-modules-jq-envoy-1 envoy running 0.0.0.0:10000->10000/tcp
dynamic-modules-jq-upstream_service-1 upstream_service running 8080/tcp

.. note::

The first build downloads the Envoy SDK from GitHub and compiles Rust crates, which can take
several minutes. Subsequent builds reuse the Docker layer cache and are much faster.

Step 2: Send a request through the proxy
*****************************************

The upstream service is a simple HTTP echo server. Without any transformation you would expect the
full request body to be visible in the upstream response.

Send a ``GET`` request to verify the proxy is running:

.. code-block:: console

$ curl -s http://localhost:10000 | jq .
{
"method": "GET",
"scheme": "http",
"path": "/",
"headers": { ... },
"query_params": {},
"body": ""
}

Notice that the ``hostname`` field is absent — it has been removed by the response ``jq`` program
(``del(.hostname)``) before the response reached your terminal.

Step 3: See the request body transform in action
*************************************************

Send a JSON body that contains a ``password`` field:

.. code-block:: console

$ curl -s http://localhost:10000 \
-X POST \
-H "Content-Type: application/json" \
-d '{"user": "alice", "password": "s3cr3t", "action": "login"}' \
| jq .body | jq -r .
{"user":"alice","action":"login"}

The upstream echo server shows only ``user`` and ``action`` in the body it received — ``password``
was stripped by the request ``jq`` program (``del(.password, .secret)``) before the request left
Envoy.

.. code-block:: console

$ curl -s http://localhost:10000 \
-X POST \
-H "Content-Type: application/json" \
-d '{"user": "alice", "password": "s3cr3t", "action": "login"}' \
| jq '.body | test("password")'
false

Step 4: See the response body transform in action
**************************************************

The upstream service always includes a ``hostname`` field in its response that identifies the backend
container. The response ``jq`` program removes it before the client sees the response.

Verify the field has been removed from the client-facing response:

.. code-block:: console

$ curl -s http://localhost:10000 | jq 'has("hostname")'
false

You can also confirm the other standard echo fields are still present:

.. code-block:: console

$ curl -s http://localhost:10000 | jq '{method, path}'
{
"method": "GET",
"path": "/"
}

Step 5: Update the jq programs
*******************************

The ``jq`` programs are part of the Envoy configuration in ``envoy.yaml``. To change them, edit
the ``filter_config`` value and rebuild the Envoy container.

The relevant section of ``envoy.yaml`` looks like this:

.. code-block:: yaml

filter_config:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"request_program": "del(.password, .secret)",
"response_program": "del(.hostname)"
}

For example, change the ``request_program`` to also remove an ``api_key`` field:

.. code-block:: yaml

filter_config:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"request_program": "del(.password, .secret, .api_key)",
"response_program": "del(.hostname)"
}

Then rebuild and restart the Envoy container:

.. code-block:: console

$ docker compose up --build -d envoy

Verify the new field is stripped:

.. code-block:: console

$ curl -s http://localhost:10000 \
-X POST \
-H "Content-Type: application/json" \
-d '{"user": "alice", "api_key": "sk-1234", "action": "query"}' \
| jq .body | jq -r .
{"user":"alice","action":"query"}

.. seealso::

:ref:`Envoy dynamic modules <arch_overview_dynamic_modules>`
Architecture overview of the dynamic modules extension point.

`envoyproxy/dynamic-modules-examples <https://github.com/envoyproxy/dynamic-modules-examples>`_
A companion repository with more dynamic module examples in both Rust and Go.

`jaq <https://github.com/01mf02/jaq>`_
The pure-Rust ``jq`` library used by this filter.

`jq manual <https://jqlang.github.io/jq/manual/>`_
Reference for the ``jq`` filter language.
11 changes: 11 additions & 0 deletions dynamic-modules-jq/jq-libs/transforms.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Example jq library demonstrating the import/include feature.
#
# Usage in envoy.yaml filter_config:
# request_program: 'import "transforms" as t; . | t::sanitize'
# response_program: 'import "transforms" as t; . | t::sanitize'

# Remove fields that should not leave the service boundary.
def sanitize: del(.internal_id, .debug_info, ._metadata);

# Reshape a user object to a public-facing representation.
def reshape: {id: .user_id, name: .display_name, email: .email};
Loading