use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use serde_json::Value;
use crate::envoy::floating_ip::{
FloatingIpAssign, FloatingIpAssignInput, FloatingIpAssignOutput, FloatingIpStatus,
FloatingIpStatusInput, FloatingIpStatusOutput,
};
use crate::envoy::{AdapterFlavor, EnvoyAdapter, InternalVerb, Tier};
use floating_ip::{FloatingIpProvider, FloatingIpTarget};
use floating_ip_adapters::{HetznerFloatingIp, OvhFloatingIp, VultrFloatingIp};
#[async_trait]
pub trait FloatingIpEnvoy {
async fn floating_ip_assign(
&self,
input: FloatingIpAssignInput,
) -> Result<FloatingIpAssignOutput>;
async fn floating_ip_status(
&self,
input: FloatingIpStatusInput,
) -> Result<FloatingIpStatusOutput>;
}
#[async_trait]
impl<T: FloatingIpProvider + ?Sized> FloatingIpEnvoy for T {
async fn floating_ip_assign(
&self,
input: FloatingIpAssignInput,
) -> Result<FloatingIpAssignOutput> {
let target = FloatingIpTarget {
attach_id: input.attach_id,
zone: input.zone,
};
let outcome = floating_ip::reconcile_assignment(self, &input.ip_id, &target).await?;
Ok(FloatingIpAssignOutput {
reassigned: outcome.reassigned,
attached_to: outcome.attached_to,
})
}
async fn floating_ip_status(
&self,
input: FloatingIpStatusInput,
) -> Result<FloatingIpStatusOutput> {
let state = self.current_assignment(&input.ip_id).await?;
Ok(FloatingIpStatusOutput {
zone: state.zone,
attached_to: state.attached_to,
})
}
}
pub async fn dispatch_floating_ip_verb(
provider: &dyn FloatingIpProvider,
verb_id: &str,
input: Value,
) -> Result<Value> {
match verb_id {
id if id == FloatingIpAssign::ID => {
let args: FloatingIpAssignInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = provider.floating_ip_assign(args).await?;
Ok(serde_json::to_value(out)?)
}
id if id == FloatingIpStatus::ID => {
let args: FloatingIpStatusInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = provider.floating_ip_status(args).await?;
Ok(serde_json::to_value(out)?)
}
other => bail!(
"{} floating-ip envoy does not support verb {other:?}",
provider.id()
),
}
}
const FLOATING_IP_VERBS: [&str; 2] = [FloatingIpAssign::ID, FloatingIpStatus::ID];
#[async_trait]
impl EnvoyAdapter for HetznerFloatingIp {
fn id(&self) -> &str {
FloatingIpProvider::id(self)
}
fn tier(&self) -> Tier {
Tier::S
}
fn flavor(&self) -> AdapterFlavor {
AdapterFlavor::Native
}
fn supported_verb_ids(&self) -> Vec<&'static str> {
FLOATING_IP_VERBS.to_vec()
}
async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
dispatch_floating_ip_verb(self, verb_id, input).await
}
}
#[async_trait]
impl EnvoyAdapter for OvhFloatingIp {
fn id(&self) -> &str {
FloatingIpProvider::id(self)
}
fn tier(&self) -> Tier {
Tier::A
}
fn flavor(&self) -> AdapterFlavor {
AdapterFlavor::Native
}
fn supported_verb_ids(&self) -> Vec<&'static str> {
FLOATING_IP_VERBS.to_vec()
}
async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
dispatch_floating_ip_verb(self, verb_id, input).await
}
}
#[async_trait]
impl EnvoyAdapter for VultrFloatingIp {
fn id(&self) -> &str {
FloatingIpProvider::id(self)
}
fn tier(&self) -> Tier {
Tier::S
}
fn flavor(&self) -> AdapterFlavor {
AdapterFlavor::Native
}
fn supported_verb_ids(&self) -> Vec<&'static str> {
FLOATING_IP_VERBS.to_vec()
}
async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
dispatch_floating_ip_verb(self, verb_id, input).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_envoy_id_is_the_provider_id_for_every_adapter() {
let cases: [(&dyn EnvoyAdapter, &str); 3] = [
(&HetznerFloatingIp::new("t"), "hetzner"),
(&OvhFloatingIp::new("t"), "ovh"),
(&VultrFloatingIp::new("t"), "vultr"),
];
for (adapter, expected) in cases {
assert_eq!(EnvoyAdapter::id(adapter), expected);
assert_eq!(
adapter.supported_verb_ids(),
vec!["floating_ip.assign", "floating_ip.status"]
);
}
}
#[tokio::test]
async fn an_unknown_verb_is_refused_and_names_the_provider() {
let err = dispatch_floating_ip_verb(
&VultrFloatingIp::new("t"),
"floating_ip.detach",
serde_json::json!({}),
)
.await
.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("vultr"), "{msg}");
assert!(msg.contains("floating_ip.detach"), "{msg}");
}
}