use std::collections::HashMap;
use anyhow::{Context, Result, bail};
use indexmap::IndexMap;
use serde::Serialize;
use tracing::{info, warn};
use crate::clever::{Clever, ListedAddon, ListedApp};
use crate::cli::ReadArgs;
use crate::model::{Addon, App, Project, Source};
pub fn run(args: ReadArgs) -> Result<()> {
if !args.all && args.apps.is_empty() && args.addons.is_empty() {
bail!("nothing to read — pass --app/--addon explicitly, or --all");
}
let clever = Clever::new()?;
let all_apps = clever
.list_apps(&args.org)
.with_context(|| format!("listing applications in org `{}`", args.org))?;
let all_addons = clever
.list_addons(&args.org)
.with_context(|| format!("listing addons in org `{}`", args.org))?;
let (selected_apps, selected_addons) = if args.all {
(all_apps.clone(), all_addons.clone())
} else {
let apps: Vec<ListedApp> = args
.apps
.iter()
.map(|needle| {
all_apps
.iter()
.find(|a| &a.name == needle || &a.app_id == needle)
.cloned()
.ok_or_else(|| {
anyhow::anyhow!("app `{needle}` not found in org `{}`", args.org)
})
})
.collect::<Result<_>>()?;
let addons: Vec<ListedAddon> = args
.addons
.iter()
.map(|needle| {
all_addons
.iter()
.find(|a| &a.name == needle || &a.addon_id == needle)
.cloned()
.ok_or_else(|| {
anyhow::anyhow!("addon `{needle}` not found in org `{}`", args.org)
})
})
.collect::<Result<_>>()?;
(apps, addons)
};
let default_region = pick_default_region(&selected_apps, &selected_addons);
let app_name_by_id: HashMap<String, String> = all_apps
.iter()
.map(|a| (a.app_id.clone(), a.name.clone()))
.collect();
let addon_name_by_id: HashMap<String, String> = all_addons
.iter()
.map(|a| (a.addon_id.clone(), a.name.clone()))
.collect();
let mut apps: IndexMap<String, App> = IndexMap::new();
for listed in &selected_apps {
info!("reading app `{}` ({})", listed.name, listed.app_id);
let env_vars = clever
.get_env(&listed.app_id)
.with_context(|| format!("reading env of app `{}`", listed.name))?;
let domains = clever
.get_domains(&listed.app_id)
.with_context(|| format!("reading domains of app `{}`", listed.name))?;
let services = clever
.get_services(&listed.app_id)
.with_context(|| format!("reading services of app `{}`", listed.name))?;
let mut dependencies: Vec<String> = Vec::new();
for s in services.addons {
match addon_name_by_id.get(&s.id) {
Some(name) => dependencies.push(name.clone()),
None => warn!(
"addon dependency `{}` of app `{}` not found in org listing",
s.id, listed.name
),
}
}
for s in services.applications {
match app_name_by_id.get(&s.id) {
Some(name) => dependencies.push(name.clone()),
None => warn!(
"app dependency `{}` of app `{}` not found in org listing",
s.id, listed.name
),
}
}
let env: IndexMap<String, String> =
env_vars.into_iter().map(|v| (v.name, v.value)).collect();
let user_domains: Vec<String> = domains
.into_iter()
.map(|d| d.hostname)
.filter(|h| !h.ends_with(".cleverapps.io"))
.collect();
let source = listed
.deploy_url
.clone()
.map(|from| Source { from, branch: None });
apps.insert(
listed.name.clone(),
App {
name: listed.name.clone(),
kind: listed.kind.clone(),
region: (listed.zone != default_region).then(|| listed.zone.clone()),
source,
domains: user_domains,
scalability: None, dependencies,
config: IndexMap::new(), env,
},
);
}
let mut addons: IndexMap<String, Addon> = IndexMap::new();
for listed in selected_addons {
info!("reading addon `{}` ({})", listed.name, listed.addon_id);
addons.insert(
listed.name.clone(),
Addon {
name: listed.name.clone(),
kind: strip_addon_suffix(&listed.provider_id).to_string(),
size: Some(listed.plan_slug),
crypted: false, region: (listed.region != default_region).then_some(listed.region),
version: None, backup_path: None,
},
);
}
let project = Project {
name: format!("imported-from-{}", args.org),
description: None,
org: args.org.clone(),
region: default_region,
variables: IndexMap::new(),
apps,
addons,
network_groups: IndexMap::new(),
};
project
.save(&args.output)
.with_context(|| format!("writing project file `{}`", args.output.display()))?;
if args.format.is_json() {
#[derive(Serialize)]
struct ReadReport {
wrote: String,
org: String,
apps: usize,
addons: usize,
}
let payload = ReadReport {
wrote: args.output.display().to_string(),
org: args.org.clone(),
apps: project.apps.len(),
addons: project.addons.len(),
};
let out = serde_json::to_string_pretty(&payload).context("serializing JSON report")?;
println!("{out}");
} else {
info!("wrote project to `{}`", args.output.display());
}
Ok(())
}
fn pick_default_region(apps: &[ListedApp], addons: &[ListedAddon]) -> String {
let mut counts: HashMap<String, usize> = HashMap::new();
for a in apps {
*counts.entry(a.zone.clone()).or_default() += 1;
}
for a in addons {
*counts.entry(a.region.clone()).or_default() += 1;
}
counts
.into_iter()
.max_by_key(|(_, c)| *c)
.map(|(k, _)| k)
.unwrap_or_else(|| "par".to_string())
}
fn strip_addon_suffix(provider_id: &str) -> &str {
provider_id.strip_suffix("-addon").unwrap_or(provider_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_addon_suffix() {
assert_eq!(strip_addon_suffix("postgresql-addon"), "postgresql");
assert_eq!(strip_addon_suffix("redis-addon"), "redis");
assert_eq!(strip_addon_suffix("cellar-addon"), "cellar");
assert_eq!(strip_addon_suffix("kv"), "kv");
}
}