Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new MergeJsonKeys functionality to transformation filter. #350

Merged
merged 17 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ message TransformationTemplate {
// If you want to nest elements inside the body, use dot separator in the
// extractor name.
MergeExtractorsToBody merge_extractors_to_body = 6;
// A set of key-value pairs to merge into the JSON body.
// Each value will be rendered separately, and then placed into the JSON body at
EItanya marked this conversation as resolved.
Show resolved Hide resolved
// the specified key.
// This can only be used when the body is parsed as JSON.
MergeJsonKeys merge_json_keys = 13;
}

// Determines how the body will be parsed.
Expand Down Expand Up @@ -326,6 +331,17 @@ message Passthrough {}

message MergeExtractorsToBody {}

message MergeJsonKeys {
message OverridableTemplate {
// Template to render
InjaTemplate tmpl = 1;
// If set to true, the template will be set even if the rendered value is empty.
bool override_empty = 2;
}
// Map of key name -> template to render into the JSON body
map<string, OverridableTemplate> json_keys = 2;
}

message HeaderBodyTransform {
// When transforming a request, setting this to true will additionally add
// "queryString", "queryStringParameters", "multiValueQueryStringParameters",
Expand Down
4 changes: 2 additions & 2 deletions bazel/repository_locations.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ REPOSITORY_LOCATIONS = dict(
# order to accommodate `check_extensions_build_config.sh`
envoy = dict(
# envoy v1.30.2
commit = "d79f6e8d453ee260e9094093b8dd31af0056e67b",
remote = "https://github.com/envoyproxy/envoy",
commit = "316374fd9927754627ded8420a1944a8b4d8663d",
remote = "https://github.com/solo-io/envoy-fork",
),
inja = dict(
# Includes unmerged modifications for
Expand Down
53 changes: 50 additions & 3 deletions source/extensions/filters/http/transformation/inja_transformer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,13 @@ TransformerInstance::TransformerInstance(ThreadLocal::Slot &tls, Envoy::Random::
env_.add_callback("clusterMetadata", 1, [this](Arguments &args) {
return cluster_metadata_callback_deprecated(args);
});
env_.add_callback("cluster_metadata", 1, [this](Arguments &args) {
env_.add_callback("cluster_metadata", [this](Arguments &args) {
EItanya marked this conversation as resolved.
Show resolved Hide resolved
return cluster_metadata_callback(args);
});
env_.add_callback("dynamic_metadata", 1, [this](Arguments &args) {
env_.add_callback("dynamic_metadata", [this](Arguments &args) {
return dynamic_metadata_callback(args);
});
env_.add_callback("host_metadata", 1, [this](Arguments &args) {
env_.add_callback("host_metadata", [this](Arguments &args) {
return host_metadata_callback(args);
});
env_.add_callback("base64_encode", 1, [this](Arguments &args) {
Expand Down Expand Up @@ -772,6 +772,21 @@ InjaTransformer::InjaTransformer(const TransformationTemplate &transformation,
merged_extractors_to_body_ = true;
break;
}
case TransformationTemplate::kMergeJsonKeys: {
if (transformation.parse_body_behavior() == TransformationTemplate::DontParse) {
EItanya marked this conversation as resolved.
Show resolved Hide resolved
throw EnvoyException("MergeJsonKeys requires parsing the body");
nfuden marked this conversation as resolved.
Show resolved Hide resolved
}
try {
for (const auto &named_extractor : transformation.merge_json_keys().json_keys()) {
EItanya marked this conversation as resolved.
Show resolved Hide resolved
merge_templates_.emplace_back(std::make_tuple(named_extractor.first, named_extractor.second.override_empty(), instance_->parse(named_extractor.second.tmpl().text())));
}

} catch (const std::exception &e) {
throw EnvoyException(
fmt::format("Failed to parse merge_body_key template {}", e.what()));
EItanya marked this conversation as resolved.
Show resolved Hide resolved
}
break;
}
case TransformationTemplate::kPassthrough:
break;
case TransformationTemplate::BODY_TRANSFORMATION_NOT_SET: {
Expand Down Expand Up @@ -945,6 +960,38 @@ void InjaTransformer::transform(Http::RequestOrResponseHeaderMap &header_map,
} else if (merged_extractors_to_body_) {
std::string output = json_body.dump();
maybe_body.emplace(output);
} else if (merge_templates_.size() > 0) {
EItanya marked this conversation as resolved.
Show resolved Hide resolved

for (const auto &merge_template : merge_templates_) {
const std::string &name = std::get<0>(merge_template);

// prepare variables for non-advanced_templates_ scenario
absl::string_view name_to_split;
json* current = nullptr;
// if (!advanced_templates_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't end up supporting json pointer templates, I think we should keep this check in and throw in the constructor at config time

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to understand better what would be entailed, I honestly don't really know the different 🙈

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's largely just handling of / and ~

spec: https://datatracker.ietf.org/doc/html/rfc6901

our fork to re-introduce support in inja: solo-io/inja#6

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm now throwing an exception at config time

name_to_split = name;
current = &json_body;
nfuden marked this conversation as resolved.
Show resolved Hide resolved
for (size_t pos = name_to_split.find("."); pos != std::string::npos;
pos = name_to_split.find(".")) {
auto &&field_name = name_to_split.substr(0, pos);
current = &(*current)[std::string(field_name)];
name_to_split = name_to_split.substr(pos + 1);
}
const auto rendered = instance_->render(std::get<2>(merge_template));
// Do not overwrite with empty unless specified
if (rendered.size() > 0 || std::get<1>(merge_template)) {
try {
auto rendered_json = json::parse(rendered);
(*current)[std::string(name_to_split)] = rendered_json;
} catch (const std::exception &e) {
ASSERT("failed to parse merge_json_key output");
EItanya marked this conversation as resolved.
Show resolved Hide resolved
(*current)[std::string(name_to_split)] = rendered;
EItanya marked this conversation as resolved.
Show resolved Hide resolved
}
}
// }
}
std::string output = json_body.dump();
maybe_body.emplace(output);
}

// DynamicMetadata transform:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class InjaTransformer : public Transformer {

absl::optional<inja::Template> body_template_;
bool merged_extractors_to_body_{};
std::vector<std::tuple<std::string, bool, inja::Template>> merge_templates_;
EItanya marked this conversation as resolved.
Show resolved Hide resolved
ThreadLocal::SlotPtr tls_;
std::unique_ptr<TransformerInstance> instance_;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,47 @@ TEST_F(InjaTransformerTest, UseBodyFunction) {
EXPECT_EQ(body.toString(), "1 1");
}

TEST_F(InjaTransformerTest, MergeJsonKeys) {
Http::TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/foo"}};
TransformationTemplate transformation;


// TransformationTemplate transformation;
// transformation.mutable_body()->set_text("{\"ext2\": \"{{header(\":path\")}\" }");

envoy::api::v2::filter::http::InjaTemplate inja_template;
inja_template.set_text("\"{{header(\":path\")}}\"");
envoy::api::v2::filter::http::MergeJsonKeys_OverridableTemplate tmpl;
(*tmpl.mutable_tmpl()) = inja_template;
(*transformation.mutable_merge_json_keys()->mutable_json_keys())["ext2"] = tmpl;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add tests for the dot logic. include edge cases to make sure we don't crash. i.e.

  • "foo.bar"
  • "foo..bar"
  • ".foo."
  • "..."
  • ".foo
  • "bar."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also empty string?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, I am going to remove this ability for now as it's not necessary. In order to ensure we can potentially add it back later I will throw an exception at config_time if a "." is present in the key


InjaTransformer transformer(transformation, rng_, google::protobuf::BoolValue(), tls_);

NiceMock<Http::MockStreamDecoderFilterCallbacks> callbacks;
Buffer::OwnedImpl body("{\"ext1\":\"123\"}");
transformer.transform(headers, &headers, body, callbacks);
EXPECT_EQ(body.toString(), "{\"ext1\":\"123\",\"ext2\":\"/foo\"}");
EItanya marked this conversation as resolved.
Show resolved Hide resolved
}

TEST_F(InjaTransformerTest, MergeJsonKeysEmpty ) {
EItanya marked this conversation as resolved.
Show resolved Hide resolved
Http::TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/foo"}};
TransformationTemplate transformation;

envoy::api::v2::filter::http::InjaTemplate inja_template;
// Should return "" and therefore not write the key at all
inja_template.set_text("{{header(\":status\")}}");
envoy::api::v2::filter::http::MergeJsonKeys_OverridableTemplate tmpl;
(*tmpl.mutable_tmpl()) = inja_template;
(*transformation.mutable_merge_json_keys()->mutable_json_keys())["ext2"] = tmpl;

InjaTransformer transformer(transformation, rng_, google::protobuf::BoolValue(), tls_);

NiceMock<Http::MockStreamDecoderFilterCallbacks> callbacks;
Buffer::OwnedImpl body("{\"ext1\":\"123\"}");
transformer.transform(headers, &headers, body, callbacks);
EXPECT_EQ(body.toString(), "{\"ext1\":\"123\"}");
}

TEST_F(InjaTransformerTest, UseDefaultNS) {
Http::TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/foo"}};
TransformationTemplate transformation;
Expand Down
Loading