Skip to content

Commit d91bec6

Browse files
committed
feat: add support for Azurite docker
Refs: #108
1 parent 045dadc commit d91bec6

File tree

4 files changed

+304
-0
lines changed

4 files changed

+304
-0
lines changed

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1515

1616
[features]
1717
default = []
18+
azurite = []
1819
blocking = ["testcontainers/blocking"]
1920
watchdog = ["testcontainers/watchdog"]
2021
http_wait = ["testcontainers/http_wait"]
@@ -138,6 +139,10 @@ openssl-sys = { version = "0.9.103", features = ["vendored"] }
138139
native-tls = { version = "0.2.12", features = ["vendored"] }
139140
pulsar = "6.3"
140141
rqlite-rs = "0.6"
142+
azure_core = "0.21.0"
143+
azure_storage_blobs = "0.21.0"
144+
azure_storage = "0.21.0"
145+
base64 = "0.22.1"
141146

142147
[[example]]
143148
name = "postgres"
@@ -178,3 +183,7 @@ required-features = ["rqlite"]
178183
[[example]]
179184
name = "zitadel"
180185
required-features = ["zitadel", "postgres"]
186+
187+
[[example]]
188+
name = "azurite"
189+
required-features = ["azurite"]

examples/azurite.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use azure_storage::{prelude::*, CloudLocation};
2+
use azure_storage_blobs::prelude::*;
3+
use futures::stream::StreamExt;
4+
use testcontainers::runners::AsyncRunner;
5+
use testcontainers_modules::azurite::{Azurite, BLOB_PORT};
6+
7+
#[tokio::main]
8+
async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
9+
let container = Azurite::default().start().await?;
10+
let container_client = ClientBuilder::with_location(
11+
CloudLocation::Emulator {
12+
address: "127.0.0.1".to_owned(),
13+
port: container.get_host_port_ipv4(BLOB_PORT).await?,
14+
},
15+
StorageCredentials::emulator(),
16+
)
17+
.container_client("container-name");
18+
19+
container_client.create().await?;
20+
let blob_client = container_client.blob_client("blob-name");
21+
blob_client
22+
.put_block_blob("hello world")
23+
.content_type("text/plain")
24+
.await?;
25+
let mut result: Vec<u8> = vec![];
26+
27+
// The stream is composed of individual calls to the get blob endpoint
28+
let mut stream = blob_client.get().into_stream();
29+
while let Some(value) = stream.next().await {
30+
let mut body = value?.data;
31+
// For each response, we stream the body instead of collecting it all
32+
// into one large allocation.
33+
while let Some(value) = body.next().await {
34+
let value = value?;
35+
result.extend(&value);
36+
}
37+
}
38+
39+
println!("result: {:?}", result);
40+
41+
Ok(())
42+
}

src/azurite/mod.rs

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
use std::{borrow::Cow, collections::BTreeMap};
2+
3+
use testcontainers::{
4+
core::{ContainerPort, WaitFor},
5+
Image,
6+
};
7+
8+
const NAME: &str = "mcr.microsoft.com/azure-storage/azurite";
9+
const TAG: &str = "3.34.0";
10+
11+
/// Port that [`Azurite`] uses internally for blob storage.
12+
pub const BLOB_PORT: ContainerPort = ContainerPort::Tcp(10000);
13+
14+
/// Port that [`Azurite`] uses internally for queue.
15+
pub const QUEUE_PORT: ContainerPort = ContainerPort::Tcp(10001);
16+
17+
/// Port that [`Azurite`] uses internally for table.
18+
const TABLE_PORT: ContainerPort = ContainerPort::Tcp(10002);
19+
20+
const AZURITE_ACCOUNTS: &str = "AZURITE_ACCOUNTS";
21+
22+
/// Module to work with [`Azurite`] inside tests.
23+
///
24+
/// This module is based on the official [`Azurite docker image`].
25+
///
26+
/// # Example
27+
/// ```
28+
/// use testcontainers_modules::{
29+
/// azurite,
30+
/// azurite::{Azurite, BLOB_PORT},
31+
/// testcontainers::runners::SyncRunner,
32+
/// };
33+
///
34+
/// let azurite = Azurite::default().start().unwrap();
35+
/// let blob_port = azurite.get_host_port_ipv4(BLOB_PORT).unwrap();
36+
///
37+
/// // do something with the started azurite instance..
38+
/// ```
39+
///
40+
/// [`Azurite`]: https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=visual-studio%2Cblob-storage
41+
/// [`Azurite docker image`]: https://hub.docker.com/r/microsoft/azure-storage-azurite
42+
#[derive(Debug, Clone)]
43+
pub struct Azurite {
44+
tag: String,
45+
env_vars: BTreeMap<String, String>,
46+
loose: bool,
47+
skip_api_version_check: bool,
48+
disable_telemetry: bool,
49+
}
50+
51+
impl Default for Azurite {
52+
fn default() -> Self {
53+
Self {
54+
tag: TAG.to_string(),
55+
env_vars: BTreeMap::new(),
56+
loose: false,
57+
skip_api_version_check: false,
58+
disable_telemetry: false,
59+
}
60+
}
61+
}
62+
impl Azurite {
63+
/// Create a new azurite instance with the latest image
64+
pub fn latest() -> Self {
65+
Self {
66+
tag: "latest".to_string(),
67+
..Self::default()
68+
}
69+
}
70+
71+
/// Overrides the image tag.
72+
/// Check https://mcr.microsoft.com/v2/azure-storage/azurite/tags/list to see available tags.
73+
pub fn with_tag(self, tag: String) -> Self {
74+
Self { tag, ..self }
75+
}
76+
77+
/// Sets the [Azurite accounts](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=visual-studio%2Ctable-storage#custom-storage-accounts-and-keys) to be used by the instance.
78+
///
79+
/// - Uses `AZURITE_ACCOUNTS` key is used to store the accounts in the environment variables.
80+
/// - The format should be: `account1:key1[:key2];account2:key1[:key2];...`
81+
pub fn with_accounts(self, accounts: String) -> Self {
82+
let mut env_vars = self.env_vars;
83+
env_vars.insert(AZURITE_ACCOUNTS.to_owned(), accounts);
84+
Self { env_vars, ..self }
85+
}
86+
87+
/// Disables strict mode
88+
pub fn with_loose(self) -> Self {
89+
Self {
90+
loose: true,
91+
..self
92+
}
93+
}
94+
95+
/// Skips API version validation
96+
pub fn with_skip_api_version_check(self) -> Self {
97+
Self {
98+
skip_api_version_check: true,
99+
..self
100+
}
101+
}
102+
103+
/// Disables telemetry data collection
104+
pub fn with_disable_telemetry(self) -> Self {
105+
Self {
106+
disable_telemetry: true,
107+
..self
108+
}
109+
}
110+
}
111+
impl Image for Azurite {
112+
fn name(&self) -> &str {
113+
NAME
114+
}
115+
116+
fn tag(&self) -> &str {
117+
&self.tag
118+
}
119+
120+
fn ready_conditions(&self) -> Vec<WaitFor> {
121+
vec![WaitFor::message_on_stdout(
122+
"Azurite Table service is successfully listening at http://0.0.0.0:10002",
123+
)]
124+
}
125+
126+
fn env_vars(
127+
&self,
128+
) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
129+
&self.env_vars
130+
}
131+
132+
fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
133+
let mut cmd = vec![
134+
String::from("azurite"),
135+
String::from("--blobHost"),
136+
String::from("0.0.0.0"),
137+
String::from("--queueHost"),
138+
String::from("0.0.0.0"),
139+
String::from("--tableHost"),
140+
String::from("0.0.0.0"),
141+
];
142+
if self.loose {
143+
cmd.push(String::from("--loose"));
144+
}
145+
if self.skip_api_version_check {
146+
cmd.push(String::from("--skipApiVersionCheck"));
147+
}
148+
if self.disable_telemetry {
149+
cmd.push(String::from("--disableTelemetry"));
150+
}
151+
cmd
152+
}
153+
154+
fn expose_ports(&self) -> &[ContainerPort] {
155+
&[BLOB_PORT, QUEUE_PORT, TABLE_PORT]
156+
}
157+
}
158+
159+
#[cfg(test)]
160+
mod tests {
161+
use azure_storage::{prelude::*, CloudLocation};
162+
use azure_storage_blobs::prelude::*;
163+
use base64::{prelude::BASE64_STANDARD, Engine};
164+
165+
use crate::azurite::{Azurite, BLOB_PORT};
166+
167+
#[tokio::test]
168+
async fn starts_with_async_runner() -> Result<(), Box<dyn std::error::Error + 'static>> {
169+
use testcontainers::runners::AsyncRunner;
170+
let azurite = Azurite::default();
171+
azurite.start().await?;
172+
Ok(())
173+
}
174+
175+
#[test]
176+
fn starts_with_sync_runner() -> Result<(), Box<dyn std::error::Error + 'static>> {
177+
use testcontainers::runners::SyncRunner;
178+
let azurite = Azurite::default();
179+
azurite.start()?;
180+
Ok(())
181+
}
182+
183+
#[test]
184+
fn starts_with_latest_tag() -> Result<(), Box<dyn std::error::Error + 'static>> {
185+
use testcontainers::runners::SyncRunner;
186+
let azurite = Azurite::latest().with_tag("latest".to_string());
187+
azurite.start()?;
188+
Ok(())
189+
}
190+
191+
#[test]
192+
fn starts_with_tag() -> Result<(), Box<dyn std::error::Error + 'static>> {
193+
use testcontainers::runners::SyncRunner;
194+
let azurite = Azurite::default().with_tag("latest".to_string());
195+
azurite.start()?;
196+
Ok(())
197+
}
198+
199+
#[test]
200+
fn starts_with_loose() -> Result<(), Box<dyn std::error::Error + 'static>> {
201+
use testcontainers::runners::SyncRunner;
202+
let azurite = Azurite::default().with_loose();
203+
azurite.start()?;
204+
Ok(())
205+
}
206+
207+
#[test]
208+
fn starts_with_with_skip_api_version_check() -> Result<(), Box<dyn std::error::Error + 'static>>
209+
{
210+
use testcontainers::runners::SyncRunner;
211+
let azurite = Azurite::default().with_skip_api_version_check();
212+
azurite.start()?;
213+
Ok(())
214+
}
215+
216+
#[tokio::test]
217+
async fn starts_with_accounts() -> Result<(), Box<dyn std::error::Error + 'static>> {
218+
use azure_core::auth::Secret;
219+
use testcontainers::runners::AsyncRunner;
220+
221+
let data = b"key1";
222+
let account_key = BASE64_STANDARD.encode(data);
223+
224+
let account_name = "account1";
225+
let container = Azurite::default()
226+
.with_accounts(format!("{}:{};", account_name, account_key))
227+
.start()
228+
.await?;
229+
230+
ClientBuilder::with_location(
231+
CloudLocation::Custom {
232+
account: account_name.to_string(),
233+
uri: format!(
234+
"http://127.0.0.1:{}/{}",
235+
container.get_host_port_ipv4(BLOB_PORT).await?,
236+
account_name
237+
),
238+
},
239+
StorageCredentials::access_key(account_name, Secret::new(account_key)),
240+
)
241+
.container_client("container-name")
242+
.create()
243+
.await?;
244+
245+
Ok(())
246+
}
247+
}

src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
#[cfg_attr(docsrs, doc(cfg(feature = "anvil")))]
1313
/// **Anvil** (local blockchain emulator for EVM-compatible development) testcontainer
1414
pub mod anvil;
15+
16+
#[cfg(feature = "azurite")]
17+
#[cfg_attr(docsrs, doc(cfg(feature = "azurite")))]
18+
/// **Azurite** (azure storage emulator) testcontainer
19+
pub mod azurite;
20+
1521
#[cfg(feature = "clickhouse")]
1622
#[cfg_attr(docsrs, doc(cfg(feature = "clickhouse")))]
1723
/// **Clickhouse** (analytics database) testcontainer

0 commit comments

Comments
 (0)