Add testcontainer for agent image with nginx

This commit is contained in:
GW_MC
2025-12-22 12:54:14 +08:00
parent 61ecd91219
commit 7db23b01df
13 changed files with 589 additions and 78 deletions

View File

@@ -9,7 +9,7 @@ path = "src/lib.rs"
[dependencies]
async-trait = "0.1.89"
testcontainers = "0.24.0"
testcontainers = { version = "0.26.0" }
shared = { path = "../../public/shared" }
tokio = { version = "1.47.0", features = ["full"] }
url = "2.5.7"

120
apps/container/src/agent.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::{error::Error, sync::Arc};
use testcontainers::{
ContainerAsync, GenericBuildableImage, GenericImage, ImageExt,
core::{AccessMode, BuildImageOptions, ContainerPort, Mount, WaitFor},
runners::{AsyncBuilder, AsyncRunner},
};
use crate::{
db::UnStartedContainer,
types::{ConfigInfoType, WithContainer},
};
pub const SOCK_NAME: &str = "yanpm-agent.sock";
const SOCK_FOLDER: &str = "/var/run/yanpm";
const NGINX_CONFIG_DIR: &str = "/etc/nginx/conf.d";
#[derive(Clone)]
pub struct AgentContainerConfig {
pub image: String,
pub tag: String,
pub container_name: String,
pub dockerfile_path: String,
pub force_build: bool,
pub agent_config: AgentConfig,
pub nginx_config: NginxConfig,
}
pub type AgentConfigInfoType = ConfigInfoType<AgentContainerInfo, ()>;
#[derive(Clone)]
pub struct AgentContainerInfo {
pub container: Arc<ContainerAsync<GenericImage>>,
pub config: AgentContainerConfig,
}
impl WithContainer for AgentContainerInfo {
fn container(&self) -> &Arc<ContainerAsync<GenericImage>> {
&self.container
}
}
#[derive(Clone)]
pub struct AgentConfig {
pub sock_folder: String, // path to be mounted to host for unix socket
pub nginx_config_dir: String, // path to be mounted to host for nginx config files, only the agent generated folder will be mounted
pub sock_perm: u32, // permissions to set on the unix socket
pub sock_gid: String, // GID to set on the unix socket
}
#[derive(Clone)]
pub struct NginxConfig {
pub expose_http: bool,
pub expose_https: bool,
}
impl AgentContainerConfig {
pub fn new(
image: String,
tag: String,
container_name: String,
dockerfile_path: String,
force_build: bool,
// agent configs
agent_config: AgentConfig,
nginx_config: NginxConfig,
) -> Self {
AgentContainerConfig {
image,
tag,
container_name,
dockerfile_path,
force_build,
// default agent configs
agent_config,
nginx_config,
}
}
pub async fn get_unstarted_container(&self) -> Result<UnStartedContainer, Box<dyn Error>> {
let mut image = GenericBuildableImage::new(&self.image, &self.tag)
.with_dockerfile(&self.dockerfile_path)
.build_image_with(BuildImageOptions::new().with_skip_if_exists(!self.force_build))
.await?;
if self.nginx_config.expose_http {
image = image.with_exposed_port(ContainerPort::Tcp(80));
}
if self.nginx_config.expose_https {
image = image.with_exposed_port(ContainerPort::Tcp(443));
}
image = image.with_wait_for(WaitFor::message_on_either_std("Starting yanpm-daemon on"));
Ok(image
.with_container_name(self.container_name.clone())
.with_env_var("YANPM_AGENT_SOCK", format!("{}/{}", SOCK_FOLDER, SOCK_NAME))
.with_env_var("YANPM_NGINX_CONFIG_DIR", NGINX_CONFIG_DIR.to_string())
.with_env_var(
"YANPM_AGENT_SOCK_PERM",
self.agent_config.sock_perm.to_string(),
)
.with_env_var("YANPM_AGENT_SOCK_GID", self.agent_config.sock_gid.clone())
.with_mount(
Mount::bind_mount(
self.agent_config.sock_folder.clone(),
SOCK_FOLDER.to_string(),
)
.with_access_mode(AccessMode::ReadWrite),
)
.with_mount(
Mount::bind_mount(
self.agent_config.nginx_config_dir.clone(),
NGINX_CONFIG_DIR.to_string(),
)
.with_access_mode(AccessMode::ReadWrite),
)
.start())
}
}

View File

