diff --git a/src/admin.rs b/src/admin.rs index ff35c2c..b8b6189 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,81 +1,23 @@ -use std::process::exit; - -use crate::config::{Context, RequestBuilderExt}; +use crate::config::Context; use crate::util; -use anyhow::{Context as anyhowContext, Result}; -use reqwest::{ - blocking::{Client, Response}, - StatusCode, -}; -use serde_json::{json, Value}; -use strum_macros::{AsRefStr, EnumString}; +use anyhow::Result; +pub use drogue_client::admin::v1::Role; +use drogue_client::admin::v1::{Client, MemberEntry}; use tabular::{Row, Table}; -#[derive(AsRefStr, EnumString)] -#[allow(non_camel_case_types)] -pub enum Roles { - admin, - manager, - reader, -} - -#[derive(AsRefStr, EnumString)] -enum ApiOp { - #[strum(to_string = "transfer-ownership")] - TransferOwnership, - #[strum(to_string = "accept-ownership")] - AcceptOwnerShip, - #[strum(to_string = "members")] - Members, -} +pub async fn member_list(config: &Context, app: &str) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); -fn craft_url(config: &Context, app: &str, end: &ApiOp) -> String { - format!( - "{}{}/apps/{}/{}", - &config.registry_url, - util::ADMIN_API_PATH, - urlencoding::encode(app), - end.as_ref() - ) -} - -fn member_get(config: &Context, app: &str) -> Result { - let client = Client::new(); - let url = craft_url(config, app, &ApiOp::Members); - - client - .get(&url) - .auth(&config.token) - .send() - .map_err(|e| { - log::error!("Error: {}", e); - exit(2); - }) - .map(|res| match res.status() { - StatusCode::OK => res, - e => { - log::error!("{}", e); - util::exit_with_code(e); - } - }) -} - -pub fn member_list(config: &Context, app: &str) -> Result<()> { - let res = member_get(config, app)?; - let body: Value = serde_json::from_str(&res.text().unwrap_or_else(|_| "{}".to_string()))?; + let res = client.get_members(app).await?; let mut table = Table::new("{:<} | {:<}"); table.add_row(Row::new().with_cell("USER").with_cell("ROLE")); - match body["members"].as_object() { + match res { Some(members) => { - for i in members.keys() { - table.add_row( - Row::new() - .with_cell(i) - .with_cell(members[i]["role"].as_str().unwrap_or_default()), - ); + for (user, entry) in members.members.iter() { + table.add_row(Row::new().with_cell(user).with_cell(entry.role)); } println!("{}", table); } @@ -86,147 +28,141 @@ pub fn member_list(config: &Context, app: &str) -> Result<()> { Ok(()) } -pub fn member_delete(_config: &Context, _app: &str, _username: &str) -> Result<()> { - // todo use a strongly typed deserialisation of members using drogue_client. - unimplemented!() - // let res = member_get(config, app)?; - // let obj = res.text().unwrap_or_else(|_| "{}".to_string()); - // - // let mut body: Value = serde_json::from_str(&obj)?; - // let mut members= body["members"].as_array().unwrap_or_default(); - // members.retain(|u| u != username); - // body["members"] = members; - // - // member_put(config, app, body).map(describe_response) -} +pub async fn member_delete(config: &Context, app: &str, username: &str) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); + + let op = match client.get_members(app).await { + Ok(Some(mut members)) => { + members.members.remove(&username.to_string()); -fn member_put(config: &Context, app: &str, data: serde_json::Value) -> Result { - let client = Client::new(); - let url = craft_url(config, app, &ApiOp::Members); + client.update_members(app, members).await + } + Ok(None) => Ok(false), + Err(e) => Err(e), + }; - client - .put(&url) - .auth(&config.token) - .json(&data) - .send() - .context("Can't update member list") + match op { + Ok(true) => { + println!("Application members updated"); + Ok(()) + } + Ok(false) => { + println!("Application not found"); + Ok(()) + } + Err(e) => Err(e.into()), + } } -pub fn member_edit(config: &Context, app: &str) -> Result<()> { - let res = member_get(config, app)?; - let body = res.text().unwrap_or_else(|_| "{}".to_string()); - let insert = util::editor(body)?; +pub async fn member_edit(config: &Context, app: &str) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); - member_put(config, app, insert).map(describe_response) + let op = match client.get_members(app).await { + Ok(Some(members)) => { + let data = util::editor(members)?; + client.update_members(app, data).await + } + Ok(None) => { + println!("Application {} not found", app); + Ok(false) + } + Err(e) => Err(e), + }; + + match op { + Ok(true) => { + println!("Application members updated"); + Ok(()) + } + Ok(false) => Ok(()), + Err(e) => Err(e.into()), + } } -pub fn member_add(config: &Context, app: &str, username: &str, role: Roles) -> Result<()> { - let res = member_get(config, app)?; - let obj = res.text().unwrap_or_else(|_| "{}".to_string()); +pub async fn member_add(config: &Context, app: &str, username: &str, role: Role) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); - let mut body: Value = serde_json::from_str(&obj)?; - body["members"][username] = serde_json::json!({"role": role.as_ref()}); + let op = match client.get_members(app).await { + Ok(Some(mut members)) => { + println!("{:?}", members); + members + .members + .insert(username.to_string(), MemberEntry { role }); - member_put(config, app, body).map(describe_response) -} + println!("{:?}", members); -pub fn transfer_app(config: &Context, app: &str, username: &str) -> Result<()> { - let client = Client::new(); - let url = craft_url(config, app, &ApiOp::TransferOwnership); - - let body = json!({ "newUser": username }); - - client - .put(&url) - .auth(&config.token) - .json(&body) - .send() - .map_err(|e| { - log::error!("Error: {}", e); - exit(2); - }) - .map(|res| match res.status() { - StatusCode::ACCEPTED => { - println!("Application transfer initated"); - println!( - "The new user can accept the transfer with \"drg admin transfer accept {}\"", - app - ); - if let Ok(console) = util::get_drogue_console_endpoint(config) { - println!("Alternatively you can share this link with the new owner :"); - println!("{}transfer/{}", console.as_str(), urlencoding::encode(app)); - } - } - e => { - log::error!("{}", e); - util::exit_with_code(e); - } - }) -} + client.update_members(app, members).await + } + Ok(None) => Ok(false), + Err(e) => Err(e), + }; -pub fn cancel_transfer(config: &Context, app: &str) -> Result<()> { - let client = Client::new(); - let url = craft_url(config, app, &ApiOp::TransferOwnership); - - client - .delete(&url) - .auth(&config.token) - .send() - .map_err(|e| { - log::error!("Error: {}", e); - exit(2); - }) - .map(|res| match res.status() { - StatusCode::NO_CONTENT => { - println!("Application transfer canceled"); - } - e => { - log::error!("{}", e); - util::exit_with_code(e); - } - }) + match op { + Ok(true) => { + println!("Application members updated"); + Ok(()) + } + Ok(false) => { + println!("Application not found"); + Ok(()) + } + Err(e) => Err(e.into()), + } } -pub fn accept_transfer(config: &Context, app: &str) -> Result<()> { - let client = Client::new(); - let url = craft_url(config, app, &ApiOp::AcceptOwnerShip); - - client - .put(&url) - .auth(&config.token) - .send() - .map_err(|e| { - log::error!("Error: {}", e); - exit(2); - }) - .map(|res| match res.status() { - StatusCode::NO_CONTENT => { - println!("Application transfer completed."); - println!("You are now the owner of application {}", app); +pub async fn transfer_app(config: &Context, app: &str, username: &str) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); + + match client.initiate_app_transfer(app, username).await { + Ok(true) => { + println!("Application transfer initated"); + println!( + "The new user can accept the transfer with \"drg admin transfer accept {}\"", + app + ); + if let Ok(console) = util::get_drogue_console_endpoint(config) { + println!("Alternatively you can share this link with the new owner :"); + println!("{}transfer/{}", console.as_str(), urlencoding::encode(app)); } - e => { - log::error!("{}", e); - util::exit_with_code(e); - } - }) + Ok(()) + } + Ok(false) => { + println!("The application does not exist"); + Ok(()) + } + Err(e) => Err(e.into()), + } } -fn describe_response(res: Response) { - match res.status() { - StatusCode::NO_CONTENT => { - println!("The member list was updated."); - } - StatusCode::BAD_REQUEST => { - println!("Invalid format: {}", res.text().unwrap_or_default()); +pub async fn cancel_transfer(config: &Context, app: &str) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); + + match client.cancel_app_transfer(app).await { + Ok(true) => { + println!("Application transfer canceled"); + Ok(()) } - StatusCode::NOT_FOUND => { - println!("Application not found."); + Ok(false) => { + println!("The application does not exist"); + Ok(()) } - StatusCode::CONFLICT => { - println!("Conflict: The resource may have been modified on the server since we retrieved it."); + Err(e) => Err(e.into()), + } +} + +pub async fn accept_transfer(config: &Context, app: &str) -> Result<()> { + let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); + + match client.accept_app_transfer(app).await { + Ok(true) => { + println!("Application transfer completed."); + println!("You are now the owner of application {}", app); + Ok(()) } - _ => { - println!("Error: Can't update member list.") + Ok(false) => { + println!("The application does not exist"); + Ok(()) } + Err(e) => Err(e.into()), } } diff --git a/src/arguments.rs b/src/arguments.rs index ec362ee..46e8606 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -1,4 +1,3 @@ -use crate::admin::Roles; use crate::{trust, util, AppId}; use crate::config::Context; @@ -7,6 +6,8 @@ use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches}; use std::convert::AsRef; use strum_macros::{AsRefStr, EnumString}; +use crate::admin::Role; + /// Drg CLI follows a "action resourceType resourceId options" pattern. /// Rarely, the resource Id is optional @@ -176,11 +177,7 @@ pub fn app_arguments() -> clap::App<'static> { .takes_value(true) .required(true) .help("Role assigned to this member") - .possible_values([ - Roles::admin.as_ref(), - Roles::manager.as_ref(), - Roles::reader.as_ref(), - ]); + .possible_values(["admin", "manager", "reader"]); let ca_key = Arg::new(Parameters::ca_key.as_ref()) .long(Parameters::ca_key.as_ref()) diff --git a/src/devices.rs b/src/devices.rs index 6e829df..002c731 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -123,7 +123,12 @@ pub async fn edit( } } -pub async fn list(config: &Context, app: AppId, labels: Option>, wide: bool) -> Result<()> { +pub async fn list( + config: &Context, + app: AppId, + labels: Option>, + wide: bool, +) -> Result<()> { let client = Client::new(reqwest::Client::new(), config.registry_url.clone(), config); let labels = util::clap_values_to_vec(labels); @@ -247,7 +252,6 @@ where // this part needs to be refactored. fn pretty_list(data: Vec, wide: bool) { - let mut header = Row::new().with_cell("NAME").with_cell("AGE"); let mut table = if wide { header.add_cell("FIRMWARE"); @@ -268,53 +272,51 @@ fn pretty_list(data: Vec, wide: bool) { .with_cell(name) .with_cell(util::age_from_timestamp(creation)); - if wide { - if let Some(firmware) = dev.status.get("firmware") { - let current = firmware["current"].as_str(); - let target = firmware["target"].as_str(); - - let mut in_sync = None; - let mut update = None; - for item in firmware["conditions"].as_array().unwrap() { - if let Some("InSync") = item["type"].as_str() { - in_sync.replace(if item["status"].as_str().unwrap() == "True" { - true - } else { - false - }); - } - - if let Some("UpdateProgress") = item["type"].as_str() { - update = item["message"].as_str().clone(); - } - } - - match (in_sync, update) { - (Some(true), _) => row.add_cell("InSync"), - (Some(false), Some(update)) => { - row.add_cell(format!("Updating ({})", update)) - } - (Some(false), _) => row.add_cell("NotInSync"), - _ => row.add_cell("Unknown"), - }; - - if let Some(current) = current { - row.add_cell(current); - } - - if let Some(target) = target { - row.add_cell(target); - } - } else { - row.add_cell(""); - row.add_cell(""); - row.add_cell(""); + if wide { + if let Some(firmware) = dev.status.get("firmware") { + let current = firmware["current"].as_str(); + let target = firmware["target"].as_str(); + + let mut in_sync = None; + let mut update = None; + for item in firmware["conditions"].as_array().unwrap() { + if let Some("InSync") = item["type"].as_str() { + in_sync.replace(if item["status"].as_str().unwrap() == "True" { + true + } else { + false + }); } - } - table.add_row(row); + if let Some("UpdateProgress") = item["type"].as_str() { + update = item["message"].as_str().clone(); + } + } + + match (in_sync, update) { + (Some(true), _) => row.add_cell("InSync"), + (Some(false), Some(update)) => row.add_cell(format!("Updating ({})", update)), + (Some(false), _) => row.add_cell("NotInSync"), + _ => row.add_cell("Unknown"), + }; + + if let Some(current) = current { + row.add_cell(current); + } + + if let Some(target) = target { + row.add_cell(target); + } + } else { + row.add_cell(""); + row.add_cell(""); + row.add_cell(""); + } } + table.add_row(row); + } + print!("{}", table); } diff --git a/src/main.rs b/src/main.rs index 1ae1357..552d62f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,8 @@ use serde_json::json; use std::process::exit; use std::str::FromStr; +use crate::admin::Role; + type AppId = String; type DeviceId = String; @@ -220,12 +222,12 @@ async fn main() -> Result<()> { let app_id = arguments::get_app_id(command, context)?; let role = command .value_of(Parameters::role.as_ref()) - .map(|r| admin::Roles::from_str(r).unwrap()) + .map(|r| Role::from_str(r).unwrap()) .unwrap(); let user = command.value_of(ResourceType::member.as_ref()).unwrap(); - admin::member_add(context, &app_id, user, role) + admin::member_add(context, &app_id, user, role).await } ResourceType::token => { let description = command.value_of(Parameters::description.as_ref()); @@ -334,7 +336,7 @@ async fn main() -> Result<()> { let app_id = arguments::get_app_id(command, context)?; let user = command.value_of(ResourceType::member.as_ref()).unwrap(); - admin::member_delete(context, app_id.as_str(), user) + admin::member_delete(context, app_id.as_str(), user).await } ResourceType::token => { let prefix = command.value_of(ResourceId::tokenPrefix.as_ref()).unwrap(); @@ -368,7 +370,7 @@ async fn main() -> Result<()> { } ResourceType::member => { let app_id = arguments::get_app_id(command, context)?; - admin::member_edit(context, &app_id) + admin::member_edit(context, &app_id).await } // The other enum variants are not exposed by clap _ => unreachable!(), @@ -404,7 +406,7 @@ async fn main() -> Result<()> { } ResourceType::member => { let app_id = arguments::get_app_id(command, context)?; - admin::member_list(context, &app_id)?; + admin::member_list(context, &app_id).await?; } ResourceType::token => { tokens::get_api_keys(context).await?; @@ -486,15 +488,15 @@ async fn main() -> Result<()> { Transfer::init => { let user = cmd.value_of(Parameters::username.as_ref()).unwrap(); let id = arguments::get_app_id(cmd, context)?; - admin::transfer_app(context, id.as_str(), user)?; + admin::transfer_app(context, id.as_str(), user).await?; } Transfer::accept => { let id = cmd.value_of(ResourceId::applicationId.as_ref()).unwrap(); - admin::accept_transfer(context, id)? + admin::accept_transfer(context, id).await? } Transfer::cancel => { let id = cmd.value_of(ResourceId::applicationId.as_ref()).unwrap(); - admin::cancel_transfer(context, id)? + admin::cancel_transfer(context, id).await? } } } diff --git a/src/util.rs b/src/util.rs index 89788e1..059928f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -23,7 +23,6 @@ use url::Url; pub const VERSION: &str = crate_version!(); pub const COMPATIBLE_DROGUE_VERSION: &str = "0.9.0"; pub const COMMAND_API_PATH: &str = "api/command/v1alpha1"; -pub const ADMIN_API_PATH: &str = "api/admin/v1alpha1"; pub fn show_json>(payload: S) { let payload = payload.into(); @@ -63,7 +62,7 @@ pub fn json_parse(data: Option<&str>) -> Result { pub fn editor(original: S) -> Result where S: Serialize, - T: DeserializeOwned + PartialEq, + T: DeserializeOwned, { let original_string = serde_yaml::to_string(&original)?;