Refactors the app deployment logic to search for and select the cheapest available offer from the SGX nodes.
309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
pub mod cli_handler;
|
|
pub mod config;
|
|
pub mod deploy;
|
|
pub mod grpc_brain;
|
|
pub mod grpc_dtpm;
|
|
pub mod packaging;
|
|
pub mod utils;
|
|
|
|
use crate::config::Config;
|
|
use crate::constants::HRATLS_APP_PORT;
|
|
use crate::utils::{block_on, shorten_string};
|
|
use detee_shared::app_proto::{
|
|
AppContract as AppContractPB, AppNodeFilters, AppNodeListResp, AppResource,
|
|
ListAppContractsReq, NewAppRes,
|
|
};
|
|
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("Did not find a SGX node that matches your criteria")]
|
|
NoValidNodeFound,
|
|
#[error("{0}")]
|
|
Dtpm(#[from] crate::sgx::grpc_dtpm::Error),
|
|
#[error("Could not read file from disk: {0}")]
|
|
FileNotFound(#[from] std::io::Error),
|
|
#[error("{0}")]
|
|
Deployment(String),
|
|
#[error(transparent)]
|
|
Reqwest(#[from] reqwest::Error),
|
|
#[error(transparent)]
|
|
Serde(#[from] serde_yaml::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 vcpus: u32,
|
|
#[tabled(rename = "Mem (MB)")]
|
|
pub memory_mib: u32,
|
|
#[tabled(rename = "Disk (GB)")]
|
|
pub disk_size_mib: u32,
|
|
#[tabled(rename = "credits/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 { vcpus, memory_mib, disk_size_mib, .. } =
|
|
brain_app_contract.resource.unwrap_or_default();
|
|
|
|
let exposed_host_ports = brain_app_contract
|
|
.mapped_ports
|
|
.iter()
|
|
.map(|port| (port.host_port, port.guest_port))
|
|
.collect::<Vec<_>>();
|
|
|
|
Self {
|
|
location,
|
|
uuid: brain_app_contract.uuid,
|
|
name: brain_app_contract.app_name,
|
|
vcpus,
|
|
memory_mib,
|
|
disk_size_mib,
|
|
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 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 {
|
|
uuid: value.uuid,
|
|
name,
|
|
node_ip: value.ip_address,
|
|
hratls_port: value
|
|
.mapped_ports
|
|
.iter()
|
|
.find(|port| port.guest_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 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!("{} nanocredits/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>, Error> {
|
|
log::debug!("This will support flags in the future, but we have only one node atm.");
|
|
let req = AppNodeFilters { ..Default::default() };
|
|
Ok(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;
|
|
use std::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(())
|
|
}
|