Skip to content

Commit ad9576c

Browse files
committed
feat: add integration and pipeline tests for set_filter_state
- Add 3 integration tests for xDS config parsing (Istio waypoint) - Add 5 HTTP pipeline tests for filter application - Fix skip_if_empty to handle '-' default values - Add :authority and :method pseudo-headers to RequestContext Signed-off-by: Eeshu-Yadav <[email protected]>
1 parent dc97aad commit ad9576c

File tree

8 files changed

+756
-443
lines changed

8 files changed

+756
-443
lines changed

orion-configuration/src/config/network_filters/http_connection_manager/http_filters.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub enum HttpFilterType {
6767
/// Istio peer metadata filter (parsed but may not be executed)
6868
PeerMetadata(peer_metadata::PeerMetadataConfig),
6969
/// Envoy set filter state filter (parsed but may not be executed)
70-
SetFilterState(set_filter_state::SetFilterStateConfig),
70+
SetFilterState(set_filter_state::SetFilterState),
7171
}
7272

7373
#[cfg(feature = "envoy-conversions")]
@@ -90,7 +90,6 @@ mod envoy_conversions {
9090
local_ratelimit::v3::LocalRateLimit as EnvoyLocalRateLimit,
9191
rbac::v3::{Rbac as EnvoyRbac, RbacPerRoute as EnvoyRbacPerRoute},
9292
router::v3::Router as EnvoyRouter,
93-
set_filter_state::v3::Config as EnvoySetFilterStateConfig,
9493
},
9594
network::http_connection_manager::v3::{
9695
http_filter::ConfigType as EnvoyConfigType, HttpFilter as EnvoyHttpFilter,
@@ -159,7 +158,7 @@ mod envoy_conversions {
159158
Router(EnvoyRouter),
160159
Ignored,
161160
PeerMetadata(super::peer_metadata::PeerMetadataConfig),
162-
SetFilterState(super::set_filter_state::SetFilterStateConfig),
161+
SetFilterState(super::set_filter_state::SetFilterState),
163162
}
164163

165164
impl TryFrom<Any> for SupportedEnvoyFilter {
@@ -179,8 +178,8 @@ mod envoy_conversions {
179178
url if url == super::peer_metadata::PeerMetadataConfig::TYPE_URL => {
180179
super::peer_metadata::PeerMetadataConfig::from_typed_struct(&parsed).map(Self::PeerMetadata)
181180
},
182-
url if url == super::set_filter_state::SetFilterStateConfig::TYPE_URL => {
183-
super::set_filter_state::SetFilterStateConfig::from_typed_struct(&parsed)
181+
url if url == super::set_filter_state::SetFilterState::TYPE_URL => {
182+
super::set_filter_state::SetFilterState::from_typed_struct(&parsed)
184183
.map(Self::SetFilterState)
185184
},
186185
_ => Err(GenericError::unsupported_variant(format!(
@@ -386,14 +385,17 @@ mod typed_struct_integration_tests {
386385

387386
#[test]
388387
fn test_try_from_any_typed_struct_set_filter_state() {
389-
// Build inner Struct for SetFilterStateConfig with one action
390-
let mut action_fields = BTreeMap::new();
391-
action_fields.insert("object_key".to_string(), Value { kind: Some(Kind::StringValue("test_key".to_string())) });
392-
393-
let mut list_values = Vec::new();
388+
// Build inner Struct for SetFilterState with one action including format_string
394389
let mut struct_fields = BTreeMap::new();
395390
struct_fields.insert("object_key".to_string(), Value { kind: Some(Kind::StringValue("test_key".to_string())) });
391+
struct_fields.insert(
392+
"format_string".to_string(),
393+
Value { kind: Some(Kind::StringValue("%REQ(:authority)%".to_string())) },
394+
);
395+
396396
let action_struct = Value { kind: Some(Kind::StructValue(Struct { fields: struct_fields })) };
397+
398+
let mut list_values = Vec::new();
397399
list_values.push(action_struct);
398400

399401
let mut fields = BTreeMap::new();
@@ -403,7 +405,7 @@ mod typed_struct_integration_tests {
403405
);
404406

405407
let typed_struct = TypedStruct {
406-
type_url: super::set_filter_state::SetFilterStateConfig::TYPE_URL.to_string(),
408+
type_url: super::set_filter_state::SetFilterState::TYPE_URL.to_string(),
407409
value: Some(Struct { fields }),
408410
};
409411

@@ -415,8 +417,8 @@ mod typed_struct_integration_tests {
415417
let parsed = SupportedEnvoyFilter::try_from(any).expect("should parse typed struct");
416418
match parsed {
417419
SupportedEnvoyFilter::SetFilterState(cfg) => {
418-
assert!(cfg.on_request_headers.is_some());
419-
let actions = cfg.on_request_headers.unwrap();
420+
assert!(!cfg.on_request_headers.is_empty());
421+
let actions = &cfg.on_request_headers;
420422
assert_eq!(actions.len(), 1);
421423
assert_eq!(actions[0].object_key, "test_key");
422424
},

orion-configuration/src/config/network_filters/http_connection_manager/http_filters/filter_registry.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
//
1717

1818
use super::peer_metadata::PeerMetadataConfig;
19-
use super::set_filter_state::SetFilterStateConfig;
19+
use super::set_filter_state::SetFilterState;
2020
use crate::config::common::GenericError;
2121
use crate::typed_struct::registry::{global_registry, GenericFilterParser};
2222
use crate::typed_struct::TypedStructFilter;
@@ -27,7 +27,7 @@ pub fn register_all_filters() -> Result<(), GenericError> {
2727
let peer_metadata_parser = GenericFilterParser::<PeerMetadataConfig>::new(PeerMetadataConfig::TYPE_URL);
2828
registry.register_dynamic(peer_metadata_parser)?;
2929

30-
let set_filter_state_parser = GenericFilterParser::<SetFilterStateConfig>::new(SetFilterStateConfig::TYPE_URL);
30+
let set_filter_state_parser = GenericFilterParser::<SetFilterState>::new(SetFilterState::TYPE_URL);
3131
registry.register_dynamic(set_filter_state_parser)?;
3232

3333
Ok(())
@@ -53,7 +53,7 @@ mod tests {
5353

5454
let registry = global_registry();
5555
assert!(registry.is_supported(PeerMetadataConfig::TYPE_URL));
56-
assert!(registry.is_supported(SetFilterStateConfig::TYPE_URL));
56+
assert!(registry.is_supported(SetFilterState::TYPE_URL));
5757
}
5858

5959
#[test]
@@ -63,6 +63,6 @@ mod tests {
6363

6464
let registry = global_registry();
6565
assert!(registry.is_supported(PeerMetadataConfig::TYPE_URL));
66-
assert!(registry.is_supported(SetFilterStateConfig::TYPE_URL));
66+
assert!(registry.is_supported(SetFilterState::TYPE_URL));
6767
}
6868
}

orion-configuration/src/config/network_filters/http_connection_manager/http_filters/set_filter_state.rs

Lines changed: 105 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717

1818
use compact_str::CompactString;
1919
use serde::{Deserialize, Serialize};
20+
use serde_json::Value as JsonValue;
2021

2122
use super::super::is_default;
23+
use crate::config::common::GenericError;
24+
use crate::typed_struct::TypedStructFilter;
2225

2326
/// Set Filter State HTTP filter configuration
24-
///
27+
///
2528
/// This filter dynamically sets filter state objects based on request data.
2629
/// Filter state can be used for routing decisions, metadata propagation,
2730
/// and internal connection handling.
@@ -36,33 +39,33 @@ pub struct SetFilterState {
3639
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
3740
pub struct FilterStateValue {
3841
/// Filter state object key (required)
39-
///
42+
///
4043
/// Examples:
4144
/// - "io.istio.connect_authority" (Istio HBONE)
4245
/// - "envoy.filters.listener.original_dst.local_ip"
4346
/// - "envoy.tcp_proxy.cluster"
4447
pub object_key: CompactString,
45-
48+
4649
/// Optional factory key for object creation
4750
/// If not specified, object_key is used for factory lookup
4851
#[serde(skip_serializing_if = "Option::is_none", default)]
4952
pub factory_key: Option<CompactString>,
50-
53+
5154
/// Format string to generate the value
5255
/// Supports Envoy substitution format strings like:
5356
/// - %REQ(:authority)% - Request header
5457
/// - %DOWNSTREAM_REMOTE_ADDRESS% - Client IP
5558
/// - %UPSTREAM_HOST% - Selected upstream
5659
pub format_string: FormatString,
57-
60+
5861
/// Make this value read-only (cannot be overridden by other filters)
5962
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
6063
pub read_only: bool,
61-
64+
6265
/// Share with upstream internal connections
6366
#[serde(skip_serializing_if = "is_default", default)]
6467
pub shared_with_upstream: SharedWithUpstream,
65-
68+
6669
/// Skip setting the value if it evaluates to empty string
6770
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
6871
pub skip_if_empty: bool,
@@ -82,13 +85,12 @@ pub enum SharedWithUpstream {
8285
}
8386

8487
/// Format string for generating filter state values
85-
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
86-
#[serde(untagged)]
88+
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
8789
pub enum FormatString {
8890
/// Plain text format string with command operators
8991
/// Example: "%REQ(:authority)%"
9092
Text(CompactString),
91-
93+
9294
/// Structured format (JSON, etc.) - future extension
9395
Structured {
9496
format: CompactString,
@@ -97,64 +99,100 @@ pub enum FormatString {
9799
},
98100
}
99101

102+
impl<'de> Deserialize<'de> for FormatString {
103+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104+
where
105+
D: serde::Deserializer<'de>,
106+
{
107+
// Deserialize into a serde_json::Value first so we can support multiple input shapes
108+
let v = JsonValue::deserialize(deserializer)?;
109+
110+
match v {
111+
JsonValue::String(s) => Ok(FormatString::Text(CompactString::from(s))),
112+
JsonValue::Object(mut map) => {
113+
// Envoy typed struct may use nested `text_format_source: { inline_string: "..." }`
114+
if let Some(tf_source) = map.remove("text_format_source") {
115+
if let Some(inline) = tf_source.get("inline_string") {
116+
if let Some(s) = inline.as_str() {
117+
return Ok(FormatString::Text(CompactString::from(s)));
118+
}
119+
}
120+
// Inline bytes or other specifiers not supported here
121+
}
122+
123+
// Also accept a direct `text_format` field
124+
if let Some(text_fmt) = map.remove("text_format") {
125+
if let Some(s) = text_fmt.as_str() {
126+
return Ok(FormatString::Text(CompactString::from(s)));
127+
}
128+
}
129+
130+
// Structured form: look for `format` key
131+
if let Some(format_field) = map.remove("format") {
132+
if let Some(s) = format_field.as_str() {
133+
let json_format = map.remove("json_format");
134+
return Ok(FormatString::Structured { format: CompactString::from(s), json_format });
135+
}
136+
}
137+
138+
Err(serde::de::Error::custom("unsupported FormatString representation"))
139+
},
140+
other => Err(serde::de::Error::custom(format!("unexpected FormatString type: {:?}", other))),
141+
}
142+
}
143+
}
144+
100145
#[cfg(feature = "envoy-conversions")]
101146
mod envoy_conversions {
102147
use super::*;
103148
use crate::config::common::*;
104-
use orion_data_plane_api::envoy_data_plane_api::{
105-
envoy::{
106-
config::core::v3::SubstitutionFormatString as EnvoySubstitutionFormatString,
107-
extensions::filters::{
108-
common::set_filter_state::v3::{
109-
FilterStateValue as EnvoyFilterStateValue,
110-
filter_state_value::{
111-
Key as EnvoyKey,
112-
Value as EnvoyValue,
113-
SharedWithUpstream as EnvoySharedWithUpstream
114-
},
149+
use orion_data_plane_api::envoy_data_plane_api::envoy::{
150+
config::core::v3::SubstitutionFormatString as EnvoySubstitutionFormatString,
151+
extensions::filters::{
152+
common::set_filter_state::v3::{
153+
filter_state_value::{
154+
Key as EnvoyKey, SharedWithUpstream as EnvoySharedWithUpstream, Value as EnvoyValue,
115155
},
116-
http::set_filter_state::v3::Config as EnvoySetFilterStateConfig,
156+
FilterStateValue as EnvoyFilterStateValue,
117157
},
158+
http::set_filter_state::v3::Config as EnvoySetFilterStateConfig,
118159
},
119160
};
120-
161+
121162
impl TryFrom<EnvoySetFilterStateConfig> for SetFilterState {
122163
type Error = GenericError;
123-
164+
124165
fn try_from(envoy: EnvoySetFilterStateConfig) -> Result<Self, Self::Error> {
125-
let on_request_headers = envoy.on_request_headers
166+
let on_request_headers = envoy
167+
.on_request_headers
126168
.into_iter()
127169
.map(FilterStateValue::try_from)
128170
.collect::<Result<Vec<_>, _>>()
129171
.with_node("on_request_headers")?;
130-
172+
131173
Ok(Self { on_request_headers })
132174
}
133175
}
134-
176+
135177
impl TryFrom<EnvoyFilterStateValue> for FilterStateValue {
136178
type Error = GenericError;
137-
179+
138180
fn try_from(envoy: EnvoyFilterStateValue) -> Result<Self, Self::Error> {
139181
let object_key = match envoy.key {
140182
Some(EnvoyKey::ObjectKey(key)) => CompactString::from(key),
141183
None => return Err(GenericError::from_msg("missing object_key in FilterStateValue")),
142184
};
143-
144-
let factory_key = if envoy.factory_key.is_empty() {
145-
None
146-
} else {
147-
Some(envoy.factory_key.into())
148-
};
149-
185+
186+
let factory_key = (!envoy.factory_key.is_empty()).then(|| envoy.factory_key.into());
187+
150188
let format_string = match envoy.value {
151189
Some(EnvoyValue::FormatString(fs)) => FormatString::try_from(fs).with_node("format_string")?,
152190
None => return Err(GenericError::from_msg("missing format_string in FilterStateValue")),
153191
};
154-
155-
let shared_with_upstream = SharedWithUpstream::try_from(envoy.shared_with_upstream)
156-
.with_node("shared_with_upstream")?;
157-
192+
193+
let shared_with_upstream =
194+
SharedWithUpstream::try_from(envoy.shared_with_upstream).with_node("shared_with_upstream")?;
195+
158196
Ok(Self {
159197
object_key,
160198
factory_key,
@@ -165,34 +203,31 @@ mod envoy_conversions {
165203
})
166204
}
167205
}
168-
206+
169207
impl TryFrom<EnvoySubstitutionFormatString> for FormatString {
170208
type Error = GenericError;
171-
209+
172210
fn try_from(envoy: EnvoySubstitutionFormatString) -> Result<Self, Self::Error> {
173211
use orion_data_plane_api::envoy_data_plane_api::envoy::config::core::v3::{
174-
substitution_format_string::Format,
175-
data_source::Specifier,
212+
data_source::Specifier, substitution_format_string::Format,
176213
};
177-
214+
178215
match envoy.format {
179216
Some(Format::TextFormat(text)) => Ok(FormatString::Text(text.into())),
180-
Some(Format::TextFormatSource(source)) => {
181-
match source.specifier {
182-
Some(Specifier::InlineString(s)) => Ok(FormatString::Text(s.into())),
183-
Some(Specifier::InlineBytes(b)) => {
184-
let s = String::from_utf8(b)
185-
.map_err(|e| GenericError::from_msg(format!("Invalid UTF-8 in format string: {}", e)))?;
186-
Ok(FormatString::Text(s.into()))
187-
},
188-
Some(Specifier::Filename(_)) => {
189-
Err(GenericError::unsupported_variant("filename format strings not supported"))
190-
},
191-
Some(Specifier::EnvironmentVariable(_)) => {
192-
Err(GenericError::unsupported_variant("environment variable format strings not supported"))
193-
},
194-
None => Err(GenericError::from_msg("missing format string specifier")),
195-
}
217+
Some(Format::TextFormatSource(source)) => match source.specifier {
218+
Some(Specifier::InlineString(s)) => Ok(FormatString::Text(s.into())),
219+
Some(Specifier::InlineBytes(b)) => {
220+
let s = String::from_utf8(b)
221+
.map_err(|e| GenericError::from_msg(format!("Invalid UTF-8 in format string: {}", e)))?;
222+
Ok(FormatString::Text(s.into()))
223+
},
224+
Some(Specifier::Filename(_)) => {
225+
Err(GenericError::unsupported_variant("filename format strings not supported"))
226+
},
227+
Some(Specifier::EnvironmentVariable(_)) => {
228+
Err(GenericError::unsupported_variant("environment variable format strings not supported"))
229+
},
230+
None => Err(GenericError::from_msg("missing format string specifier")),
196231
},
197232
Some(Format::JsonFormat(_)) => {
198233
// JSON format not yet supported - would need structured logging
@@ -204,7 +239,7 @@ mod envoy_conversions {
204239
}
205240
impl TryFrom<i32> for SharedWithUpstream {
206241
type Error = GenericError;
207-
242+
208243
fn try_from(value: i32) -> Result<Self, Self::Error> {
209244
match EnvoySharedWithUpstream::try_from(value) {
210245
Ok(EnvoySharedWithUpstream::None) => Ok(Self::None),
@@ -216,6 +251,15 @@ mod envoy_conversions {
216251
}
217252
}
218253

254+
impl TypedStructFilter for SetFilterState {
255+
const TYPE_URL: &'static str = "type.googleapis.com/envoy.extensions.filters.http.set_filter_state.v3.Config";
256+
257+
fn from_json_value(value: JsonValue) -> Result<Self, GenericError> {
258+
serde_json::from_value(value)
259+
.map_err(|e| GenericError::from_msg_with_cause("Failed to deserialize SetFilterState from JSON", e))
260+
}
261+
}
262+
219263
#[cfg(test)]
220264
mod tests {
221265
use super::*;

0 commit comments

Comments
 (0)