diff --git a/.bcr/config.yml b/.bcr/config.yml index ca41922a..95797edf 100644 --- a/.bcr/config.yml +++ b/.bcr/config.yml @@ -1,3 +1,4 @@ moduleRoots: - "." +- filter-cc - wasm-cc diff --git a/.bcr/filter-cc/metadata.template.json b/.bcr/filter-cc/metadata.template.json new file mode 100644 index 00000000..4bf3fa1c --- /dev/null +++ b/.bcr/filter-cc/metadata.template.json @@ -0,0 +1,18 @@ +{ + "homepage": "https://www.envoyproxy.io/", + "maintainers": [ + { + "github": "mmorel-35", + "github_user_id": 6032561 + }, + { + "github": "phlax", + "github_user_id": 454682 + } + ], + "repository": [ + "github:envoyproxy/examples" + ], + "versions": [], + "yanked_versions": {} +} diff --git a/.bcr/filter-cc/presubmit.yml b/.bcr/filter-cc/presubmit.yml new file mode 100644 index 00000000..9eb77fb9 --- /dev/null +++ b/.bcr/filter-cc/presubmit.yml @@ -0,0 +1,16 @@ +matrix: + unix_platform: + - debian11 + - ubuntu2404 + - macos_arm64 + bazel: + - 7.x + - 8.x + - 9.* +tasks: + verify_targets: + name: Verify build targets + platform: ${{ unix_platform }} + bazel: ${{ bazel }} + build_targets: + - "@envoy-example-filter-cc//..." diff --git a/.bcr/filter-cc/source.template.json b/.bcr/filter-cc/source.template.json new file mode 100644 index 00000000..686a9ee3 --- /dev/null +++ b/.bcr/filter-cc/source.template.json @@ -0,0 +1,5 @@ +{ + "integrity": "", + "strip_prefix": "{REPO}-{VERSION}/filter-cc", + "url": "https://github.com/{OWNER}/{REPO}/archive/refs/tags/v{TAG}.tar.gz" +} diff --git a/.github/workflows/_verify_filter.yml b/.github/workflows/_verify_filter.yml new file mode 100644 index 00000000..4d49b983 --- /dev/null +++ b/.github/workflows/_verify_filter.yml @@ -0,0 +1,67 @@ +name: Verify/Filter + +permissions: + contents: read + +on: + workflow_call: + + +jobs: + filter: + runs-on: ubuntu-24.04 + steps: + - uses: envoyproxy/toolshed/actions/bind-mounts@598eacce15ab5f208102a2fd5669292868002701 # v0.4.0 + with: + mounts: | + - src: /mnt/docker + target: /var/lib/docker + rm: true + command-pre: sudo systemctl stop docker + command-post: sudo systemctl start docker + - src: /mnt/runner-cache + target: /home/runner/.cache + chown: "runner:runner" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: examples + - run: | + printf '%s\n' 'common --config=ci' > repo.bazelrc + working-directory: examples/filter-cc + - name: Inject bzlmod envoy pin into MODULE.bazel + run: | + # The envoy module is not (yet) published to a registry, so resolve + # the current envoy main HEAD and override the module to use it. + ENVOY_SHA=$(git ls-remote https://github.com/envoyproxy/envoy refs/heads/main | head -n1 | awk '{print $1}') + echo "Resolved envoy main HEAD: ${ENVOY_SHA}" + cat >> MODULE.bazel <` + Used to make HTTP requests. + +This sandbox demonstrates how to build a native C++ HTTP filter and statically link it +into the Envoy binary using `bzlmod `_. + +The example filter is a decoder filter which reads a ``key`` and ``val`` from its +configuration, and adds them as a header to proxied requests. + +The example provides all of the pieces required to build and use an Envoy extension: + +:download:`MODULE.bazel <_include/filter-cc/MODULE.bazel>` + Declares the module and its dependency on the ``envoy`` and ``envoy_api`` Bazel modules, + and sets up the compiler toolchain. This is the file to copy when starting your own extension. + +:download:`BUILD <_include/filter-cc/BUILD>` + Builds the filter's config proto, the filter itself and an Envoy binary with the + filter statically linked, using Envoy's build macros. + +:download:`http_filter.proto <_include/filter-cc/http_filter.proto>` + The configuration proto for the filter. + +:download:`http_filter.h <_include/filter-cc/http_filter.h>` / :download:`http_filter.cc <_include/filter-cc/http_filter.cc>` + The filter itself - an implementation of ``Envoy::Http::PassThroughDecoderFilter`` + which adds the configured header to requests. + +:download:`http_filter_config.cc <_include/filter-cc/http_filter_config.cc>` + The config factory which registers the filter with Envoy, making it available + as the ``sample`` filter in Envoy configuration. + +:download:`http_filter_integration_test.cc <_include/filter-cc/http_filter_integration_test.cc>` + An integration test which spins up the filter inside Envoy's HTTP integration test + framework and asserts the header is added. + +:download:`envoy.yaml <_include/filter-cc/envoy.yaml>` + An Envoy configuration using the ``sample`` filter. + +Step 1: Build the Envoy binary with the filter +********************************************** + +.. warning:: + + These instructions for building the binary use the + `envoyproxy/envoy-build-ubuntu `_ image. + You will need 4-5GB of disk space to accommodate this image, and building Envoy + itself requires significant time, cpu and memory. + +Export ``UID`` from your host system. This will ensure that the binary created inside the +build container has the same permissions as your host user: + +.. code-block:: console + + $ export UID + +Change to the ``filter-cc`` directory and build the Envoy binary with the ``sample`` +filter statically linked: + +.. code-block:: console + + $ pwd + examples/filter-cc + $ docker compose -f docker-compose-build.yaml run --remove-orphans filter_build + +The built binary should now be in the ``bin`` folder. + +.. code-block:: console + + $ ls -l bin + total 803408 + -r-xr-xr-x 1 user user 822683320 Oct 20 10:16 envoy + +Step 2: Start all of our containers +*********************************** + +Start the composition - an Envoy proxy which uses the binary built in Step 1, and a +backend which echos back our request: + +.. code-block:: console + + $ pwd + examples/filter-cc + $ docker compose up --build -d + $ docker compose ps + + NAME COMMAND SERVICE STATUS PORTS + filter-cc-proxy-1 "/usr/local/bin/envo…" proxy running 0.0.0.0:8000->8000/tcp + filter-cc-web_service-1 "/bin/echo-server" web_service running 8080/tcp + +Step 3: Check the filter has added its header +********************************************* + +The ``sample`` filter is configured in :download:`envoy.yaml <_include/filter-cc/envoy.yaml>` +to add a ``via: sample-filter`` header to proxied requests: + +.. literalinclude:: _include/filter-cc/envoy.yaml + :language: yaml + :lines: 26-31 + :lineno-start: 26 + :emphasize-lines: 2-6 + :linenos: + +As the backend service echos the request it receives, the header added by the filter +should be visible in the response body: + +.. code-block:: console + + $ curl -s http://localhost:8000 | grep "sample-filter" + Via: sample-filter + +Step 4: Run the integration test +******************************** + +The example also provides an integration test which exercises the filter inside +Envoy's HTTP integration test framework, without the need to build - or run - the +Envoy binary: + +.. code-block:: console + + $ pwd + examples/filter-cc + $ docker compose -f docker-compose-build.yaml run --remove-orphans filter_test + +.. seealso:: + + :ref:`HTTP filters ` + Further information about Envoy's HTTP filters. + + :ref:`Envoy Bazel build ` + Building Envoy with Bazel. + + `Envoy filter example `_ + The source files for this example. diff --git a/filter-cc/http_filter.cc b/filter-cc/http_filter.cc new file mode 100644 index 00000000..2984dc46 --- /dev/null +++ b/filter-cc/http_filter.cc @@ -0,0 +1,31 @@ +#include + +#include "http_filter.h" + +namespace Envoy { +namespace Http { + +HttpSampleDecoderFilterConfig::HttpSampleDecoderFilterConfig( + const sample::Decoder& proto_config) + : key_(proto_config.key()), val_(proto_config.val()) {} + +HttpSampleDecoderFilter::HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr config) + : config_(config) {} + +const LowerCaseString HttpSampleDecoderFilter::headerKey() const { + return LowerCaseString(config_->key()); +} + +const std::string HttpSampleDecoderFilter::headerValue() const { + return config_->val(); +} + +FilterHeadersStatus HttpSampleDecoderFilter::decodeHeaders(RequestHeaderMap& headers, bool) { + // add a header + headers.addCopy(headerKey(), headerValue()); + + return FilterHeadersStatus::Continue; +} + +} // namespace Http +} // namespace Envoy diff --git a/filter-cc/http_filter.h b/filter-cc/http_filter.h new file mode 100644 index 00000000..03f25d36 --- /dev/null +++ b/filter-cc/http_filter.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "http_filter.pb.h" + +namespace Envoy { +namespace Http { + +class HttpSampleDecoderFilterConfig { +public: + HttpSampleDecoderFilterConfig(const sample::Decoder& proto_config); + + const std::string& key() const { return key_; } + const std::string& val() const { return val_; } + +private: + const std::string key_; + const std::string val_; +}; + +using HttpSampleDecoderFilterConfigSharedPtr = std::shared_ptr; + +class HttpSampleDecoderFilter : public PassThroughDecoderFilter { +public: + HttpSampleDecoderFilter(HttpSampleDecoderFilterConfigSharedPtr); + + // Http::StreamDecoderFilter + FilterHeadersStatus decodeHeaders(RequestHeaderMap&, bool) override; + +private: + const HttpSampleDecoderFilterConfigSharedPtr config_; + + const LowerCaseString headerKey() const; + const std::string headerValue() const; +}; + +} // namespace Http +} // namespace Envoy diff --git a/filter-cc/http_filter.proto b/filter-cc/http_filter.proto new file mode 100644 index 00000000..094103e1 --- /dev/null +++ b/filter-cc/http_filter.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package sample; + +import "validate/validate.proto"; + +message Decoder { + string key = 1 [(validate.rules).string.min_bytes = 1]; + string val = 2 [(validate.rules).string.min_bytes = 1]; +} diff --git a/filter-cc/http_filter_config.cc b/filter-cc/http_filter_config.cc new file mode 100644 index 00000000..366a7710 --- /dev/null +++ b/filter-cc/http_filter_config.cc @@ -0,0 +1,41 @@ +#include + +#include "envoy/registry/registry.h" + +#include "source/extensions/filters/http/common/factory_base.h" + +#include "http_filter.pb.h" +#include "http_filter.pb.validate.h" +#include "http_filter.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +class HttpSampleDecoderFilterConfigFactory + : public Extensions::HttpFilters::Common::UnifiedFactoryBase { +public: + HttpSampleDecoderFilterConfigFactory() : UnifiedFactoryBase("sample") {} + +private: + absl::StatusOr + createHttpFilterFactoryFromProtoTyped(const sample::Decoder& proto_config, + ServerFactoryContext&, ExtraFactoryContext&) override { + Http::HttpSampleDecoderFilterConfigSharedPtr config = + std::make_shared(proto_config); + + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + auto filter = new Http::HttpSampleDecoderFilter(config); + callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{filter}); + }; + } +}; + +/** + * Static registration for this sample filter. @see RegisterFactory. + */ +REGISTER_FACTORY(HttpSampleDecoderFilterConfigFactory, NamedHttpFilterConfigFactory); + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/filter-cc/http_filter_integration_test.cc b/filter-cc/http_filter_integration_test.cc new file mode 100644 index 00000000..0078c621 --- /dev/null +++ b/filter-cc/http_filter_integration_test.cc @@ -0,0 +1,47 @@ +#include "test/integration/http_integration.h" + +namespace Envoy { +class HttpFilterSampleIntegrationTest : public HttpIntegrationTest, + public testing::TestWithParam { +public: + HttpFilterSampleIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) {} + /** + * Initializer for an individual integration test. + */ + void SetUp() override { initialize(); } + + void initialize() override { + config_helper_.prependFilter( + "{ name: sample, typed_config: { \"@type\": type.googleapis.com/sample.Decoder, key: via, " + "val: sample-filter } }"); + HttpIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, HttpFilterSampleIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +TEST_P(HttpFilterSampleIntegrationTest, Test1) { + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/"}, {":authority", "host"}}; + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + + IntegrationCodecClientPtr codec_client; + FakeHttpConnectionPtr fake_upstream_connection; + FakeStreamPtr request_stream; + + codec_client = makeHttpConnection(lookupPort("http")); + auto response = codec_client->makeHeaderOnlyRequest(headers); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection)); + ASSERT_TRUE(fake_upstream_connection->waitForNewStream(*dispatcher_, request_stream)); + ASSERT_TRUE(request_stream->waitForEndStream(*dispatcher_)); + request_stream->encodeHeaders(response_headers, true); + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_EQ( + "sample-filter", + request_stream->headers().get(Http::LowerCaseString("via"))[0]->value().getStringView()); + + codec_client->close(); +} +} // namespace Envoy diff --git a/filter-cc/verify.sh b/filter-cc/verify.sh new file mode 100755 index 00000000..133f2062 --- /dev/null +++ b/filter-cc/verify.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e + +export NAME=filter-cc +export MANUAL=true +export UID + + +# shellcheck source=verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + +run_log "Build the Envoy binary with the sample filter statically linked" +"${DOCKER_COMPOSE[@]}" -f docker-compose-build.yaml run --quiet-pull --remove-orphans filter_build + +run_log "Check the compiled binary" +ls -l bin/envoy + +bring_up_example + +run_log "Test connection" +wait_for 10 bash -c "\ + responds_with \ + 'Request served by' \ + http://localhost:8000" + +run_log "Test the sample filter has added its header to the proxied request" +responds_with \ + "Via: sample-filter" \ + http://localhost:8000