detee-cli/src/sgx/mod.rs

318 lines
11 KiB
Rust

pub mod cli_handler;
pub mod config;
pub mod grpc_brain;
pub mod grpc_dtpm;
pub mod packaging;
pub mod utils;
use crate::config::Config;
use crate::snp;
use crate::utils::shorten_string;
use crate::{constants::HRATLS_APP_PORT, utils::block_on};
use detee_shared::{
app_proto::{
AppContract as AppContractPB, AppNodeFilters, AppNodeListResp, AppResource,
ListAppContractsReq, NewAppRes,
},
sgx::types::brain::Resource,
};
use grpc_brain::get_one_app_node;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use tabled::Tabled;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Config(#[from] crate::config::Error),
#[error("Could not find a contract with the ID {0}")]
AppContractNotFound(String),
#[error("Brain returned the following error: {0}")]
Brain(#[from] grpc_brain::Error),
#[error("Could not read file from disk: {0}")]
FileNotFound(#[from] std::io::Error),
}
#[derive(Tabled, Debug, Serialize, Deserialize)]
pub struct AppContract {
#[tabled(rename = "Location")]
pub location: String,
#[tabled(rename = "UUID pfx", display_with = "shorten_string")]
pub uuid: String,
pub name: String,
#[tabled(rename = "Cores")]
pub vcpu: u32,
#[tabled(rename = "Mem (MB)")]
pub memory_mb: u32,
#[tabled(rename = "Disk (MB)")]
pub disk_mb: u32,
#[tabled(rename = "LP/h")]
pub cost_h: String,
#[tabled(rename = "time left", display_with = "display_mins")]
pub time_left: u64,
#[tabled(rename = "Node IP")]
pub node_ip: String,
#[tabled(rename = "Exposed ports", display_with = "display_ports")]
pub exposed_host_ports: Vec<(u32, u32)>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PublicIndex {
packages: Vec<PackageElement>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PackageElement {
package_name: String,
package_url: String,
launch_config_url: String,
mr_enclave: [u8; 32],
}
pub static PACKAGES_INDEX: LazyLock<PublicIndex> = LazyLock::new(|| {
PublicIndex {
packages: vec![
PackageElement{
package_name: "actix-static-server".to_string(),
package_url: "https://registry.detee.ltd/sgx/packages/actix-static-server_package_2025-04-16_21-27-07.tar.gz".to_string(),
launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/actix-static-server-launch-config_001.yaml".to_string(),
mr_enclave: [97, 9, 55, 254, 254, 21, 143, 123, 239, 36, 47, 228, 8, 224, 114, 237, 159, 40, 32, 244, 54, 253, 126, 19, 13, 86, 42, 142, 248, 20, 89, 58],
},
PackageElement{
package_name: "base-package".to_string(),
package_url: "https://registry.detee.ltd/sgx/packages/base_package_2025-04-17_11-01-08.tar.gz".to_string(),
launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/base-package-launch-config_001.yaml".to_string(),
mr_enclave: [52, 183, 102, 210, 251, 219, 218, 140, 168, 118, 10, 193, 98, 240, 147, 124, 240, 189, 46, 95, 138, 172, 15, 246, 227, 114, 70, 159, 232, 212, 9, 234],
},
PackageElement{
package_name: "actix-app-info".to_string(),
package_url: "https://registry.detee.ltd/sgx/packages/actix-app-info_package_2025-04-16_21-59-38.tar.gz".to_string(),
launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/actix-app-info-launch-config_001.yaml".to_string(),
mr_enclave: [128, 0, 97, 103, 165, 103, 68, 203, 240, 145, 153, 254, 34, 129, 75, 140, 8, 186, 63, 226, 144, 129, 201, 187, 175, 66, 80, 1, 151, 114, 183, 159],
},
PackageElement{
package_name: "go-app-info".to_string(),
package_url: "https://registry.detee.ltd/sgx/packages/go-app-info_package_2025-04-16_21-39-18.tar.gz".to_string(),
launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/go-gin-app-info-launch-config_001.yaml".to_string(),
mr_enclave: [188, 233, 211, 196, 237, 6, 46, 236, 229, 173, 239, 94, 99, 172, 233, 37, 255, 20, 54, 212, 172, 30, 182, 71, 219, 76, 78, 11, 72, 68, 46, 204],
}
],
}
});
pub fn package_entry_from_name(package_name: &str) -> Option<PackageElement> {
PACKAGES_INDEX.packages.iter().find(|package| package.package_name == package_name).cloned()
}
fn display_mins(minutes: &u64) -> String {
let mins = minutes % 60;
let hours = minutes / 60;
format!("{hours}h {mins}m")
}
fn display_ports(ports: &[(u32, u32)]) -> String {
ports.iter().map(|port| format!("({}:{})", port.0, port.1,)).collect::<Vec<_>>().join(", ")
}
impl crate::HumanOutput for Vec<AppContract> {
fn human_cli_print(&self) {
let style = tabled::settings::Style::rounded();
let mut table = tabled::Table::new(self);
table.with(style);
println!("{table}");
}
}
impl From<AppContractPB> for AppContract {
fn from(brain_app_contract: AppContractPB) -> Self {
let node_pubkey = brain_app_contract.node_pubkey.clone();
let location = match block_on(get_one_app_node(AppNodeFilters {
node_pubkey: node_pubkey.clone(),
..Default::default()
})) {
Ok(node) => format!("{}, {} ({})", node.city, node.region, node.country),
Err(e) => {
log::warn!("Could not get information about node {node_pubkey} fram brain: {e:?}");
String::new()
}
};
let AppResource { vcpu, memory_mb, disk_mb, .. } =
brain_app_contract.resource.unwrap_or_default();
let exposed_host_ports = brain_app_contract
.mapped_ports
.iter()
.map(|port| (port.host_port, port.app_port))
.collect::<Vec<_>>();
Self {
location,
uuid: brain_app_contract.uuid,
name: brain_app_contract.app_name,
vcpu,
memory_mb,
disk_mb,
cost_h: format!(
"{:.4}",
(brain_app_contract.nano_per_minute * 60) as f64 / 1_000_000_000.0
),
time_left: brain_app_contract.locked_nano / brain_app_contract.nano_per_minute,
node_ip: brain_app_contract.public_ipv4,
exposed_host_ports,
}
}
}
pub async fn get_one_contract(uuid: &str) -> Result<AppContractPB, Error> {
let req = ListAppContractsReq {
admin_pubkey: Config::get_detee_wallet()?,
uuid: uuid.to_string(),
..Default::default()
};
let contracts = grpc_brain::list_contracts(req).await?;
if contracts.is_empty() {
return Err(Error::AppContractNotFound(uuid.to_string()));
}
// let _ = write_uuid_list(&contracts);
Ok(contracts[0].clone())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppDeployResponse {
pub status: String,
pub uuid: String,
pub name: String,
pub node_ip: String,
pub hratls_port: u32,
pub error: String,
}
impl crate::HumanOutput for AppDeployResponse {
fn human_cli_print(&self) {
println!("The application got deployed under the UUID: {}", self.uuid);
}
}
impl From<(NewAppRes, String)> for AppDeployResponse {
fn from((value, name): (NewAppRes, String)) -> Self {
Self {
status: value.status,
uuid: value.uuid,
name,
node_ip: value.ip_address,
hratls_port: value
.mapped_ports
.iter()
.find(|port| port.app_port == HRATLS_APP_PORT)
.map(|port| port.host_port)
.unwrap_or(HRATLS_APP_PORT),
error: value.error,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppDeleteResponse {
pub uuid: String,
pub message: String,
}
impl crate::HumanOutput for AppDeleteResponse {
fn human_cli_print(&self) {
println!("App deleted successfully: UUID: {}", self.uuid);
}
}
pub async fn get_app_node(
resource: Resource,
location: snp::deploy::Location,
) -> Result<AppNodeListResp, grpc_brain::Error> {
let app_node_filter = AppNodeFilters {
vcpus: resource.vcpu,
memory_mb: resource.memory_mb,
storage_mb: resource.disk_mb,
country: location.country.clone().unwrap_or_default(),
region: location.region.clone().unwrap_or_default(),
city: location.city.clone().unwrap_or_default(),
ip: location.node_ip.clone().unwrap_or_default(),
node_pubkey: String::new(),
};
get_one_app_node(app_node_filter).await
}
pub fn inspect_node(ip: String) -> Result<AppNodeListResp, grpc_brain::Error> {
let req = AppNodeFilters { ip, ..Default::default() };
block_on(get_one_app_node(req))
}
#[derive(Tabled, Debug, Serialize, Deserialize)]
pub struct TabledAppNode {
#[tabled(rename = "Operator")]
pub operator: String,
#[tabled(rename = "City, Region, Country")]
pub location: String,
#[tabled(rename = "IP")]
pub public_ip: String,
#[tabled(rename = "Price per unit")]
pub price: String,
#[tabled(rename = "Reports")]
pub reports: usize,
}
impl From<AppNodeListResp> for TabledAppNode {
fn from(brain_node: AppNodeListResp) -> Self {
Self {
operator: brain_node.operator,
location: brain_node.city + ", " + &brain_node.region + ", " + &brain_node.country,
public_ip: brain_node.ip,
price: format!("{} nanoLP/min", brain_node.price),
reports: brain_node.reports.len(),
}
}
}
impl super::HumanOutput for Vec<AppNodeListResp> {
fn human_cli_print(&self) {
let nodes: Vec<TabledAppNode> = self.iter().map(|n| n.clone().into()).collect();
let style = tabled::settings::Style::rounded();
let mut table = tabled::Table::new(nodes);
table.with(style);
println!("{table}");
}
}
pub fn print_nodes() -> Result<Vec<AppNodeListResp>, grpc_brain::Error> {
log::debug!("This will support flags in the future, but we have only one node atm.");
let req = AppNodeFilters { ..Default::default() };
block_on(grpc_brain::get_app_node_list(req))
}
pub async fn get_app_node_by_contract(uuid: &str) -> Result<AppNodeListResp, Error> {
let contract = get_one_contract(uuid).await?;
Ok(get_one_app_node(AppNodeFilters { node_pubkey: contract.node_pubkey, ..Default::default() })
.await?)
}
fn write_uuid_list(app_contracts: &[AppContract]) -> Result<(), Error> {
let app_uuid_list_path = Config::app_uuid_list_path()?;
let mut file = std::fs::File::create(app_uuid_list_path)?;
let output: String = app_contracts
.iter()
.map(|app| format!("{}\t{}", app.uuid, app.name).to_string())
.collect::<Vec<String>>()
.join("\n");
let output = output + "\n";
std::io::Write::write_all(&mut file, output.as_bytes())?;
Ok(())
}
pub fn append_uuid_list(uuid: &str, app_name: &str) -> Result<(), Error> {
use std::{fs::OpenOptions, io::prelude::*};
let mut file =
OpenOptions::new().create(true).append(true).open(Config::app_uuid_list_path()?).unwrap();
writeln!(file, "{uuid}\t{app_name}")?;
Ok(())
}