mirror of
https://github.com/Portabase/agent.git
synced 2026-09-10 18:16:55 +00:00
Merge branch 'main' into dev
# Conflicts: # docker-compose.yml
This commit is contained in:
+1
-1
@@ -27,5 +27,5 @@ keywords:
|
||||
- self-hosted
|
||||
- portabase
|
||||
license: Apache-2.0
|
||||
version: 1.5.0
|
||||
version: 1.5.1
|
||||
date-released: '2026-02-24'
|
||||
|
||||
Generated
+1
-1
@@ -3038,7 +3038,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portabase-agent"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "portabase-agent"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -30,7 +30,7 @@ tokio-postgres = "0.7.15"
|
||||
futures = "0.3.31"
|
||||
tracing-appender = "0.2.4"
|
||||
time = { version = "0.3.44", features = ["macros"] }
|
||||
mongodb = "3.5.0"
|
||||
mongodb = "3.5.1"
|
||||
rand = "0.9.2"
|
||||
bytes = "1.11.0"
|
||||
async-stream = "0.3.6"
|
||||
@@ -49,7 +49,7 @@ tokio-stream = "0.1.18"
|
||||
aes = "0.9.0-rc.4"
|
||||
typenum = "1.19.0"
|
||||
testcontainers = "0.27.1"
|
||||
testcontainers-modules = { version = "0.15.0", features = ["postgres", "redis"] }
|
||||
testcontainers-modules = { version = "0.15.0", features = ["postgres", "redis", "valkey"] }
|
||||
postgres = "0.19.12"
|
||||
url = "2.5.8"
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.mysql.com/)
|
||||
[](https://sqlite.org/)
|
||||
[](https://redis.io/)
|
||||
[](https://valkey.io/)
|
||||
[](https://mariadb.org/)
|
||||
[](https://www.mongodb.com/)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
|
||||
+17
-1
@@ -60,11 +60,27 @@
|
||||
{
|
||||
"name": "Test database 9 - Redis Auth",
|
||||
"type": "redis",
|
||||
"password": "m6o2L0Ukw3Og5RJUKh",
|
||||
"password": "supersecurepassword",
|
||||
"port": 6379,
|
||||
"username": "default",
|
||||
"host": "db-redis-auth",
|
||||
"generated_id": "16678160-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 10 - Valkey",
|
||||
"type": "valkey",
|
||||
"port": 6379,
|
||||
"host": "db-valkey",
|
||||
"generated_id": "16678560-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 11 - Valkey Auth",
|
||||
"type": "valkey",
|
||||
"password": "supersecurepassword",
|
||||
"port": 6379,
|
||||
"username": "default",
|
||||
"host": "db-valkey-auth",
|
||||
"generated_id": "16678561-ff7e-4c97-8c83-0adeff214681"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+29
-4
@@ -18,7 +18,7 @@ services:
|
||||
APP_ENV: development
|
||||
LOG: debug
|
||||
TZ: "Europe/Paris"
|
||||
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiZWE5OWE5ZjMtNDhkYy00MjMyLTkzMTAtNTc5YWVkY2IyZjhmIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
|
||||
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiMmVkZWFlYjctNWYwNi00MjI1LWIzNWItOGYyM2MwOTMyMTA2IiwibWFzdGVyS2V5QjY0IjoiMUh0djdtWCtYVkJxL0IzUEV2WDlZZjlQeUdVZW5oRHlXemo5THRqNW90WT0ifQ=="
|
||||
#POOLING: 1
|
||||
#DATABASES_CONFIG_FILE: "config.toml"
|
||||
extra_hosts:
|
||||
@@ -122,8 +122,32 @@ services:
|
||||
volumes:
|
||||
- redis-data-auth:/data
|
||||
environment:
|
||||
- REDIS_PASSWORD=m6o2L0Ukw3Og5RJUKh
|
||||
command: [ "redis-server", "--requirepass", "m6o2L0Ukw3Og5RJUKh", "--appendonly", "yes" ]
|
||||
- REDIS_PASSWORD=supersecurepassword
|
||||
command: [ "redis-server", "--requirepass", "supersecurepassword", "--appendonly", "yes" ]
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
db-valkey:
|
||||
image: valkey/valkey
|
||||
container_name: db-valkey
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
ports:
|
||||
- '6381:6379'
|
||||
volumes:
|
||||
- valkey-data:/data
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
db-valkey-auth:
|
||||
image: valkey/valkey
|
||||
container_name: db-valkey-auth
|
||||
command: >
|
||||
--requirepass "supersecurepassword"
|
||||
ports:
|
||||
- '6382:6379'
|
||||
volumes:
|
||||
- valkey-data-auth:/data
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
@@ -131,7 +155,6 @@ volumes:
|
||||
cargo-registry:
|
||||
cargo-git:
|
||||
# cargo-target:
|
||||
|
||||
postgres-data:
|
||||
# mariadb-data:
|
||||
# mongodb-data:
|
||||
@@ -139,6 +162,8 @@ volumes:
|
||||
# sqlite-data:
|
||||
redis-data:
|
||||
redis-data-auth:
|
||||
valkey-data:
|
||||
valkey-data-auth:
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
|
||||
@@ -17,6 +17,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
mariadb-client \
|
||||
sqlite3 \
|
||||
redis-tools \
|
||||
valkey \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -96,6 +97,7 @@ RUN apt-get update && apt-get install -y \
|
||||
mariadb-client \
|
||||
sqlite3 \
|
||||
redis-tools \
|
||||
valkey \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
|
||||
@@ -35,5 +35,5 @@ check_docker
|
||||
check_network
|
||||
|
||||
echo "Starting docker-compose..."
|
||||
docker-compose -f ./docker-compose.yml up
|
||||
docker compose -f ./docker-compose.yml up
|
||||
echo "Docker-compose started successfully."
|
||||
+10
-3
@@ -34,16 +34,23 @@ if [ -n "$TZ" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
REDIS_PORT=65515
|
||||
echo "[entrypoint] APP_ENV=$APP_ENV"
|
||||
echo "[entrypoint] Starting Redis..."
|
||||
redis-server --daemonize yes
|
||||
redis-server --port $REDIS_PORT --daemonize yes
|
||||
|
||||
echo "[entrypoint] Waiting for Redis to be ready..."
|
||||
until redis-cli ping >/dev/null 2>&1; do
|
||||
MAX_RETRIES=20
|
||||
COUNT=0
|
||||
until redis-cli -h localhost -p "$REDIS_PORT" ping >/dev/null 2>&1 ; do
|
||||
COUNT=$((COUNT+1))
|
||||
if [ $COUNT -ge $MAX_RETRIES ]; then
|
||||
echo "[ERROR] Redis did not start after $MAX_RETRIES attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "[entrypoint] Redis not ready, sleeping 1s..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "[entrypoint] Redis is ready"
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ if [[ "$OS_TYPE" == "Linux" ]]; then
|
||||
if command -v apt >/dev/null 2>&1; then
|
||||
echo "Linux detected with apt. Installing prerequisites..."
|
||||
sudo apt update
|
||||
sudo apt install -y wget gnupg lsb-release redis-tools
|
||||
sudo apt install -y wget gnupg lsb-release redis-tools valkey
|
||||
install_pg_binaries
|
||||
else
|
||||
echo "Unsupported Linux distribution. Only apt-based distros are supported."
|
||||
@@ -69,6 +69,7 @@ elif [[ "$OS_TYPE" == "Darwin" ]]; then
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
echo "macOS detected. Installing prerequisites..."
|
||||
brew install redis
|
||||
brew install valkey
|
||||
|
||||
sudo mkdir -p "$POSTGRES_BASE"
|
||||
sudo chown -R "$(whoami)" "$POSTGRES_BASE"
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::services::config::{DatabaseConfig, DbType};
|
||||
use anyhow::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use crate::domain::valkey::database::ValkeyDatabase;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Database: Send + Sync {
|
||||
@@ -31,6 +32,7 @@ impl DatabaseFactory {
|
||||
DbType::MongoDB => Arc::new(MongoDatabase::new(cfg)),
|
||||
DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)),
|
||||
DbType::Redis => Arc::new(RedisDatabase::new(cfg)),
|
||||
DbType::Valkey => Arc::new(ValkeyDatabase::new(cfg))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +47,8 @@ impl DatabaseFactory {
|
||||
DbType::MongoDB => Arc::new(MongoDatabase::new(cfg)),
|
||||
DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)),
|
||||
DbType::Redis => Arc::new(RedisDatabase::new(cfg)),
|
||||
DbType::Valkey => Arc::new(ValkeyDatabase::new(cfg))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod mysql;
|
||||
mod mongodb;
|
||||
mod sqlite;
|
||||
mod redis;
|
||||
mod valkey;
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub async fn run(
|
||||
cfg: DatabaseConfig,
|
||||
backup_dir: PathBuf,
|
||||
file_extension: &'static str,
|
||||
) -> Result<PathBuf> {
|
||||
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
|
||||
debug!("Starting Valkey backup for database {}", cfg.name);
|
||||
|
||||
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
|
||||
|
||||
let mut cmd = Command::new("valkey-cli");
|
||||
|
||||
cmd.arg("-h")
|
||||
.arg(&cfg.host)
|
||||
.arg("-p")
|
||||
.arg(cfg.port.to_string());
|
||||
|
||||
if !cfg.username.is_empty() {
|
||||
cmd.arg("--user").arg(&cfg.username);
|
||||
}
|
||||
|
||||
if !cfg.password.is_empty() {
|
||||
cmd.arg("-a").arg(&cfg.password);
|
||||
}
|
||||
|
||||
cmd.arg("--rdb").arg(&file_path);
|
||||
|
||||
debug!("Command Backup: {:?}", cmd);
|
||||
|
||||
let output = cmd.output().context("Valkey backup command failed")?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !output.status.success() {
|
||||
if stderr.contains("NOAUTH") {
|
||||
error!(
|
||||
"Valkey backup failed for {}: Authentication required (NOAUTH)",
|
||||
cfg.name
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Valkey backup failed for {}: Authentication required",
|
||||
cfg.name
|
||||
);
|
||||
} else {
|
||||
error!("Valkey backup failed for {}: {}", cfg.name, stderr);
|
||||
anyhow::bail!("Valkey backup failed for {}: {}", cfg.name, stderr);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Valkey backup completed for {}. Output: {}",
|
||||
cfg.name, stdout
|
||||
);
|
||||
|
||||
Ok(file_path)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::domain::factory::Database;
|
||||
use crate::domain::valkey::{backup, ping};
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use crate::utils::locks::{DbOpLock, FileLock};
|
||||
|
||||
pub struct ValkeyDatabase {
|
||||
cfg: DatabaseConfig,
|
||||
}
|
||||
|
||||
impl ValkeyDatabase {
|
||||
pub fn new(cfg: DatabaseConfig) -> Self {
|
||||
Self { cfg }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Database for ValkeyDatabase {
|
||||
fn file_extension(&self) -> &'static str {
|
||||
".rdb"
|
||||
}
|
||||
|
||||
async fn ping(&self) -> Result<bool> {
|
||||
ping::run(self.cfg.clone()).await
|
||||
}
|
||||
|
||||
async fn backup(&self, dir: &Path, is_test: Option<bool>) -> Result<PathBuf> {
|
||||
let test_mode = is_test.unwrap_or(false);
|
||||
if !test_mode {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
}
|
||||
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
|
||||
if !test_mode {
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
async fn restore(&self, _file: &Path, _is_test: Option<bool>) -> Result<()> {
|
||||
bail!("Restore not supported for Valkey databases")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod backup;
|
||||
pub mod database;
|
||||
mod ping;
|
||||
@@ -0,0 +1,55 @@
|
||||
use tracing::{debug, info};
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
|
||||
let mut cmd = Command::new("valkey-cli");
|
||||
cmd.arg("-h")
|
||||
.arg(&cfg.host)
|
||||
.arg("-p")
|
||||
.arg(cfg.port.to_string());
|
||||
|
||||
if !cfg.username.is_empty() {
|
||||
cmd.arg("--user").arg(&cfg.username);
|
||||
}
|
||||
|
||||
if !cfg.password.is_empty() {
|
||||
cmd.arg("-a").arg(&cfg.password);
|
||||
}
|
||||
|
||||
cmd.arg("PING");
|
||||
|
||||
debug!("Command Ping: {:?}", cmd);
|
||||
|
||||
|
||||
let result = timeout(Duration::from_secs(10), cmd.output()).await;
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
let output = output.context("Failed to execute redis-cli")?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
info!("Redis stdout: {}", stdout);
|
||||
info!("Redis stderr: {}", stderr);
|
||||
|
||||
if stderr.contains("NOAUTH") {
|
||||
info!("Redis authentication failed (NOAUTH required)");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
info!("Redis command failed with status: {:?}", output.status);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(stdout.contains("PONG"))
|
||||
}
|
||||
Err(_) => {
|
||||
info!("Timeout connecting to Redis at {}:{}", cfg.host, cfg.port);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ pub enum DbType {
|
||||
Postgresql,
|
||||
MongoDB,
|
||||
Sqlite,
|
||||
Redis
|
||||
// Add other DB types if needed
|
||||
Redis,
|
||||
Valkey
|
||||
}
|
||||
|
||||
impl DbType {
|
||||
@@ -32,6 +32,7 @@ impl DbType {
|
||||
DbType::MongoDB => "mongodb",
|
||||
DbType::Sqlite => "sqlite",
|
||||
DbType::Redis => "redis",
|
||||
DbType::Valkey => "valkey",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,17 +165,17 @@ impl ConfigService {
|
||||
};
|
||||
|
||||
let host = match db.db_type {
|
||||
DbType::Postgresql | DbType::Mysql | DbType::Mariadb | DbType::MongoDB | DbType::Redis => required(&db.host, &db.name, "host")?,
|
||||
DbType::Postgresql | DbType::Mysql | DbType::Mariadb | DbType::MongoDB | DbType::Redis | DbType::Valkey => required(&db.host, &db.name, "host")?,
|
||||
DbType::Sqlite => optional(&db.host),
|
||||
};
|
||||
|
||||
let port = match db.db_type {
|
||||
DbType::Postgresql | DbType::Mysql | DbType::Mariadb | DbType::MongoDB | DbType::Redis => required(&db.port, &db.name, "port")?,
|
||||
DbType::Postgresql | DbType::Mysql | DbType::Mariadb | DbType::MongoDB | DbType::Redis | DbType::Valkey => required(&db.port, &db.name, "port")?,
|
||||
DbType::Sqlite => db.port.unwrap_or(0),
|
||||
};
|
||||
|
||||
let database_name = match db.db_type {
|
||||
DbType::Sqlite | DbType::Redis => optional(&db.database),
|
||||
DbType::Sqlite | DbType::Redis | DbType::Valkey => optional(&db.database),
|
||||
_ => required(&db.database, &db.name, "database")?
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl Settings {
|
||||
app_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
app_env: env::var("APP_ENV").unwrap_or_else(|_| "development".into()),
|
||||
redis_url: env::var("CELERY_BROKER_URL")
|
||||
.unwrap_or_else(|_| "redis://localhost:6379/".into()),
|
||||
.unwrap_or_else(|_| "redis://localhost:65515/".into()),
|
||||
edge_key: env::var("EDGE_KEY").unwrap_or_default(),
|
||||
databases_config_file: env::var("DATABASES_CONFIG_FILE")
|
||||
.unwrap_or_else(|_| "config.json".into()),
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
mod postgres;
|
||||
mod redis;
|
||||
mod redis;
|
||||
mod valkey;
|
||||
@@ -0,0 +1,64 @@
|
||||
use tempfile::TempDir;
|
||||
use testcontainers::runners::AsyncRunner;
|
||||
use testcontainers::ContainerAsync;
|
||||
use testcontainers_modules::valkey::{Valkey};
|
||||
use url::Host;
|
||||
use crate::domain::factory::DatabaseFactory;
|
||||
use crate::services::config::{DatabaseConfig, DbType};
|
||||
use crate::tests::init_tracing_for_test;
|
||||
|
||||
async fn create_config() -> (ContainerAsync<Valkey>, DatabaseConfig) {
|
||||
let container = Valkey::default().start().await.unwrap();
|
||||
|
||||
let host = container
|
||||
.get_host()
|
||||
.await
|
||||
.unwrap_or(Host::parse("127.0.0.1").unwrap());
|
||||
|
||||
let port = container
|
||||
.get_host_port_ipv4(6379)
|
||||
.await
|
||||
.unwrap_or(6379);
|
||||
|
||||
let config = DatabaseConfig {
|
||||
name: "Test Valkey".to_string(),
|
||||
database: "valkey".to_string(),
|
||||
username: "".to_string(),
|
||||
password: "".to_string(),
|
||||
db_type: DbType::Valkey,
|
||||
port,
|
||||
host: host.to_string(),
|
||||
generated_id: "40875485-e3d2-4dfe-a26b-2a347ecc64fd".to_string(),
|
||||
path: "".to_string(),
|
||||
};
|
||||
|
||||
(container, config)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valkey_ping_test() {
|
||||
init_tracing_for_test();
|
||||
|
||||
let (_container, config) = create_config().await;
|
||||
|
||||
let db = DatabaseFactory::create_for_backup(config.clone()).await;
|
||||
let reachable = db.ping().await.unwrap_or(false);
|
||||
|
||||
assert!(reachable);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valkey_backup_test() {
|
||||
init_tracing_for_test();
|
||||
|
||||
let (_container, config) = create_config().await;
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let backup_path = temp_dir.path();
|
||||
|
||||
let db = DatabaseFactory::create_for_backup(config.clone()).await;
|
||||
|
||||
let file_path = db.backup(backup_path, Some(true)).await.unwrap();
|
||||
|
||||
assert!(file_path.is_file());
|
||||
}
|
||||
Reference in New Issue
Block a user