diff --git a/common/src/models/service.rs b/common/src/models/service.rs index 91e8e1bdf..73422912e 100644 --- a/common/src/models/service.rs +++ b/common/src/models/service.rs @@ -10,7 +10,7 @@ use comfy_table::{ }; use crossterm::style::Stylize; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, path::PathBuf}; use uuid::Uuid; #[derive(Deserialize, Serialize)] @@ -130,6 +130,8 @@ pub fn get_resources_table(resources: &Vec) -> String { let title = match x.r#type { Type::Database(_) => "Databases", Type::Secrets => "Secrets", + Type::StaticFolder => "Static Folder", + Type::Persist => "Persist", }; let elements = acc.entry(title).or_insert(Vec::new()); @@ -148,6 +150,14 @@ pub fn get_resources_table(resources: &Vec) -> String { output.push(get_secrets_table(secrets)); }; + if let Some(static_folders) = resource_groups.get("Static Folder") { + output.push(get_static_folder_table(static_folders)); + }; + + if let Some(persist) = resource_groups.get("Persist") { + output.push(get_persist_table(persist)); + }; + output.join("\n") } } @@ -200,3 +210,52 @@ fn get_secrets_table(secrets: &[&resource::Response]) -> String { "#, ) } + +fn get_static_folder_table(static_folders: &[&resource::Response]) -> String { + let mut table = Table::new(); + + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .set_header(vec![ + Cell::new("Static Folders").set_alignment(CellAlignment::Center) + ]); + + for folder in static_folders { + let path = serde_json::from_value::(folder.data.clone()) + .unwrap() + .display() + .to_string(); + + table.add_row(vec![path]); + } + + format!( + r#"These static folders can be accessed by the service +{table} +"#, + ) +} + +fn get_persist_table(persist_instances: &[&resource::Response]) -> String { + let mut table = Table::new(); + + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .set_header(vec![ + Cell::new("Persist Instances").set_alignment(CellAlignment::Center) + ]); + + for _ in persist_instances { + table.add_row(vec!["Instance"]); + } + + format!( + r#"These instances are linked to this service +{table} +"#, + ) +} diff --git a/common/src/resource.rs b/common/src/resource.rs index 0282848e0..cec5af3e8 100644 --- a/common/src/resource.rs +++ b/common/src/resource.rs @@ -23,6 +23,8 @@ pub struct Response { pub enum Type { Database(database::Type), Secrets, + StaticFolder, + Persist, } impl Response { @@ -44,6 +46,8 @@ impl Display for Type { match self { Type::Database(db_type) => write!(f, "database::{db_type}"), Type::Secrets => write!(f, "secrets"), + Type::StaticFolder => write!(f, "static_folder"), + Type::Persist => write!(f, "persist"), } } } diff --git a/deployer/src/persistence/resource/mod.rs b/deployer/src/persistence/resource/mod.rs index f649e80a3..9db028043 100644 --- a/deployer/src/persistence/resource/mod.rs +++ b/deployer/src/persistence/resource/mod.rs @@ -40,6 +40,8 @@ impl From for shuttle_common::resource::Response { pub enum Type { Database(DatabaseType), Secrets, + StaticFolder, + Persist, } impl From for shuttle_common::resource::Type { @@ -47,6 +49,8 @@ impl From for shuttle_common::resource::Type { match r#type { Type::Database(r#type) => Self::Database(r#type.into()), Type::Secrets => Self::Secrets, + Type::StaticFolder => Self::StaticFolder, + Type::Persist => Self::Persist, } } } @@ -56,6 +60,8 @@ impl From for Type { match r#type { shuttle_common::resource::Type::Database(r#type) => Self::Database(r#type.into()), shuttle_common::resource::Type::Secrets => Self::Secrets, + shuttle_common::resource::Type::StaticFolder => Self::StaticFolder, + shuttle_common::resource::Type::Persist => Self::Persist, } } } @@ -65,6 +71,8 @@ impl Display for Type { match self { Type::Database(db_type) => write!(f, "database::{db_type}"), Type::Secrets => write!(f, "secrets"), + Type::StaticFolder => write!(f, "static folder"), + Type::Persist => write!(f, "persist"), } } } diff --git a/resources/aws-rds/Cargo.toml b/resources/aws-rds/Cargo.toml index 5b9fe9ed3..cf5c883ea 100644 --- a/resources/aws-rds/Cargo.toml +++ b/resources/aws-rds/Cargo.toml @@ -9,6 +9,7 @@ keywords = ["shuttle-service", "rds"] [dependencies] async-trait = "0.1.56" paste = "1.0.7" +serde = { version = "1.0.148", features = ["derive"] } shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls"] } diff --git a/resources/aws-rds/src/lib.rs b/resources/aws-rds/src/lib.rs index 16b00212a..99d0a8fab 100644 --- a/resources/aws-rds/src/lib.rs +++ b/resources/aws-rds/src/lib.rs @@ -2,15 +2,23 @@ use async_trait::async_trait; use paste::paste; +use serde::{Deserialize, Serialize}; use shuttle_service::{ - database::{AwsRdsEngine, Type}, + database::{self, AwsRdsEngine}, error::CustomError, - Factory, ResourceBuilder, + Factory, ResourceBuilder, Type, }; +#[derive(Deserialize, Serialize)] +pub enum AwsRdsOutput { + Rds(shuttle_service::DatabaseReadyInfo), + Local(String), +} + macro_rules! aws_engine { ($feature:expr, $pool_path:path, $options_path:path, $struct_ident:ident) => { paste! { + #[derive(Serialize)] #[cfg(feature = $feature)] #[doc = "A resource connected to an AWS RDS " $struct_ident " instance"] pub struct $struct_ident{ @@ -21,28 +29,43 @@ macro_rules! aws_engine { #[doc = "Gets a `sqlx::Pool` connected to an AWS RDS " $struct_ident " instance"] #[async_trait] impl ResourceBuilder<$pool_path> for $struct_ident { + const TYPE: Type = Type::Database(database::Type::AwsRds(AwsRdsEngine::$struct_ident)); + + type Output = AwsRdsOutput; + fn new() -> Self { Self { local_uri: None } } - async fn build(self, factory: &mut dyn Factory) -> Result<$pool_path, shuttle_service::Error> { - let connection_string = match factory.get_environment() { - shuttle_service::Environment::Production => { + async fn output(self, factory: &mut dyn Factory) -> Result { + let info = match factory.get_environment() { + shuttle_service::Environment::Production => AwsRdsOutput::Rds( factory - .get_db_connection_string(Type::AwsRds(AwsRdsEngine::$struct_ident)) + .get_db_connection(database::Type::AwsRds(AwsRdsEngine::$struct_ident)) .await? - } + ), shuttle_service::Environment::Local => { if let Some(local_uri) = self.local_uri { - local_uri + AwsRdsOutput::Local(local_uri) } else { - factory - .get_db_connection_string(Type::AwsRds(AwsRdsEngine::$struct_ident)) - .await? + AwsRdsOutput::Rds( + factory + .get_db_connection(database::Type::AwsRds(AwsRdsEngine::$struct_ident)) + .await? + ) } } }; + Ok(info) + } + + async fn build(build_data: &Self::Output) -> Result<$pool_path, shuttle_service::Error> { + let connection_string = match build_data { + AwsRdsOutput::Local(local_uri) => local_uri.clone(), + AwsRdsOutput::Rds(info) => info.connection_string_private(), + }; + let pool = $options_path::new() .min_connections(1) .max_connections(5) diff --git a/resources/persist/src/lib.rs b/resources/persist/src/lib.rs index 84d3ce458..86b2c7190 100644 --- a/resources/persist/src/lib.rs +++ b/resources/persist/src/lib.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; use bincode::{deserialize_from, serialize_into, Error as BincodeError}; use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use shuttle_common::project::ProjectName; +use shuttle_service::Type; use shuttle_service::{Factory, ResourceBuilder}; use std::fs; use std::fs::File; @@ -23,8 +24,10 @@ pub enum PersistError { Deserialize(BincodeError), } +#[derive(Serialize)] pub struct Persist; +#[derive(Deserialize, Serialize, Clone)] pub struct PersistInstance { service_name: ProjectName, } @@ -66,18 +69,26 @@ impl PersistInstance { #[async_trait] impl ResourceBuilder for Persist { + const TYPE: Type = Type::Persist; + + type Output = PersistInstance; + fn new() -> Self { Self {} } - async fn build( + async fn output( self, factory: &mut dyn Factory, - ) -> Result { + ) -> Result { Ok(PersistInstance { service_name: factory.get_service_name(), }) } + + async fn build(build_data: &Self::Output) -> Result { + Ok(build_data.clone()) + } } #[cfg(test)] diff --git a/resources/shared-db/src/lib.rs b/resources/shared-db/src/lib.rs index 0e16c39f5..17be1c147 100644 --- a/resources/shared-db/src/lib.rs +++ b/resources/shared-db/src/lib.rs @@ -1,134 +1,20 @@ #![doc = include_str!("../README.md")] -use async_trait::async_trait; -use serde::Serialize; -use shuttle_service::{ - database, error::CustomError, DatabaseReadyInfo, Error, Factory, ResourceBuilder, Type, -}; - -#[cfg(feature = "postgres")] -#[derive(Serialize)] -pub struct Postgres { - local_uri: Option, -} - -#[cfg(feature = "postgres")] -/// Get an `sqlx::PgPool` from any factory -#[async_trait] -impl ResourceBuilder for Postgres { - const TYPE: Type = Type::Database(database::Type::Shared(database::SharedEngine::Postgres)); - - type Output = DatabaseReadyInfo; - - fn new() -> Self { - Self { local_uri: None } - } - - async fn output(self, factory: &mut dyn Factory) -> Result { - // let info = match factory.get_environment() { - // shuttle_service::Environment::Production => { - let info = factory - .get_db_connection(database::Type::Shared(database::SharedEngine::Postgres)) - .await?; - // } - // shuttle_service::Environment::Local => { - // if let Some(local_uri) = self.local_uri { - // local_uri - // } else { - // factory - // .get_db_connection(database::Type::Shared(database::SharedEngine::Postgres)) - // .await? - // } - // } - // }; - - Ok(info) - } - - async fn build(build_data: &Self::Output) -> Result { - let connection_string = build_data.connection_string_private(); - - let pool = sqlx::postgres::PgPoolOptions::new() - .min_connections(1) - .max_connections(5) - .connect(&connection_string) - .await - .map_err(CustomError::new)?; - - Ok(pool) - } -} - -#[cfg(feature = "postgres")] -impl Postgres { - /// Use a custom connection string for local runs - pub fn local_uri(mut self, local_uri: &str) -> Self { - self.local_uri = Some(local_uri.to_string()); - - self - } -} - #[cfg(feature = "mongodb")] -pub struct MongoDb { - local_uri: Option, -} - -/// Get a `mongodb::Database` from any factory +mod mongo; #[cfg(feature = "mongodb")] -#[async_trait] -impl ResourceBuilder for MongoDb { - fn new() -> Self { - Self { local_uri: None } - } - - async fn build(self, factory: &mut dyn Factory) -> Result { - let connection_string = match factory.get_environment() { - shuttle_service::Environment::Production => factory - .get_db_connection_string(database::Type::Shared(database::SharedEngine::MongoDb)) - .await - .map_err(CustomError::new)?, - shuttle_service::Environment::Local => { - if let Some(local_uri) = self.local_uri { - local_uri - } else { - factory - .get_db_connection_string(database::Type::Shared( - database::SharedEngine::MongoDb, - )) - .await - .map_err(CustomError::new)? - } - } - }; +pub use mongo::MongoDb; - let mut client_options = mongodb::options::ClientOptions::parse(connection_string) - .await - .map_err(CustomError::new)?; - client_options.min_pool_size = Some(1); - client_options.max_pool_size = Some(5); - - let client = mongodb::Client::with_options(client_options).map_err(CustomError::new)?; - - // Return a handle to the database defined at the end of the connection string, which is the users provisioned - // database - let database = client.default_database(); +#[cfg(feature = "postgres")] +mod postgres; - match database { - Some(database) => Ok(database), - None => Err(crate::Error::Database( - "mongodb connection string missing default database".into(), - )), - } - } -} +#[cfg(feature = "postgres")] +pub use postgres::Postgres; -#[cfg(feature = "mongodb")] -impl MongoDb { - /// Use a custom connection string for local runs - pub fn local_uri(mut self, local_uri: &str) -> Self { - self.local_uri = Some(local_uri.to_string()); +use serde::{Deserialize, Serialize}; - self - } +#[derive(Deserialize, Serialize)] +pub enum SharedDbOutput { + Shared(shuttle_service::DatabaseReadyInfo), + Local(String), } diff --git a/resources/shared-db/src/mongo.rs b/resources/shared-db/src/mongo.rs new file mode 100644 index 000000000..489d0c049 --- /dev/null +++ b/resources/shared-db/src/mongo.rs @@ -0,0 +1,83 @@ +use async_trait::async_trait; +use serde::Serialize; +use shuttle_service::{database, error::CustomError, Error, Factory, ResourceBuilder, Type}; + +use crate::SharedDbOutput; + +#[derive(Serialize)] +pub struct MongoDb { + local_uri: Option, +} + +/// Get a `mongodb::Database` from any factory +#[async_trait] +impl ResourceBuilder for MongoDb { + const TYPE: Type = Type::Database(database::Type::Shared(database::SharedEngine::MongoDb)); + + type Output = SharedDbOutput; + + fn new() -> Self { + Self { local_uri: None } + } + + async fn output(self, factory: &mut dyn Factory) -> Result { + let info = match factory.get_environment() { + shuttle_service::Environment::Production => SharedDbOutput::Shared( + factory + .get_db_connection(database::Type::Shared(database::SharedEngine::MongoDb)) + .await + .map_err(CustomError::new)?, + ), + shuttle_service::Environment::Local => { + if let Some(local_uri) = self.local_uri { + SharedDbOutput::Local(local_uri) + } else { + SharedDbOutput::Shared( + factory + .get_db_connection(database::Type::Shared( + database::SharedEngine::MongoDb, + )) + .await + .map_err(CustomError::new)?, + ) + } + } + }; + Ok(info) + } + + async fn build(build_data: &Self::Output) -> Result { + let connection_string = match build_data { + SharedDbOutput::Local(local_uri) => local_uri.clone(), + SharedDbOutput::Shared(info) => info.connection_string_private(), + }; + + let mut client_options = mongodb::options::ClientOptions::parse(connection_string) + .await + .map_err(CustomError::new)?; + client_options.min_pool_size = Some(1); + client_options.max_pool_size = Some(5); + + let client = mongodb::Client::with_options(client_options).map_err(CustomError::new)?; + + // Return a handle to the database defined at the end of the connection string, which is the users provisioned + // database + let database = client.default_database(); + + match database { + Some(database) => Ok(database), + None => Err(Error::Database( + "mongodb connection string missing default database".into(), + )), + } + } +} + +impl MongoDb { + /// Use a custom connection string for local runs + pub fn local_uri(mut self, local_uri: &str) -> Self { + self.local_uri = Some(local_uri.to_string()); + + self + } +} diff --git a/resources/shared-db/src/postgres.rs b/resources/shared-db/src/postgres.rs new file mode 100644 index 000000000..bc3537381 --- /dev/null +++ b/resources/shared-db/src/postgres.rs @@ -0,0 +1,72 @@ +use async_trait::async_trait; +use serde::Serialize; +use shuttle_service::{database, error::CustomError, Error, Factory, ResourceBuilder, Type}; + +use crate::SharedDbOutput; + +#[derive(Serialize)] +pub struct Postgres { + local_uri: Option, +} + +/// Get an `sqlx::PgPool` from any factory +#[async_trait] +impl ResourceBuilder for Postgres { + const TYPE: Type = Type::Database(database::Type::Shared(database::SharedEngine::Postgres)); + + type Output = SharedDbOutput; + + fn new() -> Self { + Self { local_uri: None } + } + + async fn output(self, factory: &mut dyn Factory) -> Result { + let info = match factory.get_environment() { + shuttle_service::Environment::Production => SharedDbOutput::Shared( + factory + .get_db_connection(database::Type::Shared(database::SharedEngine::Postgres)) + .await?, + ), + shuttle_service::Environment::Local => { + if let Some(local_uri) = self.local_uri { + SharedDbOutput::Local(local_uri) + } else { + SharedDbOutput::Shared( + factory + .get_db_connection(database::Type::Shared( + database::SharedEngine::Postgres, + )) + .await?, + ) + } + } + }; + + Ok(info) + } + + async fn build(build_data: &Self::Output) -> Result { + let connection_string = match build_data { + SharedDbOutput::Local(local_uri) => local_uri.clone(), + SharedDbOutput::Shared(info) => info.connection_string_private(), + }; + + let pool = sqlx::postgres::PgPoolOptions::new() + .min_connections(1) + .max_connections(5) + .connect(&connection_string) + .await + .map_err(CustomError::new)?; + + Ok(pool) + } +} + +impl Postgres { + /// Use a custom connection string for local runs + pub fn local_uri(mut self, local_uri: &str) -> Self { + self.local_uri = Some(local_uri.to_string()); + + self + } +} diff --git a/resources/static-folder/Cargo.toml b/resources/static-folder/Cargo.toml index 87a05e6f8..0a441caff 100644 --- a/resources/static-folder/Cargo.toml +++ b/resources/static-folder/Cargo.toml @@ -9,6 +9,7 @@ keywords = ["shuttle-service", "static-folder"] [dependencies] async-trait = "0.1.56" fs_extra = "1.3.0" +serde = { version = "1.0.148", features = ["derive"] } shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } tracing = "0.1.37" diff --git a/resources/static-folder/src/lib.rs b/resources/static-folder/src/lib.rs index 4a3f3c775..fca1ccc68 100644 --- a/resources/static-folder/src/lib.rs +++ b/resources/static-folder/src/lib.rs @@ -1,12 +1,14 @@ use async_trait::async_trait; use fs_extra::dir::{copy, CopyOptions}; +use serde::Serialize; use shuttle_service::{ error::{CustomError, Error as ShuttleError}, - Factory, ResourceBuilder, + Factory, ResourceBuilder, Type, }; use std::path::{Path, PathBuf}; use tracing::{error, trace}; +#[derive(Serialize)] pub struct StaticFolder<'a> { /// The folder to reach at runtime. Defaults to `static` folder: &'a str, @@ -28,11 +30,18 @@ impl<'a> StaticFolder<'a> { #[async_trait] impl<'a> ResourceBuilder for StaticFolder<'a> { + const TYPE: Type = Type::StaticFolder; + + type Output = PathBuf; + fn new() -> Self { Self { folder: "static" } } - async fn build(self, factory: &mut dyn Factory) -> Result { + async fn output( + self, + factory: &mut dyn Factory, + ) -> Result { let folder = Path::new(self.folder); trace!(?folder, "building static folder"); @@ -82,6 +91,10 @@ impl<'a> ResourceBuilder for StaticFolder<'a> { } } } + + async fn build(build_data: &Self::Output) -> Result { + Ok(build_data.clone()) + } } impl From for shuttle_service::Error { @@ -104,7 +117,7 @@ mod tests { use std::path::PathBuf; use async_trait::async_trait; - use shuttle_service::{Factory, ResourceBuilder}; + use shuttle_service::{DatabaseReadyInfo, Factory, ResourceBuilder}; use tempfile::{Builder, TempDir}; use crate::StaticFolder; @@ -155,10 +168,10 @@ mod tests { #[async_trait] impl Factory for MockFactory { - async fn get_db_connection_string( + async fn get_db_connection( &mut self, _db_type: shuttle_service::database::Type, - ) -> Result { + ) -> Result { panic!("no static folder test should try to get a db connection string") } @@ -199,7 +212,7 @@ mod tests { // Call plugin let static_folder = StaticFolder::new(); - let actual_folder = static_folder.build(&mut factory).await.unwrap(); + let actual_folder = static_folder.output(&mut factory).await.unwrap(); assert_eq!( actual_folder, @@ -222,7 +235,7 @@ mod tests { let _ = static_folder .folder("/etc") - .build(&mut factory) + .output(&mut factory) .await .unwrap(); } @@ -241,7 +254,7 @@ mod tests { let _ = static_folder .folder("../escape") - .build(&mut factory) + .output(&mut factory) .await .unwrap(); }