feat(migrate): per-tenant onboarding backfill + revert + CLI (#132 part 2)
CI / Check (pull_request) Successful in 8m15s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped

Wire the id-preserving mappers into a runnable, idempotent, reversible migration:
- backfill_onboarded_targets(db, dry_run): repositories -> onboarded_targets;
  dast_targets fold into the linked repo target (append LiveUrl, promote type,
  relink dast_scan_runs/pentest_sessions history) or migrate standalone. Skips
  existing (by _id), writes a schema_migrations marker; dry-run computes the
  report without writing. Legacy collections are never deleted.
- revert(db): replay the relink log, drop onboarded_targets + the log, clear the
  marker — restores the pre-migration state.
- CLI: `compliance-agent migrate onboarding [--all | --tenant <id>] [--dry-run]
  [--revert]`, with DatabasePool::list_tenant_ids for --all.
- database.rs: collection_named<T> accessor + list_tenant_ids helper.

Integration test (real Mongo, local-only — CI is --lib) covers fold + relink +
idempotency + revert end to end. 5 mapper unit tests run in CI.

Closes #132.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-07-10 19:49:52 +02:00
co-authored by Claude Fable 5
parent fa3a6b71fb
commit 0d83859bcf
5 changed files with 407 additions and 1 deletions
+54 -1
View File
@@ -1,4 +1,50 @@
use compliance_agent::{agent, api, config, database, scheduler, ssh, webhooks};
use compliance_agent::{agent, api, config, database, migrate, scheduler, ssh, webhooks};
/// Run the `migrate onboarding` subcommand and exit. Backfills (or reverts) the
/// unified `onboarded_targets` collection per tenant.
///
/// Usage: `compliance-agent migrate onboarding [--all | --tenant <id>] [--dry-run] [--revert]`
async fn run_migration(
args: &[String],
pool: &database::DatabasePool,
) -> Result<(), compliance_agent::error::AgentError> {
if args.get(2).map(String::as_str) != Some("onboarding") {
eprintln!(
"usage: compliance-agent migrate onboarding [--all | --tenant <id>] [--dry-run] [--revert]"
);
std::process::exit(2);
}
let has = |flag: &str| args.iter().any(|a| a == flag);
let dry_run = has("--dry-run");
let revert = has("--revert");
let tenant = args
.iter()
.position(|a| a == "--tenant")
.and_then(|i| args.get(i + 1))
.cloned();
let tenants: Vec<String> = if has("--all") {
pool.list_tenant_ids().await?
} else if let Some(t) = tenant {
vec![t]
} else {
eprintln!("specify --all or --tenant <id>");
std::process::exit(2);
};
for tenant_id in tenants {
let db = pool.for_tenant_id(&tenant_id).await?;
if revert {
migrate::onboarding::revert(&db).await?;
println!("[{tenant_id}] reverted onboarding backfill");
} else {
let report = migrate::onboarding::backfill_onboarded_targets(&db, dry_run).await?;
let prefix = if dry_run { "(dry-run) " } else { "" };
println!("[{tenant_id}] {prefix}{report:?}");
}
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -31,6 +77,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_pool =
database::DatabasePool::connect(&config.mongodb_uri, &config.mongodb_database).await?;
// One-shot subcommands run and exit without starting the servers.
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("migrate") {
run_migration(&args, &db_pool).await?;
return Ok(());
}
let agent = agent::ComplianceAgent::new(config.clone(), db_pool);
tracing::info!("Starting scheduler...");