Discover applications on the cluster and the host for backups

Kubernetes workloads are grouped by their Helm instance label into
applications, each offering what is worth backing up: data volumes, a
PostgreSQL dump instead of the database's own volume, and optionally the
namespace manifests. Caches are listed but not preselected. Host
applications come from running systemd services that declare a state or
working directory. Selecting components creates one strategy each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-03 20:50:11 +02:00
parent 4efff51aa7
commit 51364fdd76
16 changed files with 1050 additions and 17 deletions

View File

@ -1,9 +1,10 @@
//! /api/backups: targets, strategies, records, manual runs.
use application::backup_service::StrategyStatus;
use application::backup_service::{ApplicationBackupPlan, StrategyStatus};
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use domain::application::Application;
use domain::backup::{BackupRecord, BackupSource, BackupStrategy, BackupTarget, StorageKind};
use domain::jobs::JobKind;
use serde::{Deserialize, Serialize};
@ -31,6 +32,55 @@ pub fn router() -> Router<AppState> {
)
.route("/strategies/{id}/run", post(run_strategy))
.route("/strategies/{id}/records", get(records))
.route(
"/applications",
get(list_applications).post(create_from_application),
)
}
// ---- applications ----
#[derive(Deserialize, ToSchema)]
pub struct ApplicationBackupRequest {
pub application_id: String,
pub component_ids: Vec<String>,
pub schedule: String,
pub target_id: Uuid,
pub retention: u32,
#[serde(default)]
pub passphrase: Option<String>,
}
#[utoipa::path(get, path = "/api/backups/applications", tag = "backups", security(("bearer" = [])),
responses((status = 200, body = Vec<Object>)))]
async fn list_applications(
State(s): State<AppState>,
_: AuthUser,
) -> Result<Json<Vec<Application>>, ApiError> {
Ok(Json(s.backups.applications().await?))
}
#[utoipa::path(post, path = "/api/backups/applications", tag = "backups", security(("bearer" = [])),
request_body = ApplicationBackupRequest, responses((status = 201, body = Vec<StrategyView>), (status = 404), (status = 422)))]
async fn create_from_application(
State(s): State<AppState>,
_: AdminUser,
Json(req): Json<ApplicationBackupRequest>,
) -> Result<(StatusCode, Json<Vec<StrategyView>>), ApiError> {
let plan = ApplicationBackupPlan {
schedule: req.schedule,
target_id: req.target_id,
retention: req.retention,
passphrase: req.passphrase.filter(|p| !p.is_empty()),
};
let created = s
.backups
.create_from_application(&req.application_id, &req.component_ids, plan)
.await?;
Ok((
StatusCode::CREATED,
Json(created.into_iter().map(Into::into).collect()),
))
}
// ---- targets ----

View File

@ -130,6 +130,7 @@ impl AppState {
} = adapters;
let pool_for_findings = pool.clone();
let cluster_gateway = cluster.clone();
let cluster_gateway_for_backups = cluster.clone();
let users = Arc::new(SqliteUsers(pool.clone()));
let hasher = Arc::new(Argon2Hasher);
let auth = AuthService::new(
@ -141,6 +142,8 @@ impl AppState {
);
let cipher = Arc::new(AesGcmCipher::from_hex(&cfg.master_key)?);
let backups = Arc::new(BackupService::new(BackupDeps {
cluster: cluster_gateway_for_backups,
host: inspector.clone(),
targets: Arc::new(SqliteBackupTargets(pool.clone())),
strategies: Arc::new(SqliteBackupStrategies(pool.clone())),
records: Arc::new(SqliteBackupRecords(pool.clone())),

View File

@ -34,6 +34,7 @@ impl Modify for BearerAuth {
crate::backups::delete_target, crate::backups::test_target, crate::backups::list_strategies, crate::backups::get_strategy,
crate::backups::create_strategy, crate::backups::update_strategy, crate::backups::delete_strategy,
crate::backups::run_strategy, crate::backups::records,
crate::backups::list_applications, crate::backups::create_from_application,
crate::dashboard::dashboard,
),
modifiers(&BearerAuth)