use serde::Deserialize;
use serde_json::{Value, json};
use super::run;
use super::spaces::{Spaces, parse};
use crate::Result;
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudCredentials {
pub found: bool,
#[serde(default)]
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudKind {
pub image: String,
pub kind: String,
pub supported: bool,
#[serde(default)]
pub reason: String,
#[serde(default)]
pub machine_type: String,
#[serde(default)]
pub usd_per_hour: f64,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudProvider {
pub name: String,
pub title: String,
pub tier: String,
pub connected: bool,
#[serde(default)]
pub default: bool,
#[serde(default)]
pub credentials: CloudCredentials,
#[serde(default)]
pub account: String,
#[serde(default)]
pub profile: String,
#[serde(default)]
pub region: String,
#[serde(default)]
pub zone: String,
#[serde(default)]
pub project: String,
#[serde(default)]
pub environment: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub ttl_hours: u32,
#[serde(default)]
pub kinds: Vec<CloudKind>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudResource {
pub provider: String,
pub id: String,
#[serde(rename = "type")]
pub resource_type: String,
#[serde(default)]
pub sandbox: String,
#[serde(default)]
pub machine: String,
#[serde(default)]
pub region: String,
#[serde(default)]
pub state: String,
#[serde(default)]
pub created: String,
#[serde(default)]
pub expires: String,
#[serde(default)]
pub expired: bool,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudStatus {
#[serde(default)]
pub default_on: String,
pub providers: Vec<CloudProvider>,
#[serde(default)]
pub resources: Vec<CloudResource>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudCheck {
pub name: String,
pub ok: bool,
#[serde(default)]
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudTestReport {
pub provider: String,
pub ok: bool,
#[serde(default)]
pub account: String,
pub checks: Vec<CloudCheck>,
}
#[derive(Debug, Clone, PartialEq, Default, uniffi::Record)]
pub struct CloudConnected {
pub provider: CloudProvider,
pub checks: Vec<CloudCheck>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, uniffi::Record)]
pub struct CloudDisconnected {
pub provider: String,
pub disconnected: bool,
#[serde(default)]
pub left: Vec<CloudResource>,
}
#[derive(Debug, Clone, PartialEq, Default, uniffi::Record)]
pub struct CloudSweepItem {
pub resource: CloudResource,
pub action: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Default, uniffi::Record)]
pub struct CloudSweepReport {
pub dry_run: bool,
pub resources: Vec<CloudSweepItem>,
}
#[derive(Debug, Clone, PartialEq, Default, uniffi::Record)]
pub struct CloudTarget {
pub provider: String,
#[uniffi(default = None)]
pub profile: Option<String>,
#[uniffi(default = None)]
pub region: Option<String>,
#[uniffi(default = None)]
pub zone: Option<String>,
#[uniffi(default = None)]
pub project: Option<String>,
#[uniffi(default = None)]
pub environment: Option<String>,
}
impl CloudTarget {
fn args(&self) -> Value {
json!({
"provider": self.provider,
"profile": self.profile,
"region": self.region,
"zone": self.zone,
"project": self.project,
"environment": self.environment,
})
}
}
fn connected(v: Value) -> Result<CloudConnected> {
let checks = parse(v.get("checks").cloned().unwrap_or_else(|| json!([])))?;
Ok(CloudConnected {
provider: parse(v)?,
checks,
})
}
fn sweep(v: Value) -> Result<CloudSweepReport> {
let dry_run = v.get("dry_run").and_then(Value::as_bool).unwrap_or(true);
let rows = v
.get("resources")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let mut resources = Vec::with_capacity(rows.len());
for row in rows {
let action = row
.get("action")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let reason = row
.get("reason")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
resources.push(CloudSweepItem {
resource: parse(row)?,
action,
reason,
});
}
Ok(CloudSweepReport { dry_run, resources })
}
#[uniffi::export]
impl Spaces {
pub async fn cloud_status(&self, provider: Option<String>) -> Result<CloudStatus> {
let host = self.tool_caller();
run(async move {
parse(
host.tool_value("cloud_status", json!({"provider": provider}))
.await?,
)
})
.await
}
pub async fn cloud_connect(
&self,
target: CloudTarget,
make_default: bool,
ttl_hours: Option<u32>,
) -> Result<CloudConnected> {
let host = self.tool_caller();
run(async move {
let mut args = target.args();
args["make_default"] = json!(make_default);
args["ttl_hours"] = json!(ttl_hours);
connected(host.tool_value("cloud_connect", args).await?)
})
.await
}
pub async fn cloud_test(&self, target: CloudTarget) -> Result<CloudTestReport> {
let host = self.tool_caller();
run(async move { parse(host.tool_value("cloud_test", target.args()).await?) }).await
}
pub async fn cloud_disconnect(&self, provider: String) -> Result<CloudDisconnected> {
let host = self.tool_caller();
run(async move {
parse(
host.tool_value("cloud_disconnect", json!({"provider": provider}))
.await?,
)
})
.await
}
pub async fn cloud_sweep(
&self,
provider: Option<String>,
dry_run: bool,
all: bool,
) -> Result<CloudSweepReport> {
let host = self.tool_caller();
run(async move {
sweep(
host.tool_value(
"cloud_sweep",
json!({"provider": provider, "dry_run": dry_run, "all": all}),
)
.await?,
)
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_wire_rows_decode_into_the_binding_records() {
let wire = serde_json::to_value(cua_spaces::cloud::CloudConnected {
provider: cua_spaces::cloud::CloudProvider {
name: "aws".into(),
title: "AWS".into(),
tier: "vm".into(),
connected: true,
label: "AWS · us-west-2".into(),
kinds: vec![cua_spaces::cloud::CloudKind {
image: "linux".into(),
kind: "container".into(),
supported: true,
machine_type: "t3.medium".into(),
usd_per_hour: 0.0416,
..Default::default()
}],
..Default::default()
},
checks: vec![cua_spaces::cloud::CloudCheck {
name: "credentials".into(),
ok: true,
detail: "account 1".into(),
}],
})
.unwrap();
let c = connected(wire).unwrap();
assert_eq!(c.provider.label, "AWS · us-west-2");
assert_eq!(c.provider.kinds[0].machine_type, "t3.medium");
assert_eq!(c.checks[0].name, "credentials");
let wire = serde_json::to_value(cua_spaces::cloud::CloudSweepReport {
dry_run: true,
resources: vec![cua_spaces::cloud::CloudSweepItem {
resource: cua_spaces::cloud::CloudResource {
provider: "aws".into(),
id: "i-1".into(),
resource_type: "instance".into(),
expired: true,
..Default::default()
},
action: "delete".into(),
reason: "expired".into(),
}],
})
.unwrap();
let r = sweep(wire).unwrap();
assert!(r.dry_run);
assert_eq!(r.resources[0].resource.resource_type, "instance");
assert_eq!(r.resources[0].action, "delete");
}
}