Compare commits
2 Commits
7db23b01df
...
5cffb0a519
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cffb0a519 | ||
|
|
6e85bda13f |
170
apps/agent/docker/s6/cont-init.d/20-install-reload-wrapper
Normal file
170
apps/agent/docker/s6/cont-init.d/20-install-reload-wrapper
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# This init script installs a minimal nginx reload wrapper and a sudoers
|
||||||
|
# entry so the `yanpm-agent` user can perform a controlled reload via sudo.
|
||||||
|
|
||||||
|
WRAPPER_PATH="/usr/local/sbin/yanpm-nginx-reload"
|
||||||
|
SUDOERS_PATH="/etc/sudoers.d/yanpm-agent"
|
||||||
|
AGENT_USER="${YANPM_AGENT_USER:-yanpm-agent}"
|
||||||
|
|
||||||
|
# validate wrapper
|
||||||
|
VALIDATE_PATH="/usr/local/sbin/yanpm-nginx-validate"
|
||||||
|
# validate file wrapper
|
||||||
|
VALIDATE_FILE_PATH="/usr/local/sbin/yanpm-nginx-validate-file"
|
||||||
|
|
||||||
|
echo "[cont-init.d] install-reload-wrapper: setting up nginx reload helper"
|
||||||
|
|
||||||
|
# find nginx binary
|
||||||
|
NGINX_BIN="$(command -v nginx || true)"
|
||||||
|
if [ -z "${NGINX_BIN}" ]; then
|
||||||
|
echo "Warning: nginx binary not found in PATH; wrapper will still be created but may fail at runtime." >&2
|
||||||
|
NGINX_BIN="/usr/sbin/nginx"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create wrapper
|
||||||
|
mkdir -p /usr/local/sbin /etc/sudoers.d
|
||||||
|
|
||||||
|
cat > "${WRAPPER_PATH}" <<- 'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exec "@NGINX_BIN@" -c /etc/nginx/nginx.conf -s reload
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace placeholder with actual path
|
||||||
|
sed -i "s|@NGINX_BIN@|${NGINX_BIN}|g" "${WRAPPER_PATH}" || true
|
||||||
|
|
||||||
|
chmod 0750 "${WRAPPER_PATH}"
|
||||||
|
chown root:root "${WRAPPER_PATH}" || true
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
# Create validate wrapper
|
||||||
|
cat > "${VALIDATE_PATH}" <<- 'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exec "@NGINX_BIN@" -c /etc/nginx/nginx.conf -t
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace placeholder with actual path in validate wrapper
|
||||||
|
sed -i "s|@NGINX_BIN@|${NGINX_BIN}|g" "${VALIDATE_PATH}" || true
|
||||||
|
|
||||||
|
chmod 0750 "${VALIDATE_PATH}"
|
||||||
|
chown root:root "${VALIDATE_PATH}" || true
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
# Create validate file wrapper (secure)
|
||||||
|
cat > "${VALIDATE_FILE_PATH}" <<-'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <nginx-config-file>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
INPUT="$1"
|
||||||
|
|
||||||
|
# Resolve absolute path
|
||||||
|
if command -v readlink >/dev/null 2>&1; then
|
||||||
|
TARGET="$(readlink -f -- "$INPUT" 2>/dev/null || true)"
|
||||||
|
elif command -v realpath >/dev/null 2>&1; then
|
||||||
|
TARGET="$(realpath -- "$INPUT" 2>/dev/null || true)"
|
||||||
|
else
|
||||||
|
echo "Error: no path resolver (readlink/realpath) available" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$TARGET" ]; then
|
||||||
|
echo "Error: cannot resolve path: $INPUT" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Must be a regular file and not a symlink
|
||||||
|
if [ ! -f "$TARGET" ] || [ -L "$TARGET" ]; then
|
||||||
|
echo "Error: ${TARGET} is not a regular file" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# must be created by agent user
|
||||||
|
AGENT_UID="$(id -u yanpm-agent 2>/dev/null || true)"
|
||||||
|
if [ -z "$AGENT_UID" ]; then
|
||||||
|
echo "Error: yanpm-agent user not found" >&2
|
||||||
|
exit 6
|
||||||
|
fi
|
||||||
|
|
||||||
|
FILE_UID="$(stat -c %u -- "$TARGET" 2>/dev/null || true)"
|
||||||
|
if [ "$FILE_UID" != "$AGENT_UID" ]; then
|
||||||
|
echo "Error: ${TARGET} not owned by yanpm-agent user" >&2
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure file is not world-writable; allow typical 664 (rw-rw-r--)
|
||||||
|
if command -v stat >/dev/null 2>&1; then
|
||||||
|
MODE="$(stat -c %a -- "$TARGET" 2>/dev/null || true)"
|
||||||
|
if [ -n "$MODE" ]; then
|
||||||
|
OTHERS=$(( MODE % 10 ))
|
||||||
|
if [ $(( OTHERS & 2 )) -ne 0 ]; then
|
||||||
|
echo "Error: ${TARGET} is world-writable" >&2
|
||||||
|
exit 8
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v find >/dev/null 2>&1; then
|
||||||
|
if find "$TARGET" -maxdepth 0 -perm /002 -print -quit >/dev/null 2>&1; then
|
||||||
|
echo "Error: ${TARGET} is world-writable" >&2
|
||||||
|
exit 8
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "@NGINX_BIN@" -c "$TARGET" -t
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace placeholder with actual path in validate file wrapper
|
||||||
|
sed -i "s|@NGINX_BIN@|${NGINX_BIN}|g" "${VALIDATE_FILE_PATH}" || true
|
||||||
|
chmod 0750 "${VALIDATE_FILE_PATH}"
|
||||||
|
chown root:root "${VALIDATE_FILE_PATH}" || true
|
||||||
|
|
||||||
|
echo "Created wrapper: ${WRAPPER_PATH} (owned by root, mode 750)"
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
# Ensure sudoers entry exists allowing the agent to run only this wrapper as root
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
echo "sudo present; creating sudoers entry"
|
||||||
|
cat > "${SUDOERS_PATH}" <<- EOF
|
||||||
|
# Allow ${AGENT_USER} to run the nginx reload and validate wrappers without a password
|
||||||
|
${AGENT_USER} ALL=(root) NOPASSWD: ${WRAPPER_PATH}, ${VALIDATE_PATH}, ${VALIDATE_FILE_PATH}
|
||||||
|
EOF
|
||||||
|
chmod 0440 "${SUDOERS_PATH}" || true
|
||||||
|
echo "Wrote sudoers entry: ${SUDOERS_PATH}"
|
||||||
|
else
|
||||||
|
echo "sudo not found; attempting to install"
|
||||||
|
if command -v apk >/dev/null 2>&1; then
|
||||||
|
apk add --no-cache sudo || true
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update || true
|
||||||
|
apt-get install -y sudo || true
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y sudo || true
|
||||||
|
else
|
||||||
|
echo "No known package manager to install sudo; please ensure sudo is available in the image." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
cat > "${SUDOERS_PATH}" <<- EOF
|
||||||
|
# Allow ${AGENT_USER} to run the nginx reload and validate wrappers without a password
|
||||||
|
${AGENT_USER} ALL=(root) NOPASSWD: ${WRAPPER_PATH}, ${VALIDATE_PATH}, ${VALIDATE_FILE_PATH}
|
||||||
|
EOF
|
||||||
|
chmod 0440 "${SUDOERS_PATH}" || true
|
||||||
|
echo "Installed sudo and wrote sudoers entry: ${SUDOERS_PATH}"
|
||||||
|
else
|
||||||
|
echo "Failed to install sudo; the agent will not be able to reload nginx via sudo." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
2
apps/agent/justfile
Normal file
2
apps/agent/justfile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
build-docker:
|
||||||
|
docker build -t yanpm/agent:latest .
|
||||||
@@ -93,6 +93,17 @@ impl ReloadCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reload the running nginx master process (no -c) so it reloads its configured main config
|
// reload the running nginx master process (no -c) so it reloads its configured main config
|
||||||
run_cmd("nginx", &["-s", "reload"], 10).await
|
// Prefer the restricted sudo wrapper if available, fall back to direct nginx reload.
|
||||||
|
// TODO: allow configuring the path to the wrapper
|
||||||
|
match run_cmd("sudo", &["-n", "/usr/local/sbin/yanpm-nginx-reload"], 10).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"sudo reload wrapper failed, falling back to direct nginx reload: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
run_cmd("nginx", &["-s", "reload"], 10).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::commands::{run::run_cmd, write_config::INTERNAL_CONFIG_FOLDER_NAME};
|
use crate::commands::{run::run_cmd, write_config::INTERNAL_CONFIG_FOLDER_NAME};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -17,7 +19,51 @@ impl ValidateCommand {
|
|||||||
pub async fn validate_all(
|
pub async fn validate_all(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<(i32, String), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(i32, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
run_cmd("nginx", &["-t"], 10).await
|
// Try a normal config test first. If it fails due to pid permission
|
||||||
|
// errors (common when running unprivileged against /run/nginx.pid),
|
||||||
|
// retry with a writable pid override so validation can succeed.
|
||||||
|
match run_cmd("nginx", &["-t"], 10).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(e) => {
|
||||||
|
info!(
|
||||||
|
"nginx -t failed: {}. Trying with privileged wrapper or writable pid override.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let es = e.to_string();
|
||||||
|
if es.contains("/run/nginx.pid") && es.contains("Permission denied") {
|
||||||
|
// Try privileged validate wrapper if available (allows the agent to run
|
||||||
|
// nginx -t via sudo without modifying the main config).
|
||||||
|
match run_cmd(
|
||||||
|
"sudo",
|
||||||
|
// TODO: allow configuring the path to the wrapper
|
||||||
|
&["-n", "/usr/local/sbin/yanpm-nginx-validate"],
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(res) => return Ok(res),
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Privileged validate wrapper failed: {}. Falling back to writable pid override.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
// Fallback to the existing writable-pid override if sudo wrapper
|
||||||
|
// isn't available or fails.
|
||||||
|
let pid_path = format!(
|
||||||
|
"{}/yanpm-validate-{}.pid",
|
||||||
|
std::env::temp_dir().display(),
|
||||||
|
std::process::id()
|
||||||
|
);
|
||||||
|
let g_arg = format!("pid {};", pid_path);
|
||||||
|
let args_vec = ["-t".to_string(), "-g".to_string(), g_arg];
|
||||||
|
let args_ref: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect();
|
||||||
|
return run_cmd("nginx", args_ref.as_slice(), 10).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn validate(
|
pub async fn validate(
|
||||||
@@ -37,11 +83,84 @@ impl ValidateCommand {
|
|||||||
return Err(format!("Config file not found: {}", full_path.display()).into());
|
return Err(format!("Config file not found: {}", full_path.display()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
run_cmd(
|
// Create a temporary wrapper nginx config that provides the required
|
||||||
"nginx",
|
// top-level sections (`events` and `http`) and includes the fragment.
|
||||||
&["-t", "-c", full_path.to_str().ok_or("invalid config path")?],
|
let fragment_path = full_path.to_str().ok_or("invalid config path")?.to_string();
|
||||||
10,
|
|
||||||
)
|
let mut tmp_path = std::env::temp_dir();
|
||||||
.await
|
let tmp_name = format!("yanpm-validate-{}-{}.conf", timestamp, std::process::id());
|
||||||
|
tmp_path.push(tmp_name);
|
||||||
|
|
||||||
|
let wrapper = format!(
|
||||||
|
"worker_processes 1;\nevents {{ worker_connections 1024; }}\nhttp {{\n include {};\n}}\n",
|
||||||
|
fragment_path
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the temporary wrapper file
|
||||||
|
tokio::fs::write(&tmp_path, wrapper).await?;
|
||||||
|
let tmp_path_str = tmp_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or("invalid temp config path")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Run the test against the wrapper, telling nginx to place its pid
|
||||||
|
// somewhere writable so the config test doesn't fail with permission
|
||||||
|
// errors when running as an unprivileged user.
|
||||||
|
let result = match run_cmd("nginx", &["-t", "-c", &tmp_path_str], 10).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(e) => {
|
||||||
|
info!(
|
||||||
|
"nginx -t failed: {}. Trying with privileged wrapper or writable pid override.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let es = e.to_string();
|
||||||
|
if es.contains("/run/nginx.pid") && es.contains("Permission denied") {
|
||||||
|
// Try privileged validate wrapper if available (allows the agent to run
|
||||||
|
// nginx -t via sudo without modifying the main config).
|
||||||
|
match run_cmd(
|
||||||
|
"sudo",
|
||||||
|
// TODO: allow configuring the path to the wrapper
|
||||||
|
&[
|
||||||
|
"-n",
|
||||||
|
"/usr/local/sbin/yanpm-nginx-validate-file",
|
||||||
|
&tmp_path_str,
|
||||||
|
],
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(res) => return Ok(res),
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Privileged validate wrapper failed: {}. Falling back to writable pid override.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let pid_path = format!(
|
||||||
|
"{}/yanpm-validate-{}.pid",
|
||||||
|
std::env::temp_dir().display(),
|
||||||
|
std::process::id()
|
||||||
|
);
|
||||||
|
let g_arg = format!("pid {};", pid_path);
|
||||||
|
|
||||||
|
let args_vec = [
|
||||||
|
"-t".to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
tmp_path_str.clone(),
|
||||||
|
"-g".to_string(),
|
||||||
|
g_arg,
|
||||||
|
];
|
||||||
|
let args_ref: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect();
|
||||||
|
|
||||||
|
return run_cmd("nginx", args_ref.as_slice(), 10).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = tokio::fs::remove_file(&tmp_path).await;
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::os::unix::fs::PermissionsExt;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use crate::commands::run::to_file_name;
|
use crate::commands::run::to_file_name;
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ impl WriteConfigCommand {
|
|||||||
let path = self.nginx_config_dir.clone();
|
let path = self.nginx_config_dir.clone();
|
||||||
// ensure main config dir exists
|
// ensure main config dir exists
|
||||||
tokio::fs::create_dir_all(&path).await?;
|
tokio::fs::create_dir_all(&path).await?;
|
||||||
|
info!("Writing config to {:?}", path.join(&filename));
|
||||||
|
|
||||||
// create YANPM subdir where fragment files live
|
// create YANPM subdir where fragment files live
|
||||||
let yanpm_dir = path.join(INTERNAL_CONFIG_FOLDER_NAME);
|
let yanpm_dir = path.join(INTERNAL_CONFIG_FOLDER_NAME);
|
||||||
@@ -62,7 +64,7 @@ impl WriteConfigCommand {
|
|||||||
|
|
||||||
// set explicit permissions (rw-r-----)
|
// set explicit permissions (rw-r-----)
|
||||||
tokio::fs::set_permissions(&final_path, std::fs::Permissions::from_mode(0o640)).await?;
|
tokio::fs::set_permissions(&final_path, std::fs::Permissions::from_mode(0o640)).await?;
|
||||||
|
info!("Config written and permissions set for {:?}", final_path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub async fn validate(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_code, _output) = match nginx_controller
|
let resp = match nginx_controller
|
||||||
.validate(¶ms.config_name, params.timestamp)
|
.validate(¶ms.config_name, params.timestamp)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ pub async fn validate(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(axum::http::StatusCode::OK,).into_response()
|
(axum::http::StatusCode::OK, axum::Json(resp)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use clap::{Arg, Command};
|
use clap::{Arg, Command};
|
||||||
use container::{
|
use container::containers::{
|
||||||
|
ConfigInfoType,
|
||||||
db::{DBInfo, sqlite::SQLiteContainer},
|
db::{DBInfo, sqlite::SQLiteContainer},
|
||||||
types::ConfigInfoType,
|
|
||||||
};
|
};
|
||||||
use migration::{generate_entity, migrate_database};
|
use migration::{generate_entity, migrate_database};
|
||||||
use shared::db_type::DBType;
|
use shared::db_type::DBType;
|
||||||
|
|||||||
40
apps/container/src/containers.rs
Normal file
40
apps/container/src/containers.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
pub mod agent;
|
||||||
|
pub mod db;
|
||||||
|
|
||||||
|
use std::{pin::Pin, sync::Arc};
|
||||||
|
|
||||||
|
use testcontainers::{ContainerAsync, GenericImage, TestcontainersError};
|
||||||
|
|
||||||
|
use crate::containers::{
|
||||||
|
agent::AgentContainerInfo,
|
||||||
|
db::{ContainerizedDBInfo, PreExistingDBInfo},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type UnStartedContainer =
|
||||||
|
Pin<Box<dyn Future<Output = Result<ContainerAsync<GenericImage>, TestcontainersError>> + Send>>;
|
||||||
|
|
||||||
|
pub type AgentConfigInfoType = ConfigInfoType<AgentContainerInfo, ()>;
|
||||||
|
|
||||||
|
pub type DBConfigInfoType = ConfigInfoType<ContainerizedDBInfo, PreExistingDBInfo>;
|
||||||
|
|
||||||
|
pub trait WithContainer {
|
||||||
|
fn container(&self) -> &Arc<ContainerAsync<GenericImage>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WithoutContainer {
|
||||||
|
fn on_delete(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithoutContainer for () {
|
||||||
|
fn on_delete(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ConfigInfoType<T, U>
|
||||||
|
where
|
||||||
|
T: WithContainer,
|
||||||
|
U: WithoutContainer,
|
||||||
|
{
|
||||||
|
Containerized(T),
|
||||||
|
PreExisting(U),
|
||||||
|
}
|
||||||
@@ -5,10 +5,7 @@ use testcontainers::{
|
|||||||
runners::{AsyncBuilder, AsyncRunner},
|
runners::{AsyncBuilder, AsyncRunner},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{WithContainer, containers::UnStartedContainer};
|
||||||
db::UnStartedContainer,
|
|
||||||
types::{ConfigInfoType, WithContainer},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const SOCK_NAME: &str = "yanpm-agent.sock";
|
pub const SOCK_NAME: &str = "yanpm-agent.sock";
|
||||||
const SOCK_FOLDER: &str = "/var/run/yanpm";
|
const SOCK_FOLDER: &str = "/var/run/yanpm";
|
||||||
@@ -25,8 +22,6 @@ pub struct AgentContainerConfig {
|
|||||||
pub nginx_config: NginxConfig,
|
pub nginx_config: NginxConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AgentConfigInfoType = ConfigInfoType<AgentContainerInfo, ()>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AgentContainerInfo {
|
pub struct AgentContainerInfo {
|
||||||
pub container: Arc<ContainerAsync<GenericImage>>,
|
pub container: Arc<ContainerAsync<GenericImage>>,
|
||||||
@@ -5,18 +5,15 @@ pub mod sqlite;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use shared::db_type::DBType;
|
use shared::db_type::DBType;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::future::Future;
|
use std::sync::Arc;
|
||||||
use std::{pin::Pin, sync::Arc};
|
|
||||||
use url::Host;
|
use url::Host;
|
||||||
|
|
||||||
use testcontainers::{ContainerAsync, GenericImage, TestcontainersError};
|
use testcontainers::{ContainerAsync, GenericImage};
|
||||||
|
|
||||||
use crate::{ConfigInfoType, WithContainer, WithoutContainer};
|
use crate::{
|
||||||
|
WithContainer, WithoutContainer,
|
||||||
pub type UnStartedContainer =
|
containers::{DBConfigInfoType, UnStartedContainer},
|
||||||
Pin<Box<dyn Future<Output = Result<ContainerAsync<GenericImage>, TestcontainersError>> + Send>>;
|
};
|
||||||
|
|
||||||
pub type DBConfigInfoType = ConfigInfoType<ContainerizedDBInfo, PreExistingDBInfo>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PreExistingDBInfo {
|
pub struct PreExistingDBInfo {
|
||||||
@@ -9,9 +9,12 @@ use testcontainers::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ConfigInfoType,
|
ConfigInfoType,
|
||||||
db::{
|
containers::{
|
||||||
ContainerizedDBInfo, DBConfigInfoType, DBInfo, UnStartedContainer,
|
UnStartedContainer,
|
||||||
config::{DatabaseContainerConfig, OptionalContainerConfig},
|
db::{
|
||||||
|
ContainerizedDBInfo, DBConfigInfoType, DBInfo,
|
||||||
|
config::{DatabaseContainerConfig, OptionalContainerConfig},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ impl DBInfo<OptionalContainerConfig> for PostgreSQLContainer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ConfigInfoType::Containerized(ContainerizedDBInfo {
|
ConfigInfoType::Containerized(ContainerizedDBInfo {
|
||||||
db_type: crate::db::DBType::PostgreSQL,
|
db_type: crate::containers::db::DBType::PostgreSQL,
|
||||||
container: Arc::new(pg_container),
|
container: Arc::new(pg_container),
|
||||||
container_name: self.config.container_name.clone(),
|
container_name: self.config.container_name.clone(),
|
||||||
database_name: self.config.database_name.clone(),
|
database_name: self.config.database_name.clone(),
|
||||||
@@ -4,7 +4,7 @@ use async_trait::async_trait;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ConfigInfoType,
|
ConfigInfoType,
|
||||||
db::{DBConfigInfoType, DBInfo, PreExistingDBInfo, UnStartedContainer},
|
containers::db::{DBConfigInfoType, DBInfo, PreExistingDBInfo, UnStartedContainer},
|
||||||
util::to_absolute_path,
|
util::to_absolute_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
|
|||||||
.expect("Failed to create SQLite database file");
|
.expect("Failed to create SQLite database file");
|
||||||
//
|
//
|
||||||
ConfigInfoType::PreExisting(PreExistingDBInfo {
|
ConfigInfoType::PreExisting(PreExistingDBInfo {
|
||||||
db_type: crate::db::DBType::SQLite,
|
db_type: crate::containers::db::DBType::SQLite,
|
||||||
url: sqlite_url,
|
url: sqlite_url,
|
||||||
on_delete: {
|
on_delete: {
|
||||||
let db_path = self.get_db_absolute_path();
|
let db_path = self.get_db_absolute_path();
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use shared::db_type::DBType;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum EnvFileType {
|
pub enum EnvFileType {
|
||||||
DotEnv,
|
DotEnv,
|
||||||
@@ -11,25 +9,16 @@ pub enum EnvFileType {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EnvFile {
|
pub struct EnvFile {
|
||||||
pub file_type: EnvFileType,
|
pub file_type: EnvFileType,
|
||||||
pub db_type: DBType,
|
|
||||||
pub db_url: String,
|
|
||||||
//
|
//
|
||||||
buffer: serde_json::Value,
|
buffer: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnvFile {
|
impl EnvFile {
|
||||||
pub fn new(file_type: EnvFileType, db_type: DBType, db_url: String) -> Self {
|
pub fn new(file_type: EnvFileType) -> Self {
|
||||||
let mut env_file = EnvFile {
|
EnvFile {
|
||||||
file_type,
|
file_type,
|
||||||
db_type,
|
|
||||||
db_url,
|
|
||||||
buffer: serde_json::Value::Object(serde_json::Map::new()),
|
buffer: serde_json::Value::Object(serde_json::Map::new()),
|
||||||
};
|
}
|
||||||
|
|
||||||
env_file._write_line_buffer("DATABASE__TYPE", &env_file.db_type.to_string());
|
|
||||||
env_file._write_line_buffer("DATABASE__URL", &env_file.db_url.to_string());
|
|
||||||
|
|
||||||
env_file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_line(&mut self, key: &str, value: &str) {
|
pub fn write_line(&mut self, key: &str, value: &str) {
|
||||||
@@ -131,12 +120,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_env_file_write_yaml() {
|
fn test_env_file_write_yaml() {
|
||||||
let mut env_file_nested = EnvFile::new(
|
let mut env_file_nested = EnvFile::new(EnvFileType::Yaml);
|
||||||
EnvFileType::Yaml,
|
|
||||||
DBType::SQLite,
|
|
||||||
"mysql://user:pass@localhost/db".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
env_file_nested.write_line("DATABASE__TYPE", "SQLite");
|
||||||
|
env_file_nested.write_line("DATABASE__URL", "mysql://user:pass@localhost/db");
|
||||||
let mut output_stream = Vec::new();
|
let mut output_stream = Vec::new();
|
||||||
env_file_nested.write(&mut output_stream, false);
|
env_file_nested.write(&mut output_stream, false);
|
||||||
let output_string = String::from_utf8(output_stream).unwrap();
|
let output_string = String::from_utf8(output_stream).unwrap();
|
||||||
@@ -150,11 +137,9 @@ DATABASE:
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_env_file_write_env() {
|
fn test_env_file_write_env() {
|
||||||
let mut env_file_nested = EnvFile::new(
|
let mut env_file_nested = EnvFile::new(EnvFileType::DotEnv);
|
||||||
EnvFileType::DotEnv,
|
env_file_nested.write_line("DATABASE__TYPE", "PostgreSQL");
|
||||||
DBType::PostgreSQL,
|
env_file_nested.write_line("DATABASE__URL", "postgres://user:pass@localhost/db");
|
||||||
"postgres://user:pass@localhost/db".to_string(),
|
|
||||||
);
|
|
||||||
let mut output_stream = Vec::new();
|
let mut output_stream = Vec::new();
|
||||||
env_file_nested.write(&mut output_stream, true);
|
env_file_nested.write(&mut output_stream, true);
|
||||||
let output_string = String::from_utf8(output_stream).unwrap();
|
let output_string = String::from_utf8(output_stream).unwrap();
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
pub mod agent;
|
pub mod containers;
|
||||||
pub mod db;
|
|
||||||
mod env;
|
mod env;
|
||||||
pub mod types;
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
agent::AgentConfigInfoType,
|
containers::{
|
||||||
db::DBConfigInfoType,
|
AgentConfigInfoType, ConfigInfoType, DBConfigInfoType, WithContainer, WithoutContainer,
|
||||||
types::{ConfigInfoType, WithContainer, WithoutContainer},
|
},
|
||||||
util::{
|
util::{
|
||||||
await_termination_signal, remove_file_if_exists, stop_container, to_absolute_path,
|
await_termination_signal, remove_file_if_exists, stop_container, to_absolute_path,
|
||||||
write_env_files,
|
write_env_files,
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use container::agent::{AgentConfig, AgentContainerConfig, AgentContainerInfo, NginxConfig};
|
use container::{
|
||||||
use container::start_attached;
|
Config,
|
||||||
use container::types::ConfigInfoType;
|
containers::{
|
||||||
use container::{Config, agent};
|
ConfigInfoType,
|
||||||
|
agent::{AgentConfig, AgentContainerConfig, AgentContainerInfo, NginxConfig},
|
||||||
use container::db::DBInfo;
|
db::DBInfo,
|
||||||
|
},
|
||||||
|
start_attached,
|
||||||
|
};
|
||||||
|
|
||||||
/// Command line arguments
|
/// Command line arguments
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -17,6 +20,12 @@ struct Args {
|
|||||||
db_type: String,
|
db_type: String,
|
||||||
|
|
||||||
// agent related
|
// agent related
|
||||||
|
/// agent image name
|
||||||
|
#[arg(long, default_value = "yanpm/agent", env = "AGENT_IMAGE_NAME")]
|
||||||
|
agent_image: String,
|
||||||
|
/// agent image tag
|
||||||
|
#[arg(long, default_value = "latest", env = "AGENT_IMAGE_TAG")]
|
||||||
|
agent_image_tag: String,
|
||||||
/// force build agent image
|
/// force build agent image
|
||||||
#[arg(long, default_value_t = false, env = "AGENT_FORCE_BUILD")]
|
#[arg(long, default_value_t = false, env = "AGENT_FORCE_BUILD")]
|
||||||
agent_force_build: bool,
|
agent_force_build: bool,
|
||||||
@@ -65,7 +74,7 @@ async fn main() {
|
|||||||
println!("Starting container with database type: {}", args.db_type);
|
println!("Starting container with database type: {}", args.db_type);
|
||||||
let db_config = match args.db_type.to_lowercase().as_str() {
|
let db_config = match args.db_type.to_lowercase().as_str() {
|
||||||
"postgres" | "pg" | "pgsql" => {
|
"postgres" | "pg" | "pgsql" => {
|
||||||
use container::db::postgresql::PostgreSQLContainer;
|
use container::containers::db::postgresql::PostgreSQLContainer;
|
||||||
println!("Using PostgreSQL database");
|
println!("Using PostgreSQL database");
|
||||||
PostgreSQLContainer::new(None)
|
PostgreSQLContainer::new(None)
|
||||||
.await
|
.await
|
||||||
@@ -74,7 +83,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
"sqlite" | "sql" => {
|
"sqlite" | "sql" => {
|
||||||
println!("Using SQLite database");
|
println!("Using SQLite database");
|
||||||
use container::db::sqlite::SQLiteContainer;
|
use container::containers::db::sqlite::SQLiteContainer;
|
||||||
SQLiteContainer::new(None)
|
SQLiteContainer::new(None)
|
||||||
.await
|
.await
|
||||||
.get_db_container_config_info()
|
.get_db_container_config_info()
|
||||||
@@ -89,7 +98,7 @@ async fn main() {
|
|||||||
|
|
||||||
let agent_container = if let Some(agent_config) = &args.agent_container_config {
|
let agent_container = if let Some(agent_config) = &args.agent_container_config {
|
||||||
println!(
|
println!(
|
||||||
"Agent container will be used with socket path: {} and nginx config dir: {}",
|
"Agent container will be used with socket folder: {} and nginx config dir: {}",
|
||||||
agent_config.agent_config.sock_folder, agent_config.agent_config.nginx_config_dir
|
agent_config.agent_config.sock_folder, agent_config.agent_config.nginx_config_dir
|
||||||
);
|
);
|
||||||
Some(agent_config.get_unstarted_container().await)
|
Some(agent_config.get_unstarted_container().await)
|
||||||
@@ -168,8 +177,9 @@ async fn parse_args() -> ParsedArgs {
|
|||||||
ParsedArgs {
|
ParsedArgs {
|
||||||
db_type: args.db_type,
|
db_type: args.db_type,
|
||||||
agent_container_config: Some(AgentContainerConfig {
|
agent_container_config: Some(AgentContainerConfig {
|
||||||
image: "yanpm-agent".to_string(),
|
// TODO: allow customization of these fields via CLI args
|
||||||
tag: "latest".to_string(),
|
image: args.agent_image,
|
||||||
|
tag: args.agent_image_tag,
|
||||||
container_name: format!("yanpm-agent-container-{}", time),
|
container_name: format!("yanpm-agent-container-{}", time),
|
||||||
dockerfile_path,
|
dockerfile_path,
|
||||||
force_build: args.agent_force_build,
|
force_build: args.agent_force_build,
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use testcontainers::{ContainerAsync, GenericImage};
|
|
||||||
|
|
||||||
pub trait WithContainer {
|
|
||||||
fn container(&self) -> &Arc<ContainerAsync<GenericImage>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait WithoutContainer {
|
|
||||||
fn on_delete(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WithoutContainer for () {
|
|
||||||
fn on_delete(&self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ConfigInfoType<T, U>
|
|
||||||
where
|
|
||||||
T: WithContainer,
|
|
||||||
U: WithoutContainer,
|
|
||||||
{
|
|
||||||
Containerized(T),
|
|
||||||
PreExisting(U),
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,11 @@ use tokio::signal::unix::{SignalKind, signal};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
API_CONFIG_PATH, DB_CONFIG_PATH,
|
API_CONFIG_PATH, DB_CONFIG_PATH,
|
||||||
agent::{AgentConfigInfoType, AgentContainerInfo, SOCK_NAME},
|
containers::{
|
||||||
db::DBConfigInfoType,
|
AgentConfigInfoType, ConfigInfoType, DBConfigInfoType, WithContainer, WithoutContainer,
|
||||||
|
agent::SOCK_NAME,
|
||||||
|
},
|
||||||
env::{self, EnvFile},
|
env::{self, EnvFile},
|
||||||
types::{ConfigInfoType, WithContainer, WithoutContainer},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// relative to the current working directory
|
// relative to the current working directory
|
||||||
@@ -30,7 +31,10 @@ pub fn write_env_files(db_config: &DBConfigInfoType, agent_config: &Option<Agent
|
|||||||
DBConfigInfoType::PreExisting(config) => (config.db_type.clone(), config.url.clone()),
|
DBConfigInfoType::PreExisting(config) => (config.db_type.clone(), config.url.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut api_env = EnvFile::new(env::EnvFileType::Yaml, db_type, db_url);
|
let mut api_env = EnvFile::new(env::EnvFileType::Yaml);
|
||||||
|
api_env.write_line("DATABASE__TYPE", db_type.to_string().as_str());
|
||||||
|
api_env.write_line("DATABASE__URL", db_url.as_str());
|
||||||
|
|
||||||
let mut db_env = api_env.clone();
|
let mut db_env = api_env.clone();
|
||||||
db_env.file_type = env::EnvFileType::DotEnv;
|
db_env.file_type = env::EnvFileType::DotEnv;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user