From a99d01c99fa1cc6ebea3115faa570e2ba23548d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:11:52 +0000 Subject: [PATCH 1/4] Initial plan From 706db49ae5aa9cd8a064546ae08e89e3897fd6a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:34:20 +0000 Subject: [PATCH 2/4] Add dynamic-modules-jq example with Rust jq body transform filter Co-authored-by: phlax <454682+phlax@users.noreply.github.com> --- dynamic-modules-jq/Cargo.lock | 691 +++++++++++++++++++++++ dynamic-modules-jq/Cargo.toml | 16 + dynamic-modules-jq/Dockerfile | 36 ++ dynamic-modules-jq/README.md | 2 + dynamic-modules-jq/docker-compose.yaml | 14 + dynamic-modules-jq/envoy.yaml | 56 ++ dynamic-modules-jq/jq-libs/transforms.jq | 11 + dynamic-modules-jq/src/lib.rs | 289 ++++++++++ dynamic-modules-jq/verify.sh | 33 ++ 9 files changed, 1148 insertions(+) create mode 100644 dynamic-modules-jq/Cargo.lock create mode 100644 dynamic-modules-jq/Cargo.toml create mode 100644 dynamic-modules-jq/Dockerfile create mode 100644 dynamic-modules-jq/README.md create mode 100644 dynamic-modules-jq/docker-compose.yaml create mode 100644 dynamic-modules-jq/envoy.yaml create mode 100644 dynamic-modules-jq/jq-libs/transforms.jq create mode 100644 dynamic-modules-jq/src/lib.rs create mode 100755 dynamic-modules-jq/verify.sh diff --git a/dynamic-modules-jq/Cargo.lock b/dynamic-modules-jq/Cargo.lock new file mode 100644 index 00000000..c2ddc3c6 --- /dev/null +++ b/dynamic-modules-jq/Cargo.lock @@ -0,0 +1,691 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "envoy-dynamic-module-jq" +version = "0.1.0" +dependencies = [ + "envoy-proxy-dynamic-modules-rust-sdk", + "jaq-core", + "jaq-json", + "jaq-std", + "serde", + "serde_json", +] + +[[package]] +name = "envoy-proxy-dynamic-modules-rust-sdk" +version = "0.1.0" +source = "git+https://github.com/envoyproxy/envoy?rev=6d9bb7d9a85d616b220d1f8fe67b61f82bbdb8d3#6d9bb7d9a85d616b220d1f8fe67b61f82bbdb8d3" +dependencies = [ + "bindgen", + "mockall", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hifijson" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7763b98ba8a24f59e698bf9ab197e7676c640d6455d1580b4ce7dc560f0f0d" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jaq-core" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77526a72eb79412c29fd141767a6549bbfcb1cb40e00556fe16532d5e878e098" +dependencies = [ + "dyn-clone", + "once_cell", + "typed-arena", +] + +[[package]] +name = "jaq-json" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01dbdbd07b076e8403abac68ce7744d93e2ecd953bbc44bf77bf00e1e81172bc" +dependencies = [ + "foldhash", + "hifijson", + "indexmap", + "jaq-core", + "jaq-std", + "serde_json", +] + +[[package]] +name = "jaq-std" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c264fe397c981705976c71f1bfe020382b9eda52ae950e57fe885e147bdd67d" +dependencies = [ + "aho-corasick", + "base64", + "chrono", + "jaq-core", + "libm", + "log", + "regex-lite", + "urlencoding", +] + +[[package]] +name = "js-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "wasm-bindgen" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/dynamic-modules-jq/Cargo.toml b/dynamic-modules-jq/Cargo.toml new file mode 100644 index 00000000..f84a0bbf --- /dev/null +++ b/dynamic-modules-jq/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "envoy-dynamic-module-jq" +version = "0.1.0" +edition = "2021" + +[lib] +name = "envoy_dynamic_module_jq" +crate-type = ["cdylib"] + +[dependencies] +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"] } diff --git a/dynamic-modules-jq/Dockerfile b/dynamic-modules-jq/Dockerfile new file mode 100644 index 00000000..c2dddee3 --- /dev/null +++ b/dynamic-modules-jq/Dockerfile @@ -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"] diff --git a/dynamic-modules-jq/README.md b/dynamic-modules-jq/README.md new file mode 100644 index 00000000..93b591f0 --- /dev/null +++ b/dynamic-modules-jq/README.md @@ -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). diff --git a/dynamic-modules-jq/docker-compose.yaml b/dynamic-modules-jq/docker-compose.yaml new file mode 100644 index 00000000..9b8c3855 --- /dev/null +++ b/dynamic-modules-jq/docker-compose.yaml @@ -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 diff --git a/dynamic-modules-jq/envoy.yaml b/dynamic-modules-jq/envoy.yaml new file mode 100644 index 00000000..e5f68e03 --- /dev/null +++ b/dynamic-modules-jq/envoy.yaml @@ -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(.internal_id, .debug_info)" + } + - 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 diff --git a/dynamic-modules-jq/jq-libs/transforms.jq b/dynamic-modules-jq/jq-libs/transforms.jq new file mode 100644 index 00000000..506b59a3 --- /dev/null +++ b/dynamic-modules-jq/jq-libs/transforms.jq @@ -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}; diff --git a/dynamic-modules-jq/src/lib.rs b/dynamic-modules-jq/src/lib.rs new file mode 100644 index 00000000..7858023b --- /dev/null +++ b/dynamic-modules-jq/src/lib.rs @@ -0,0 +1,289 @@ +//! Envoy Dynamic Module: jq/jaq HTTP Body Transform Filter +//! +//! This module implements an HTTP filter that transforms request and/or response JSON bodies +//! using jq programs compiled with jaq-core. +//! +//! Configuration is a JSON object with optional fields: +//! - `request_program`: jq program string to transform the request body +//! - `response_program`: jq program string to transform the response body +//! - `max_body_bytes`: maximum body size to buffer (default: 1MB); larger bodies pass through + +use envoy_proxy_dynamic_modules_rust_sdk::*; +use jaq_core::{load, Compiler, Ctx, Native, RcIter}; +use jaq_json::Val; +use load::{Arena, File, Loader}; +use serde::Deserialize; + +declare_init_functions!(init, new_http_filter_config_fn); + +fn init() -> bool { + true +} + +fn new_http_filter_config_fn( + _envoy_filter_config: &mut EC, + _filter_name: &str, + filter_config: &[u8], +) -> Option>> { + let config_str = std::str::from_utf8(filter_config).unwrap_or(""); + FilterConfig::new(config_str).map(|c| Box::new(c) as Box>) +} + +/// Raw deserialized config from the Envoy filter config JSON. +#[derive(Deserialize)] +struct FilterConfigData { + request_program: Option, + response_program: Option, + max_body_bytes: Option, +} + +/// Compiled filter configuration, shared across all requests. +pub struct FilterConfig { + request_filter: Option>>, + response_filter: Option>>, + max_body_bytes: usize, +} + +const DEFAULT_MAX_BODY_BYTES: usize = 1024 * 1024; // 1MB + +fn compile_jq(program_str: &str) -> Option>> { + let program = File { + code: program_str, + path: (), + }; + let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs())); + let arena = Arena::default(); + let modules = match loader.load(&arena, program) { + Ok(m) => m, + Err(e) => { + eprintln!("[dynamic-modules-jq] jq parse error: {:?}", e); + return None; + } + }; + match Compiler::default() + .with_funs(jaq_std::funs().chain(jaq_json::funs())) + .compile(modules) + { + Ok(f) => Some(f), + Err(e) => { + eprintln!("[dynamic-modules-jq] jq compile error: {:?}", e); + None + } + } +} + +fn run_jq( + filter: &jaq_core::Filter>, + input: serde_json::Value, +) -> Result { + let inputs = RcIter::new(core::iter::empty()); + let mut results = filter.run((Ctx::new([], &inputs), Val::from(input))); + match results.next() { + Some(Ok(v)) => Ok(serde_json::Value::from(v)), + Some(Err(e)) => Err(format!("jq execution error: {:?}", e)), + None => Err("jq filter produced no output".to_string()), + } +} + +impl FilterConfig { + fn new(config_str: &str) -> Option { + let data: FilterConfigData = match serde_json::from_str(config_str) { + Ok(d) => d, + Err(e) => { + eprintln!("[dynamic-modules-jq] config parse error: {}", e); + return None; + } + }; + + let request_filter = match &data.request_program { + Some(p) => match compile_jq(p) { + Some(f) => Some(f), + None => return None, + }, + None => None, + }; + + let response_filter = match &data.response_program { + Some(p) => match compile_jq(p) { + Some(f) => Some(f), + None => return None, + }, + None => None, + }; + + Some(FilterConfig { + request_filter, + response_filter, + max_body_bytes: data.max_body_bytes.unwrap_or(DEFAULT_MAX_BODY_BYTES), + }) + } +} + +impl HttpFilterConfig for FilterConfig { + fn new_http_filter(&self, _envoy: &mut EHF) -> Box> { + Box::new(Filter { + request_filter: self.request_filter.clone(), + response_filter: self.response_filter.clone(), + max_body_bytes: self.max_body_bytes, + }) + } +} + +/// Per-request filter state. +pub struct Filter { + request_filter: Option>>, + response_filter: Option>>, + max_body_bytes: usize, +} + +fn is_json_content_type(ct: &[u8]) -> bool { + let ct = std::str::from_utf8(ct).unwrap_or(""); + ct.contains("application/json") +} + +fn transform_jq( + jq_filter: &jaq_core::Filter>, + body_bytes: &[u8], +) -> Result, String> { + // Parse JSON + let input: serde_json::Value = + serde_json::from_slice(body_bytes).map_err(|e| format!("JSON parse error: {}", e))?; + + // Run jq filter + let output = run_jq(jq_filter, input)?; + + // Serialize output + serde_json::to_vec(&output).map_err(|e| format!("JSON serialize error: {}", e)) +} + +fn collect_body(body_data: Vec>) -> Vec { + let mut body = Vec::new(); + for chunk in &body_data { + body.extend_from_slice(chunk.as_slice()); + } + body +} + +impl HttpFilter for Filter { + fn on_request_body( + &mut self, + envoy_filter: &mut EHF, + end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_body_status { + let jq_filter = match &self.request_filter { + Some(f) => f, + None => { + return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::Continue + } + }; + + if !end_of_stream { + return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::StopIterationAndBuffer; + } + + // Check Content-Type + let is_json = envoy_filter + .get_request_header_value("content-type") + .map(|ct| is_json_content_type(ct.as_slice())) + .unwrap_or(false); + + if !is_json { + return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::Continue; + } + + // Check body size before reading + let body_size = envoy_filter.get_buffered_request_body_size(); + if body_size > self.max_body_bytes { + return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::Continue; + } + + // Collect body into owned bytes then drop the EnvoyMutBuffer borrows + let body_bytes = { + let body_data = match envoy_filter.get_buffered_request_body() { + Some(d) => d, + None => { + return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::Continue + } + }; + collect_body(body_data) + }; + + let result = transform_jq(jq_filter, &body_bytes); + match result { + Ok(output_bytes) => { + envoy_filter.drain_buffered_request_body(body_bytes.len()); + envoy_filter.append_buffered_request_body(&output_bytes); + let new_len = output_bytes.len().to_string(); + envoy_filter.set_request_header("content-length", new_len.as_bytes()); + } + Err(e) => { + let msg = format!("jq transform error: {}", e); + envoy_filter.send_response(500, vec![], Some(msg.as_bytes()), None); + return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::StopIterationNoBuffer; + } + } + + abi::envoy_dynamic_module_type_on_http_filter_request_body_status::Continue + } + + fn on_response_body( + &mut self, + envoy_filter: &mut EHF, + end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_response_body_status { + let jq_filter = match &self.response_filter { + Some(f) => f, + None => { + return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::Continue + } + }; + + if !end_of_stream { + return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::StopIterationAndBuffer; + } + + // Check Content-Type + let is_json = envoy_filter + .get_response_header_value("content-type") + .map(|ct| is_json_content_type(ct.as_slice())) + .unwrap_or(false); + + if !is_json { + return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::Continue; + } + + // Check body size before reading + let body_size = envoy_filter.get_buffered_response_body_size(); + if body_size > self.max_body_bytes { + return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::Continue; + } + + // Collect body into owned bytes then drop the EnvoyMutBuffer borrows + let body_bytes = { + let body_data = match envoy_filter.get_buffered_response_body() { + Some(d) => d, + None => { + return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::Continue + } + }; + collect_body(body_data) + }; + + let result = transform_jq(jq_filter, &body_bytes); + match result { + Ok(output_bytes) => { + envoy_filter.drain_buffered_response_body(body_bytes.len()); + envoy_filter.append_buffered_response_body(&output_bytes); + let new_len = output_bytes.len().to_string(); + envoy_filter.set_response_header("content-length", new_len.as_bytes()); + } + Err(e) => { + let msg = format!("jq transform error: {}", e); + envoy_filter.send_response(500, vec![], Some(msg.as_bytes()), None); + return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::StopIterationNoBuffer; + } + } + + abi::envoy_dynamic_module_type_on_http_filter_response_body_status::Continue + } +} diff --git a/dynamic-modules-jq/verify.sh b/dynamic-modules-jq/verify.sh new file mode 100755 index 00000000..6bf58f80 --- /dev/null +++ b/dynamic-modules-jq/verify.sh @@ -0,0 +1,33 @@ +#!/bin/bash -e + +export NAME=dynamic-modules-jq +export PORT_PROXY="${DYNAMIC_MODULES_JQ_PORT_PROXY:-10530}" + +# shellcheck source=verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + +wait_for 30 bash -c "responds_with 'POST' http://localhost:${PORT_PROXY}" + +run_log "Send a request with sensitive fields — the password should be stripped before reaching upstream" +responds_with \ + "alice" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"user":"alice","password":"s3cr3t"}' \ + "http://localhost:${PORT_PROXY}" + +run_log "Verify the password value is absent from what the upstream echoed back" +responds_without \ + "s3cr3t" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"user":"alice","password":"s3cr3t"}' \ + "http://localhost:${PORT_PROXY}" + +run_log "Non-JSON bodies pass through unchanged" +responds_with \ + "hello" \ + -X POST \ + -H "Content-Type: text/plain" \ + -d "hello" \ + "http://localhost:${PORT_PROXY}" From 6713774ee17531f7af32b63a535ba726e450be23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:40:06 +0000 Subject: [PATCH 3/4] Fix code review issues: extract helper, improve error messages, document SDK pin Co-authored-by: phlax <454682+phlax@users.noreply.github.com> --- dynamic-modules-jq/Cargo.toml | 2 ++ dynamic-modules-jq/src/lib.rs | 36 +++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dynamic-modules-jq/Cargo.toml b/dynamic-modules-jq/Cargo.toml index f84a0bbf..99a27359 100644 --- a/dynamic-modules-jq/Cargo.toml +++ b/dynamic-modules-jq/Cargo.toml @@ -8,6 +8,8 @@ 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" diff --git a/dynamic-modules-jq/src/lib.rs b/dynamic-modules-jq/src/lib.rs index 7858023b..2f3905f7 100644 --- a/dynamic-modules-jq/src/lib.rs +++ b/dynamic-modules-jq/src/lib.rs @@ -85,6 +85,17 @@ fn run_jq( } } +/// Compile an optional jq program string into a filter, returning `None` if the program is +/// absent or fails to compile (compilation errors are logged to stderr). +fn compile_optional_jq( + program: Option<&str>, +) -> Option>>> { + match program { + Some(p) => compile_jq(p).map(Some), + None => Some(None), + } +} + impl FilterConfig { fn new(config_str: &str) -> Option { let data: FilterConfigData = match serde_json::from_str(config_str) { @@ -95,21 +106,8 @@ impl FilterConfig { } }; - let request_filter = match &data.request_program { - Some(p) => match compile_jq(p) { - Some(f) => Some(f), - None => return None, - }, - None => None, - }; - - let response_filter = match &data.response_program { - Some(p) => match compile_jq(p) { - Some(f) => Some(f), - None => return None, - }, - None => None, - }; + let request_filter = compile_optional_jq(data.request_program.as_deref())?; + let response_filter = compile_optional_jq(data.response_program.as_deref())?; Some(FilterConfig { request_filter, @@ -217,8 +215,8 @@ impl HttpFilter for Filter { envoy_filter.set_request_header("content-length", new_len.as_bytes()); } Err(e) => { - let msg = format!("jq transform error: {}", e); - envoy_filter.send_response(500, vec![], Some(msg.as_bytes()), None); + eprintln!("[dynamic-modules-jq] request body transform failed: {}", e); + envoy_filter.send_response(500, vec![], Some(b"body transform failed"), None); return abi::envoy_dynamic_module_type_on_http_filter_request_body_status::StopIterationNoBuffer; } } @@ -278,8 +276,8 @@ impl HttpFilter for Filter { envoy_filter.set_response_header("content-length", new_len.as_bytes()); } Err(e) => { - let msg = format!("jq transform error: {}", e); - envoy_filter.send_response(500, vec![], Some(msg.as_bytes()), None); + eprintln!("[dynamic-modules-jq] response body transform failed: {}", e); + envoy_filter.send_response(500, vec![], Some(b"body transform failed"), None); return abi::envoy_dynamic_module_type_on_http_filter_response_body_status::StopIterationNoBuffer; } } From 37bfe2c2d617cf031d0a8da3d034f2dff20d5f2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:09:53 +0000 Subject: [PATCH 4/4] Add docs-first example.rst for dynamic-modules-jq, update envoy.yaml and verify.sh Co-authored-by: phlax <454682+phlax@users.noreply.github.com> --- dynamic-modules-jq/envoy.yaml | 2 +- dynamic-modules-jq/example.rst | 189 +++++++++++++++++++++++++++++++++ dynamic-modules-jq/verify.sh | 13 ++- 3 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 dynamic-modules-jq/example.rst diff --git a/dynamic-modules-jq/envoy.yaml b/dynamic-modules-jq/envoy.yaml index e5f68e03..eb1d043c 100644 --- a/dynamic-modules-jq/envoy.yaml +++ b/dynamic-modules-jq/envoy.yaml @@ -35,7 +35,7 @@ static_resources: value: | { "request_program": "del(.password, .secret)", - "response_program": "del(.internal_id, .debug_info)" + "response_program": "del(.hostname)" } - name: envoy.filters.http.router typed_config: diff --git a/dynamic-modules-jq/example.rst b/dynamic-modules-jq/example.rst new file mode 100644 index 00000000..3d5ca28e --- /dev/null +++ b/dynamic-modules-jq/example.rst @@ -0,0 +1,189 @@ +.. _install_sandboxes_dynamic_modules_jq: + +Dynamic modules jq filter +========================== + +.. sidebar:: Requirements + + .. include:: _include/docker-env-setup-link.rst + + :ref:`curl ` + Used to make HTTP requests. + + :ref:`jq ` + Used to parse and pretty-print JSON responses. + +`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 `_ engine — a pure-Rust implementation of the +`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 ` + Architecture overview of the dynamic modules extension point. + + `envoyproxy/dynamic-modules-examples `_ + A companion repository with more dynamic module examples in both Rust and Go. + + `jaq `_ + The pure-Rust ``jq`` library used by this filter. + + `jq manual `_ + Reference for the ``jq`` filter language. diff --git a/dynamic-modules-jq/verify.sh b/dynamic-modules-jq/verify.sh index 6bf58f80..a6323140 100755 --- a/dynamic-modules-jq/verify.sh +++ b/dynamic-modules-jq/verify.sh @@ -6,9 +6,9 @@ export PORT_PROXY="${DYNAMIC_MODULES_JQ_PORT_PROXY:-10530}" # shellcheck source=verify-common.sh . "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" -wait_for 30 bash -c "responds_with 'POST' http://localhost:${PORT_PROXY}" +wait_for 30 bash -c "responds_with 'method' http://localhost:${PORT_PROXY}" -run_log "Send a request with sensitive fields — the password should be stripped before reaching upstream" +run_log "Request body: sensitive fields are stripped before reaching upstream" responds_with \ "alice" \ -X POST \ @@ -16,7 +16,7 @@ responds_with \ -d '{"user":"alice","password":"s3cr3t"}' \ "http://localhost:${PORT_PROXY}" -run_log "Verify the password value is absent from what the upstream echoed back" +run_log "Request body: stripped field is absent from what upstream echoes back" responds_without \ "s3cr3t" \ -X POST \ @@ -24,6 +24,13 @@ responds_without \ -d '{"user":"alice","password":"s3cr3t"}' \ "http://localhost:${PORT_PROXY}" +run_log "Response body: hostname field is stripped from upstream response" +responds_without \ + '"hostname"' \ + -H "Content-Type: application/json" \ + -d '{}' \ + "http://localhost:${PORT_PROXY}" + run_log "Non-JSON bodies pass through unchanged" responds_with \ "hello" \