use anyhow::{anyhow, Context, Result};
use reqwest::Client as HttpClient;
use serde::de::DeserializeOwned;
use serde_json::json;
use std::env;
use std::net::IpAddr;
use crate::server::digitalocean::server::{DropletConfig, DropletDto};
use crate::server::digitalocean::ssh_key::DigitalOceanSshKey;
const DO_API_BASE: &str = "https://api.digitalocean.com/v2";
#[derive(Debug, Clone)]
pub struct DoClient {
http: HttpClient,
token: String,
}
impl DoClient {
pub fn from_env() -> Result<Self> {
let token =
env::var("DIGITALOCEAN_API_TOKEN").context("DIGITALOCEAN_API_TOKEN env var not set")?;
Ok(Self {
http: HttpClient::new(),
token,
})
}
fn url(&self, path: &str) -> String {
format!("{DO_API_BASE}{path}")
}
async fn unwrap_envelope<T: DeserializeOwned>(resp: reqwest::Response, key: &str) -> Result<T> {
let mut envelope: serde_json::Value = resp.json().await?;
let body = envelope
.get_mut(key)
.map(|v| v.take())
.ok_or_else(|| anyhow!("DO response missing '{key}' field"))?;
serde_json::from_value(body).with_context(|| format!("decoding '{key}' from DO response"))
}
pub(super) async fn create_droplet(&self, cfg: &DropletConfig) -> Result<DropletDto> {
let resp = self
.http
.post(self.url("/droplets"))
.bearer_auth(&self.token)
.json(cfg)
.send()
.await?
.error_for_status()?;
Self::unwrap_envelope(resp, "droplet").await
}
pub(super) async fn get_droplet(&self, id: u32) -> Result<DropletDto> {
let resp = self
.http
.get(self.url(&format!("/droplets/{id}")))
.bearer_auth(&self.token)
.send()
.await?
.error_for_status()?;
Self::unwrap_envelope(resp, "droplet").await
}
pub(super) async fn destroy_droplet(&self, id: u32) -> Result<()> {
self.http
.delete(self.url(&format!("/droplets/{id}")))
.bearer_auth(&self.token)
.send()
.await?
.error_for_status()
.context("destroying droplet")?;
Ok(())
}
pub(super) async fn assign_floating_ip(&self, ip: IpAddr, droplet_id: u32) -> Result<()> {
let body = json!({ "type": "assign", "droplet_id": droplet_id });
self.http
.post(self.url(&format!("/floating_ips/{ip}/actions")))
.bearer_auth(&self.token)
.json(&body)
.send()
.await
.context("assigning floating IP")?;
Ok(())
}
pub(super) async fn list_ssh_keys(&self) -> Result<Vec<DigitalOceanSshKey>> {
let resp = self
.http
.get(self.url("/account/keys"))
.bearer_auth(&self.token)
.send()
.await?
.error_for_status()?;
Self::unwrap_envelope(resp, "ssh_keys").await
}
pub(super) async fn create_ssh_key(
&self,
name: &str,
public_key: &str,
) -> Result<DigitalOceanSshKey> {
let body = json!({ "name": name, "public_key": public_key });
let resp = self
.http
.post(self.url("/account/keys"))
.bearer_auth(&self.token)
.json(&body)
.send()
.await?
.error_for_status()?;
Self::unwrap_envelope(resp, "ssh_key").await
}
pub(super) async fn destroy_ssh_key(&self, id: u32) -> Result<()> {
self.http
.delete(self.url(&format!("/account/keys/{id}")))
.bearer_auth(&self.token)
.send()
.await?
.error_for_status()
.context("destroying SSH key")?;
Ok(())
}
}