feature/containerize-dependency #2
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
target/
|
||||||
|
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug
|
||||||
|
target
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# Generated by cargo mutants
|
||||||
|
# Contains mutation testing data
|
||||||
|
**/mutants.out*/
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
2129
Cargo.lock
generated
Normal file
2129
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"apps/container",
|
||||||
|
]
|
||||||
|
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
module_inception = "allow"
|
||||||
1
apps/container/.gitignore
vendored
Normal file
1
apps/container/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
generated/
|
||||||
17
apps/container/Cargo.toml
Normal file
17
apps/container/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "container-simulate"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "container"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
testcontainers = "0.24.0"
|
||||||
|
shared = { path = "../../public/shared" }
|
||||||
|
tokio = { version = "1.47.0", features = ["full"] }
|
||||||
|
url = "2.5.7"
|
||||||
|
clap = { version = "4.5.48", features = ["derive", "env"] }
|
||||||
|
path-clean = "1.0.1"
|
||||||
59
apps/container/src/db.rs
Normal file
59
apps/container/src/db.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod postgresql;
|
||||||
|
pub mod sqlite;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use shared::db_type::DBType;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::{pin::Pin, sync::Arc};
|
||||||
|
use url::Host;
|
||||||
|
|
||||||
|
use testcontainers::{ContainerAsync, GenericImage, TestcontainersError};
|
||||||
|
|
||||||
|
use crate::{ConfigInfoType, WithContainer, WithoutContainer};
|
||||||
|
|
||||||
|
pub type UnStartedContainer =
|
||||||
|
Pin<Box<dyn Future<Output = Result<ContainerAsync<GenericImage>, TestcontainersError>> + Send>>;
|
||||||
|
|
||||||
|
pub type DBConfigInfoType = ConfigInfoType<ContainerizedDBInfo, PreExistingDBInfo>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PreExistingDBInfo {
|
||||||
|
pub db_type: DBType,
|
||||||
|
pub url: String,
|
||||||
|
pub on_delete: Arc<dyn Fn() + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithoutContainer for PreExistingDBInfo {
|
||||||
|
fn on_delete(&self) {
|
||||||
|
(self.on_delete)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ContainerizedDBInfo {
|
||||||
|
pub db_type: DBType,
|
||||||
|
pub container: Arc<ContainerAsync<GenericImage>>,
|
||||||
|
pub container_name: String,
|
||||||
|
pub database_name: String,
|
||||||
|
pub host: Host,
|
||||||
|
pub port: u16,
|
||||||
|
pub url: String,
|
||||||
|
pub user: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithContainer for ContainerizedDBInfo {
|
||||||
|
fn container(&self) -> &Arc<ContainerAsync<GenericImage>> {
|
||||||
|
&self.container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DBInfo<T> {
|
||||||
|
async fn new(config: Option<T>) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
async fn get_db_container_config_info(&self) -> DBConfigInfoType;
|
||||||
|
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()>;
|
||||||
|
}
|
||||||
53
apps/container/src/db/config.rs
Normal file
53
apps/container/src/db/config.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
pub struct OptionalContainerConfig {
|
||||||
|
pub image: Option<String>,
|
||||||
|
pub tag: Option<String>,
|
||||||
|
pub container_name: Option<String>,
|
||||||
|
pub database_name: Option<String>,
|
||||||
|
pub user: Option<String>,
|
||||||
|
pub password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ContainerConfig {
|
||||||
|
pub image: String,
|
||||||
|
pub tag: String,
|
||||||
|
pub container_name: String,
|
||||||
|
pub database_name: String,
|
||||||
|
pub user: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionalContainerConfig {
|
||||||
|
pub fn fill_with(&self, other: &ContainerConfig) -> ContainerConfig {
|
||||||
|
ContainerConfig {
|
||||||
|
image: self.image.clone().unwrap_or_else(|| other.image.clone()),
|
||||||
|
tag: self.tag.clone().unwrap_or_else(|| other.tag.clone()),
|
||||||
|
container_name: self
|
||||||
|
.container_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| other.container_name.clone()),
|
||||||
|
database_name: self
|
||||||
|
.database_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| other.database_name.clone()),
|
||||||
|
user: self.user.clone().unwrap_or_else(|| other.user.clone()),
|
||||||
|
password: self
|
||||||
|
.password
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| other.password.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OptionalContainerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
image: None,
|
||||||
|
tag: None,
|
||||||
|
container_name: None,
|
||||||
|
database_name: None,
|
||||||
|
user: None,
|
||||||
|
password: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
apps/container/src/db/postgresql.rs
Normal file
95
apps/container/src/db/postgresql.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use testcontainers::{
|
||||||
|
GenericImage, ImageExt,
|
||||||
|
core::{IntoContainerPort, WaitFor},
|
||||||
|
runners::AsyncRunner,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ConfigInfoType,
|
||||||
|
db::{
|
||||||
|
ContainerizedDBInfo, DBConfigInfoType, DBInfo, UnStartedContainer,
|
||||||
|
config::{ContainerConfig, OptionalContainerConfig},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn get_default_config() -> ContainerConfig {
|
||||||
|
ContainerConfig {
|
||||||
|
container_name: "yanpm-postgres".to_string(),
|
||||||
|
database_name: "postgres".to_string(),
|
||||||
|
user: "postgres".to_string(),
|
||||||
|
password: "postgres".to_string(),
|
||||||
|
image: "postgres".to_string(),
|
||||||
|
tag: "16-alpine".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgreSQLContainer {
|
||||||
|
pub config: ContainerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DBInfo<OptionalContainerConfig> for PostgreSQLContainer {
|
||||||
|
async fn get_db_container_config_info(&self) -> DBConfigInfoType {
|
||||||
|
let pg_container = self
|
||||||
|
.get_unstarted_container()
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.expect("Failed to start PostgreSQL container");
|
||||||
|
let pg_host = pg_container.get_host().await.expect("Failed to get host");
|
||||||
|
let pg_host_port = pg_container
|
||||||
|
.get_host_port_ipv4(5432.tcp())
|
||||||
|
.await
|
||||||
|
.expect("Failed to get host port");
|
||||||
|
let pg_url = format!(
|
||||||
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
|
self.config.user,
|
||||||
|
self.config.password,
|
||||||
|
pg_host,
|
||||||
|
pg_host_port,
|
||||||
|
self.config.database_name
|
||||||
|
);
|
||||||
|
|
||||||
|
ConfigInfoType::Containerized(ContainerizedDBInfo {
|
||||||
|
db_type: crate::db::DBType::PostgreSQL,
|
||||||
|
container: Arc::new(pg_container),
|
||||||
|
container_name: self.config.container_name.clone(),
|
||||||
|
database_name: self.config.database_name.clone(),
|
||||||
|
host: pg_host,
|
||||||
|
port: pg_host_port,
|
||||||
|
url: pg_url,
|
||||||
|
user: self.config.user.clone(),
|
||||||
|
password: self.config.password.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn new(user_default_config: Option<OptionalContainerConfig>) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let default_config = get_default_config();
|
||||||
|
let config = user_default_config
|
||||||
|
.unwrap_or_default()
|
||||||
|
.fill_with(&default_config);
|
||||||
|
|
||||||
|
PostgreSQLContainer {
|
||||||
|
config: config.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()> {
|
||||||
|
Ok(
|
||||||
|
GenericImage::new(self.config.image.clone(), self.config.tag.clone())
|
||||||
|
.with_exposed_port(5432.tcp())
|
||||||
|
.with_wait_for(WaitFor::message_on_stderr(
|
||||||
|
"database system is ready to accept connections",
|
||||||
|
))
|
||||||
|
.with_container_name(self.config.container_name.clone())
|
||||||
|
.with_env_var("POSTGRES_USER", self.config.user.clone())
|
||||||
|
.with_env_var("POSTGRES_PASSWORD", self.config.password.clone())
|
||||||
|
.start(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
113
apps/container/src/db/sqlite.rs
Normal file
113
apps/container/src/db/sqlite.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ConfigInfoType,
|
||||||
|
db::{DBConfigInfoType, DBInfo, PreExistingDBInfo, UnStartedContainer},
|
||||||
|
util::to_absolute_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ContainerConfig {
|
||||||
|
// Add any SQLite-specific configuration options here if needed
|
||||||
|
pub database_name: String,
|
||||||
|
pub absolute_dir_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OptionalContainerConfig {
|
||||||
|
// Add any optional configuration fields here
|
||||||
|
pub database_name: Option<String>,
|
||||||
|
pub absolute_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionalContainerConfig {
|
||||||
|
pub fn fill_with(&self, other: &ContainerConfig) -> ContainerConfig {
|
||||||
|
ContainerConfig {
|
||||||
|
database_name: self
|
||||||
|
.database_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| other.database_name.clone()),
|
||||||
|
absolute_dir_path: self
|
||||||
|
.absolute_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| other.absolute_dir_path.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OptionalContainerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
database_name: None,
|
||||||
|
absolute_path: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_config() -> ContainerConfig {
|
||||||
|
ContainerConfig {
|
||||||
|
database_name: "sqlite".to_string(),
|
||||||
|
absolute_dir_path: to_absolute_path("./generated/sqlite"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SQLiteContainer {
|
||||||
|
pub config: ContainerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SQLiteContainer {
|
||||||
|
fn get_db_absolute_path(&self) -> PathBuf {
|
||||||
|
self.config
|
||||||
|
.absolute_dir_path
|
||||||
|
.join(&self.config.database_name)
|
||||||
|
.with_extension("db")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
|
||||||
|
async fn get_db_container_config_info(&self) -> DBConfigInfoType {
|
||||||
|
// sqlite filepath url does not include the "sqlite://" prefix
|
||||||
|
let sqlite_url = format!("{}", self.get_db_absolute_path().to_string_lossy());
|
||||||
|
// create the file
|
||||||
|
std::fs::create_dir_all(&self.config.absolute_dir_path)
|
||||||
|
.expect("Failed to create directories for SQLite database");
|
||||||
|
std::fs::File::create(self.get_db_absolute_path())
|
||||||
|
.expect("Failed to create SQLite database file");
|
||||||
|
//
|
||||||
|
ConfigInfoType::PreExisting(PreExistingDBInfo {
|
||||||
|
db_type: crate::db::DBType::SQLite,
|
||||||
|
url: sqlite_url,
|
||||||
|
on_delete: {
|
||||||
|
let db_path = self.get_db_absolute_path();
|
||||||
|
Arc::new(move || {
|
||||||
|
// delete the sqlite database file
|
||||||
|
if db_path.exists() {
|
||||||
|
if let Err(e) = std::fs::remove_file(&db_path) {
|
||||||
|
eprintln!("Failed to delete SQLite database file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn new(user_default_config: Option<OptionalContainerConfig>) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let default_config = get_default_config();
|
||||||
|
let config = user_default_config
|
||||||
|
.unwrap_or_default()
|
||||||
|
.fill_with(&default_config);
|
||||||
|
|
||||||
|
SQLiteContainer {
|
||||||
|
config: config.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()> {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
36
apps/container/src/env.rs
Normal file
36
apps/container/src/env.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use shared::db_type::DBType;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum EnvFileType {
|
||||||
|
DotEnv,
|
||||||
|
Yaml,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct EnvFile {
|
||||||
|
pub file_type: EnvFileType,
|
||||||
|
pub db_type: DBType,
|
||||||
|
pub db_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvFile {
|
||||||
|
pub fn write(self, path: impl AsRef<std::path::Path>) {
|
||||||
|
let path_ref = path.as_ref();
|
||||||
|
println!("Config file path: {}", path_ref.display());
|
||||||
|
let mut config_file =
|
||||||
|
std::fs::File::create(path_ref).expect("Failed to create config file");
|
||||||
|
//
|
||||||
|
self._write_line(&mut config_file, "DB_TYPE", &self.db_type.to_string());
|
||||||
|
self._write_line(&mut config_file, "DATABASE_URL", &self.db_url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _write_line(&self, file: &mut std::fs::File, key: &str, value: &str) {
|
||||||
|
match self.file_type {
|
||||||
|
EnvFileType::DotEnv => writeln!(file, "{}={}", key, value),
|
||||||
|
EnvFileType::Yaml => writeln!(file, "{}: \"{}\"", key, value),
|
||||||
|
}
|
||||||
|
.expect("Failed to write to config file");
|
||||||
|
}
|
||||||
|
}
|
||||||
95
apps/container/src/lib.rs
Normal file
95
apps/container/src/lib.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
pub mod db;
|
||||||
|
mod env;
|
||||||
|
pub mod types;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::DBConfigInfoType,
|
||||||
|
types::{ConfigInfoType, WithContainer, WithoutContainer},
|
||||||
|
util::{
|
||||||
|
await_termination_signal, remove_file_if_exists, stop_container, to_absolute_path,
|
||||||
|
write_env_files,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub database: DBConfigInfoType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// relative to the pwd
|
||||||
|
const API_CONFIG_PATH: &str = "../api/generated-config.yaml";
|
||||||
|
const DB_CONFIG_PATH: &str = "../../public/database/.env.generated";
|
||||||
|
|
||||||
|
pub struct DetachedHandle<'a> {
|
||||||
|
stopped: bool,
|
||||||
|
pub config: &'a Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DetachedHandle<'a> {
|
||||||
|
pub fn new(config: &'a Config) -> Self {
|
||||||
|
DetachedHandle {
|
||||||
|
stopped: false,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(&mut self) {
|
||||||
|
if self.stopped {
|
||||||
|
eprintln!("Attempted to stop an already stopped DetachedHandle.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.stopped = true;
|
||||||
|
stop(self.config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Drop for DetachedHandle<'a> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.stopped {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"Warning: DetachedHandle was dropped without calling stop(). The container may still be running."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start(config: &Config) {
|
||||||
|
let db_config = &config.database;
|
||||||
|
//
|
||||||
|
// write the config files for the api server and database client
|
||||||
|
println!("Writing config files...");
|
||||||
|
write_env_files(&db_config);
|
||||||
|
println!("Config files written to:");
|
||||||
|
println!(" - {}", to_absolute_path(API_CONFIG_PATH).display());
|
||||||
|
println!(" - {}", to_absolute_path(DB_CONFIG_PATH).display());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop(config: &Config) {
|
||||||
|
let db_config = &config.database;
|
||||||
|
// stop the container
|
||||||
|
println!("Stopping container...");
|
||||||
|
stop_container(db_config, "database".to_string()).await;
|
||||||
|
// remove the generated config file
|
||||||
|
println!("Removing generated config file...");
|
||||||
|
remove_file_if_exists(DB_CONFIG_PATH);
|
||||||
|
remove_file_if_exists(API_CONFIG_PATH);
|
||||||
|
println!("Container stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_attached(config: &Config) {
|
||||||
|
start(config).await;
|
||||||
|
|
||||||
|
// wait for user input, ctrl+c, or other signals before exiting
|
||||||
|
println!("Press Ctrl+C, or send SIGTERM to stop the container...");
|
||||||
|
await_termination_signal().await;
|
||||||
|
|
||||||
|
stop(config).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub async fn start_detached(config: &'_ Config) -> DetachedHandle<'_> {
|
||||||
|
start(config).await;
|
||||||
|
DetachedHandle::new(config)
|
||||||
|
}
|
||||||
53
apps/container/src/main.rs
Normal file
53
apps/container/src/main.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use container::Config;
|
||||||
|
use container::start_attached;
|
||||||
|
|
||||||
|
use container::db::DBInfo;
|
||||||
|
|
||||||
|
/// Command line arguments
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Database type to use: 'postgres' or 'sqlite'. Can also be set with DB_TYPE env var.
|
||||||
|
#[arg(long, default_value = "sqlite", env = "DB_TYPE")]
|
||||||
|
db_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Parse command line arguments and environment variables
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
println!("Starting container with database type: {}", args.db_type);
|
||||||
|
let db_config = match args.db_type.to_lowercase().as_str() {
|
||||||
|
"postgres" | "pg" | "pgsql" => {
|
||||||
|
use container::db::postgresql::PostgreSQLContainer;
|
||||||
|
println!("Using PostgreSQL database");
|
||||||
|
PostgreSQLContainer::new(None)
|
||||||
|
.await
|
||||||
|
.get_db_container_config_info()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"sqlite" | "sql" => {
|
||||||
|
println!("Using SQLite database");
|
||||||
|
use container::db::sqlite::SQLiteContainer;
|
||||||
|
SQLiteContainer::new(None)
|
||||||
|
.await
|
||||||
|
.get_db_container_config_info()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
eprintln!("Unknown db_type: {}. Use 'postgres' or 'sqlite'", other);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("Database configuration obtained.");
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
database: db_config,
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Starting container...");
|
||||||
|
start_attached(&config).await;
|
||||||
|
println!("Container stopped. Exiting...");
|
||||||
|
}
|
||||||
21
apps/container/src/types.rs
Normal file
21
apps/container/src/types.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use testcontainers::{ContainerAsync, GenericImage};
|
||||||
|
|
||||||
|
pub trait WithContainer {
|
||||||
|
fn container(&self) -> &Arc<ContainerAsync<GenericImage>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WithoutContainer {
|
||||||
|
fn on_delete(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ConfigInfoType<T, U>
|
||||||
|
where
|
||||||
|
T: WithContainer,
|
||||||
|
U: WithoutContainer,
|
||||||
|
{
|
||||||
|
Containerized(T),
|
||||||
|
PreExisting(U),
|
||||||
|
}
|
||||||
114
apps/container/src/util.rs
Normal file
114
apps/container/src/util.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use path_clean::PathClean;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
API_CONFIG_PATH, DB_CONFIG_PATH,
|
||||||
|
db::DBConfigInfoType,
|
||||||
|
env::{self, EnvFile},
|
||||||
|
types::{ConfigInfoType, WithContainer, WithoutContainer},
|
||||||
|
};
|
||||||
|
|
||||||
|
// relative to the current working directory
|
||||||
|
pub fn to_absolute_path(path: &str) -> PathBuf {
|
||||||
|
if Path::new(path).is_absolute() {
|
||||||
|
return PathBuf::from(path);
|
||||||
|
}
|
||||||
|
std::env::current_dir()
|
||||||
|
.map(|cwd| cwd.join(path))
|
||||||
|
.unwrap_or_else(|_| PathBuf::from(path))
|
||||||
|
.clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_env_files(db_config: &DBConfigInfoType) {
|
||||||
|
let api_config_path_absolute = to_absolute_path(API_CONFIG_PATH);
|
||||||
|
let db_config_path_absolute = to_absolute_path(DB_CONFIG_PATH);
|
||||||
|
|
||||||
|
let (db_type, db_url) = match db_config {
|
||||||
|
DBConfigInfoType::Containerized(config) => (config.db_type.clone(), config.url.clone()),
|
||||||
|
DBConfigInfoType::PreExisting(config) => (config.db_type.clone(), config.url.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let api_env_file = EnvFile {
|
||||||
|
file_type: env::EnvFileType::Yaml,
|
||||||
|
db_type: db_type,
|
||||||
|
db_url: db_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut db_env_file = api_env_file.clone();
|
||||||
|
db_env_file.file_type = env::EnvFileType::DotEnv;
|
||||||
|
|
||||||
|
api_env_file.write(&api_config_path_absolute);
|
||||||
|
db_env_file.write(&db_config_path_absolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop_container(
|
||||||
|
config: &ConfigInfoType<impl WithContainer, impl WithoutContainer>,
|
||||||
|
config_name: String,
|
||||||
|
) {
|
||||||
|
match config {
|
||||||
|
ConfigInfoType::Containerized(container_info) => {
|
||||||
|
let container = container_info.container();
|
||||||
|
if let Err(e) = container.stop().await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to stop container: {}. With error: {}",
|
||||||
|
config_name, e
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("Container {} stopped successfully.", config_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigInfoType::PreExisting(pre_existing_info) => {
|
||||||
|
pre_existing_info.on_delete();
|
||||||
|
println!(
|
||||||
|
"Pre-existing resource for {} cleaned up successfully.",
|
||||||
|
config_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_file_if_exists(path: &str) {
|
||||||
|
let path = std::path::Path::new(path);
|
||||||
|
if path.exists() {
|
||||||
|
if let Err(e) = std::fs::remove_file(path) {
|
||||||
|
eprintln!("Failed to remove file {}: {}", path.display(), e);
|
||||||
|
} else {
|
||||||
|
println!("Removed existing file: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn await_termination_signal() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
println!("\nReceived Ctrl+C, stopping container...");
|
||||||
|
}
|
||||||
|
_ = async {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
|
||||||
|
sigterm.recv().await;
|
||||||
|
println!("\nReceived SIGTERM, stopping container...");
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
// On non-Unix systems, just wait indefinitely
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
}
|
||||||
|
} => {}
|
||||||
|
_ = async {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let mut sigquit = signal(SignalKind::quit()).expect("Failed to register SIGQUIT handler");
|
||||||
|
sigquit.recv().await;
|
||||||
|
println!("\nReceived SIGQUIT, stopping container...");
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
// On non-Unix systems, just wait indefinitely
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
}
|
||||||
|
} => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user