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}