Skip to content

Commit

Permalink
support dynamic metadata as extractor input
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-taussig-solo committed Feb 26, 2024
1 parent 7780a9c commit 3175ca9
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 37 deletions.
107 changes: 81 additions & 26 deletions source/extensions/filters/http/transformation/inja_transformer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,25 @@ getHeader(const Http::RequestOrResponseHeaderMap &header_map,

} // namespace

ExtractionSource
Extractor::determineSource(const envoy::api::v2::filter::http::Extraction &extractor) {
if (extractor.has_header()) {
return ExtractionSource::HEADER;
} else if (extractor.has_body()) {
return ExtractionSource::BODY;
} else if (extractor.has_dynamic_metadata()) {
return ExtractionSource::DYNAMIC_METADATA;
} else {
throw EnvoyException("No source specified for extraction");
}
}

Extractor::Extractor(const envoy::api::v2::filter::http::Extraction &extractor)
: headername_(extractor.header()), body_(extractor.has_body()),
: headername_(extractor.header()), body_(extractor.has_body()), dynamic_metadata_(extractor.dynamic_metadata()),
group_(extractor.subgroup()),
extract_regex_(Solo::Regex::Utility::parseStdRegex(extractor.regex())),
replacement_text_(extractor.has_replacement_text() ? std::make_optional(extractor.replacement_text().value()) : std::nullopt),
mode_(extractor.mode()) {
mode_(extractor.mode()), source_(determineSource(extractor)) {
// mark count == number of sub groups, and we need to add one for match number
// 0 so we test for < instead of <= see:
// http://www.cplusplus.com/reference/regex/basic_regex/mark_count/
Expand Down Expand Up @@ -89,47 +102,89 @@ Extractor::Extractor(const envoy::api::v2::filter::http::Extraction &extractor)
}
}

absl::optional<std::string>
Extractor::getDynamicMetadataValue(Http::StreamFilterCallbacks &callbacks) const {
Upstream::ClusterInfoConstSharedPtr ci = callbacks.clusterInfo();
if (!ci) {
ENVOY_STREAM_LOG(debug, "No cluster info found for dynamic metadata", callbacks);
return absl::nullopt;
}

const envoy::config::core::v3::Metadata *cluster_metadata{};
cluster_metadata = &ci->metadata();
const ProtobufWkt::Value &value = Envoy::Config::Metadata::metadataValue(cluster_metadata, SoloHttpFilterNames::get().Transformation, dynamic_metadata_);

if (value.kind_case() == ProtobufWkt::Value::kStringValue && !value.string_value().empty()) {
return value.string_value();
} else if (value.string_value().empty()) {
ENVOY_STREAM_LOG(debug, "Dynamic metadata value not found at key: {}", callbacks, dynamic_metadata_);
}

return absl::nullopt; // Return empty if dynamic metadata not found or if value is empty
}

