diff --git a/Cargo.toml b/Cargo.toml index c27c881..d8bdc99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,11 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0.99" -atlas-local = { git = "https://github.com/mongodb/atlas-local-lib.git", rev = "36f56065e891bbe045beeb46489dd7d4142dbd41" } +atlas-local = { git = "https://github.com/mongodb/atlas-local-lib.git", rev = "323a879b8385e8e572f8545e3273fafa5698f8c6" } bollard = "0.19.2" napi = { version = "3.0.0", features = ["async", "anyhow"] } napi-derive = "3.0.0" +semver = "1.0.26" [build-dependencies] napi-build = "2" diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 83a47ac..265d5f2 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -20,16 +20,30 @@ test('smoke test', async (t) => { return } - // TODO: Implement once createDeployment is added - // let deploymentName = "test_deployment" - // await client.createDeployment(...) + // Skip test after client creation on Windows + // Note all Windows return win32 including 64 bit + if (process.platform === 'win32') { + t.pass('Skipping end-to-end test on Windows') + return + } + + // Count initial deployments + let start_deployments_count = (await client.listDeployments()).length + + // Create deployment + let createDeploymentOptions = { + name: "test_deployment", + } + await client.createDeployment(createDeploymentOptions) - // List deployments - // We don't care about the number, we're just testing that the method doesn't fail - await client.listDeployments() + // Count deployments after creation + let after_create_deployment_count = (await client.listDeployments()).length + t.assert(after_create_deployment_count - start_deployments_count === 1) - // TODO: Uncommment when createDeployment is added - // await client.deleteDeployment(deploymentName) + // Delete deployment + await client.deleteDeployment(createDeploymentOptions.name) - t.pass() + // Count deployments after deletion + let after_delete_deployment_count = (await client.listDeployments()).length + t.assert(start_deployments_count === after_delete_deployment_count) }) diff --git a/index.d.ts b/index.d.ts index 151af5a..87f361d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,6 +2,7 @@ /* eslint-disable */ export declare class Client { static connect(): Client + createDeployment(createDeplomentOptions: CreateDeploymentOptions): Promise listDeployments(): Promise> deleteDeployment(deploymentName: string): Promise } @@ -12,6 +13,24 @@ export declare const enum BindingType { Specific = 'Specific' } +export interface CreateDeploymentOptions { + name?: string + image?: string + mongodbVersion?: string + creationSource?: CreationSource + localSeedLocation?: string + mongodbInitdbDatabase?: string + mongodbInitdbRootPasswordFile?: string + mongodbInitdbRootPassword?: string + mongodbInitdbRootUsernameFile?: string + mongodbInitdbRootUsername?: string + mongotLogFile?: string + runnerLogFile?: string + doNotTrack?: boolean + telemetryBaseUrl?: string + mongodbPortBinding?: MongoDBPortBinding +} + export interface CreationSource { type: CreationSourceType source: string @@ -20,7 +39,7 @@ export interface CreationSource { export declare const enum CreationSourceType { AtlasCLI = 'AtlasCLI', Container = 'Container', - MCP = 'MCP', + MCPServer = 'MCPServer', Other = 'Other' } diff --git a/src/lib.rs b/src/lib.rs index 4b3b33b..7f35f1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,19 @@ impl Client { }) } + #[napi] + pub async fn create_deployment( + &self, + create_deploment_options: crate::models::create_deployment::CreateDeploymentOptions, + ) -> Result<()> { + let options: atlas_local::models::CreateDeploymentOptions = create_deploment_options.into(); + self + .client + .create_deployment(&options) + .await + .context("create deployment") + } + #[napi] pub async fn list_deployments(&self) -> Result> { self diff --git a/src/models/create_deployment.rs b/src/models/create_deployment.rs new file mode 100644 index 0000000..3b74fd5 --- /dev/null +++ b/src/models/create_deployment.rs @@ -0,0 +1,160 @@ +use crate::models::list_deployments::{CreationSource, MongoDBPortBinding}; +use napi_derive::napi; +use semver::Version; + +#[napi(object)] +pub struct CreateDeploymentOptions { + // Identifiers + pub name: Option, + + // Image details + pub image: Option, + pub mongodb_version: Option, + + // Creation source + pub creation_source: Option, + + // Initial database configuration + pub local_seed_location: Option, + pub mongodb_initdb_database: Option, + pub mongodb_initdb_root_password_file: Option, + pub mongodb_initdb_root_password: Option, + pub mongodb_initdb_root_username_file: Option, + pub mongodb_initdb_root_username: Option, + + // Logging + pub mongot_log_file: Option, + pub runner_log_file: Option, + + // Telemetry + pub do_not_track: Option, + pub telemetry_base_url: Option, + + // Port configuration + pub mongodb_port_binding: Option, +} + +impl From for atlas_local::models::CreateDeploymentOptions { + fn from(source: CreateDeploymentOptions) -> Self { + let version: Option = match source.mongodb_version.as_deref() { + Some("latest") => None, + None => None, + Some(ver_string) => { + // If malformed Version if given, it will panic here + Some(Version::parse(ver_string).expect("Parse version string")) + } + }; + + Self { + name: source.name, + image: source.image, + mongodb_version: version, + creation_source: source + .creation_source + .map(atlas_local::models::CreationSource::from), + local_seed_location: source.local_seed_location, + mongodb_initdb_database: source.mongodb_initdb_database, + mongodb_initdb_root_password_file: source.mongodb_initdb_root_password_file, + mongodb_initdb_root_password: source.mongodb_initdb_root_password, + mongodb_initdb_root_username_file: source.mongodb_initdb_root_username_file, + mongodb_initdb_root_username: source.mongodb_initdb_root_username, + mongot_log_file: source.mongot_log_file, + runner_log_file: source.runner_log_file, + do_not_track: source.do_not_track, + telemetry_base_url: source.telemetry_base_url, + mongodb_port_binding: source + .mongodb_port_binding + .map(atlas_local::models::MongoDBPortBinding::from), + } + } +} + +#[cfg(test)] +mod tests { + use crate::models::list_deployments::{BindingType, CreationSourceType}; + + use super::*; + + #[test] + fn test_lib_create_deployment_options_from_create_deployment_options() { + let create_deployment_options = CreateDeploymentOptions { + name: Some("test_deployment".to_string()), + image: Some("mongodb/mongodb-atlas-local".to_string()), + mongodb_version: Some("8.0.0".to_string()), + creation_source: Some(CreationSource { + source_type: CreationSourceType::MCPServer, + source: "MCPSERVER".to_string(), + }), + local_seed_location: Some("/host/seed-data".to_string()), + mongodb_initdb_database: Some("testdb".to_string()), + mongodb_initdb_root_password_file: Some("/run/secrets/password".to_string()), + mongodb_initdb_root_password: Some("password123".to_string()), + mongodb_initdb_root_username_file: Some("/run/secrets/username".to_string()), + mongodb_initdb_root_username: Some("admin".to_string()), + mongot_log_file: Some("/tmp/mongot.log".to_string()), + runner_log_file: Some("/tmp/runner.log".to_string()), + do_not_track: Some(false), + telemetry_base_url: Some("https://telemetry.example.com".to_string()), + mongodb_port_binding: Some(MongoDBPortBinding { + binding_type: BindingType::Loopback, + ip: "127.0.0.1".to_string(), + port: 27017, + }), + }; + let lib_create_deployment_options: atlas_local::models::CreateDeploymentOptions = + create_deployment_options.into(); + assert_eq!( + lib_create_deployment_options.name, + Some("test_deployment".to_string()) + ); + assert_eq!( + lib_create_deployment_options.image, + Some("mongodb/mongodb-atlas-local".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongodb_version, + Some(Version::new(8, 0, 0)) + ); + assert_eq!( + lib_create_deployment_options.creation_source, + Some(atlas_local::models::CreationSource::MCPServer) + ); + assert_eq!( + lib_create_deployment_options.local_seed_location, + Some("/host/seed-data".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongodb_initdb_database, + Some("testdb".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongodb_initdb_root_password_file, + Some("/run/secrets/password".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongodb_initdb_root_password, + Some("password123".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongodb_initdb_root_username_file, + Some("/run/secrets/username".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongodb_initdb_root_username, + Some("admin".to_string()) + ); + assert_eq!( + lib_create_deployment_options.mongot_log_file, + Some("/tmp/mongot.log".to_string()) + ); + assert_eq!( + lib_create_deployment_options.runner_log_file, + Some("/tmp/runner.log".to_string()) + ); + assert_eq!(lib_create_deployment_options.do_not_track, Some(false)); + assert_eq!( + lib_create_deployment_options.telemetry_base_url, + Some("https://telemetry.example.com".to_string()) + ); + } +} diff --git a/src/models/list_deployments.rs b/src/models/list_deployments.rs index 5d29c5d..6f457e7 100644 --- a/src/models/list_deployments.rs +++ b/src/models/list_deployments.rs @@ -1,3 +1,5 @@ +use std::net::IpAddr; + use napi_derive::napi; #[napi(object)] @@ -35,6 +37,7 @@ pub struct Deployment { } #[napi(string_enum)] +#[derive(PartialEq, Debug)] pub enum State { Created, Dead, @@ -46,6 +49,7 @@ pub enum State { } #[napi(object)] +#[derive(PartialEq, Debug)] pub struct MongoDBPortBinding { #[napi(js_name = "type")] pub binding_type: BindingType, @@ -54,6 +58,7 @@ pub struct MongoDBPortBinding { } #[napi(string_enum)] +#[derive(PartialEq, Debug)] pub enum BindingType { Loopback, // 127.0.0.1 AnyInterface, // 0.0.0.0 @@ -61,12 +66,14 @@ pub enum BindingType { } #[napi(string_enum)] +#[derive(PartialEq, Debug)] pub enum MongodbType { Community, Enterprise, } #[napi(object)] +#[derive(PartialEq, Debug)] pub struct CreationSource { #[napi(js_name = "type")] pub source_type: CreationSourceType, @@ -74,10 +81,11 @@ pub struct CreationSource { } #[napi(string_enum)] +#[derive(PartialEq, Debug)] pub enum CreationSourceType { AtlasCLI, Container, - MCP, + MCPServer, Other, } @@ -143,6 +151,27 @@ impl From for MongoDBPortBinding { } } +impl From for atlas_local::models::MongoDBPortBinding { + fn from(source: MongoDBPortBinding) -> Self { + match source.binding_type { + BindingType::Loopback => atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::Loopback, + port: source.port, + }, + BindingType::AnyInterface => atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::AnyInterface, + port: source.port, + }, + BindingType::Specific => atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::Specific( + source.ip.parse::().expect("Parse IP address"), + ), + port: source.port, + }, + } + } +} + impl From for MongodbType { fn from(source: atlas_local::models::MongodbType) -> Self { match source { @@ -165,6 +194,10 @@ impl From for CreationSource { source_type: CreationSourceType::Container, source: "CONTAINER".to_string(), }, + CreationSourceSource::MCPServer => CreationSource { + source_type: CreationSourceType::MCPServer, + source: "MCPSERVER".to_string(), + }, CreationSourceSource::Unknown(source) => CreationSource { source_type: CreationSourceType::Other, source, @@ -172,3 +205,219 @@ impl From for CreationSource { } } } + +impl From for atlas_local::models::CreationSource { + fn from(source: CreationSource) -> Self { + match source.source_type { + CreationSourceType::AtlasCLI => atlas_local::models::CreationSource::AtlasCLI, + CreationSourceType::Container => atlas_local::models::CreationSource::Container, + CreationSourceType::MCPServer => atlas_local::models::CreationSource::MCPServer, + CreationSourceType::Other => atlas_local::models::CreationSource::Unknown(source.source), + } + } +} + +#[cfg(test)] +mod tests { + use semver::Version; + + use super::*; + + #[test] + fn test_deployment_from_lib_deployment() { + let lib_deployment = atlas_local::models::Deployment { + container_id: "container_id".to_string(), + name: Some("test_deployment".to_string()), + state: atlas_local::models::State::Running, + port_bindings: Some(atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::Loopback, + port: 27017, + }), + mongodb_type: atlas_local::models::MongodbType::Community, + mongodb_version: Version::new(8, 0, 0), + creation_source: Some(atlas_local::models::CreationSource::AtlasCLI), + local_seed_location: Some("/host/seed-data".to_string()), + mongodb_initdb_database: Some("testdb".to_string()), + mongodb_initdb_root_password_file: Some("/run/secrets/password".to_string()), + mongodb_initdb_root_password: Some("password123".to_string()), + mongodb_initdb_root_username_file: Some("/run/secrets/username".to_string()), + mongodb_initdb_root_username: Some("admin".to_string()), + mongot_log_file: Some("/tmp/mongot.log".to_string()), + runner_log_file: Some("/tmp/runner.log".to_string()), + do_not_track: Some("false".to_string()), + telemetry_base_url: Some("https://telemetry.example.com".to_string()), + }; + + let deployment: Deployment = lib_deployment.into(); + + assert_eq!(deployment.container_id, "container_id"); + assert_eq!(deployment.name, Some("test_deployment".to_string())); + assert_eq!(deployment.state, State::Running); + assert!(deployment.port_bindings.is_some()); + let port_binding = deployment.port_bindings.unwrap(); + assert_eq!(port_binding.binding_type, BindingType::Loopback); + assert_eq!(port_binding.ip, "127.0.0.1"); + assert_eq!(port_binding.port, 27017); + assert_eq!(deployment.mongodb_type, MongodbType::Community); + assert_eq!(deployment.mongodb_version, "8.0.0"); + assert_eq!( + deployment.creation_source, + Some(CreationSource { + source_type: CreationSourceType::AtlasCLI, + source: "ATLASCLI".to_string(), + }) + ); + assert_eq!( + deployment.local_seed_location, + Some("/host/seed-data".to_string()) + ); + assert_eq!( + deployment.mongodb_initdb_database, + Some("testdb".to_string()) + ); + assert_eq!( + deployment.mongodb_initdb_root_password_file, + Some("/run/secrets/password".to_string()) + ); + assert_eq!( + deployment.mongodb_initdb_root_password, + Some("password123".to_string()) + ); + assert_eq!( + deployment.mongodb_initdb_root_username_file, + Some("/run/secrets/username".to_string()) + ); + assert_eq!( + deployment.mongodb_initdb_root_username, + Some("admin".to_string()) + ); + assert_eq!( + deployment.mongot_log_file, + Some("/tmp/mongot.log".to_string()) + ); + assert_eq!( + deployment.runner_log_file, + Some("/tmp/runner.log".to_string()) + ); + assert_eq!(deployment.do_not_track, Some("false".to_string())); + assert_eq!( + deployment.telemetry_base_url, + Some("https://telemetry.example.com".to_string()) + ); + } + + #[test] + fn test_mongodb_port_binding_from_lib_mongodb_port_binding_loopback() { + let lib_mongodb_port_binding = atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::Loopback, + port: 27017, + }; + let mongodb_port_binding: MongoDBPortBinding = lib_mongodb_port_binding.into(); + assert_eq!(mongodb_port_binding.binding_type, BindingType::Loopback); + assert_eq!(mongodb_port_binding.ip, "127.0.0.1"); + assert_eq!(mongodb_port_binding.port, 27017); + } + + #[test] + fn test_mongodb_port_binding_from_lib_mongodb_port_binding_any_interface() { + let lib_mongodb_port_binding = atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::AnyInterface, + port: 27017, + }; + let mongodb_port_binding: MongoDBPortBinding = lib_mongodb_port_binding.into(); + assert_eq!(mongodb_port_binding.binding_type, BindingType::AnyInterface); + assert_eq!(mongodb_port_binding.ip, "0.0.0.0"); + assert_eq!(mongodb_port_binding.port, 27017); + } + + #[test] + fn test_mongodb_port_binding_from_lib_mongodb_port_binding_specific() { + let lib_mongodb_port_binding = atlas_local::models::MongoDBPortBinding { + binding_type: atlas_local::models::BindingType::Specific("192.0.2.0".parse().unwrap()), + port: 27017, + }; + let mongodb_port_binding: MongoDBPortBinding = lib_mongodb_port_binding.into(); + assert_eq!(mongodb_port_binding.binding_type, BindingType::Specific); + assert_eq!(mongodb_port_binding.ip, "192.0.2.0"); + assert_eq!(mongodb_port_binding.port, 27017); + } + + #[test] + fn test_mongodb_port_binding_lib_into_mongodb_port_binding_loopback() { + let mongodb_port_binding = MongoDBPortBinding { + binding_type: BindingType::Loopback, + ip: "127.0.0.1".to_string(), + port: 27017, + }; + let lib_mongodb_port_binding: atlas_local::models::MongoDBPortBinding = + mongodb_port_binding.into(); + assert_eq!( + lib_mongodb_port_binding.binding_type, + atlas_local::models::BindingType::Loopback + ); + assert_eq!(lib_mongodb_port_binding.port, 27017); + } + + #[test] + fn test_mongodb_port_binding_lib_into_mongodb_port_binding_any_interface() { + let mongodb_port_binding = MongoDBPortBinding { + binding_type: BindingType::AnyInterface, + ip: "0.0.0.0".to_string(), + port: 27017, + }; + let lib_mongodb_port_binding: atlas_local::models::MongoDBPortBinding = + mongodb_port_binding.into(); + assert_eq!( + lib_mongodb_port_binding.binding_type, + atlas_local::models::BindingType::AnyInterface + ); + assert_eq!(lib_mongodb_port_binding.port, 27017); + } + #[test] + fn test_mongodb_port_binding_lib_into_mongodb_port_binding_specific() { + let mongodb_port_binding = MongoDBPortBinding { + binding_type: BindingType::Specific, + ip: "192.0.2.0".to_string(), + port: 27017, + }; + let lib_mongodb_port_binding: atlas_local::models::MongoDBPortBinding = + mongodb_port_binding.into(); + assert_eq!( + lib_mongodb_port_binding.binding_type, + atlas_local::models::BindingType::Specific("192.0.2.0".parse().unwrap()) + ); + assert_eq!(lib_mongodb_port_binding.port, 27017); + } + + #[test] + fn test_creation_source_from_lib_creation_source_atlas_cli() { + let lib_creation_source = atlas_local::models::CreationSource::AtlasCLI; + let creation_source: CreationSource = lib_creation_source.into(); + assert_eq!(creation_source.source_type, CreationSourceType::AtlasCLI); + assert_eq!(creation_source.source, "ATLASCLI"); + } + + #[test] + fn test_creation_source_from_lib_creation_source_container() { + let lib_creation_source = atlas_local::models::CreationSource::Container; + let creation_source: CreationSource = lib_creation_source.into(); + assert_eq!(creation_source.source_type, CreationSourceType::Container); + assert_eq!(creation_source.source, "CONTAINER"); + } + + #[test] + fn test_creation_source_from_lib_creation_source_mcp_server() { + let lib_creation_source = atlas_local::models::CreationSource::MCPServer; + let creation_source: CreationSource = lib_creation_source.into(); + assert_eq!(creation_source.source_type, CreationSourceType::MCPServer); + assert_eq!(creation_source.source, "MCPSERVER"); + } + + #[test] + fn test_creation_source_from_lib_creation_source_unknown() { + let lib_creation_source = atlas_local::models::CreationSource::Unknown("test".to_string()); + let creation_source: CreationSource = lib_creation_source.into(); + assert_eq!(creation_source.source_type, CreationSourceType::Other); + assert_eq!(creation_source.source, "test"); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 0d7467e..917fa27 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1,2 @@ +pub mod create_deployment; pub mod list_deployments;