pdk_test/services/flex/
mod.rs

1// Copyright (c) 2025, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! Flex Gateway service configurations
6//!
7//! This module provides predefined types and configurations for Flex Gateway services
8//! in integration tests. It supports both static and dynamic configurations,
9//! API management and policy testing scenarios.
10//!
11
12use std::collections::HashMap;
13use std::time::Duration;
14use versions::Versioning;
15
16use crate::config::{Config, ContainerConfig};
17use crate::container::Container;
18use crate::error::TestError;
19use crate::image::Image;
20use crate::port::{Port, PortAccess};
21use crate::probe::{MessageProbe, MessageSource};
22use crate::service::Service;
23use crate::services::flex::dynamic::DynamicFlexConfig;
24
25const FLEX_LOCAL_BASE: &str = "/usr/local/share";
26const FLEX_CONFIG_BASE: &str = "mulesoft/flex-gateway/conf.d";
27const FLEX_SCHEMA: &str = "http";
28
29/// Default Flex Docker image name.
30pub const FLEX_IMAGE_NAME: &str = "mulesoft/flex-gateway";
31
32mod api;
33mod dynamic;
34mod gcl;
35mod policy;
36
37#[cfg(feature = "experimental")]
38mod service;
39
40pub use api::{ApiConfig, ApiConfigBuilder};
41pub use policy::{PolicyConfig, PolicyConfigBuilder};
42
43#[cfg(feature = "experimental")]
44pub use service::{UpstreamServiceConfig, UpstreamServiceConfigBuilder};
45
46/// Configuration for a local Flex service.
47#[derive(Debug, Clone)]
48pub struct FlexConfig {
49    hostname: String,
50    image_name: String,
51    version: String,
52    config_mounts: Vec<(String, String)>,
53    ports: Vec<Port>,
54    timeout: Duration,
55    dynamic_config: DynamicFlexConfig,
56}
57
58impl FlexConfig {
59    /// Returns the Flex hostname.
60    pub fn hostname(&self) -> &str {
61        &self.hostname
62    }
63
64    /// Returns the Flex Docker version.
65    pub fn version(&self) -> &str {
66        &self.version
67    }
68
69    /// Returns the Flex Docker image name.
70    pub fn image_name(&self) -> &str {
71        &self.image_name
72    }
73
74    /// Returns the map of Flex configuration mount points.
75    pub fn config_mounts(&self) -> &[(String, String)] {
76        &self.config_mounts
77    }
78
79    /// Returns the list of Flex listening ports.
80    pub fn ports(&self) -> &[Port] {
81        &self.ports
82    }
83
84    /// Returns the Flex readiness timeout.
85    pub fn timeout(&self) -> Duration {
86        self.timeout
87    }
88
89    /// Creates a new Flex configuration with default settings.
90    pub fn new() -> Self {
91        Self {
92            hostname: "local-flex".to_string(),
93            version: std::env::var("PDK_TEST_FLEX_IMAGE_VERSION")
94                .unwrap_or_else(|_| "latest".to_string()),
95            image_name: std::env::var("PDK_TEST_FLEX_IMAGE_NAME")
96                .unwrap_or_else(|_| FLEX_IMAGE_NAME.to_string()),
97            config_mounts: vec![],
98            ports: vec![],
99            timeout: Duration::from_secs(60),
100            dynamic_config: DynamicFlexConfig::new(),
101        }
102    }
103
104    /// Creates a builder for initialize a [`FlexConfig`].
105    pub fn builder() -> FlexConfigBuilder {
106        FlexConfigBuilder::new()
107    }
108}
109
110impl Default for FlexConfig {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116/// A builder for initilizing a [`FlexConfig`].
117#[derive(Debug)]
118pub struct FlexConfigBuilder {
119    config: FlexConfig,
120}
121
122impl FlexConfigBuilder {
123    fn new() -> Self {
124        Self {
125            config: FlexConfig::new(),
126        }
127    }
128
129    /// Sets the Flex hostname.
130    /// By default set to "local-flex".
131    pub fn hostname<T: Into<String>>(self, hostname: T) -> Self {
132        Self {
133            config: FlexConfig {
134                hostname: hostname.into(),
135                ..self.config
136            },
137        }
138    }
139
140    /// Sets the Flex Docker version.
141    /// By default set to "latest".
142    pub fn version<T: Into<String>>(self, version: T) -> Self {
143        Self {
144            config: FlexConfig {
145                version: version.into(),
146                ..self.config
147            },
148        }
149    }
150
151    /// Sets the Flex Docker image name.
152    /// By default set to [`FLEX_IMAGE_NAME`].
153    pub fn image_name<T: Into<String>>(self, image_name: T) -> Self {
154        Self {
155            config: FlexConfig {
156                image_name: image_name.into(),
157                ..self.config
158            },
159        }
160    }
161
162    /// Sets the Flex readiness timeout.
163    pub fn timeout(self, timeout: Duration) -> Self {
164        Self {
165            config: FlexConfig {
166                timeout,
167                ..self.config
168            },
169        }
170    }
171
172    /// Sets the map of Flex configuration mount points.
173    pub fn config_mounts<T, S, D>(self, config_mounts: T) -> Self
174    where
175        T: IntoIterator<Item = (S, D)>,
176        S: Into<String>,
177        D: Into<String>,
178    {
179        Self {
180            config: FlexConfig {
181                config_mounts: config_mounts
182                    .into_iter()
183                    .map(|(s, d)| (s.into(), d.into()))
184                    .collect(),
185                ..self.config
186            },
187        }
188    }
189
190    /// Sets the Flex listening ports.
191    pub fn ports<T>(self, ports: T) -> Self
192    where
193        T: IntoIterator<Item = Port>,
194    {
195        Self {
196            config: FlexConfig {
197                ports: ports.into_iter().collect(),
198                ..self.config
199            },
200        }
201    }
202
203    pub fn with_api(self, api: ApiConfig) -> Self {
204        let mut ports = self.config.ports;
205        ports.push(api.port);
206        Self {
207            config: FlexConfig {
208                ports,
209                dynamic_config: self.config.dynamic_config.with_api(api),
210                ..self.config
211            },
212        }
213    }
214
215    #[cfg(feature = "experimental")]
216    pub fn with_upstream_service(self, service: UpstreamServiceConfig) -> Self {
217        Self {
218            config: FlexConfig {
219                dynamic_config: self.config.dynamic_config.with_upstream_service(service),
220                ..self.config
221            },
222        }
223    }
224
225    /// Builds a new [`FlexConfig`].
226    pub fn build(self) -> FlexConfig {
227        self.config
228    }
229}
230
231fn readiness(version: &str, timeout: Duration) -> Result<MessageProbe, TestError> {
232    let versioning = Versioning::new(version)
233        .ok_or_else(|| TestError::Startup(format!("Unable to parse Flex version `{version}`.")))?;
234    let times = if version == "latest" || versioning >= Versioning::new("1.7.0").unwrap() {
235        1
236    } else if versioning >= Versioning::new("1.4.0").unwrap() {
237        2
238    } else {
239        1
240    };
241
242    Ok(MessageProbe::builder("cds: added/updated")
243        .times(times)
244        .timeout(timeout)
245        .source(MessageSource::StdOut)
246        .build())
247}
248
249impl Config for FlexConfig {
250    fn hostname(&self) -> &str {
251        &self.hostname
252    }
253
254    fn port(&self) -> Port {
255        self.ports.first().cloned().unwrap_or_default()
256    }
257
258    fn schema(&self) -> &str {
259        FLEX_SCHEMA
260    }
261
262    fn to_container_config(&self) -> Result<ContainerConfig, TestError> {
263        let config_mounts = self.config_mounts.iter();
264        let dynamic = self.dynamic_config.dirs()?;
265
266        let mounts = config_mounts.chain(dynamic.iter()).map(|(host, flex)| {
267            (
268                host.clone(),
269                FLEX_LOCAL_BASE.to_string(),
270                format!("{FLEX_CONFIG_BASE}/{flex}"),
271            )
272        });
273        let ports = self.ports.iter().map(|&m| PortAccess::published(m));
274
275        Ok(ContainerConfig::builder(
276            self.hostname.clone(),
277            Image::from_repository(&self.image_name).with_version(&self.version),
278        )
279        .ports(ports)
280        .mounts(mounts)
281        .readiness(readiness(&self.version, self.timeout)?)
282        .build())
283    }
284}
285
286/// Represents a Flex service instance.
287#[derive(Default, Clone)]
288pub struct Flex {
289    sockets: HashMap<Port, String>,
290}
291
292impl Flex {
293    /// Returns the external URL for a Flex listening `port`.
294    /// If the port was not configured, returns [`None`].
295    pub fn external_url(&self, port: Port) -> Option<String> {
296        self.sockets
297            .get(&port)
298            .map(|socket| format!("{FLEX_SCHEMA}://{socket}"))
299    }
300}
301
302impl Service for Flex {
303    type Config = FlexConfig;
304
305    fn new(_config: &Self::Config, container: &Container) -> Self {
306        Self {
307            sockets: container.sockets().clone(),
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use std::time::Duration;
315
316    use crate::error::TestError;
317
318    use super::readiness;
319
320    #[test]
321    fn readiness_for_1_3_0() -> Result<(), TestError> {
322        let readiness = readiness("1.3.0", Duration::from_secs(1))?;
323        assert_eq!(readiness.times(), 1);
324
325        Ok(())
326    }
327
328    #[test]
329    fn readiness_for_1_6_0() -> Result<(), TestError> {
330        let readiness = readiness("1.6.0", Duration::from_secs(1))?;
331        assert_eq!(readiness.times(), 2);
332
333        Ok(())
334    }
335
336    #[test]
337    fn readiness_for_1_7_0() -> Result<(), TestError> {
338        let readiness = readiness("1.7.0", Duration::from_secs(1))?;
339        assert_eq!(readiness.times(), 1);
340
341        Ok(())
342    }
343
344    #[test]
345    fn readiness_for_latest() -> Result<(), TestError> {
346        let readiness = readiness("latest", Duration::from_secs(1))?;
347        assert_eq!(readiness.times(), 1);
348
349        Ok(())
350    }
351}