testcontainers_modules/google_cloud_sdk_emulators/
mod.rs

1use std::borrow::Cow;
2
3use testcontainers::{
4    core::{ContainerPort, WaitFor},
5    Image,
6};
7
8const NAME: &str = "google/cloud-sdk";
9const TAG: &str = "362.0.0-emulators";
10
11const HOST: &str = "0.0.0.0";
12/// Port that the [`Bigtable`] emulator container has internally
13/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
14///
15/// [`Bigtable`]: https://cloud.google.com/bigtable
16pub const BIGTABLE_PORT: u16 = 8086;
17/// Port that the [`Datastore`] emulator container has internally
18/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
19///
20/// [`Datastore`]: https://cloud.google.com/datastore
21pub const DATASTORE_PORT: u16 = 8081;
22/// Port that the [`Firestore`] emulator container has internally
23/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
24///
25/// [`Firestore`]: https://cloud.google.com/firestore
26pub const FIRESTORE_PORT: u16 = 8080;
27/// Port that the [`Pub/Sub`] emulator container has internally
28/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
29///
30/// [`Pub/Sub`]: https://cloud.google.com/pubsub
31pub const PUBSUB_PORT: u16 = 8085;
32/// Port that the [`Spanner`] emulator container has internally
33/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
34///
35/// [`Spanner`]: https://cloud.google.com/spanner
36pub const SPANNER_PORT: u16 = 9010;
37
38/// Configuration for Google Cloud SDK emulator command-line arguments.
39///
40/// This struct specifies which Google Cloud service emulator to run and
41/// the network configuration for the emulator.
42#[derive(Debug, Clone)]
43pub struct CloudSdkCmd {
44    /// The hostname or IP address to bind the emulator to.
45    pub host: String,
46    /// The port number to expose the emulator on.
47    pub port: u16,
48    /// The specific Google Cloud service emulator to run.
49    pub emulator: Emulator,
50}
51
52/// Enum representing the different Google Cloud service emulators available.
53///
54/// Each variant corresponds to a specific Google Cloud service that can be
55/// emulated locally for testing purposes.
56#[derive(Debug, Clone, Eq, PartialEq)]
57pub enum Emulator {
58    /// Cloud Bigtable emulator for NoSQL wide-column database testing.
59    Bigtable,
60    /// Cloud Datastore emulator for NoSQL document database testing.
61    Datastore {
62        /// A project ID
63        project: String,
64    },
65    /// Cloud Firestore emulator for NoSQL document database testing.
66    Firestore,
67    /// Cloud Pub/Sub emulator for messaging service testing.
68    PubSub,
69    /// Cloud Spanner emulator for globally distributed relational database testing.
70    Spanner,
71}
72
73impl IntoIterator for &CloudSdkCmd {
74    type Item = String;
75    type IntoIter = <Vec<String> as IntoIterator>::IntoIter;
76
77    fn into_iter(self) -> Self::IntoIter {
78        let (emulator, project) = match &self.emulator {
79            Emulator::Bigtable => ("bigtable", None),
80            Emulator::Datastore { project } => ("datastore", Some(project)),
81            Emulator::Firestore => ("firestore", None),
82            Emulator::PubSub => ("pubsub", None),
83            Emulator::Spanner => ("spanner", None),
84        };
85        let mut args = vec![
86            "gcloud".to_owned(),
87            "beta".to_owned(),
88            "emulators".to_owned(),
89            emulator.to_owned(),
90            "start".to_owned(),
91        ];
92        if let Some(project) = project {
93            args.push("--project".to_owned());
94            args.push(project.to_owned());
95        }
96        args.push("--host-port".to_owned());
97        args.push(format!("{}:{}", self.host, self.port));
98
99        args.into_iter()
100    }
101}
102
103/// Module to work with Google Cloud SDK emulators inside of tests.
104///
105/// Starts an instance of the Google Cloud SDK emulators based on the official
106/// [`Google Cloud SDK docker image`]. This module provides local emulators for
107/// various Google Cloud services including Bigtable, Datastore, Firestore, Pub/Sub,
108/// and Spanner.
109///
110/// # Example
111/// ```
112/// use testcontainers_modules::{
113///     google_cloud_sdk_emulators::CloudSdk, testcontainers::runners::SyncRunner,
114/// };
115///
116/// let pubsub_emulator = CloudSdk::pubsub().start().unwrap();
117/// let host = pubsub_emulator.get_host().unwrap();
118/// let port = pubsub_emulator.get_host_port_ipv4(8085).unwrap();
119///
120/// // Use the Pub/Sub emulator at {host}:{port}
121/// ```
122///
123/// [`Google Cloud SDK docker image`]: https://hub.docker.com/r/google/cloud-sdk
124#[derive(Debug, Clone)]
125pub struct CloudSdk {
126    exposed_ports: Vec<ContainerPort>,
127    ready_condition: WaitFor,
128    cmd: CloudSdkCmd,
129}
130
131impl Image for CloudSdk {
132    fn name(&self) -> &str {
133        NAME
134    }
135
136    fn tag(&self) -> &str {
137        TAG
138    }
139
140    fn ready_conditions(&self) -> Vec<WaitFor> {
141        vec![self.ready_condition.clone()]
142    }
143
144    fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
145        &self.cmd
146    }
147
148    fn expose_ports(&self) -> &[ContainerPort] {
149        &self.exposed_ports
150    }
151}
152
153impl CloudSdk {
154    fn new(port: u16, emulator: Emulator, ready_condition: WaitFor) -> Self {
155        let cmd = CloudSdkCmd {
156            host: HOST.to_owned(),
157            port,
158            emulator,
159        };
160        Self {
161            exposed_ports: vec![ContainerPort::Tcp(port)],
162            ready_condition,
163            cmd,
164        }
165    }
166
167    /// Creates a new CloudSdk instance configured for Cloud Bigtable emulation.
168    ///
169    /// The Bigtable emulator will be available on port 8086.
170    ///
171    /// # Example
172    /// ```
173    /// use testcontainers_modules::google_cloud_sdk_emulators::CloudSdk;
174    ///
175    /// let bigtable = CloudSdk::bigtable();
176    /// ```
177    pub fn bigtable() -> Self {
178        Self::new(
179            BIGTABLE_PORT,
180            Emulator::Bigtable,
181            WaitFor::message_on_stderr("[bigtable] Cloud Bigtable emulator running on"),
182        )
183    }
184
185    /// Creates a new CloudSdk instance configured for Cloud Firestore emulation.
186    ///
187    /// The Firestore emulator will be available on port 8080.
188    ///
189    /// # Example
190    /// ```
191    /// use testcontainers_modules::google_cloud_sdk_emulators::CloudSdk;
192    ///
193    /// let firestore = CloudSdk::firestore();
194    /// ```
195    pub fn firestore() -> Self {
196        Self::new(
197            FIRESTORE_PORT,
198            Emulator::Firestore,
199            WaitFor::message_on_stderr("[firestore] Dev App Server is now running"),
200        )
201    }
202
203    /// Creates a new CloudSdk instance configured for Cloud Datastore emulation.
204    ///
205    /// The Datastore emulator will be available on port 8081.
206    ///
207    /// # Arguments
208    /// * `project` - The Google Cloud project ID to use for the Datastore emulator
209    ///
210    /// # Example
211    /// ```
212    /// use testcontainers_modules::google_cloud_sdk_emulators::CloudSdk;
213    ///
214    /// let datastore = CloudSdk::datastore("my-test-project");
215    /// ```
216    pub fn datastore(project: impl Into<String>) -> Self {
217        let project = project.into();
218        Self::new(
219            DATASTORE_PORT,
220            Emulator::Datastore { project },
221            WaitFor::message_on_stderr("[datastore] Dev App Server is now running"),
222        )
223    }
224
225    /// Creates a new CloudSdk instance configured for Cloud Pub/Sub emulation.
226    ///
227    /// The Pub/Sub emulator will be available on port 8085.
228    ///
229    /// # Example
230    /// ```
231    /// use testcontainers_modules::google_cloud_sdk_emulators::CloudSdk;
232    ///
233    /// let pubsub = CloudSdk::pubsub();
234    /// ```
235    pub fn pubsub() -> Self {
236        Self::new(
237            PUBSUB_PORT,
238            Emulator::PubSub,
239            WaitFor::message_on_stderr("[pubsub] INFO: Server started, listening on"),
240        )
241    }
242
243    /// Creates a new CloudSdk instance configured for Cloud Spanner emulation.
244    ///
245    /// The Spanner emulator will be available on port 9010.
246    ///
247    /// # Example
248    /// ```
249    /// use testcontainers_modules::google_cloud_sdk_emulators::CloudSdk;
250    ///
251    /// let spanner = CloudSdk::spanner();
252    /// ```
253    pub fn spanner() -> Self {
254        Self::new(
255            SPANNER_PORT, // gRPC port
256            Emulator::Spanner,
257            WaitFor::message_on_stderr("Cloud Spanner emulator running"),
258        )
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use std::ops::Range;
265
266    use crate::{google_cloud_sdk_emulators, testcontainers::runners::SyncRunner};
267
268    const RANDOM_PORTS: Range<u16> = 32768..65535;
269
270    #[test]
271    fn bigtable_emulator_expose_port() -> Result<(), Box<dyn std::error::Error + 'static>> {
272        let _ = pretty_env_logger::try_init();
273        let node = (google_cloud_sdk_emulators::CloudSdk::bigtable()).start()?;
274        let port = node.get_host_port_ipv4(google_cloud_sdk_emulators::BIGTABLE_PORT)?;
275        assert!(RANDOM_PORTS.contains(&port), "Port {port} not found");
276        Ok(())
277    }
278
279    #[test]
280    fn datastore_emulator_expose_port() -> Result<(), Box<dyn std::error::Error + 'static>> {
281        let _ = pretty_env_logger::try_init();
282        let node = google_cloud_sdk_emulators::CloudSdk::datastore("test").start()?;
283        let port = node.get_host_port_ipv4(google_cloud_sdk_emulators::DATASTORE_PORT)?;
284        assert!(RANDOM_PORTS.contains(&port), "Port {port} not found");
285        Ok(())
286    }
287
288    #[test]
289    fn firestore_emulator_expose_port() -> Result<(), Box<dyn std::error::Error + 'static>> {
290        let _ = pretty_env_logger::try_init();
291        let node = google_cloud_sdk_emulators::CloudSdk::firestore().start()?;
292        let port = node.get_host_port_ipv4(google_cloud_sdk_emulators::FIRESTORE_PORT)?;
293        assert!(RANDOM_PORTS.contains(&port), "Port {port} not found");
294        Ok(())
295    }
296
297    #[test]
298    fn pubsub_emulator_expose_port() -> Result<(), Box<dyn std::error::Error + 'static>> {
299        let _ = pretty_env_logger::try_init();
300        let node = google_cloud_sdk_emulators::CloudSdk::pubsub().start()?;
301        let port = node.get_host_port_ipv4(google_cloud_sdk_emulators::PUBSUB_PORT)?;
302        assert!(RANDOM_PORTS.contains(&port), "Port {port} not found");
303        Ok(())
304    }
305
306    #[test]
307    fn spanner_emulator_expose_port() -> Result<(), Box<dyn std::error::Error + 'static>> {
308        let _ = pretty_env_logger::try_init();
309        let node = google_cloud_sdk_emulators::CloudSdk::spanner().start()?;
310        let port = node.get_host_port_ipv4(google_cloud_sdk_emulators::SPANNER_PORT)?;
311        assert!(RANDOM_PORTS.contains(&port), "Port {port} not found");
312        Ok(())
313    }
314}