Skip to main content

praxis_filter/pipeline/
build.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Pipeline construction and ordering diagnostics.
5
6use std::{collections::HashMap, mem, sync::Arc};
7
8use praxis_core::{config::FilterEntry, id::IdGenerator, time::SystemTimeSource};
9use tracing::{debug, warn};
10
11use super::{FilterPipeline, body::compute_body_capabilities, filter::PipelineFilter};
12use crate::{FilterError, any_filter::AnyFilter, registry::FilterRegistry};
13
14// -----------------------------------------------------------------------------
15// FilterPipeline Factory
16// -----------------------------------------------------------------------------
17
18impl FilterPipeline {
19    /// Build a pipeline by instantiating each filter entry via the registry.
20    ///
21    /// Conditions are moved out of entries via [`mem::take`] to avoid
22    /// cloning. After this call, each entry's condition vecs are empty.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`FilterError`] if any filter fails to instantiate.
27    #[expect(clippy::too_many_lines, reason = "pipeline construction is inherently sequential")]
28    pub fn build(entries: &mut [FilterEntry], registry: &FilterRegistry) -> Result<Self, FilterError> {
29        let mut filters = Vec::with_capacity(entries.len());
30        for (filter_id, entry) in entries.iter_mut().enumerate() {
31            let filter = registry.create(&entry.filter_type, &entry.config)?;
32            warn_tcp_unsupported_fields(&filter, entry);
33            let has_conditions = !entry.conditions.is_empty() || !entry.response_conditions.is_empty();
34            debug!(
35                filter = filter.name(),
36                conditions = has_conditions,
37                "filter added to pipeline"
38            );
39            let mut pf = PipelineFilter::new(
40                filter_id,
41                filter,
42                mem::take(&mut entry.conditions),
43                mem::take(&mut entry.response_conditions),
44            );
45            pf.failure_mode = entry.failure_mode;
46            pf.name = entry.name.as_ref().map(|n| Arc::from(n.as_str()));
47            filters.push(pf);
48        }
49        let body_capabilities = compute_body_capabilities(&filters);
50        let compression = extract_compression_config(&filters);
51
52        Ok(Self {
53            body_capabilities,
54            compression,
55            filters,
56            health_registry: None,
57            id_generator: Arc::new(IdGenerator::new()),
58            kv_stores: None,
59            pipeline_extensions: Vec::new(),
60            time_source: Arc::new(SystemTimeSource),
61        })
62    }
63
64    /// Build a pipeline with branch chain resolution.
65    ///
66    /// Like [`build`], but also resolves `branch_chains` on each
67    /// filter entry into runtime `ResolvedBranch` types using
68    /// the provided chain lookup table.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`FilterError`] if any filter fails to instantiate
73    /// or any branch chain reference is unresolvable.
74    ///
75    /// [`build`]: FilterPipeline::build
76    pub fn build_with_chains(
77        entries: &mut [FilterEntry],
78        registry: &FilterRegistry,
79        chains: &HashMap<&str, &[FilterEntry]>,
80    ) -> Result<Self, FilterError> {
81        let filters = super::build_branch::resolve_chain_filters(entries, registry, chains, 0)?;
82        let body_capabilities = compute_body_capabilities(&filters);
83        let compression = extract_compression_config(&filters);
84        Ok(Self {
85            body_capabilities,
86            compression,
87            filters,
88            health_registry: None,
89            id_generator: Arc::new(IdGenerator::new()),
90            kv_stores: None,
91            pipeline_extensions: Vec::new(),
92            time_source: Arc::new(SystemTimeSource),
93        })
94    }
95
96    /// Validate the pipeline for structural misconfigurations that
97    /// would cause runtime failures (502s, unreachable filters,
98    /// cluster mismatches).
99    ///
100    /// ```
101    /// use praxis_filter::{FailureMode, FilterEntry, FilterPipeline, FilterRegistry};
102    ///
103    /// let registry = FilterRegistry::with_builtins();
104    /// let mut entries = vec![FilterEntry {
105    ///     branch_chains: None,
106    ///     filter_type: "load_balancer".into(),
107    ///     config: serde_yaml::from_str("clusters: []").unwrap(),
108    ///     conditions: vec![],
109    ///     name: None,
110    ///     response_conditions: vec![],
111    ///     failure_mode: FailureMode::default(),
112    /// }];
113    /// let pipeline = FilterPipeline::build(&mut entries, &registry).unwrap();
114    /// let errors = pipeline.ordering_errors(&entries, false);
115    /// assert!(
116    ///     errors
117    ///         .iter()
118    ///         .any(|e| e.contains("without a preceding router"))
119    /// );
120    /// ```
121    ///
122    /// [`build`]: FilterPipeline::build
123    pub fn ordering_errors(&self, entries: &[FilterEntry], allow_open_security: bool) -> Vec<String> {
124        let names: Vec<&str> = self.filters.iter().map(|pf| pf.filter.name()).collect();
125
126        let mut errors = Vec::new();
127
128        super::checks::check_lb_without_cluster_selector(&names, &mut errors);
129        super::checks::check_unconditional_static_response(&names, &self.filters, &mut errors);
130        super::checks::check_conditional_security(&names, &self.filters, &mut errors);
131        super::checks::check_open_security_filters(&names, &self.filters, allow_open_security, &mut errors);
132        super::checks::check_duplicate_routers(&names, &mut errors);
133        super::checks::check_duplicate_load_balancers(&names, &mut errors);
134        super::checks::check_misaligned_clusters(entries, &mut errors);
135        super::checks::check_duplicate_rewrite_filters(&names, entries, &mut errors);
136
137        errors
138    }
139
140    /// Check for non-fatal ordering advisories.
141    ///
142    /// Currently detects: all routers conditional with no fallback.
143    ///
144    /// ```
145    /// use praxis_filter::{FailureMode, FilterEntry, FilterPipeline, FilterRegistry};
146    ///
147    /// let registry = FilterRegistry::with_builtins();
148    /// let mut entries = vec![FilterEntry {
149    ///     branch_chains: None,
150    ///     filter_type: "router".into(),
151    ///     config: serde_yaml::from_str("routes: []").unwrap(),
152    ///     conditions: vec![praxis_core::config::Condition::When(
153    ///         praxis_core::config::ConditionMatch {
154    ///             path: None,
155    ///             path_prefix: Some("/api".to_owned()),
156    ///             methods: None,
157    ///             headers: None,
158    ///         },
159    ///     )],
160    ///     name: None,
161    ///     response_conditions: vec![],
162    ///     failure_mode: FailureMode::default(),
163    /// }];
164    /// let pipeline = FilterPipeline::build(&mut entries, &registry).unwrap();
165    /// let warnings = pipeline.ordering_warnings();
166    /// assert!(
167    ///     warnings
168    ///         .iter()
169    ///         .any(|w| w.contains("all router filters are conditional"))
170    /// );
171    /// ```
172    pub fn ordering_warnings(&self) -> Vec<String> {
173        let names: Vec<&str> = self.filters.iter().map(|pf| pf.filter.name()).collect();
174
175        let mut warnings = Vec::new();
176
177        super::checks::check_router_without_lb(&names, &mut warnings);
178        super::checks::check_all_routers_conditional(&names, &self.filters, &mut warnings);
179
180        warnings
181    }
182}
183
184// -----------------------------------------------------------------------------
185// Utility Functions
186// -----------------------------------------------------------------------------
187
188/// Warn when a TCP filter has conditions or branch chains configured.
189///
190/// TCP filters do not support conditions or branching; these fields
191/// are silently ignored at runtime. Logging at build time helps
192/// operators catch misconfigurations.
193fn warn_tcp_unsupported_fields(filter: &AnyFilter, entry: &FilterEntry) {
194    if !matches!(filter, AnyFilter::Tcp(_)) {
195        return;
196    }
197    if !entry.conditions.is_empty() || !entry.response_conditions.is_empty() {
198        warn!(
199            filter = filter.name(),
200            "TCP filter has conditions that will be ignored; \
201             conditions are only evaluated for HTTP filters"
202        );
203    }
204    if entry.branch_chains.is_some() {
205        warn!(
206            filter = filter.name(),
207            "TCP filter has branch_chains that will be ignored; \
208             branching is only supported for HTTP filters"
209        );
210    }
211}
212
213/// Scan the filter list for a compression filter and extract its config.
214fn extract_compression_config(
215    filters: &[PipelineFilter],
216) -> Option<crate::builtins::http::payload_processing::compression_config::CompressionConfig> {
217    for pf in filters {
218        if let AnyFilter::Http(f) = &pf.filter
219            && let Some(cfg) = f.compression_config()
220        {
221            return Some(cfg.clone());
222        }
223    }
224    None
225}