feat(apps): add saleor and harden netbird repair

This commit is contained in:
archipelago
2026-05-19 20:11:22 -04:00
parent 56f956973e
commit 522c046525
14 changed files with 631 additions and 21 deletions
@@ -31,6 +31,7 @@ fn is_platform_managed_app(app_id: &str) -> bool {
| "fedimint"
| "fedimint-gateway"
| "indeedhub"
| "saleor"
| "immich"
)
}
@@ -500,6 +501,15 @@ pub(super) fn all_container_names(package_id: &str) -> Vec<String> {
"netbird-dashboard".into(),
"netbird-server".into(),
],
"saleor" => vec![
"saleor-db".into(),
"saleor-cache".into(),
"saleor-api".into(),
"saleor-worker".into(),
"saleor-jaeger".into(),
"saleor-mailpit".into(),
"saleor".into(),
],
"nostr-vpn" => vec![
"nostr-vpn".into(),
"archy-nostr-vpn".into(),
@@ -589,6 +599,7 @@ pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
format!("{}/penpot-postgres", base),
],
"netbird" => vec![format!("{}/netbird", base)],
"saleor" => vec![format!("{}/saleor", base), format!("{}/saleor-db", base)],
_ => vec![format!("{}/{}", base, package_id)],
}
}
@@ -1068,6 +1079,13 @@ pub(super) async fn get_app_config(
None,
None,
),
"saleor" => (
vec!["9000:80".to_string(), "8000:8000".to_string()],
vec!["/var/lib/archipelago/saleor:/app/media".to_string()],
vec![],
None,
None,
),
"nostr-rs-relay" => (
vec!["18081:8080".to_string()],
vec!["/var/lib/archipelago/nostr-rs-relay:/usr/src/app/db".to_string()],
@@ -289,6 +289,15 @@ pub(super) fn startup_order(package_id: &str) -> &'static [&'static str] {
&["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"]
}
"netbird" => &["netbird-server", "netbird-dashboard", "netbird"],
"saleor" => &[
"saleor-db",
"saleor-cache",
"saleor-jaeger",
"saleor-mailpit",
"saleor-api",
"saleor-worker",
"saleor",
],
"penpot" | "penpot-frontend" => &[
"penpot-postgres",
"penpot-valkey",
@@ -244,6 +244,9 @@ impl RpcHandler {
if package_id == "netbird" {
return self.install_netbird_stack().await;
}
if package_id == "saleor" {
return self.install_saleor_stack().await;
}
// Dependency checks. Prefer the scanner's cached package state so a
// congested Podman API does not turn an already-running dependency into
+493 -17
View File
@@ -99,6 +99,7 @@ async fn repair_stack_before_adopt(stack_name: &str) {
}
"indeedhub" => repair_indeedhub_network_aliases().await,
"netbird" => repair_netbird_unified_origin().await,
"saleor" => repair_saleor_network_aliases().await,
_ => {}
}
}
@@ -151,23 +152,9 @@ async fn repair_netbird_unified_origin() {
.unwrap_or_else(|| "127.0.0.1".to_string());
let _ = write_netbird_config_files(&host_ip).await;
let names = tokio::process::Command::new("podman")
.args(["ps", "-a", "--format", "{{.Names}}"])
.output()
.await
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default();
let has_proxy = names.lines().any(|n| n.trim() == "netbird");
let has_dashboard = names.lines().any(|n| n.trim() == "netbird-dashboard");
if has_proxy && has_dashboard {
return;
}
if has_proxy && !has_dashboard {
for container in ["netbird", "netbird-dashboard"] {
let _ = tokio::process::Command::new("podman")
.args(["rm", "-f", "netbird"])
.args(["rm", "-f", container])
.output()
.await;
}
@@ -177,6 +164,9 @@ async fn repair_netbird_unified_origin() {
.output()
.await;
let _ = pull_image_with_retry(NETBIRD_DASHBOARD_IMAGE).await;
let _ = pull_image_with_retry(NETBIRD_PROXY_IMAGE).await;
let _ = tokio::process::Command::new("podman")
.args([
"run",
@@ -210,6 +200,75 @@ async fn repair_netbird_unified_origin() {
])
.output()
.await;
let _ = tokio::process::Command::new("podman")
.args([
"network",
"disconnect",
"-f",
"netbird-net",
"netbird-server",
])
.output()
.await;
let _ = tokio::process::Command::new("podman")
.args([
"network",
"connect",
"--alias",
"netbird-server",
"netbird-net",
"netbird-server",
])
.output()
.await;
let _ = tokio::process::Command::new("podman")
.args(["restart", "netbird-server"])
.output()
.await;
}
async fn repair_saleor_network_aliases() {
let _ = tokio::process::Command::new("podman")
.args(["network", "create", "saleor-net"])
.output()
.await;
for (container, alias) in [
("saleor-db", "db"),
("saleor-cache", "cache"),
("saleor-jaeger", "jaeger"),
("saleor-mailpit", "mailpit"),
("saleor-api", "api"),
("saleor-worker", "worker"),
("saleor", "saleor"),
] {
let exists = tokio::process::Command::new("podman")
.args(["container", "exists", container])
.status()
.await
.map(|s| s.success())
.unwrap_or(false);
if !exists {
continue;
}
let _ = tokio::process::Command::new("podman")
.args(["network", "disconnect", "-f", "saleor-net", container])
.output()
.await;
let _ = tokio::process::Command::new("podman")
.args([
"network",
"connect",
"--alias",
alias,
"saleor-net",
container,
])
.output()
.await;
}
}
async fn run_required_stack_command(
@@ -382,6 +441,12 @@ const REGISTRY: &str = "146.59.87.168:3000/lfg2025";
const NETBIRD_DASHBOARD_IMAGE: &str = "docker.io/netbirdio/dashboard:v2.38.0";
const NETBIRD_SERVER_IMAGE: &str = "docker.io/netbirdio/netbird-server:0.71.2";
const NETBIRD_PROXY_IMAGE: &str = "docker.io/library/nginx:1.27-alpine";
const SALEOR_API_IMAGE: &str = "ghcr.io/saleor/saleor:3.23";
const SALEOR_DASHBOARD_IMAGE: &str = "ghcr.io/saleor/saleor-dashboard:3.23";
const SALEOR_POSTGRES_IMAGE: &str = "docker.io/library/postgres:15-alpine";
const SALEOR_VALKEY_IMAGE: &str = "docker.io/valkey/valkey:8.1-alpine";
const SALEOR_JAEGER_IMAGE: &str = "docker.io/jaegertracing/jaeger:latest";
const SALEOR_MAILPIT_IMAGE: &str = "docker.io/axllent/mailpit:latest";
/// Pull an image with retry and exponential backoff (3 attempts).
async fn pull_image_with_retry(image: &str) -> Result<()> {
@@ -1574,6 +1639,415 @@ impl RpcHandler {
"message": "NetBird self-hosted stack installed",
}))
}
/// Install Saleor stack (PostgreSQL + Valkey + API + worker + dashboard + Jaeger + Mailpit).
pub(super) async fn install_saleor_stack(&self) -> Result<serde_json::Value> {
if let Some(adopted) = adopt_stack_if_exists(
"saleor",
"saleor",
&[
"saleor-db",
"saleor-cache",
"saleor-jaeger",
"saleor-mailpit",
"saleor-api",
"saleor-worker",
"saleor",
],
)
.await?
{
return Ok(adopted);
}
install_log("INSTALL START: saleor stack (postgres + valkey + api + worker + dashboard)")
.await;
info!("Installing Saleor stack");
let images = [
SALEOR_POSTGRES_IMAGE,
SALEOR_VALKEY_IMAGE,
SALEOR_API_IMAGE,
SALEOR_DASHBOARD_IMAGE,
SALEOR_JAEGER_IMAGE,
SALEOR_MAILPIT_IMAGE,
];
self.set_install_phase("saleor", InstallPhase::PullingImage)
.await;
let n_images = images.len() as u64;
for (i, image) in images.iter().enumerate() {
self.set_install_progress("saleor", i as u64, n_images)
.await;
pull_image_with_retry(image)
.await
.with_context(|| format!("Failed to pull Saleor image: {}", image))?;
}
self.set_install_progress("saleor", n_images, n_images)
.await;
for name in [
"saleor",
"saleor-api",
"saleor-worker",
"saleor-db",
"saleor-cache",
"saleor-jaeger",
"saleor-mailpit",
] {
let _ = tokio::process::Command::new("podman")
.args(["rm", "-f", name])
.status()
.await;
}
let _ = tokio::process::Command::new("podman")
.args(["network", "rm", "-f", "saleor-net"])
.status()
.await;
self.set_install_phase("saleor", InstallPhase::CreatingContainer)
.await;
let _ = tokio::process::Command::new("sudo")
.args([
"mkdir",
"-p",
"/var/lib/archipelago/saleor",
"/var/lib/archipelago/saleor-db",
"/var/lib/archipelago/saleor-cache",
])
.output()
.await;
let user = std::env::var("USER").unwrap_or_else(|_| "archipelago".to_string());
for dir in [
"/var/lib/archipelago/saleor",
"/var/lib/archipelago/saleor-db",
"/var/lib/archipelago/saleor-cache",
] {
let _ = tokio::process::Command::new("sudo")
.args(["chown", "-R", &format!("{}:{}", user, user), dir])
.output()
.await;
}
let _ = tokio::process::Command::new("podman")
.args(["network", "create", "saleor-net"])
.status()
.await;
let db_pass = super::config::read_or_generate_secret("saleor-db-password").await;
let secret_key = super::config::read_or_generate_secret("saleor-secret-key").await;
let host_ip = &self.config.host_ip;
let dashboard_url = format!("http://{}:9000/", host_ip);
let api_url = format!("http://{}:8000/graphql/", host_ip);
let allowed_hosts = format!("localhost,127.0.0.1,api,saleor-api,{}", host_ip);
let database_url = format!("postgres://saleor:{}@db/saleor", db_pass);
let mut db_cmd = tokio::process::Command::new("podman");
db_cmd.args([
"run",
"-d",
"--name",
"saleor-db",
"--network",
"saleor-net",
"--network-alias",
"db",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--cap-add=CHOWN",
"--cap-add=DAC_OVERRIDE",
"--cap-add=FOWNER",
"--cap-add=SETGID",
"--cap-add=SETUID",
"--security-opt=no-new-privileges:true",
"--memory=512m",
"--pids-limit=4096",
"--health-cmd=pg_isready -U saleor || exit 1",
"--health-interval=30s",
"--health-retries=3",
"-v",
"/var/lib/archipelago/saleor-db:/var/lib/postgresql/data",
"-e",
"POSTGRES_USER=saleor",
"-e",
&format!("POSTGRES_PASSWORD={}", db_pass),
"-e",
"POSTGRES_DB=saleor",
SALEOR_POSTGRES_IMAGE,
]);
run_required_stack_command("saleor", "create postgres", &mut db_cmd).await?;
let mut cache_cmd = tokio::process::Command::new("podman");
cache_cmd.args([
"run",
"-d",
"--name",
"saleor-cache",
"--network",
"saleor-net",
"--network-alias",
"cache",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--cap-add=SETGID",
"--cap-add=SETUID",
"--security-opt=no-new-privileges:true",
"--memory=128m",
"--pids-limit=2048",
"--health-cmd=valkey-cli ping || exit 1",
"--health-interval=30s",
"--health-retries=3",
"-v",
"/var/lib/archipelago/saleor-cache:/data",
SALEOR_VALKEY_IMAGE,
]);
run_required_stack_command("saleor", "create cache", &mut cache_cmd).await?;
let mut jaeger_cmd = tokio::process::Command::new("podman");
jaeger_cmd.args([
"run",
"-d",
"--name",
"saleor-jaeger",
"--network",
"saleor-net",
"--network-alias",
"jaeger",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=512m",
"--pids-limit=4096",
"-p",
"16686:16686",
"-p",
"4317:4317",
"-p",
"4318:4318",
"--tmpfs",
"/tmp:rw,nosuid,nodev,size=128m",
SALEOR_JAEGER_IMAGE,
]);
run_required_stack_command("saleor", "create jaeger", &mut jaeger_cmd).await?;
let mut mailpit_cmd = tokio::process::Command::new("podman");
mailpit_cmd.args([
"run",
"-d",
"--name",
"saleor-mailpit",
"--network",
"saleor-net",
"--network-alias",
"mailpit",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=128m",
"--pids-limit=2048",
"-p",
"1025:1025",
"-p",
"8025:8025",
SALEOR_MAILPIT_IMAGE,
]);
run_required_stack_command("saleor", "create mailpit", &mut mailpit_cmd).await?;
tokio::time::sleep(std::time::Duration::from_secs(8)).await;
let saleor_env = vec![
"-e".to_string(),
"CACHE_URL=redis://cache:6379/0".to_string(),
"-e".to_string(),
"CELERY_BROKER_URL=redis://cache:6379/1".to_string(),
"-e".to_string(),
format!("DATABASE_URL={}", database_url),
"-e".to_string(),
"DEFAULT_CHANNEL_SLUG=default-channel".to_string(),
"-e".to_string(),
"DEFAULT_FROM_EMAIL=noreply@example.com".to_string(),
"-e".to_string(),
"EMAIL_URL=smtp://mailpit:1025".to_string(),
"-e".to_string(),
format!("SECRET_KEY={}", secret_key),
"-e".to_string(),
"OTEL_SERVICE_NAME=saleor".to_string(),
"-e".to_string(),
"OTEL_TRACES_EXPORTER=otlp".to_string(),
"-e".to_string(),
"OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317".to_string(),
"-e".to_string(),
"HTTP_IP_FILTER_ALLOW_LOOPBACK_IPS=True".to_string(),
"-e".to_string(),
"HTTP_IP_FILTER_ENABLED=False".to_string(),
"-e".to_string(),
format!("DASHBOARD_URL={}", dashboard_url),
"-e".to_string(),
format!("ALLOWED_HOSTS={}", allowed_hosts),
];
let mut migrate_cmd = tokio::process::Command::new("podman");
migrate_cmd.args([
"run",
"--rm",
"--network",
"saleor-net",
"-v",
"/var/lib/archipelago/saleor:/app/media",
]);
migrate_cmd.args(&saleor_env);
migrate_cmd.args([SALEOR_API_IMAGE, "python3", "manage.py", "migrate"]);
run_required_stack_command("saleor", "run migrations", &mut migrate_cmd).await?;
let mut populate_cmd = tokio::process::Command::new("podman");
populate_cmd.args([
"run",
"--rm",
"--network",
"saleor-net",
"-v",
"/var/lib/archipelago/saleor:/app/media",
]);
populate_cmd.args(&saleor_env);
populate_cmd.args([
SALEOR_API_IMAGE,
"python3",
"manage.py",
"populatedb",
"--createsuperuser",
]);
let populate = populate_cmd.output().await;
if let Ok(output) = populate {
if !output.status.success() {
install_log(&format!(
"INSTALL WARN: saleor - populate sample data skipped: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
.await;
}
}
let mut api_cmd = tokio::process::Command::new("podman");
api_cmd.args([
"run",
"-d",
"--name",
"saleor-api",
"--network",
"saleor-net",
"--network-alias",
"api",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--cap-add=CHOWN",
"--cap-add=DAC_OVERRIDE",
"--cap-add=FOWNER",
"--cap-add=SETGID",
"--cap-add=SETUID",
"--security-opt=no-new-privileges:true",
"--memory=1g",
"--pids-limit=4096",
"-p",
"8000:8000",
"-v",
"/var/lib/archipelago/saleor:/app/media",
]);
api_cmd.args(&saleor_env);
api_cmd.arg(SALEOR_API_IMAGE);
run_required_stack_command("saleor", "create api", &mut api_cmd).await?;
let mut worker_cmd = tokio::process::Command::new("podman");
worker_cmd.args([
"run",
"-d",
"--name",
"saleor-worker",
"--network",
"saleor-net",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--cap-add=CHOWN",
"--cap-add=DAC_OVERRIDE",
"--cap-add=FOWNER",
"--cap-add=SETGID",
"--cap-add=SETUID",
"--security-opt=no-new-privileges:true",
"--memory=1g",
"--pids-limit=4096",
"-v",
"/var/lib/archipelago/saleor:/app/media",
]);
worker_cmd.args(&saleor_env);
worker_cmd.args([
SALEOR_API_IMAGE,
"celery",
"-A",
"saleor",
"--app=saleor.celeryconf:app",
"worker",
"--loglevel=info",
"-B",
]);
run_required_stack_command("saleor", "create worker", &mut worker_cmd).await?;
self.set_install_phase("saleor", InstallPhase::StartingContainer)
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let mut dashboard_cmd = tokio::process::Command::new("podman");
dashboard_cmd.args([
"run",
"-d",
"--name",
"saleor",
"--network",
"saleor-net",
"--restart=unless-stopped",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=256m",
"--pids-limit=2048",
"-p",
"9000:80",
"-e",
&format!("API_URL={}", api_url),
"-e",
&format!("APP_MOUNT_URI={}", dashboard_url),
SALEOR_DASHBOARD_IMAGE,
]);
run_required_stack_command("saleor", "create dashboard", &mut dashboard_cmd).await?;
wait_for_stack_containers(
"saleor",
&[
"saleor-db",
"saleor-cache",
"saleor-jaeger",
"saleor-mailpit",
"saleor-api",
"saleor-worker",
"saleor",
],
120,
)
.await?;
self.set_install_phase("saleor", InstallPhase::WaitingHealthy)
.await;
self.set_install_phase("saleor", InstallPhase::PostInstall)
.await;
self.set_install_phase("saleor", InstallPhase::Done).await;
self.clear_install_progress("saleor").await;
install_log("INSTALL OK: saleor stack").await;
info!("Saleor stack installed");
Ok(serde_json::json!({
"success": true,
"package_id": "saleor",
"message": "Saleor stack installed (7 containers)",
}))
}
}
async fn read_or_generate_b64_secret(name: &str) -> String {
@@ -1616,6 +2090,8 @@ async fn write_netbird_config_files(host_ip: &str) -> Result<()> {
dashboardRedirectURIs:
- "{public_origin}/nb-auth"
- "{public_origin}/nb-silent-auth"
dashboardPostLogoutRedirectURIs:
- "{public_origin}/"
cliRedirectURIs:
- "http://localhost:53000/"
store:
@@ -1665,7 +2141,7 @@ LETSENCRYPT_DOMAIN=none
proxy_read_timeout 1d;
}}
location ~ ^/(api|oauth2)/ {{
location ~ ^/(api|oauth2)(/|$) {{
proxy_pass http://netbird-server:80;
}}
@@ -63,6 +63,12 @@ impl DockerPackageScanner {
"indeedhub-build_ffmpeg-worker_1",
"netbird-server",
"netbird-dashboard",
"saleor-api",
"saleor-worker",
"saleor-db",
"saleor-cache",
"saleor-jaeger",
"saleor-mailpit",
"buildx_buildkit_default",
];
@@ -283,6 +289,7 @@ fn get_app_tier(app_id: &str) -> &'static str {
"uptime-kuma" => "recommended",
"grafana" => "recommended",
"searxng" => "recommended",
"saleor" => "recommended",
"tailscale" | "netbird" => "recommended",
"portainer" => "recommended",
// Optional: everything else
@@ -488,6 +495,13 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
repo: "https://github.com/netbirdio/netbird".to_string(),
tier: "",
},
"saleor" => AppMetadata {
title: "Saleor".to_string(),
description: "Composable commerce platform with GraphQL API and dashboard".to_string(),
icon: "/assets/img/app-icons/saleor.svg".to_string(),
repo: "https://github.com/saleor/saleor".to_string(),
tier: "",
},
"gitea" => AppMetadata {
title: "Gitea".to_string(),
description: "Self-hosted Git service with repository and package hosting".to_string(),
@@ -716,6 +730,7 @@ fn requires_reachable_launch(app_id: &str) -> bool {
| "tailscale"
| "immich"
| "searxng"
| "saleor"
)
}
@@ -171,6 +171,12 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> {
"netbird" => Some("NETBIRD_DASHBOARD_IMAGE"),
"netbird-dashboard" => Some("NETBIRD_DASHBOARD_IMAGE"),
"netbird-server" => Some("NETBIRD_SERVER_IMAGE"),
"saleor" => Some("SALEOR_DASHBOARD_IMAGE"),
"saleor-api" | "saleor-worker" => Some("SALEOR_API_IMAGE"),
"saleor-db" => Some("SALEOR_POSTGRES_IMAGE"),
"saleor-cache" => Some("SALEOR_VALKEY_IMAGE"),
"saleor-jaeger" => Some("SALEOR_JAEGER_IMAGE"),
"saleor-mailpit" => Some("SALEOR_MAILPIT_IMAGE"),
// Fedimint
"fedimint" | "fedimintd" => Some("FEDIMINT_IMAGE"),
@@ -307,6 +313,15 @@ pub fn containers_for_stack(app_id: &str) -> Vec<(&'static str, &'static str)> {
("netbird-dashboard", "NETBIRD_DASHBOARD_IMAGE"),
("netbird-server", "NETBIRD_SERVER_IMAGE"),
],
"saleor" => vec![
("saleor-db", "SALEOR_POSTGRES_IMAGE"),
("saleor-cache", "SALEOR_VALKEY_IMAGE"),
("saleor-api", "SALEOR_API_IMAGE"),
("saleor-worker", "SALEOR_API_IMAGE"),
("saleor-jaeger", "SALEOR_JAEGER_IMAGE"),
("saleor-mailpit", "SALEOR_MAILPIT_IMAGE"),
("saleor", "SALEOR_DASHBOARD_IMAGE"),
],
_ => vec![],
}
}