use crate::client::{OrdinaryApiClient, compress_zstd};
use anyhow::bail;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use hashbrown::HashSet;
use hyper::StatusCode;
use ordinary_config::{OrdinaryApiLimits, OrdinaryConfig};
use ordinary_monitor::client::{IndexDbs, OrdinaryMonitorClient};
use ordinary_types::flexbuffer_reader_to_json;
use reqwest::Response;
use serde_json::Value;
use uuid::Uuid;
fn set_contacts(app_config: &mut OrdinaryConfig) -> anyhow::Result<()> {
if app_config.contacts.is_none() || app_config.contacts.as_ref().is_some_and(Vec::is_empty) {
if let Ok(contacts) = std::env::var("ORDINARY_CONTACTS") {
app_config.contacts = Some(
contacts
.split(',')
.map(ToString::to_string)
.collect::<Vec<String>>(),
);
} else {
bail!(
r#""contacts" array is empty in `ordinary.json` and no `ORDINARY_CONTACTS` environment variable is present.
add email address(es) to "contacts" array or [comma delimited] to `ORDINARY_CONTACTS` (e.g. ORDINARY_CONTACTS=qwer@example.com,asdf@example.com)."#
);
}
}
Ok(())
}
async fn check_limits(
api_client: &OrdinaryApiClient<'_>,
correlation_id: &str,
app_config: &mut OrdinaryConfig,
) -> anyhow::Result<()> {
let api_limits: OrdinaryApiLimits = api_client
.client
.get(format!("{}/.ordinary/limits", api_client.addr))
.header("x-correlation-id", correlation_id)
.send()
.await?
.json()
.await?;
let privileged_domains = api_limits
.privileged_domains
.iter()
.cloned()
.collect::<HashSet<_>>();
app_config.check_config_against_limits(&api_limits, &privileged_domains)?;
Ok(())
}
async fn check_port(res: Response) -> anyhow::Result<u16> {
let status = res.status();
let res = res.bytes().await?;
if res.len() == 2 {
let port = u16::from_be_bytes([res[0], res[1]]);
match status {
StatusCode::OK => {
if port > 0 {
tracing::info!("success. running on port: {port}");
} else {
tracing::info!("success");
}
}
_ => {
if port > 0 {
tracing::warn!("operation invalid. status: {status}, running on port: {port}");
} else {
tracing::warn!("operation invalid. status: {status}");
}
}
}
Ok(port)
} else if !res.is_empty() {
let message = std::str::from_utf8(&res)?;
bail!("operation failed. status: {status}, message: {message}")
} else {
bail!("operation failed. status: {status}")
}
}
pub async fn deploy(api_client: &OrdinaryApiClient<'_>, proj_path: &str) -> anyhow::Result<u16> {
let correlation_id = api_client
.correlation_id
.unwrap_or(Uuid::new_v4())
.to_string();
let mut app_config = OrdinaryConfig::get(proj_path)?.for_send()?;
app_config.validate()?;
set_contacts(&mut app_config)?;
check_limits(api_client, &correlation_id, &mut app_config).await?;
let access_token = api_client
.get_access(None, Some(correlation_id.clone()))
.await?;
tracing::info!("deploying app...");
let res = api_client
.client
.put(format!("{}/v1/app/deploy", api_client.addr))
.body(compress_zstd(
serde_json::to_string(&app_config)?.as_bytes(),
)?)
.header("x-correlation-id", correlation_id)
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
)
.header("Content-Type", "application/json")
.send()
.await?;
check_port(res).await
}
pub async fn kill(api_client: &OrdinaryApiClient<'_>, proj_path: &str) -> anyhow::Result<()> {
let correlation_id = api_client.correlation_id.map(|id| id.to_string());
let config = OrdinaryConfig::get(proj_path)?;
let access_token = api_client.get_access(None, correlation_id.clone()).await?;
tracing::info!("killing app...");
let mut req = api_client
.client
.put(format!("{}/v1/app/kill", api_client.addr))
.query(&[("d", config.domain)])
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = correlation_id {
req = req.header("x-correlation-id", correlation_id);
}
let res = req.send().await?;
if res.status().is_success() {
tracing::info!("app killed.");
Ok(())
} else {
bail!("{}", res.status())
}
}
pub async fn restart(api_client: &OrdinaryApiClient<'_>, proj_path: &str) -> anyhow::Result<u16> {
let correlation_id = api_client.correlation_id.map(|id| id.to_string());
let config = OrdinaryConfig::get(proj_path)?;
let access_token = api_client.get_access(None, correlation_id.clone()).await?;
tracing::info!("restarting app...");
let mut req = api_client
.client
.post(format!("{}/v1/app/restart", api_client.addr))
.query(&[("d", config.domain)])
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = correlation_id {
req = req.header("x-correlation-id", correlation_id);
}
let res = req.send().await?;
check_port(res).await
}
pub async fn erase(api_client: &OrdinaryApiClient<'_>, proj_path: &str) -> anyhow::Result<()> {
let correlation_id = api_client.correlation_id.map(|id| id.to_string());
let config = OrdinaryConfig::get(proj_path)?;
let access_token = api_client.get_access(None, correlation_id.clone()).await?;
tracing::info!("erasing app...");
let mut req = api_client
.client
.delete(format!("{}/v1/app/erase", api_client.addr))
.query(&[("d", config.domain)])
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = correlation_id {
req = req.header("x-correlation-id", correlation_id);
}
let res = req.send().await?;
if res.status().is_success() {
tracing::info!("app erased.");
Ok(())
} else {
bail!("{}", res.status())
}
}
#[allow(clippy::missing_panics_doc, clippy::ref_option)]
pub fn logs_search(
proj_path: &str,
query: &str,
format: &str,
limit: &Option<usize>,
) -> anyhow::Result<String> {
let config = OrdinaryConfig::get(proj_path)?;
let monitor_client = OrdinaryMonitorClient::new(&config.domain, vec![IndexDbs::Tantivy])?;
match format {
"all" => monitor_client.tantivy_search_all(query),
"top" => monitor_client.tantivy_search_top(query, limit),
"count" => Ok(monitor_client.tantivy_search_count(query)?.to_string()),
_ => bail!("invalid format '{format}' -- try 'all', 'top' or 'count'"),
}
}
pub async fn accounts_list(
api_client: &OrdinaryApiClient<'_>,
proj_path: &str,
) -> anyhow::Result<String> {
let correlation_id = api_client.correlation_id.map(|id| id.to_string());
let config = OrdinaryConfig::get(proj_path)?;
let access_token = api_client.get_access(None, correlation_id.clone()).await?;
tracing::info!("fetching...");
let mut req = api_client
.client
.get(format!("{}/v1/app/accounts", api_client.addr))
.query(&[("d", config.domain)])
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = correlation_id {
req = req.header("x-correlation-id", correlation_id);
}
let res = req.send().await?.bytes().await?;
let root = flexbuffers::Reader::get_root(&res[..])?;
let mut out = vec![];
for account in &root.as_vector() {
let mut account_out = vec![];
let account_name_bytes = account
.as_vector()
.idx(0)
.as_vector()
.iter()
.map(|v| v.as_u8())
.collect::<Vec<u8>>();
let account_name = std::str::from_utf8(&account_name_bytes)?;
account_out.push(Value::from(account_name));
if let Some(auth_config) = &config.auth {
for claim_field in &auth_config.access_token.claims {
let json_val = flexbuffer_reader_to_json(
&claim_field.kind,
&account.as_vector().idx(claim_field.idx as usize),
)?;
account_out.push(json_val);
}
}
out.push(account_out);
}
tracing::info!("done.");
Ok(serde_json::to_string(&out)?)
}