use crate::{
AdvancedFeatures, AttestationResponse, ComposeManifest, CvmInfo, CvmStateResponse,
DeploymentConfig, DeploymentResponse, DockerConfig, Error, NetworkInfoResponse, PubkeyResponse,
Result, SystemStatsResponse, TeeClient, TeePodDiscoveryResponse, VmConfig,
};
use std::time::Duration;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::Path;
pub struct TeeDeployer {
client: TeeClient,
selected_teepod: Option<(u64, String)>,
}
impl TeeDeployer {
pub fn new(api_key: String, api_endpoint: Option<String>) -> Result<Self> {
let config = DeploymentConfig {
api_key,
api_url: api_endpoint
.unwrap_or_else(|| "https://cloud-api.phala.network/api/v1".to_string()),
docker_compose: String::new(),
env_vars: HashMap::new(),
teepod_id: 0,
image: String::new(),
vm_config: None,
};
Ok(Self {
client: TeeClient::new(config)?,
selected_teepod: None,
})
}
pub async fn discover_teepod(&mut self) -> Result<TeePodDiscoveryResponse> {
let teepods = self.client.get_available_teepods().await?;
let nodes = &teepods.nodes;
if nodes.is_empty() {
return Err(Error::Api {
status_code: 400,
message: "No available TEEPods found".into(),
});
}
let node = &nodes[0];
let image = node.images[0].name.clone();
self.selected_teepod = Some((node.teepod_id, image));
Ok(teepods)
}
pub async fn select_teepod(&mut self, teepod_id: u64) -> Result<()> {
let teepods = self.client.get_available_teepods().await?;
for node in &teepods.nodes {
if node.teepod_id == teepod_id {
let image = node.images[0].name.clone();
self.selected_teepod = Some((teepod_id, image));
return Ok(());
}
}
Err(Error::Api {
status_code: 404,
message: format!("TEEPod with ID {} not found or not available", teepod_id),
})
}
pub async fn deploy_compose(
&self,
docker_compose_file: &str,
app_name: &str,
env_vars: HashMap<String, String>,
vcpu: Option<u64>,
memory: Option<u64>,
disk_size: Option<u64>,
) -> Result<DeploymentResponse> {
let (teepod_id, image) = self.selected_teepod.as_ref().ok_or_else(|| Error::Api {
status_code: 400,
message: "No TEEPod selected. Call discover_teepod() or select_teepod() first".into(),
})?;
let vm_config = json!({
"name": app_name,
"compose_manifest": {
"docker_compose_file": docker_compose_file,
"name": app_name,
"features": ["kms", "tproxy-net"]
},
"vcpu": vcpu.unwrap_or(1),
"memory": memory.unwrap_or(1024),
"disk_size": disk_size.unwrap_or(10),
"teepod_id": teepod_id,
"image": image
});
let env_vars_vec: Vec<(String, String)> = env_vars.into_iter().collect();
let pubkey_response = self.client.get_pubkey_for_config(&vm_config).await?;
let pubkey = pubkey_response.app_env_encrypt_pubkey;
let salt = pubkey_response.app_id_salt;
let deployment = self
.client
.deploy_with_config_do_encrypt(vm_config, &env_vars_vec, &pubkey, &salt)
.await?;
if let Some(mut details) = deployment.details.clone() {
details.insert(
"teepod_id".to_string(),
serde_json::Value::Number(serde_json::Number::from(*teepod_id)),
);
details.insert(
"image".to_string(),
serde_json::Value::String(image.clone()),
);
let mut deployment_with_details = deployment.clone();
deployment_with_details.details = Some(details);
Ok(deployment_with_details)
} else {
let mut details = HashMap::new();
details.insert(
"teepod_id".to_string(),
serde_json::Value::Number(serde_json::Number::from(*teepod_id)),
);
details.insert(
"image".to_string(),
serde_json::Value::String(image.clone()),
);
let mut deployment_with_details = deployment.clone();
deployment_with_details.details = Some(details);
Ok(deployment_with_details)
}
}
pub async fn deploy_compose_from_file<P: AsRef<Path>>(
&self,
compose_path: P,
app_name: &str,
env_vars: HashMap<String, String>,
vcpu: Option<u64>,
memory: Option<u64>,
disk_size: Option<u64>,
) -> Result<DeploymentResponse> {
let content = std::fs::read_to_string(compose_path)
.map_err(|e| Error::Configuration(format!("Failed to read compose file: {}", e)))?;
self.deploy_compose(&content, app_name, env_vars, vcpu, memory, disk_size)
.await
}
pub async fn deploy_compose_from_string(
&self,
yaml_content: &str,
app_name: &str,
env_vars: HashMap<String, String>,
vcpu: Option<u64>,
memory: Option<u64>,
disk_size: Option<u64>,
) -> Result<DeploymentResponse> {
self.deploy_compose(yaml_content, app_name, env_vars, vcpu, memory, disk_size)
.await
}
pub async fn deploy_simple_service(
&self,
image: &str,
service_name: &str,
app_name: &str,
env_vars: HashMap<String, String>,
ports: Option<Vec<String>>,
volumes: Option<Vec<String>>,
command: Option<Vec<String>>,
vcpu: Option<u64>,
memory: Option<u64>,
disk_size: Option<u64>,
) -> Result<DeploymentResponse> {
let mut yaml = String::from("services:\n");
yaml.push_str(&format!(" {}:\n", service_name));
yaml.push_str(&format!(" image: {}\n", image));
if let Some(ports) = &ports {
yaml.push_str(" ports:\n");
for port in ports {
yaml.push_str(&format!(" - \"{}\"\n", port));
}
}
if let Some(volumes) = &volumes {
yaml.push_str(" volumes:\n");
for volume in volumes {
yaml.push_str(&format!(" - {}\n", volume));
}
}
if let Some(command) = &command {
yaml.push_str(" command: [");
for (i, cmd) in command.iter().enumerate() {
if i > 0 {
yaml.push_str(", ");
}
yaml.push_str(&format!("\"{}\"", cmd));
}
yaml.push_str("]\n");
}
if !env_vars.is_empty() {
yaml.push_str(" environment:\n");
for (key, value) in &env_vars {
yaml.push_str(&format!(" {}: {}\n", key, value));
}
}
self.deploy_compose(&yaml, app_name, env_vars, vcpu, memory, disk_size)
.await
}
pub async fn update_deployment(
&self,
app_id: &str,
compose_content: Option<&str>,
env_vars: Option<HashMap<String, String>>,
) -> Result<Value> {
let compose_response = self.client.get_compose(app_id).await?;
let mut compose_file = compose_response.compose_file;
if let Some(new_config) = compose_content {
if let Some(manifest) = compose_file.get_mut("compose_manifest") {
if let Some(obj) = manifest.as_object_mut() {
obj.insert("docker_compose_file".to_string(), json!(new_config));
}
}
}
let response = self
.client
.update_compose(app_id, compose_file, env_vars, compose_response.env_pubkey)
.await?;
Ok(json!({
"status": "updated",
"app_id": app_id,
"details": response
}))
}
pub fn create_vm_config(
&self,
docker_compose_file: &str,
app_name: &str,
vcpu: Option<u64>,
memory: Option<u64>,
disk_size: Option<u64>,
) -> Result<VmConfig> {
let (teepod_id, image) = self.selected_teepod.as_ref().ok_or_else(|| Error::Api {
status_code: 400,
message: "No TEEPod selected. Call discover_teepod() or select_teepod() first".into(),
})?;
let vm_config = VmConfig {
name: app_name.to_string(),
compose_manifest: ComposeManifest {
name: app_name.to_string(),
features: vec!["kms".to_string(), "tproxy-net".to_string()],
docker_compose_file: docker_compose_file.to_string(),
},
vcpu: vcpu.unwrap_or(1) as u32,
memory: memory.unwrap_or(1024) as u32,
disk_size: disk_size.unwrap_or(10) as u32,
teepod_id: *teepod_id,
image: image.to_string(),
advanced_features: AdvancedFeatures {
tproxy: true,
kms: true,
public_sys_info: true,
public_logs: true,
docker_config: DockerConfig {
username: String::new(),
password: String::new(),
registry: None,
},
listed: true,
},
};
Ok(vm_config)
}
pub fn create_vm_config_from_file<P: AsRef<Path>>(
&self,
compose_path: P,
app_name: &str,
vcpu: Option<u64>,
memory: Option<u64>,
disk_size: Option<u64>,
) -> Result<VmConfig> {
let content = std::fs::read_to_string(compose_path)
.map_err(|e| Error::Configuration(format!("Failed to read compose file: {}", e)))?;
self.create_vm_config(&content, app_name, vcpu, memory, disk_size)
}
pub async fn get_pubkey_for_config(&self, vm_config: &Value) -> Result<PubkeyResponse> {
self.client
.get_pubkey_for_config(vm_config)
.await
}
pub async fn deploy_with_encrypted_env(
&self,
vm_config: Value,
encrypted_env: String,
app_env_encrypt_pubkey: &str,
app_id_salt: &str,
) -> Result<DeploymentResponse> {
let response = self
.client
.deploy_with_config_encrypted_env(
vm_config,
encrypted_env,
app_env_encrypt_pubkey,
app_id_salt,
)
.await?;
Ok(response)
}
pub async fn provision_eliza(
&self,
name: String,
character_file: String,
env_keys: Vec<String>,
image: String,
) -> Result<(String, String)> {
self.client
.provision_eliza(name, character_file, env_keys, image)
.await
}
pub async fn create_eliza_vm(
&self,
app_id: &str,
encrypted_env: &str,
) -> Result<DeploymentResponse> {
self.client.create_eliza_vm(app_id, encrypted_env).await
}
pub async fn get_network_info(&self, app_id: &str) -> Result<NetworkInfoResponse> {
self.client.get_network_info(app_id).await
}
pub async fn get_system_stats(&self, app_id: &str) -> Result<SystemStatsResponse> {
self.client.get_system_stats(app_id).await
}
pub async fn stop(&self, app_id: &str) -> Result<CvmInfo> {
self.client.stop_cvm(app_id).await }
pub async fn shutdown(&self, app_id: &str) -> Result<CvmInfo> {
self.client.shutdown_cvm(app_id).await }
pub async fn start(&self, app_id: &str) -> Result<CvmInfo> {
self.client.start_cvm(app_id).await }
pub async fn delete(&self, app_id: &str) -> Result<()> {
self.client.delete_cvm(app_id).await }
pub async fn get_attestation(&self, app_id: &str) -> Result<AttestationResponse> {
self.client.get_attestation(app_id).await }
pub async fn get_status(&self, app_id: &str) -> Result<CvmStateResponse> {
self.client.get_state(app_id).await }
pub async fn wait_until_running(&self, app_id: &str, timeout: Duration) -> Result<()> {
let start = std::time::Instant::now();
loop {
if start.elapsed() > timeout {
return Err(Error::Api {
status_code: 408,
message: format!(
"CVM {} did not reach running state within {:?}",
app_id, timeout
),
});
}
match self.client.get_state(app_id).await {
Ok(state) if state.is_running => return Ok(()),
Ok(_) => {}
Err(_) => {}
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
}
pub fn get_client(&self) -> &TeeClient {
&self.client
}
}
pub struct TeeDeployerBuilder {
api_key: Option<String>,
api_endpoint: Option<String>,
}
impl TeeDeployerBuilder {
pub fn new() -> Self {
Self {
api_key: None,
api_endpoint: None,
}
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_api_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.api_endpoint = Some(endpoint.into());
self
}
pub fn build(self) -> Result<TeeDeployer> {
let api_key = self
.api_key
.ok_or_else(|| Error::Configuration("API key is required".into()))?;
TeeDeployer::new(api_key, self.api_endpoint)
}
}