@@ -9,7 +9,7 @@ pub struct OptionalContainerConfig {
}
#[derive(Clone)]
pub struct ContainerConfig {
pub struct DatabaseContainerConfig {
pub image: String,
pub tag: String,
pub container_name: String,
@@ -19,8 +19,8 @@ pub struct ContainerConfig {
}
impl OptionalContainerConfig {
pub fn fill_with(&self, other: &ContainerConfig) -> ContainerConfig {
ContainerConfig {
pub fn fill_with(&self, other: &DatabaseContainerConfig) -> DatabaseContainerConfig {
DatabaseContainerConfig {
image: self.image.clone().unwrap_or_else(|| other.image.clone()),
tag: self.tag.clone().unwrap_or_else(|| other.tag.clone()),
container_name: self

View File

@@ -11,12 +11,12 @@ use crate::{
ConfigInfoType,
db::{
ContainerizedDBInfo, DBConfigInfoType, DBInfo, UnStartedContainer,
config::{ContainerConfig, OptionalContainerConfig},
config::{DatabaseContainerConfig, OptionalContainerConfig},
},
};
pub fn get_default_config() -> ContainerConfig {
ContainerConfig {
pub fn get_default_config() -> DatabaseContainerConfig {
DatabaseContainerConfig {
container_name: "yanpm-postgres".to_string(),
database_name: "postgres".to_string(),
user: "postgres".to_string(),
@@ -27,7 +27,7 @@ pub fn get_default_config() -> ContainerConfig {
}
pub struct PostgreSQLContainer {
pub config: ContainerConfig,
pub config: DatabaseContainerConfig,
}
#[async_trait]

View File

@@ -32,6 +32,10 @@ impl EnvFile {
env_file
}
pub fn write_line(&mut self, key: &str, value: &str) {
self._write_line_buffer(key, value);
}
pub fn write(&mut self, stream: &mut dyn Write, with_prefix: bool) {
self._write_buffer(stream, with_prefix);
}

View File

@@ -1,9 +1,11 @@
pub mod agent;
pub mod db;
mod env;
pub mod types;
mod util;
use crate::{
agent::AgentConfigInfoType,
db::DBConfigInfoType,
types::{ConfigInfoType, WithContainer, WithoutContainer},
util::{
@@ -15,6 +17,7 @@ use crate::{
#[derive(Clone)]
pub struct Config {
pub database: DBConfigInfoType,
pub agent: Option<AgentConfigInfoType>,
}
// relative to the pwd
@@ -56,26 +59,29 @@ impl<'a> Drop for DetachedHandle<'a> {
}
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);
write_env_files(&config.database, &config.agent);
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;
println!("Stopping database container...");
stop_container(&config.database, "database".to_string()).await;
if let Some(agent) = &config.agent {
println!("Stopping agent container...");
stop_container(agent, "agent".to_string()).await;
}
println!("Container stopped.");
// 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.");
println!("Generated config files removed.");
}
pub async fn start_attached(config: &Config) {

View File

@@ -1,6 +1,10 @@
use std::sync::Arc;
use clap::Parser;
use container::Config;
use container::agent::{AgentConfig, AgentContainerConfig, AgentContainerInfo, NginxConfig};
use container::start_attached;
use container::types::ConfigInfoType;
use container::{Config, agent};
use container::db::DBInfo;
@@ -11,12 +15,52 @@ 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,
// agent related
/// force build agent image
#[arg(long, default_value_t = false, env = "AGENT_FORCE_BUILD")]
agent_force_build: bool,
/// dockerfile path for building agent image
#[arg(long, env = "AGENT_DOCKERFILE_PATH", required = false)]
agent_dockerfile_path: Option<String>,
/// host's location to mount nginx config files folder generated by the agent
#[arg(long, env = "AGENT_NGINX_CONFIG_DIR", required = false)]
agent_nginx_config_dir: Option<String>,
/// host's location folder to mount the unix socket files
#[arg(long, env = "AGENT_SOCK_PATH", required = false)]
agent_sock_path: Option<String>,
/// socket permissions to set on the unix socket
#[arg(long, default_value = "660", env = "AGENT_SOCK_PERM", required = false)]
agent_sock_perm: u32,
/// socket GID to set on the unix socket
#[arg(long, default_value = "", env = "AGENT_SOCK_GID", required = false)]
agent_sock_gid: String,
/// nginx expose http port
#[arg(
long,
default_value_t = true,
env = "AGENT_NGINX_EXPOSE_HTTP",
required = false
)]
agent_nginx_expose_http: bool,
/// nginx expose https port
#[arg(
long,
default_value_t = false,
env = "AGENT_NGINX_EXPOSE_HTTPS",
required = false
)]
agent_nginx_expose_https: bool,
}
struct ParsedArgs {
db_type: String,
agent_container_config: Option<AgentContainerConfig>,
}
#[tokio::main]
async fn main() {
// Parse command line arguments and environment variables
let args = Args::parse();
let args = parse_args().await;
println!("Starting container with database type: {}", args.db_type);
let db_config = match args.db_type.to_lowercase().as_str() {
@@ -43,11 +87,97 @@ async fn main() {
};
println!("Database configuration obtained.");
let agent_container = if let Some(agent_config) = &args.agent_container_config {
println!(
"Agent container will be used with socket path: {} and nginx config dir: {}",
agent_config.agent_config.sock_folder, agent_config.agent_config.nginx_config_dir
);
Some(agent_config.get_unstarted_container().await)
} else {
println!("No agent container configuration provided, skipping agent setup.");
None
};
let config = Config {
database: db_config,
agent: match agent_container {
Some(Ok(container)) => Some(ConfigInfoType::Containerized(AgentContainerInfo {
container: Arc::new(container.await.expect("Failed to start agent container")),
config: args.agent_container_config.expect("Invalid config state"),
})),
Some(Err(e)) => {
eprintln!("Failed to set up agent container: {}", e);
std::process::exit(1);
}
None => None,
},
};
println!("Starting container...");
start_attached(&config).await;
println!("Container stopped. Exiting...");
}
async fn parse_args() -> ParsedArgs {
// Parse command line arguments and environment variables
let args = Args::parse();
// if any required args are missing, do not start agent
let dockerfile_path = match args.agent_dockerfile_path {
None => {
println!("Agent dockerfile path not provided, skipping agent setup.");
return ParsedArgs {
db_type: args.db_type,
agent_container_config: None,
};
}
Some(path) => path,
};
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let agent_config = AgentConfig {
sock_folder: match args.agent_sock_path {
None => {
// create a temp dir for the socket path
let temp_dir = std::env::temp_dir().join(format!("yanpm-agent-sock-{}", time));
std::fs::create_dir_all(&temp_dir)
.expect("Failed to create temp dir for agent socket");
temp_dir.to_string_lossy().to_string()
}
Some(path) => path,
},
nginx_config_dir: match args.agent_nginx_config_dir {
None => {
// create a temp dir for the nginx config dir
let temp_dir =
std::env::temp_dir().join(format!("yanpm-agent-nginx-configs-{}", time));
std::fs::create_dir_all(&temp_dir)
.expect("Failed to create temp dir for agent nginx configs");
temp_dir.to_string_lossy().to_string()
}
Some(path) => path,
},
sock_perm: args.agent_sock_perm,
sock_gid: args.agent_sock_gid.clone(),
};
ParsedArgs {
db_type: args.db_type,
agent_container_config: Some(AgentContainerConfig {
image: "yanpm-agent".to_string(),
tag: "latest".to_string(),
container_name: format!("yanpm-agent-container-{}", time),
dockerfile_path,
force_build: args.agent_force_build,
agent_config,
nginx_config: NginxConfig {
expose_http: args.agent_nginx_expose_http,
expose_https: args.agent_nginx_expose_https,
},
}),
}
}

View File

@@ -10,6 +10,10 @@ pub trait WithoutContainer {
fn on_delete(&self);
}
impl WithoutContainer for () {
fn on_delete(&self) {}
}
#[derive(Clone)]
pub enum ConfigInfoType<T, U>
where

View File

@@ -4,6 +4,7 @@ use tokio::signal::unix::{SignalKind, signal};
use crate::{
API_CONFIG_PATH, DB_CONFIG_PATH,
agent::{AgentConfigInfoType, AgentContainerInfo, SOCK_NAME},
db::DBConfigInfoType,
env::{self, EnvFile},
types::{ConfigInfoType, WithContainer, WithoutContainer},
@@ -20,7 +21,7 @@ pub fn to_absolute_path(path: &str) -> PathBuf {
.clean()
}
pub fn write_env_files(db_config: &DBConfigInfoType) {
pub fn write_env_files(db_config: &DBConfigInfoType, agent_config: &Option<AgentConfigInfoType>) {
let api_config_path_absolute = to_absolute_path(API_CONFIG_PATH);
let db_config_path_absolute = to_absolute_path(DB_CONFIG_PATH);
@@ -33,6 +34,20 @@ pub fn write_env_files(db_config: &DBConfigInfoType) {
let mut db_env = api_env.clone();
db_env.file_type = env::EnvFileType::DotEnv;
// agent related env vars
if let Some(agent) = agent_config
&& let ConfigInfoType::Containerized(agent) = agent
{
api_env.write_line(
"AGENT__SOCK__PATH",
format!("{}/{}", &agent.config.agent_config.sock_folder, SOCK_NAME).as_str(),
);
api_env.write_line(
"AGENT__NGINX__CONFIG__DIR",
&agent.config.agent_config.nginx_config_dir,
);
}
let mut api_file =
std::fs::File::create(&api_config_path_absolute).expect("Failed to create API config file");