absl::string_view
Extractor::extract(Http::StreamFilterCallbacks &callbacks,
const Http::RequestOrResponseHeaderMap &header_map,
GetBodyFunc &body) const {
if (body_) {
const std::string &string_body = body();
absl::string_view sv(string_body);
return extractValue(callbacks, sv);
} else {
const Http::HeaderMap::GetResult header_entries = getHeader(header_map, headername_);
if (header_entries.empty()) {
switch (source_) {
case ExtractionSource::BODY: {
const std::string &string_body = body();
return extractValue(callbacks, absl::string_view(string_body));
}
case ExtractionSource::HEADER: {
const Http::HeaderMap::GetResult header_entries = getHeader(header_map, headername_);
if (header_entries.empty()) {
return "";
}
return extractValue(callbacks, header_entries[0]->value().getStringView());
}
case ExtractionSource::DYNAMIC_METADATA: {
auto value = getDynamicMetadataValue(callbacks);
if (value) {
return extractValue(callbacks, *value);
}
return "";
}
const auto &header_value = header_entries[0]->value().getStringView();
return extractValue(callbacks, header_value);
default:
throw EnvoyException("Unsupported extraction source");
}
}

std::string
Extractor::replace(Http::StreamFilterCallbacks &callbacks,
const Http::RequestOrResponseHeaderMap &header_map,
GetBodyFunc &body) const {
if (body_) {
const std::string &string_body = body();
absl::string_view sv(string_body);
// Lambda to decide between replaceIndividualValue and replaceAllValues
auto replaceValue = [&](absl::string_view value) -> std::string {
if (mode_ == ExtractionApi::SINGLE_REPLACE) {
return replaceIndividualValue(callbacks, sv);
} else {
return replaceAllValues(callbacks, sv);
return replaceIndividualValue(callbacks, value);
} else { // Assume REPLACE_ALL
return replaceAllValues(callbacks, value);
}
} else {
const Http::HeaderMap::GetResult header_entries = getHeader(header_map, headername_);
if (header_entries.empty()) {
return "";
};

switch (source_) {
case ExtractionSource::HEADER: {
const Http::HeaderMap::GetResult header_entries = getHeader(header_map, headername_);
if (header_entries.empty()) {
return "";
}
return replaceValue(header_entries[0]->value().getStringView());
}
const auto &header_value = header_entries[0]->value().getStringView();
if (mode_ == ExtractionApi::SINGLE_REPLACE) {
return replaceIndividualValue(callbacks, header_value);
} else {
return replaceAllValues(callbacks, header_value);
case ExtractionSource::BODY: {
const std::string &string_body = body();
return replaceValue(absl::string_view(string_body));
}
case ExtractionSource::DYNAMIC_METADATA: {
auto value = getDynamicMetadataValue(callbacks);
if (value) {
return replaceValue(*value);
}
return "";
}
default:
throw EnvoyException("Unsupported extraction source");
}
}

Expand Down
11 changes: 11 additions & 0 deletions source/extensions/filters/http/transformation/inja_transformer.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ class TransformerInstance {
Envoy::Random::RandomGenerator &rng_;
};

enum class ExtractionSource {
HEADER,
BODY,
DYNAMIC_METADATA,
};

class Extractor : Logger::Loggable<Logger::Id::filter> {
public:
Extractor(const envoy::api::v2::filter::http::Extraction &extractor);
Expand All @@ -94,13 +100,18 @@ class Extractor : Logger::Loggable<Logger::Id::filter> {
absl::string_view value) const;
std::string replaceAllValues(Http::StreamFilterCallbacks &callbacks,
absl::string_view value) const;
// Helper function to determine the extraction source type
static ExtractionSource determineSource(const envoy::api::v2::filter::http::Extraction &extractor);
absl::optional<std::string> getDynamicMetadataValue(Http::StreamFilterCallbacks &callbacks) const;

const Http::LowerCaseString headername_;
const bool body_;
const std::string dynamic_metadata_;
const unsigned int group_;
const std::regex extract_regex_;
const std::optional<std::string> replacement_text_;
const ExtractionApi::Mode mode_;
const ExtractionSource source_;
};

class InjaTransformer : public Transformer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,72 @@ TEST(Extraction, ReplaceAllNoMatch) {
EXPECT_EQ("", res);
}

TEST(Extraction, ReplaceAllValuesFromDynamicMetadata) {
Http::TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/foo"}, {"foo", "bar"}};

std::string dynamic_metadata_key = "dynamic_key";
std::string dynamic_metadata_value = "foo foo foo";

ExtractionApi extractor;
extractor.set_dynamic_metadata(dynamic_metadata_key);
extractor.set_regex("foo");
extractor.set_subgroup(0);
extractor.mutable_replacement_text()->set_value("BAZ");
extractor.set_mode(ExtractionApi::REPLACE_ALL);

// Mock callback setup to simulate environment
NiceMock<Http::MockStreamDecoderFilterCallbacks> callbacks;

// Setup dynamic metadata within mock cluster info
envoy::config::core::v3::Metadata dynamic_metadata;
auto& metadata_map = (*dynamic_metadata.mutable_filter_metadata())[SoloHttpFilterNames::get().Transformation];
(*metadata_map.mutable_fields())[dynamic_metadata_key].set_string_value(dynamic_metadata_value);

auto mock_cluster_info = std::make_shared<NiceMock<Upstream::MockClusterInfo>>();
ON_CALL(*mock_cluster_info, metadata()).WillByDefault(ReturnRef(dynamic_metadata));
ON_CALL(callbacks, clusterInfo()).WillByDefault(Return(mock_cluster_info));

// Define the body function, though it's not used for dynamic metadata extraction
std::string body("not json body");
GetBodyFunc bodyfunc = [&body]() -> const std::string & { return body; };

std::string res(Extractor(extractor).replace(callbacks, headers, bodyfunc));
EXPECT_EQ("BAZ BAZ BAZ", res);
}

TEST(Extraction, ReplaceIndividualValueFromDynamicMetadata) {
Http::TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/foo"}, {"foo", "bar"}};

std::string dynamic_metadata_key = "dynamic_key";
std::string dynamic_metadata_value = "foo foo foo";

ExtractionApi extractor;
extractor.set_dynamic_metadata(dynamic_metadata_key);
extractor.set_regex("(foo).*");
extractor.set_subgroup(1);
extractor.mutable_replacement_text()->set_value("BAZ");
extractor.set_mode(ExtractionApi::SINGLE_REPLACE);

// Mock callback setup to simulate environment
NiceMock<Http::MockStreamDecoderFilterCallbacks> callbacks;

// Setup dynamic metadata within mock cluster info
envoy::config::core::v3::Metadata dynamic_metadata;
auto& metadata_map = (*dynamic_metadata.mutable_filter_metadata())[SoloHttpFilterNames::get().Transformation];
(*metadata_map.mutable_fields())[dynamic_metadata_key].set_string_value(dynamic_metadata_value);

auto mock_cluster_info = std::make_shared<NiceMock<Upstream::MockClusterInfo>>();
ON_CALL(*mock_cluster_info, metadata()).WillByDefault(ReturnRef(dynamic_metadata));
ON_CALL(callbacks, clusterInfo()).WillByDefault(Return(mock_cluster_info));

// Define the body function, though it's not used for dynamic metadata extraction
std::string body("not json body");
GetBodyFunc bodyfunc = [&body]() -> const std::string & { return body; };

std::string res(Extractor(extractor).replace(callbacks, headers, bodyfunc));
EXPECT_EQ("BAZ foo foo", res);
}

} // namespace Transformation
} // namespace HttpFilters
} // namespace Extensions
Expand Down
Loading

0 comments on commit 3175ca9

Please sign in to comment.