router: add filter_state internal redirect predicate#45904
Conversation
Adds the envoy.internal_redirect_predicates.filter_state predicate, which decides whether to follow an internal redirect by comparing a filter state string object (set earlier in the request by another filter) against a configured value: allow_value follows only on a match, deny_value follows unless it matches, and allow_if_absent controls behavior when the object is missing (default: do not follow). No change for existing configs; the predicate has effect only when added to an InternalRedirectPolicy. Uses a raw string with a required oneof allow_value / deny_value rather than a StringMatcher, because the predicate factory does not receive a CommonFactoryContext (needed by StringMatcher for regex/custom). This stays additively upgradeable to a StringMatcher option later without an API break. Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
|
CC @envoyproxy/api-shepherds: Your approval is needed for changes made to |
adisuissa
left a comment
There was a problem hiding this comment.
Thanks.
Left a high-level question about the design.
| // whether an otherwise-enabled internal redirect is actually taken, without | ||
| // changing route matching. | ||
| // | ||
| // The predicate reads the filter-state object named ``filter_state_key`` (a |
There was a problem hiding this comment.
From a high-level perspective:
it seems that in the future this will have a matcher that is dependent on the expected type. The most basic one will be a string matcher, but the API should be designed in a way that allows other types of matchers.
There was a problem hiding this comment.
I thought that using oneof will allow in the future to add others like string matcher with backward compatibility
oneof policy {
string allow_value = 2; // existing - exact string match
string deny_value = 3; // existing - exact string match
StringMatcher allow_string_matcher = 5; // NEW - regex, prefix, suffix, etc.
StringMatcher deny_string_matcher = 6; // NEW - regex, prefix, suffix, etc.
}
if you have something better in mind let me know I can change it
There was a problem hiding this comment.
IIUC, @adisuissa means that the type of data store in the filter state might be more than a string.
From an API point of view, the policy and the filter_state_key is coupled by the type of the filter state's type.
What happens if the filter state pointed by filter_state_key stores something other than string?
An alternative might be:
string filter_state_key = 1;
enum MatchAction {
UNSPECIFIED = 0;
ALLOW = 1;
DENY = 2;
}
MatchAction match_action = 2;
oneof matcher {
string string_value = 1;
// other types of matcher for extension
}
With all the above said, isn't it simpler for this to just read a boolean from a filter state that dictates whether to follow the redirect? What's the use case that requires a string value to be compared against?
There was a problem hiding this comment.
I didn't see a boolean could work and perhaps it is cleaner
Do you have something like this in mind?
message FilterStateConfig {
// Name of the filter-state boolean object. If true, follows the redirect.
string redirect_enabled_key = 1 [(validate.rules).string = {min_len: 1}];
// Whether to follow the redirect when the filter-state object is absent.
// Defaults to false (do not redirect if the gate wasn't explicitly set).
bool redirect_if_absent = 2;
}
There was a problem hiding this comment.
I think this is a lot cleaner while being really flexible - whichever filter producing the filter state can decide what to do.
There was a problem hiding this comment.
the code is cleaner too (and just a couple of lines) I will do some exploratory test and update the PR, thanks
|
Assigning internal_redirect code-owners: |
penguingao
left a comment
There was a problem hiding this comment.
Thanks for the contribution. This looks great. Just need to figure out the API discussion.
| // whether an otherwise-enabled internal redirect is actually taken, without | ||
| // changing route matching. | ||
| // | ||
| // The predicate reads the filter-state object named ``filter_state_key`` (a |
There was a problem hiding this comment.
IIUC, @adisuissa means that the type of data store in the filter state might be more than a string.
From an API point of view, the policy and the filter_state_key is coupled by the type of the filter state's type.
What happens if the filter state pointed by filter_state_key stores something other than string?
An alternative might be:
string filter_state_key = 1;
enum MatchAction {
UNSPECIFIED = 0;
ALLOW = 1;
DENY = 2;
}
MatchAction match_action = 2;
oneof matcher {
string string_value = 1;
// other types of matcher for extension
}
With all the above said, isn't it simpler for this to just read a boolean from a filter state that dictates whether to follow the redirect? What's the use case that requires a string value to be compared against?
| A fourth predicate :ref:`filter_state | ||
| <envoy_v3_api_msg_extensions.internal_redirect.filter_state.v3.FilterStateConfig>` | ||
| can be used to gate the redirect on a filter state value set earlier in the request by another | ||
| filter, allowing a per-request decision about whether an internal redirect is followed. |
There was a problem hiding this comment.
looks like we just need it to be per-request, in which case, it's really just a boolean to be delivered.
| const envoy::extensions::internal_redirect::filter_state::v3::FilterStateConfig&>( | ||
| config, ProtobufMessage::getStrictValidationVisitor()); | ||
|
|
||
| using ProtoConfig = envoy::extensions::internal_redirect::filter_state::v3::FilterStateConfig; |
There was a problem hiding this comment.
If you move this above line 19, you can use it with the call to downcastAndValidate as well.
| EXPECT_EQ(predicate->name(), "envoy.internal_redirect_predicates.filter_state"); | ||
| } | ||
|
|
||
| // allow_value: follow only when the gate equals the configured value. |
There was a problem hiding this comment.
the tests here are not "config_test" - we might want to just rename this file to reflect the nature of this test file.
Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
16a6b09 to
862c0d3
Compare
Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
| format_string: | ||
| text_format_source: | ||
| inline_string: "true" | ||
|
|
There was a problem hiding this comment.
I don't know if there was a reason to not have this generic bool, I added it to be able to write bool directly in filter state, if this is not correct let me know and I will remove and store "true" as string in the filter state
|
@penguingao @adisuissa If you have some time to take a look, I will appreciate |
penguingao
left a comment
There was a problem hiding this comment.
Thanks! This looks great to me modulo a few nits.
| @@ -0,0 +1,8 @@ | |||
| Added :ref:`filter_state | |||
| <envoy_v3_api_msg_extensions.internal_redirect.filter_state.v3.FilterStateConfig>`, | |||
| a new internal redirect predicate that gates redirect decisions on a boolean filter-state | |||
There was a problem hiding this comment.
nit: s/a new internal redirect/an internal redirect
this will not be new in some future point.
| object set earlier in the request lifecycle (for example by a Lua filter, ext_proc, | ||
| ``set_filter_state``, or a dynamic module). The predicate follows the redirect when the | ||
| boolean value is true, enabling per-request redirect control without changing route | ||
| matching. Also added the generic ``envoy.bool`` filter state factory for creating |
There was a problem hiding this comment.
nit: "Also added ..." here belongs to release note instead of the static documentation.
| std::unique_ptr<StreamInfo::FilterState::Object> | ||
| createFromBytes(absl::string_view data) const override { | ||
| bool value = false; | ||
| (void)absl::SimpleAtob(data, &value); |
There was a problem hiding this comment.
I'd prefer we throw if this is false.
it's better to reject bad config than taking an assumption.
| } | ||
|
|
||
| private: | ||
| using ProtoConfig = envoy::extensions::internal_redirect::filter_state::v3::FilterStateConfig; |
- changelog: use 'an' instead of 'a new', remove envoy.bool sentence - GenericBoolObjectFactory: throw on invalid input instead of defaulting to false - config.h: remove duplicate ProtoConfig typedef Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
- changelog: use 'an' instead of 'a new', remove envoy.bool sentence - GenericBoolObjectFactory: throw on invalid input instead of defaulting to false - config.h: move ProtoConfig typedef to private class scope to avoid duplication Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
|
This LGTM. @botengyao for API approval and merge, unless @adisuissa has more feedbacks. |
|
/retest |
|
@botengyao ping for review |
botengyao
left a comment
There was a problem hiding this comment.
here is a pass, thanks for adding this support.
| createFromBytes(absl::string_view data) const override { | ||
| bool value = false; | ||
| if (!absl::SimpleAtob(data, &value)) { | ||
| throw EnvoyException(fmt::format("Invalid boolean value: '{}'", data)); |
There was a problem hiding this comment.
This can happen in request time, and we want to avoid the exceptions, e.g., config HTTP set_filter_state with factory_key: envoy.bool and a value such as %REQ(x-redirect-enabled)%. and then send x-redirect-enabled: garbage, it will crash. We need to return nullptr here, and is this tested? I don't see it.
As pointed out in review, throwing an exception for invalid boolean values can crash at request time when values come from headers (e.g., using %REQ(x-redirect-enabled)% with garbage input). Return nullptr instead to gracefully handle invalid input, consistent with other filter state factories that return nullptr on bad input. Added test for invalid value from request header. Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
Add tests for setting boolean filter_state values directly from request headers using format_string (e.g., %REQ(x-enable-failover)%). Tests cover: - Valid header values (1, false) create filter_state correctly - Invalid header values (garbage) return nullptr without crashing This validates the fix from PR review where createFromBytes() now returns nullptr instead of throwing for invalid boolean values. Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
8240502 to
4da4a30
Compare
Commit Message:
router: add
filter_stateinternal redirect predicateAdds the
envoy.internal_redirect_predicates.filter_statepredicate, which decideswhether to follow an internal redirect based on a boolean filter state object. The
predicate reads a
BoolAccessorat the configuredfilter_state_keyand acceptsthe redirect if the value is true (or false if
invert: true).Also adds a generic
envoy.boolfilter state factory that accepts:true,t,yes,y,1for true;false,f,no,n,0for false (case-insensitive).Example configuration:
Setting from Lua:
Risk Level: Low — self-contained extension, inert unless explicitly configured.
Testing: Unit tests for predicate and factory. Integration tests with Docker validate
end-to-end behavior.
Docs Changes: Added
envoy.boolfactory towell_known_filter_state.rst.Release Notes: Added
changelogs/current/new_features/router__added-filter-state-internal-redirect-predicate.rst.[Generative AI usage disclosure: Generative AI (Claude) was used to assist with this change;
I have reviewed and understand all of the code.]