use crate::client::Client;
use crate::output;
use clap::{Args, Subcommand};
#[derive(Args)]
pub struct RegistryArgs {
#[command(subcommand)]
pub command: RegistryCommand,
}
#[derive(Subcommand)]
pub enum RegistryCommand {
Token(TokenArgs),
}
#[derive(Args)]
pub struct TokenArgs {
#[command(subcommand)]
pub command: TokenCommand,
}
#[derive(Subcommand)]
pub enum TokenCommand {
Create(CreateArgs),
List,
Delete(DeleteArgs),
}
#[derive(Args)]
pub struct CreateArgs {
#[arg(long)]
name: String,
#[arg(long)]
read: bool,
#[arg(long)]
write: bool,
#[arg(long, conflicts_with = "org")]
repo: Option<String>,
#[arg(long)]
org: Option<String>,
#[arg(long = "scope")]
scopes: Vec<String>,
}
#[derive(Args)]
pub struct DeleteArgs {
id: String,
}
pub async fn run(args: RegistryArgs) -> Result<output::Response, Box<dyn std::error::Error>> {
match args.command {
RegistryCommand::Token(t) => match t.command {
TokenCommand::Create(a) => create(a).await,
TokenCommand::List => list().await,
TokenCommand::Delete(a) => delete(a).await,
},
}
}
fn build_scopes(args: &CreateArgs) -> Result<Vec<String>, String> {
if !args.scopes.is_empty() {
return Ok(args.scopes.clone());
}
let target = match (&args.repo, &args.org) {
(Some(repo), None) => repo.clone(),
(None, Some(org)) => format!("{org}/*"),
(Some(_), Some(_)) => return Err("--repo and --org are mutually exclusive".into()),
(None, None) => {
return Err("specify a target: --repo <org/repo>, --org <org>, or --scope".into())
}
};
let mut actions = Vec::new();
if args.read || args.write {
actions.push("pull");
}
if args.write {
actions.push("push");
}
if actions.is_empty() {
return Err("specify --read and/or --write (or an explicit --scope)".into());
}
Ok(actions.iter().map(|a| format!("{a}:{target}")).collect())
}
async fn create(args: CreateArgs) -> Result<output::Response, Box<dyn std::error::Error>> {
let scopes = build_scopes(&args)?;
let client = Client::from_config()?;
let body = serde_json::json!({ "name": args.name, "scopes": scopes });
let resp: serde_json::Value = client.post("/registry/tokens", &body).await?;
let token = resp
.get("token")
.and_then(|v| v.as_str())
.unwrap_or_default();
output::success(&format!("Created registry token '{}'", args.name));
output::detail("Scopes", &scopes.join(", "));
output::detail("Token", token);
output::line("\nThis is the only time the token is shown — store it now.");
Ok(output::Response::ok("token", resp))
}
async fn list() -> Result<output::Response, Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let resp: serde_json::Value = client.get("/registry/tokens").await?;
let tokens = resp
.get("tokens")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if tokens.is_empty() {
output::info("No registry tokens.");
return Ok(output::Response::items(tokens));
}
for t in &tokens {
let id = t.get("id").and_then(|v| v.as_str()).unwrap_or("");
let name = t.get("name").and_then(|v| v.as_str()).unwrap_or("");
let scopes = t
.get("scopes")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
output::line(&format!("{id} {name} [{scopes}]"));
}
Ok(output::Response::items(tokens))
}
async fn delete(args: DeleteArgs) -> Result<output::Response, Box<dyn std::error::Error>> {
let client = Client::from_config()?;
client
.delete_empty(&format!("/registry/tokens/{}", args.id))
.await?;
output::success(&format!("Revoked registry token {}", args.id));
Ok(output::Response::deleted(&args.id))
}
#[cfg(test)]
mod tests {
use super::*;
fn args(
read: bool,
write: bool,
repo: Option<&str>,
org: Option<&str>,
scopes: &[&str],
) -> CreateArgs {
CreateArgs {
name: "t".into(),
read,
write,
repo: repo.map(String::from),
org: org.map(String::from),
scopes: scopes.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn read_org_expands_to_pull_wildcard() {
let s = build_scopes(&args(true, false, None, Some("fangorn"), &[])).unwrap();
assert_eq!(s, vec!["pull:fangorn/*"]);
}
#[test]
fn write_repo_grants_pull_and_push() {
let s = build_scopes(&args(false, true, Some("fangorn/mail"), None, &[])).unwrap();
assert_eq!(s, vec!["pull:fangorn/mail", "push:fangorn/mail"]);
}
#[test]
fn explicit_scope_overrides_flags() {
let s = build_scopes(&args(
true,
false,
None,
Some("ignored"),
&["delete:fangorn/x"],
))
.unwrap();
assert_eq!(s, vec!["delete:fangorn/x"]);
}
#[test]
fn requires_a_target() {
assert!(build_scopes(&args(true, false, None, None, &[])).is_err());
}
#[test]
fn requires_an_access_level() {
assert!(build_scopes(&args(false, false, None, Some("fangorn"), &[])).is_err());
}
#[test]
fn repo_and_org_conflict() {
assert!(build_scopes(&args(
true,
false,
Some("fangorn/mail"),
Some("fangorn"),
&[]
))
.is_err());
}
}