use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use anyhow::{anyhow, Context, Result};
use std::net::IpAddr;
use std::time::Duration;
use crate::server::cloudinit::CloudConfig;
use crate::server::digitalocean::client::DoClient;
use crate::server::digitalocean::floating_ip::FloatingIp;
use crate::server::digitalocean::ssh_key::DigitalOceanSshKey;
use crate::server::{InnisfreeServer, ServerSpec};
pub const DO_REGION: &str = "sfo2";
pub const DO_SIZE: &str = "s-1vcpu-1gb";
pub const DO_IMAGE: &str = "debian-13-x64";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DropletStatus {
New,
Active,
Off,
Archive,
#[serde(other)]
Unknown,
}
#[derive(Debug, Deserialize)]
struct Network {
ip_address: IpAddr,
#[serde(rename = "type")]
kind: NetworkKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
enum NetworkKind {
Public,
Private,
}
#[derive(Debug, Deserialize, Default)]
pub(super) struct Networks {
#[serde(default)]
v4: Vec<Network>,
}
#[derive(Debug, Deserialize)]
pub(super) struct DropletDto {
pub(super) id: u32,
pub(super) status: DropletStatus,
#[serde(default)]
pub(super) networks: Networks,
}
#[derive(Debug)]
pub struct Droplet {
pub id: u32,
networks: Networks,
ssh_pubkey: Option<DigitalOceanSshKey>,
client: DoClient,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct DropletConfig {
pub image: String,
name: String,
region: String,
size: String,
user_data: String,
ssh_keys: Vec<u32>,
}
impl DropletConfig {
pub fn new() -> Self {
Default::default()
}
}
impl Default for DropletConfig {
fn default() -> Self {
DropletConfig {
image: DO_IMAGE.to_string(),
name: "innisfree".to_string(),
region: DO_REGION.to_string(),
size: DO_SIZE.to_string(),
user_data: String::default(),
ssh_keys: vec![],
}
}
}
impl Droplet {
pub(super) async fn new(client: DoClient, spec: &ServerSpec) -> Result<Self> {
tracing::debug!("Creating new DigitalOcean Droplet");
let mut cc = CloudConfig::new(
&spec.ssh_client_keypair,
&spec.ssh_server_keypair,
&spec.wg_mgr,
&spec.services,
)?;
let account_keys = client.list_ssh_keys().await?;
if account_keys.is_empty() {
tracing::warn!("No SSH pubkeys found via API");
}
cc.authorize_ssh_keys(account_keys.into_iter().map(|k| k.public_key));
let user_data: String = cc.try_into()?;
let do_ssh_key = client
.create_ssh_key(&spec.name, &spec.ssh_client_keypair.public)
.await?;
let ssh_keys: Vec<u32> = vec![do_ssh_key.id];
let droplet_config = DropletConfig {
name: spec.name.clone(),
user_data,
ssh_keys,
..DropletConfig::new()
};
let dto = client.create_droplet(&droplet_config).await?;
tracing::debug!("Server created, waiting for networking");
wait_for_boot(&client, dto.id).await.map(|dto| Droplet {
id: dto.id,
networks: dto.networks,
ssh_pubkey: Some(do_ssh_key),
client,
})
}
}
async fn wait_for_boot(client: &DoClient, id: u32) -> Result<DropletDto> {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
let dto = client
.get_droplet(id)
.await
.context("polling droplet boot status")?;
if dto.status == DropletStatus::Active {
return Ok(dto);
}
tracing::info!("Server still booting, waiting...");
}
}
#[async_trait]
impl InnisfreeServer for Droplet {
fn ipv4_address(&self) -> Result<IpAddr> {
self.networks
.v4
.iter()
.find(|n| n.kind == NetworkKind::Public)
.map(|n| n.ip_address)
.ok_or_else(|| anyhow!("droplet {} has no public IPv4 network", self.id))
}
async fn assign_floating_ip(&self, floating_ip: IpAddr) -> Result<()> {
let f = FloatingIp {
ip: floating_ip,
droplet_id: self.id,
};
f.assign(&self.client).await
}
async fn destroy(&self) -> Result<()> {
if let Some(k) = &self.ssh_pubkey {
k.destroy(&self.client).await?;
} else {
tracing::warn!("No API pubkey associated with droplet, not destroying");
}
self.client.destroy_droplet(self.id).await?;
tracing::debug!("Droplet destroyed");
Ok(())
}
}