trifid/tfcli/src/host.rs

892 lines
29 KiB
Rust

use crate::api::APIErrorResponse;
use crate::{HostCommands, HostOverrideCommands, TableStyle};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
use std::net::{Ipv4Addr, SocketAddrV4};
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
use comfy_table::presets::UTF8_FULL;
use comfy_table::Table;
use url::Url;
pub async fn host_main(command: HostCommands, server: Url) -> Result<(), Box<dyn Error>> {
match command {
HostCommands::List { table_style } => list_hosts(server, table_style).await,
HostCommands::Create {
name,
network_id,
role_id,
ip_address,
listen_port,
lighthouse,
relay,
static_address,
} => {
create_host(
name,
network_id,
role_id,
ip_address,
listen_port,
lighthouse,
relay,
static_address,
server,
)
.await
}
HostCommands::Lookup { id } => get_host(id, server).await,
HostCommands::Delete { id } => delete_host(id, server).await,
HostCommands::Update {
id,
listen_port,
static_address,
name,
ip,
role,
} => update_host(id, listen_port, static_address, name, ip, role, server).await,
HostCommands::Block { id } => block_host(id, server).await,
HostCommands::Enroll { id } => enroll_host(id, server).await,
HostCommands::Overrides { command } => match command {
HostOverrideCommands::List { id } => list_overrides(id, server).await,
HostOverrideCommands::Set {
id,
key,
boolean,
string,
numeric,
} => set_override(id, key, boolean, numeric, string, server).await,
HostOverrideCommands::Unset { id, key } => unset_override(id, key, server).await,
},
}
}
#[derive(Deserialize)]
pub struct HostListResp {
pub data: Vec<Host>,
}
#[derive(Serialize, Deserialize)]
pub struct HostMetadata {
#[serde(rename = "lastSeenAt")]
pub last_seen_at: String,
pub version: String,
pub platform: String,
#[serde(rename = "updateAvailable")]
pub update_available: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Host {
pub id: String,
#[serde(rename = "organizationID")]
pub organization_id: String,
#[serde(rename = "networkID")]
pub network_id: String,
#[serde(rename = "roleID")]
pub role_id: String,
pub name: String,
#[serde(rename = "ipAddress")]
pub ip_address: String,
#[serde(rename = "staticAddresses")]
pub static_addresses: Vec<SocketAddrV4>,
#[serde(rename = "listenPort")]
pub listen_port: i64,
#[serde(rename = "isLighthouse")]
pub is_lighthouse: bool,
#[serde(rename = "isRelay")]
pub is_relay: bool,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "isBlocked")]
pub is_blocked: bool,
pub metadata: HostMetadata,
}
pub async fn list_hosts(server: Url, table_style: TableStyle) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.get(server.join("/v1/hosts?pageSize=5000")?)
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
let resp: HostListResp = res.json().await?;
if resp.data.is_empty() {
println!("No hosts found");
return Ok(());
}
if matches!(table_style, TableStyle::List) {
for host in &resp.data {
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(
" Static Addresses: {}",
host.static_addresses
.iter()
.map(|u| u.to_string())
.collect::<Vec<_>>()
.join(", ")
);
println!(" Listen Port: {}", host.listen_port);
println!(
" Type: {}",
if host.is_lighthouse {
"Lighthouse"
} else if host.is_relay {
"Relay"
} else {
"Host"
}
);
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!(
"Client Update Available: {}",
host.metadata.update_available
);
println!(" Created: {}", host.created_at);
println!();
}
return Ok(());
}
let mut table = Table::new();
match table_style {
TableStyle::List => unreachable!(),
TableStyle::Basic => (),
TableStyle::Pretty => { table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS) ; },
};
table.set_header(vec!["ID", "Name", "Organization ID", "Network ID", "Role ID", "IP Address", "Static Addresses", "Listen Port", "Type", "Blocked", "Last Seen"]);
for host in &resp.data {
table.add_row(vec![host.id.as_str(), &host.name, &host.organization_id, &host.network_id, &host.role_id, &host.ip_address, &host.static_addresses.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(" "), &host.listen_port.to_string(), if host.is_lighthouse { "Lighthouse" } else if host.is_relay { "Relay" } else { "Host" }, if host.is_blocked { "true" } else { "false" }, &host.metadata.last_seen_at]);
}
println!("{table}");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error listing hosts: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostCreateBody {
pub name: String,
#[serde(rename = "networkID")]
pub network_id: String,
#[serde(rename = "roleID")]
pub role_id: String,
#[serde(rename = "ipAddress")]
pub ip_address: Ipv4Addr,
#[serde(rename = "listenPort")]
pub listen_port: u16,
#[serde(rename = "isLighthouse")]
pub is_lighthouse: bool,
#[serde(rename = "isRelay")]
pub is_relay: bool,
#[serde(rename = "staticAddresses")]
pub static_addresses: Vec<SocketAddrV4>,
}
#[derive(Serialize, Deserialize)]
pub struct HostGetMetadata {}
#[derive(Serialize, Deserialize)]
pub struct HostGetResponse {
pub data: Host,
pub metadata: HostGetMetadata,
}
#[allow(clippy::too_many_arguments)]
pub async fn create_host(
name: String,
network_id: String,
role_id: String,
ip_address: Ipv4Addr,
listen_port: Option<u16>,
lighthouse: bool,
relay: bool,
static_address: Option<SocketAddrV4>,
server: Url,
) -> Result<(), Box<dyn Error>> {
if lighthouse && relay {
eprintln!("[error] Error creating host: a host cannot be both a lighthouse and a relay at the same time");
std::process::exit(1);
}
if (lighthouse || relay) && static_address.is_none() {
eprintln!("[error] Error creating host: a relay or lighthouse must have a static IP");
std::process::exit(1);
}
if (lighthouse || relay) && listen_port.is_none() {
eprintln!("[error] Error creating host: a relay or lighthouse must have a listen port");
std::process::exit(1);
}
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.post(server.join("/v1/hosts")?)
.json(&HostCreateBody {
name,
network_id,
role_id,
ip_address,
listen_port: listen_port.unwrap_or(0),
is_lighthouse: lighthouse,
is_relay: relay,
static_addresses: static_address.map_or(vec![], |u| vec![u]),
})
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
let host: Host = res.json::<HostGetResponse>().await?.data;
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(
" Static Addresses: {}",
host.static_addresses
.iter()
.map(|u| u.to_string())
.collect::<Vec<_>>()
.join(", ")
);
println!(" Listen Port: {}", host.listen_port);
println!(
" Type: {}",
if host.is_lighthouse {
"Lighthouse"
} else if host.is_relay {
"Relay"
} else {
"Host"
}
);
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!(
"Client Update Available: {}",
host.metadata.update_available
);
println!(" Created: {}", host.created_at);
println!();
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error creating host: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
pub async fn get_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.get(server.join(&format!("/v1/hosts/{}", id))?)
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
let host: Host = res.json::<HostGetResponse>().await?.data;
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(
" Static Addresses: {}",
host.static_addresses
.iter()
.map(|u| u.to_string())
.collect::<Vec<_>>()
.join(", ")
);
println!(" Listen Port: {}", host.listen_port);
println!(
" Type: {}",
if host.is_lighthouse {
"Lighthouse"
} else if host.is_relay {
"Relay"
} else {
"Host"
}
);
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!(
"Client Update Available: {}",
host.metadata.update_available
);
println!(" Created: {}", host.created_at);
println!();
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error listing hosts: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
pub async fn delete_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.delete(server.join(&format!("/v1/hosts/{}", id))?)
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
println!("Host removed");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error removing host: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostUpdateBody {
#[serde(rename = "listenPort")]
pub listen_port: u16,
#[serde(rename = "staticAddresses")]
pub static_addresses: Vec<SocketAddrV4>,
pub name: Option<String>,
pub ip: Option<Ipv4Addr>,
pub role: Option<String>,
}
pub async fn update_host(
id: String,
listen_port: Option<u16>,
static_address: Option<SocketAddrV4>,
name: Option<String>,
ip: Option<Ipv4Addr>,
role: Option<String>,
server: Url,
) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.put(server.join(&format!("/v1/hosts/{}?extension=extended_hosts", id))?)
.json(&HostUpdateBody {
listen_port: listen_port.unwrap_or(0),
static_addresses: static_address.map_or_else(Vec::new, |u| vec![u]),
name,
ip,
role,
})
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
let host: Host = res.json::<HostGetResponse>().await?.data;
println!(" Host: {} ({})", host.name, host.id);
println!(" Organization: {}", host.organization_id);
println!(" Network: {}", host.network_id);
println!(" Role: {}", host.role_id);
println!(" IP Address: {}", host.ip_address);
println!(
" Static Addresses: {}",
host.static_addresses
.iter()
.map(|u| u.to_string())
.collect::<Vec<_>>()
.join(", ")
);
println!(" Listen Port: {}", host.listen_port);
println!(
" Type: {}",
if host.is_lighthouse {
"Lighthouse"
} else if host.is_relay {
"Relay"
} else {
"Host"
}
);
println!(" Blocked: {}", host.is_blocked);
println!(" Last Seen: {}", host.metadata.last_seen_at);
println!(" Client Version: {}", host.metadata.version);
println!(" Platform: {}", host.metadata.platform);
println!(
"Client Update Available: {}",
host.metadata.update_available
);
println!(" Created: {}", host.created_at);
println!();
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error updating host: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentCodeResponseMetadata {}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentCode {
pub code: String,
#[serde(rename = "lifetimeSeconds")]
pub lifetime_seconds: i64,
}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentResponseData {
#[serde(rename = "enrollmentCode")]
pub enrollment_code: EnrollmentCode,
}
#[derive(Serialize, Deserialize)]
pub struct EnrollmentResponse {
pub data: EnrollmentResponseData,
pub metadata: EnrollmentCodeResponseMetadata,
}
pub async fn enroll_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.post(server.join(&format!("/v1/hosts/{}/enrollment-code", id))?)
.header("content-length", 0)
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
let resp: EnrollmentResponse = res.json().await?;
println!(
"Enrollment code generated. Enroll the host with the following code: {}",
resp.data.enrollment_code.code
);
println!("This code will be valid for {} seconds, at which point you will need to generate a new code", resp.data.enrollment_code.lifetime_seconds);
println!("If this host is blocked, a successful re-enrollment will unblock it.");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error blocking host: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
pub async fn block_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.post(server.join(&format!("/v1/hosts/{}/block", id))?)
.header("Content-Length", "0")
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
println!("Host blocked. To unblock it, re-enroll the host.");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error blocking host: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideResponse {
pub data: HostConfigOverrideData,
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideData {
pub overrides: Vec<HostConfigOverrideDataOverride>,
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideDataOverride {
pub key: String,
pub value: HostConfigOverrideDataOverrideValue,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum HostConfigOverrideDataOverrideValue {
Boolean(bool),
Numeric(i64),
Other(String),
}
pub async fn list_overrides(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?)
.bearer_auth(token)
.send()
.await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!(
"Value: {}",
match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v),
}
);
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error looking up config overrides: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct SetOverrideRequest {
pub overrides: Vec<HostConfigOverrideDataOverride>,
}
pub async fn set_override(
id: String,
key: String,
boolean: Option<bool>,
numeric: Option<i64>,
other: Option<String>,
server: Url,
) -> Result<(), Box<dyn Error>> {
if boolean.is_none() && numeric.is_none() && other.is_none() {
eprintln!("[error] no value provided: you must provide at least --boolean, --numeric, or --string");
std::process::exit(1);
} else if boolean.is_some() && numeric.is_some()
|| boolean.is_some() && other.is_some()
|| numeric.is_some() && other.is_some()
{
eprintln!("[error] multiple values provided: you must provide only one of --boolean, --numeric, or --string");
std::process::exit(1);
}
let val;
if let Some(v) = boolean {
val = HostConfigOverrideDataOverrideValue::Boolean(v);
} else if let Some(v) = numeric {
val = HostConfigOverrideDataOverrideValue::Numeric(v);
} else if let Some(v) = other {
val = HostConfigOverrideDataOverrideValue::Other(v);
} else {
unreachable!();
}
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?)
.bearer_auth(token.clone())
.send()
.await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
let mut others: Vec<HostConfigOverrideDataOverride> = vec![];
for c_override in resp.data.overrides {
if c_override.key != key {
others.push(c_override);
}
}
others.push(HostConfigOverrideDataOverride { key, value: val });
let res = client
.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?)
.bearer_auth(token.clone())
.json(&SetOverrideRequest { overrides: others })
.send()
.await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!(
"Value: {}",
match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v),
}
);
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
println!("Override set successfully");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error setting config overrides: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error setting config overrides: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}
pub async fn unset_override(id: String, key: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client
.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?)
.bearer_auth(token.clone())
.send()
.await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
let mut others: Vec<HostConfigOverrideDataOverride> = vec![];
for c_override in resp.data.overrides {
if c_override.key != key {
others.push(c_override);
}
}
let res = client
.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?)
.bearer_auth(token.clone())
.json(&SetOverrideRequest { overrides: others })
.send()
.await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!(
"Value: {}",
match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v),
}
);
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
println!("Override unset successfully");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error unsetting config overrides: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!(
"[error] Error unsetting config overrides: {} {}",
resp.errors[0].code, resp.errors[0].message
);
std::process::exit(1);
}
Ok(())
}