use clap::Subcommand;
use crate::client;
use crate::config::ProjectConfig;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("no deployment matching {0}")]
NoMatch(String),
#[error(transparent)]
Client(#[from] crate::client::ClientError),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, clap::Args)]
pub struct AliasArgs {
#[arg(long, env = "BOATRAMP_SERVER", global = true)]
server: Option<String>,
#[arg(long, env = "BOATRAMP_SITE", global = true)]
site: Option<String>,
#[command(subcommand)]
command: AliasCommand,
}
#[derive(Debug, Subcommand)]
enum AliasCommand {
Set {
name: String,
deployment: String,
},
Rm {
name: String,
},
Ls,
}
#[derive(Debug, serde::Serialize)]
#[serde(tag = "result", rename_all = "snake_case")]
pub enum AliasOutput {
List {
site: String,
aliases: Vec<AliasEntry>,
},
Set {
site: String,
name: String,
deployment: String,
},
Removed { site: String, name: String },
}
#[derive(Debug, serde::Serialize)]
pub struct AliasEntry {
pub name: String,
pub deployment: String,
}
impl crate::output::Rendered for AliasOutput {
fn render_human(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
match self {
Self::List { site, aliases } => {
if aliases.is_empty() {
writeln!(w, "no aliases for {site}")?;
}
for a in aliases {
writeln!(w, "{} -> {}", a.name, short(&a.deployment))?;
}
Ok(())
}
Self::Set {
site,
name,
deployment,
} => writeln!(w, "alias {name} -> {} for {site}", short(deployment)),
Self::Removed { site, name } => {
writeln!(w, "removed alias {name} from {site}")
}
}
}
}
pub async fn run(args: AliasArgs, config: &ProjectConfig) -> Result<AliasOutput> {
let (server, site) = client::resolve_target(args.server, args.site, config)?;
let cp = client::ControlPlane::new(
server,
client::http_client(client::token(config).as_deref()),
client::resolve_project(config),
);
let out = match args.command {
AliasCommand::Ls => {
let aliases = cp.list_aliases(&site).await?;
AliasOutput::List {
site,
aliases: aliases
.into_iter()
.map(|(name, deployment)| AliasEntry { name, deployment })
.collect(),
}
}
AliasCommand::Set { name, deployment } => {
let list = cp.fetch_deployments(&site).await?;
let id =
resolve_id(&list, &deployment).ok_or_else(|| Error::NoMatch(deployment.clone()))?;
cp.set_alias(&site, &name, &id).await?;
AliasOutput::Set {
site,
name,
deployment: id,
}
}
AliasCommand::Rm { name } => {
cp.remove_alias(&site, &name).await?;
AliasOutput::Removed { site, name }
}
};
Ok(out)
}
fn short(id: &str) -> &str {
&id[..id.len().min(12)]
}
fn resolve_id(list: &boatramp_core::deploy::DeploymentList, wanted: &str) -> Option<String> {
if list.deployments.iter().any(|e| e.id == wanted) {
return Some(wanted.to_string());
}
let mut matches = list.deployments.iter().filter(|e| e.id.starts_with(wanted));
if let Some(first) = matches.next() {
return matches.next().is_none().then(|| first.id.clone());
}
if wanted.len() == 64 && wanted.bytes().all(|b| b.is_ascii_hexdigit()) {
return Some(wanted.to_string());
}
None
}