diff --git a/CODEOWNERS b/CODEOWNERS index 90b5d3a35482c..c54b4198bf5a2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -186,3 +186,5 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp # Original IP detection /*/extensions/http/original_ip_detection/custom_header @rgs1 @alyssawilk @antoniovicente /*/extensions/http/original_ip_detection/xff @rgs1 @alyssawilk @antoniovicente +# set_metadata extension +/*/extensions/filters/http/set_metadata @aguinet @snowp diff --git a/api/BUILD b/api/BUILD index 04b94ff211fd4..136e248fbf154 100644 --- a/api/BUILD +++ b/api/BUILD @@ -209,6 +209,7 @@ proto_library( "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", + "//envoy/extensions/filters/http/set_metadata/v3:pkg", "//envoy/extensions/filters/http/squash/v3:pkg", "//envoy/extensions/filters/http/tap/v3:pkg", "//envoy/extensions/filters/http/wasm/v3:pkg", diff --git a/api/envoy/extensions/filters/http/set_metadata/v3/BUILD b/api/envoy/extensions/filters/http/set_metadata/v3/BUILD new file mode 100644 index 0000000000000..ee92fb652582e --- /dev/null +++ b/api/envoy/extensions/filters/http/set_metadata/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_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto b/api/envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto new file mode 100644 index 0000000000000..7b2cf0e0965d8 --- /dev/null +++ b/api/envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.set_metadata.v3; + +import "google/protobuf/struct.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.set_metadata.v3"; +option java_outer_classname = "SetMetadataProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Set-Metadata Filter] +// +// This filters adds or updates dynamic metadata with static data. +// +// [#extension: envoy.filters.http.set_metadata] + +message Config { + // The metadata namespace. + string metadata_namespace = 1 [(validate.rules).string = {min_len: 1}]; + + // The value to update the namespace with. See + // :ref:`the filter documentation ` for + // more information on how this value is merged with potentially existing + // ones. + google.protobuf.Struct value = 2; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index eac32a5cdfd38..0703d9ee7defe 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -92,6 +92,7 @@ proto_library( "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", + "//envoy/extensions/filters/http/set_metadata/v3:pkg", "//envoy/extensions/filters/http/squash/v3:pkg", "//envoy/extensions/filters/http/tap/v3:pkg", "//envoy/extensions/filters/http/wasm/v3:pkg", diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index 5bd61f14dc44b..0899edd07c1e2 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -41,6 +41,7 @@ HTTP filters rate_limit_filter rbac_filter router_filter + set_metadata_filter squash_filter tap_filter wasm_filter diff --git a/docs/root/configuration/http/http_filters/set_metadata_filter.rst b/docs/root/configuration/http/http_filters/set_metadata_filter.rst new file mode 100644 index 0000000000000..b56367e0105f0 --- /dev/null +++ b/docs/root/configuration/http/http_filters/set_metadata_filter.rst @@ -0,0 +1,52 @@ +.. _config_http_filters_set_metadata: + +Set Metadata +============ +* :ref:`v3 API reference ` +* This filter should be configured with the name *envoy.filters.http.set_metadata*. + +This filters adds or updates dynamic metadata with static data. + +Dynamic metadata values are updated with the following scheme. If a key +does not exists, it's just copied into the current metadata. If the key exists +but has a different type, it is replaced by the new value. Otherwise: + + * for scalar values (null, string, number, boolean) are replaced with the new value + * for lists: new values are added to the current list + * for structures: recursively apply this scheme + +For instance, if the namespace already contains this structure: + +.. code-block:: yaml + + myint: 1 + mylist: ["a"] + mykey: ["val"] + mytags: + tag0: 1 + +and the value to set is: + +.. code-block:: yaml + + myint: 2 + mylist: ["b","c"] + mykey: 1 + mytags: + tag1: 1 + +After applying this filter, the namespace will contain: + +.. code-block:: yaml + + myint: 2 + mylist: ["a","b","c"] + mykey: 1 + mytags: + tag0: 1 + tag1: 1 + +Statistics +---------- + +Currently, this filter generates no statistics. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index 04b94ff211fd4..136e248fbf154 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -209,6 +209,7 @@ proto_library( "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", + "//envoy/extensions/filters/http/set_metadata/v3:pkg", "//envoy/extensions/filters/http/squash/v3:pkg", "//envoy/extensions/filters/http/tap/v3:pkg", "//envoy/extensions/filters/http/wasm/v3:pkg", diff --git a/generated_api_shadow/envoy/extensions/filters/http/set_metadata/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/http/set_metadata/v3/BUILD new file mode 100644 index 0000000000000..ee92fb652582e --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/set_metadata/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_udpa//udpa/annotations:pkg"], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto b/generated_api_shadow/envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto new file mode 100644 index 0000000000000..7b2cf0e0965d8 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/set_metadata/v3/set_metadata.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.set_metadata.v3; + +import "google/protobuf/struct.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.set_metadata.v3"; +option java_outer_classname = "SetMetadataProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Set-Metadata Filter] +// +// This filters adds or updates dynamic metadata with static data. +// +// [#extension: envoy.filters.http.set_metadata] + +message Config { + // The metadata namespace. + string metadata_namespace = 1 [(validate.rules).string = {min_len: 1}]; + + // The value to update the namespace with. See + // :ref:`the filter documentation ` for + // more information on how this value is merged with potentially existing + // ones. + google.protobuf.Struct value = 2; +} diff --git a/source/common/protobuf/utility.cc b/source/common/protobuf/utility.cc index 1dfed5a161719..f29d8f6125eb3 100644 --- a/source/common/protobuf/utility.cc +++ b/source/common/protobuf/utility.cc @@ -1056,4 +1056,43 @@ 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) { + auto& obj_fields = *obj.mutable_fields(); + + for (const auto& [key, val] : with.fields()) { + auto& obj_key = obj_fields[key]; + + // If the types are different, the last one wins. + const auto val_kind = val.kind_case(); + if (val_kind != obj_key.kind_case()) { + obj_key = val; + continue; + } + + // 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: + obj_key = val; + break; + // If we got a structure, recursively update. + case ProtobufWkt::Value::kStructValue: + update(*obj_key.mutable_struct_value(), val.struct_value()); + break; + // For lists, append the new values. + case ProtobufWkt::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: + break; + } + } +} + } // namespace Envoy diff --git a/source/common/protobuf/utility.h b/source/common/protobuf/utility.h index cacafc69238aa..2feee5bcadb2f 100644 --- a/source/common/protobuf/utility.h +++ b/source/common/protobuf/utility.h @@ -665,6 +665,25 @@ class TimestampUtil { ProtobufWkt::Timestamp& timestamp); }; +class StructUtil { +public: + /** + * Recursively updates in-place a protobuf structure with keys from another + * object. + * + * The merging strategy is the following. If a key from \p other does not + * exists, it's just copied into \p obj. If the key exists but has a + * different type, it is replaced by the new value. Otherwise: + * - for scalar values (null, string, number, boolean) are replaced with the new value + * - for lists: new values are added to the current list + * - for structures: recursively apply this scheme + * + * @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); +}; + } // namespace Envoy namespace std { diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index ee8ab2a61b716..6f2b772c46607 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -100,6 +100,7 @@ EXTENSIONS = { "envoy.filters.http.ratelimit": "//source/extensions/filters/http/ratelimit:config", "envoy.filters.http.rbac": "//source/extensions/filters/http/rbac:config", "envoy.filters.http.router": "//source/extensions/filters/http/router:config", + "envoy.filters.http.set_metadata": "//source/extensions/filters/http/set_metadata:config", "envoy.filters.http.squash": "//source/extensions/filters/http/squash:config", "envoy.filters.http.tap": "//source/extensions/filters/http/tap:config", "envoy.filters.http.wasm": "//source/extensions/filters/http/wasm:config", diff --git a/source/extensions/filters/http/set_metadata/BUILD b/source/extensions/filters/http/set_metadata/BUILD new file mode 100644 index 0000000000000..c598779c64138 --- /dev/null +++ b/source/extensions/filters/http/set_metadata/BUILD @@ -0,0 +1,40 @@ +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 = "set_metadata_filter_lib", + srcs = ["set_metadata_filter.cc"], + hdrs = ["set_metadata_filter.h"], + deps = [ + "//include/envoy/server:filter_config_interface", + "//source/common/http:utility_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "@envoy_api//envoy/extensions/filters/http/set_metadata/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + category = "envoy.filters.http", + security_posture = "robust_to_untrusted_downstream_and_upstream", + deps = [ + "//include/envoy/registry", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/set_metadata:set_metadata_filter_lib", + "@envoy_api//envoy/extensions/filters/http/set_metadata/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/set_metadata/config.cc b/source/extensions/filters/http/set_metadata/config.cc new file mode 100644 index 0000000000000..2d2f35cd2a7ca --- /dev/null +++ b/source/extensions/filters/http/set_metadata/config.cc @@ -0,0 +1,34 @@ +#include "extensions/filters/http/set_metadata/config.h" + +#include + +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.h" +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/protobuf/utility.h" + +#include "extensions/filters/http/set_metadata/set_metadata_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SetMetadataFilter { + +Http::FilterFactoryCb SetMetadataConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::set_metadata::v3::Config& proto_config, + const std::string&, Server::Configuration::FactoryContext&) { + ConfigSharedPtr filter_config(std::make_shared(proto_config)); + + return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter( + Http::StreamDecoderFilterSharedPtr{new SetMetadataFilter(filter_config)}); + }; +} + +REGISTER_FACTORY(SetMetadataConfig, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace SetMetadataFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/set_metadata/config.h b/source/extensions/filters/http/set_metadata/config.h new file mode 100644 index 0000000000000..32350b0f6af56 --- /dev/null +++ b/source/extensions/filters/http/set_metadata/config.h @@ -0,0 +1,31 @@ +#pragma once + +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.h" +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SetMetadataFilter { + +/** + * Config registration for the header-to-metadata filter. @see NamedHttpFilterConfigFactory. + */ +class SetMetadataConfig + : public Common::FactoryBase { +public: + SetMetadataConfig() : FactoryBase(HttpFilterNames::get().SetMetadata) {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::set_metadata::v3::Config& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace SetMetadataFilter +} // 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 new file mode 100644 index 0000000000000..98295d3cda3d8 --- /dev/null +++ b/source/extensions/filters/http/set_metadata/set_metadata_filter.cc @@ -0,0 +1,49 @@ +#include "extensions/filters/http/set_metadata/set_metadata_filter.h" + +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.h" + +#include "common/config/well_known_names.h" +#include "common/http/utility.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/filters/http/well_known_names.h" + +#include "absl/strings/str_format.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SetMetadataFilter { + +Config::Config(const envoy::extensions::filters::http::set_metadata::v3::Config& proto_config) { + namespace_ = proto_config.metadata_namespace(); + value_ = proto_config.value(); +} + +SetMetadataFilter::SetMetadataFilter(const ConfigSharedPtr config) : config_(config) {} + +SetMetadataFilter::~SetMetadataFilter() = default; + +Http::FilterHeadersStatus SetMetadataFilter::decodeHeaders(Http::RequestHeaderMap&, bool) { + const absl::string_view metadata_namespace = config_->metadataNamespace(); + auto& metadata = *decoder_callbacks_->streamInfo().dynamicMetadata().mutable_filter_metadata(); + ProtobufWkt::Struct& org_fields = metadata[metadata_namespace]; + const ProtobufWkt::Struct& to_merge = config_->value(); + + StructUtil::update(org_fields, to_merge); + + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterDataStatus SetMetadataFilter::decodeData(Buffer::Instance&, bool) { + return Http::FilterDataStatus::Continue; +} + +void SetMetadataFilter::setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) { + decoder_callbacks_ = &callbacks; +} + +} // namespace SetMetadataFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/set_metadata/set_metadata_filter.h b/source/extensions/filters/http/set_metadata/set_metadata_filter.h new file mode 100644 index 0000000000000..a5a0957a9f818 --- /dev/null +++ b/source/extensions/filters/http/set_metadata/set_metadata_filter.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.h" + +#include "common/common/logger.h" + +#include "extensions/filters/http/common/pass_through_filter.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SetMetadataFilter { + +class Config : public ::Envoy::Router::RouteSpecificFilterConfig, + public Logger::Loggable { +public: + Config(const envoy::extensions::filters::http::set_metadata::v3::Config& config); + + absl::string_view metadataNamespace() const { return namespace_; } + const ProtobufWkt::Struct& value() { return value_; } + +private: + std::string namespace_; + ProtobufWkt::Struct value_; +}; + +using ConfigSharedPtr = std::shared_ptr; + +class SetMetadataFilter : public Http::PassThroughDecoderFilter, + public Logger::Loggable { +public: + SetMetadataFilter(const ConfigSharedPtr config); + ~SetMetadataFilter() override; + + // Http::StreamFilterBase + void onDestroy() override {} + + // StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override; + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override; + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks&) override; + +private: + const ConfigSharedPtr config_; + Http::StreamDecoderFilterCallbacks* decoder_callbacks_; +}; + +} // namespace SetMetadataFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index ddb9e1e6346d4..d1904f8686735 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -88,6 +88,8 @@ class HttpFilterNameValues { const std::string KillRequest = "envoy.filters.http.kill_request"; // External Processing filter const std::string ExternalProcessing = "envoy.filters.http.ext_proc"; + // Set metadata filter + const std::string SetMetadata = "envoy.filters.http.set_metadata"; }; using HttpFilterNames = ConstSingleton; diff --git a/test/common/protobuf/utility_test.cc b/test/common/protobuf/utility_test.cc index 97b25a67bcade..743e1d8c99905 100644 --- a/test/common/protobuf/utility_test.cc +++ b/test/common/protobuf/utility_test.cc @@ -2018,4 +2018,100 @@ TEST(TypeUtilTest, TypeUrlHelperFunction) { TypeUtil::descriptorFullNameToTypeUrl("envoy.config.filter.http.ip_tagging.v2.IPTagging")); } +class StructUtilTest : public ProtobufUtilityTest { +protected: + ProtobufWkt::Struct updateSimpleStruct(const ProtobufWkt::Value& v0, + const ProtobufWkt::Value& v1) { + ProtobufWkt::Struct obj, with; + (*obj.mutable_fields())["key"] = v0; + (*with.mutable_fields())["key"] = v1; + StructUtil::update(obj, with); + EXPECT_EQ(obj.fields().size(), 1); + return obj; + } +}; + +TEST_F(StructUtilTest, StructUtilUpdateScalars) { + { + const auto obj = updateSimpleStruct(ValueUtil::stringValue("v0"), ValueUtil::stringValue("v1")); + EXPECT_EQ(obj.fields().at("key").string_value(), "v1"); + } + + { + const auto obj = updateSimpleStruct(ValueUtil::numberValue(0), ValueUtil::numberValue(1)); + EXPECT_EQ(obj.fields().at("key").number_value(), 1); + } + + { + const auto obj = updateSimpleStruct(ValueUtil::boolValue(false), ValueUtil::boolValue(true)); + EXPECT_EQ(obj.fields().at("key").bool_value(), true); + } + + { + const auto obj = updateSimpleStruct(ValueUtil::nullValue(), ValueUtil::nullValue()); + EXPECT_EQ(obj.fields().at("key").kind_case(), ProtobufWkt::Value::KindCase::kNullValue); + } +} + +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.number_value(), 1); + } + + { + const auto obj = + 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.string_value(), "v1"); + } +} + +TEST_F(StructUtilTest, StructUtilUpdateList) { + ProtobufWkt::Struct obj, with; + auto& list = *(*obj.mutable_fields())["key"].mutable_list_value(); + list.add_values()->set_string_value("v0"); + + auto& with_list = *(*with.mutable_fields())["key"].mutable_list_value(); + with_list.add_values()->set_number_value(1); + const auto v2 = MessageUtil::keyValueStruct("subkey", "str"); + *with_list.add_values()->mutable_struct_value() = v2; + + StructUtil::update(obj, with); + ASSERT_THAT(obj.fields().size(), 1); + const auto& list_vals = list.values(); + EXPECT_TRUE(ValueUtil::equal(list_vals[0], ValueUtil::stringValue("v0"))); + EXPECT_TRUE(ValueUtil::equal(list_vals[1], ValueUtil::numberValue(1))); + EXPECT_TRUE(ValueUtil::equal(list_vals[2], ValueUtil::structValue(v2))); +} + +TEST_F(StructUtilTest, StructUtilUpdateNewKey) { + ProtobufWkt::Struct obj, with; + (*obj.mutable_fields())["key0"].set_number_value(1); + (*with.mutable_fields())["key1"].set_number_value(1); + StructUtil::update(obj, with); + + const auto& fields = obj.fields(); + EXPECT_TRUE(ValueUtil::equal(fields.at("key0"), ValueUtil::numberValue(1))); + EXPECT_TRUE(ValueUtil::equal(fields.at("key1"), ValueUtil::numberValue(1))); +} + +TEST_F(StructUtilTest, StructUtilUpdateRecursiveStruct) { + ProtobufWkt::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); + 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"))); +} + } // namespace Envoy diff --git a/test/extensions/filters/http/set_metadata/BUILD b/test/extensions/filters/http/set_metadata/BUILD new file mode 100644 index 0000000000000..9be09d9040f25 --- /dev/null +++ b/test/extensions/filters/http/set_metadata/BUILD @@ -0,0 +1,39 @@ +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 = "set_metadata_filter_test", + srcs = [ + "set_metadata_filter_test.cc", + ], + extension_name = "envoy.filters.http.set_metadata", + deps = [ + "//source/extensions/filters/http/set_metadata:config", + "//test/integration:http_integration_lib", + "//test/mocks/api:api_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.filters.http.set_metadata", + deps = [ + "//source/extensions/filters/http/set_metadata:config", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/server:instance_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/set_metadata/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/set_metadata/config_test.cc b/test/extensions/filters/http/set_metadata/config_test.cc new file mode 100644 index 0000000000000..f702cabd708b4 --- /dev/null +++ b/test/extensions/filters/http/set_metadata/config_test.cc @@ -0,0 +1,48 @@ +#include + +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.h" +#include "envoy/extensions/filters/http/set_metadata/v3/set_metadata.pb.validate.h" + +#include "extensions/filters/http/set_metadata/config.h" +#include "extensions/filters/http/set_metadata/set_metadata_filter.h" + +#include "test/mocks/server/factory_context.h" +#include "test/mocks/server/instance.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace SetMetadataFilter { + +using SetMetadataProtoConfig = envoy::extensions::filters::http::set_metadata::v3::Config; + +TEST(SetMetadataFilterConfigTest, SimpleConfig) { + const std::string yaml = R"EOF( +metadata_namespace: thenamespace +value: + mynumber: 20 + mylist: ["b"] + tags: + mytag1: 1 + )EOF"; + + SetMetadataProtoConfig proto_config; + TestUtility::loadFromYamlAndValidate(yaml, proto_config); + + testing::NiceMock context; + SetMetadataConfig factory; + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callbacks; + EXPECT_CALL(filter_callbacks, addStreamDecoderFilter(_)); + cb(filter_callbacks); +} + +} // namespace SetMetadataFilter +} // 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 new file mode 100644 index 0000000000000..ad644a71e419b --- /dev/null +++ b/test/extensions/filters/http/set_metadata/set_metadata_filter_test.cc @@ -0,0 +1,143 @@ +#include + +#include "common/protobuf/protobuf.h" + +#include "extensions/filters/http/set_metadata/set_metadata_filter.h" +#include "extensions/filters/http/well_known_names.h" + +#include "test/mocks/http/mocks.h" +#include "test/mocks/stream_info/mocks.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 HttpFilters { +namespace SetMetadataFilter { + +class SetMetadataIntegrationTest : public testing::Test { + +public: + SetMetadataIntegrationTest() = default; + + void runFilter(envoy::config::core::v3::Metadata& metadata, const std::string& yaml_config) { + envoy::extensions::filters::http::set_metadata::v3::Config ext_config; + TestUtility::loadFromYaml(yaml_config, ext_config); + auto config = std::make_shared(ext_config); + auto filter = std::make_shared(config); + + Http::TestRequestHeaderMapImpl headers; + + NiceMock decoder_callbacks; + NiceMock req_info; + filter->setDecoderFilterCallbacks(decoder_callbacks); + + EXPECT_CALL(decoder_callbacks, streamInfo()).WillRepeatedly(ReturnRef(req_info)); + EXPECT_CALL(req_info, dynamicMetadata()).WillRepeatedly(ReturnRef(metadata)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter->decodeHeaders(headers, true)); + Buffer::OwnedImpl buffer; + EXPECT_EQ(Http::FilterDataStatus::Continue, filter->decodeData(buffer, true)); + filter->onDestroy(); + } + + void checkKeyInt(const ProtobufWkt::Struct& s, const char* key, int val) { + const auto& fields = s.fields(); + const auto it = fields.find(key); + ASSERT_TRUE(it != fields.end()); + const auto& pbval = it->second; + ASSERT_EQ(pbval.kind_case(), ProtobufWkt::Value::kNumberValue); + EXPECT_EQ(pbval.number_value(), val); + } +}; + +TEST_F(SetMetadataIntegrationTest, TestTagsHeaders) { + const std::string yaml_config = R"EOF( + metadata_namespace: thenamespace + value: + tags: + mytag0: 1 + )EOF"; + + envoy::config::core::v3::Metadata metadata; + runFilter(metadata, yaml_config); + + // Verify that `metadata` contains `{"thenamespace": {"tags": {"mytag0": 1}}}` + const auto& filter_metadata = metadata.filter_metadata(); + const auto it_namespace = filter_metadata.find("thenamespace"); + ASSERT_TRUE(it_namespace != filter_metadata.end()); + const auto& fields = it_namespace->second.fields(); + const auto it_tags = fields.find("tags"); + ASSERT_TRUE(it_tags != fields.end()); + const auto& tags = it_tags->second; + ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + checkKeyInt(tags.struct_value(), "mytag0", 1); +} + +TEST_F(SetMetadataIntegrationTest, TestTagsHeadersUpdate) { + envoy::config::core::v3::Metadata metadata; + + { + const std::string yaml_config = R"EOF( + metadata_namespace: thenamespace + value: + mynumber: 10 + mylist: ["a"] + tags: + mytag0: 1 + )EOF"; + + runFilter(metadata, yaml_config); + } + { + const std::string yaml_config = R"EOF( + metadata_namespace: thenamespace + value: + mynumber: 20 + mylist: ["b"] + tags: + mytag1: 1 + )EOF"; + + runFilter(metadata, yaml_config); + } + + // Verify that `metadata` contains: + // ``{"thenamespace": {number: 20, mylist: ["a","b"], "tags": {"mytag0": 1, "mytag1": 1}}}`` + const auto& filter_metadata = metadata.filter_metadata(); + const auto it_namespace = filter_metadata.find("thenamespace"); + ASSERT_TRUE(it_namespace != filter_metadata.end()); + const auto& namespace_ = it_namespace->second; + + checkKeyInt(namespace_, "mynumber", 20); + + const auto& fields = namespace_.fields(); + const auto it_mylist = fields.find("mylist"); + ASSERT_TRUE(it_mylist != fields.end()); + const auto& mylist = it_mylist->second; + ASSERT_EQ(mylist.kind_case(), ProtobufWkt::Value::kListValue); + const auto& vals = mylist.list_value().values(); + ASSERT_EQ(vals.size(), 2); + ASSERT_EQ(vals[0].kind_case(), ProtobufWkt::Value::kStringValue); + EXPECT_EQ(vals[0].string_value(), "a"); + ASSERT_EQ(vals[1].kind_case(), ProtobufWkt::Value::kStringValue); + EXPECT_EQ(vals[1].string_value(), "b"); + + const auto it_tags = fields.find("tags"); + ASSERT_TRUE(it_tags != fields.end()); + const auto& tags = it_tags->second; + ASSERT_EQ(tags.kind_case(), ProtobufWkt::Value::kStructValue); + const auto& tags_struct = tags.struct_value(); + + checkKeyInt(tags_struct, "mytag0", 1); + checkKeyInt(tags_struct, "mytag1", 1); +} + +} // namespace SetMetadataFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy