Skip to content

Commit

Permalink
feat: add Microsoft SQL Server module (#72)
Browse files Browse the repository at this point in the history
This PR adds a Microsoft SQL Server module based on the official Docker
images.

https://hub.docker.com/_/microsoft-mssql-server

Thanks!

---------

Co-authored-by: Artem Medvedev <[email protected]>
  • Loading branch information
kymmt90 and DDtKey authored Nov 29, 2023
1 parent 0941512 commit 0c579ca
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 0 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ google_cloud_sdk_emulators = []
kafka = []
minio = []
mongo = []
mssql_server = []
mysql = []
neo4j = []
orientdb = []
Expand Down Expand Up @@ -55,12 +56,21 @@ reqwest = { version = "0.11.20", features = ["blocking"] }
serde = { version = "1.0.188", features = [ "derive" ] }
serde_json = "1.0.107"
tokio = { version = "1", features = ["macros"] }
tokio-util = { version = "0.7.10", features = ["compat"] }
zookeeper = "0.8"

# To use Tiberius on macOS, rustls is needed instead of native-tls
# https://github.com/prisma/tiberius/tree/v0.12.2#encryption-tlsssl
tiberius = { version = "0.12.2", default-features = false, features = ["tds73", "rustls"] }

[[example]]
name = "postgres"
required-features = ["postgres"]

[[example]]
name = "neo4j"
required-features = ["neo4j"]

[[example]]
name = "mssql_server"
required-features = ["mssql_server"]
31 changes: 31 additions & 0 deletions examples/mssql_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use testcontainers_modules::{mssql_server::MssqlServer, testcontainers::clients::Cli};
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
let docker = Cli::default();
let image = MssqlServer::default();
let container = docker.run(image);

// Build Tiberius config
let mut config = tiberius::Config::new();
config.port(container.get_host_port_ipv4(1433));
config.authentication(tiberius::AuthMethod::sql_server(
"sa",
"yourStrong(!)Password",
));
config.trust_cert();

// Connect to the database
let tcp = TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;
let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;

// Run a test query
let stream = client.query("SELECT 1 + 1", &[]).await?;
let row = stream.into_row().await?.unwrap();
assert_eq!(row.get::<i32, _>(0).unwrap(), 2);

Ok(())
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub mod minio;
#[cfg(feature = "mongo")]
#[cfg_attr(docsrs, doc(cfg(feature = "mongo")))]
pub mod mongo;
#[cfg(feature = "mssql_server")]
#[cfg_attr(docsrs, doc(cfg(feature = "mssql_server")))]
pub mod mssql_server;
#[cfg(feature = "mysql")]
#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
pub mod mysql;
Expand Down
188 changes: 188 additions & 0 deletions src/mssql_server/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use std::collections::HashMap;

use testcontainers::{core::WaitFor, Image};

/// [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server) module
/// for [testcontainers](https://crates.io/crates/testcontainers).
///
/// This module is based on the
/// [official Microsoft SQL Server for Linux Docker image](https://hub.docker.com/_/microsoft-mssql-server).
/// Only amd64 images are available for SQL Server. If you use Apple silicon machines,
/// you need to configure Rosetta emulation.
///
/// * [Change Docker Desktop settings on Mac | Docker Docs](https://docs.docker.com/desktop/settings/mac/#general)
///
/// # Example
///
/// ```
/// use testcontainers::clients;
/// use testcontainers_modules::mssql_server;
///
/// let docker = clients::Cli::default();
/// let mssql_server = docker.run(mssql_server::MssqlServer::default());
/// let ado_connection_string = format!(
/// "Server=tcp:127.0.0.1,{};Database=test;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True;",
/// mssql_server.get_host_port_ipv4(1433)
/// );
/// ```
///
/// # Environment variables
///
/// Refer to the [documentation](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-configure-environment-variables)
/// for a complite list of environment variables.
///
/// Following environment variables are required.
/// A image provided by this module has default values for them.
///
/// ## `ACCEPT_EULA`
///
/// You need to accept the [End-User Licensing Agreement](https://go.microsoft.com/fwlink/?linkid=857698)
/// before using the SQL Server image provided by this module.
/// To accept EULA, you can set this environment variable to `Y`.
/// The default value is `Y`.
///
/// ## `MSSQL_SA_PASSWORD`
///
/// The SA user password. This password is required to conform to the
/// [strong password policy](https://learn.microsoft.com/en-us/sql/relational-databases/security/password-policy?view=sql-server-ver15#password-complexity).
/// The default value is `yourStrong(!)Password`.
///
/// ## `MSSQL_PID`
///
/// The edition of SQL Server.
/// The default value is `Developer`, which will run the container using the Developer Edition.
#[derive(Debug)]
pub struct MssqlServer {
env_vars: HashMap<String, String>,
}

impl MssqlServer {
const NAME: &'static str = "mcr.microsoft.com/mssql/server";
const TAG: &'static str = "2022-CU10-ubuntu-22.04";
const DEFAULT_SA_PASSWORD: &'static str = "yourStrong(!)Password";

/// Sets the password as `MSSQL_SA_PASSWORD`.
pub fn with_sa_password(self, password: impl Into<String>) -> Self {
let mut env_vars = self.env_vars;
env_vars.insert("MSSQL_SA_PASSWORD".to_owned(), password.into());

Self { env_vars }
}
}

impl Default for MssqlServer {
fn default() -> Self {
let mut env_vars = HashMap::new();
env_vars.insert("ACCEPT_EULA".to_owned(), "Y".to_owned());
env_vars.insert(
"MSSQL_SA_PASSWORD".to_owned(),
Self::DEFAULT_SA_PASSWORD.to_owned(),
);
env_vars.insert("MSSQL_PID".to_owned(), "Developer".to_owned());

Self { env_vars }
}
}

impl Image for MssqlServer {
type Args = ();

fn name(&self) -> String {
Self::NAME.to_owned()
}

fn tag(&self) -> String {
Self::TAG.to_owned()
}

fn ready_conditions(&self) -> Vec<WaitFor> {
// Wait until all system databases are recovered
vec![
WaitFor::message_on_stdout("SQL Server is now ready for client connections"),
WaitFor::message_on_stdout("Recovery is complete"),
]
}

fn env_vars(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
Box::new(self.env_vars.iter())
}
}

#[cfg(test)]
mod tests {
use std::error;

use testcontainers::{clients, RunnableImage};
use tiberius::{AuthMethod, Client, Config};
use tokio::net::TcpStream;
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};

use super::*;

#[tokio::test]
async fn one_plus_one() -> Result<(), Box<dyn error::Error>> {
let docker = clients::Cli::default();
let container = docker.run(MssqlServer::default());
let config = new_config(container.get_host_port_ipv4(1433), "yourStrong(!)Password");
let mut client = get_mssql_client(config).await?;

let stream = client.query("SELECT 1 + 1", &[]).await?;
let row = stream.into_row().await?.unwrap();

assert_eq!(row.get::<i32, _>(0).unwrap(), 2);

Ok(())
}

#[tokio::test]
async fn custom_sa_password() -> Result<(), Box<dyn error::Error>> {
let docker = clients::Cli::default();
let image = MssqlServer::default().with_sa_password("yourStrongPassword123!");
let container = docker.run(image);
let config = new_config(container.get_host_port_ipv4(1433), "yourStrongPassword123!");
let mut client = get_mssql_client(config).await?;

let stream = client.query("SELECT 1 + 1", &[]).await?;
let row = stream.into_row().await?.unwrap();

assert_eq!(row.get::<i32, _>(0).unwrap(), 2);

Ok(())
}

#[tokio::test]
async fn custom_version() -> Result<(), Box<dyn error::Error>> {
let docker = clients::Cli::default();
let image = RunnableImage::from(MssqlServer::default()).with_tag("2019-CU23-ubuntu-20.04");
let container = docker.run(image);
let config = new_config(container.get_host_port_ipv4(1433), "yourStrong(!)Password");
let mut client = get_mssql_client(config).await?;

let stream = client.query("SELECT @@VERSION", &[]).await?;
let row = stream.into_row().await?.unwrap();

assert!(row.get::<&str, _>(0).unwrap().contains("2019"));

Ok(())
}

async fn get_mssql_client(
config: Config,
) -> Result<Client<Compat<TcpStream>>, Box<dyn error::Error>> {
let tcp = TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;

let client = Client::connect(config, tcp.compat_write()).await?;

Ok(client)
}

fn new_config(port: u16, password: &str) -> Config {
let mut config = Config::new();
config.port(port);
config.authentication(AuthMethod::sql_server("sa", password));
config.trust_cert();

config
}
}

0 comments on commit 0c579ca

Please sign in to comment.