cloud/provider/
floating_ip_envoy.rs1use anyhow::{bail, Context, Result};
37use async_trait::async_trait;
38use serde_json::Value;
39
40use crate::envoy::floating_ip::{
41 FloatingIpAssign, FloatingIpAssignInput, FloatingIpAssignOutput, FloatingIpStatus,
42 FloatingIpStatusInput, FloatingIpStatusOutput,
43};
44use crate::envoy::{AdapterFlavor, EnvoyAdapter, InternalVerb, Tier};
45use floating_ip::{FloatingIpProvider, FloatingIpTarget};
46use floating_ip_adapters::{HetznerFloatingIp, OvhFloatingIp, VultrFloatingIp};
47
48#[async_trait]
56pub trait FloatingIpEnvoy {
57 async fn floating_ip_assign(
59 &self,
60 input: FloatingIpAssignInput,
61 ) -> Result<FloatingIpAssignOutput>;
62
63 async fn floating_ip_status(
65 &self,
66 input: FloatingIpStatusInput,
67 ) -> Result<FloatingIpStatusOutput>;
68}
69
70#[async_trait]
71impl<T: FloatingIpProvider + ?Sized> FloatingIpEnvoy for T {
72 async fn floating_ip_assign(
73 &self,
74 input: FloatingIpAssignInput,
75 ) -> Result<FloatingIpAssignOutput> {
76 let target = FloatingIpTarget {
77 attach_id: input.attach_id,
78 zone: input.zone,
79 };
80 let outcome = floating_ip::reconcile_assignment(self, &input.ip_id, &target).await?;
83 Ok(FloatingIpAssignOutput {
84 reassigned: outcome.reassigned,
85 attached_to: outcome.attached_to,
86 })
87 }
88
89 async fn floating_ip_status(
90 &self,
91 input: FloatingIpStatusInput,
92 ) -> Result<FloatingIpStatusOutput> {
93 let state = self.current_assignment(&input.ip_id).await?;
94 Ok(FloatingIpStatusOutput {
95 zone: state.zone,
96 attached_to: state.attached_to,
97 })
98 }
99}
100
101pub async fn dispatch_floating_ip_verb(
108 provider: &dyn FloatingIpProvider,
109 verb_id: &str,
110 input: Value,
111) -> Result<Value> {
112 match verb_id {
113 id if id == FloatingIpAssign::ID => {
114 let args: FloatingIpAssignInput =
115 serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
116 let out = provider.floating_ip_assign(args).await?;
117 Ok(serde_json::to_value(out)?)
118 }
119 id if id == FloatingIpStatus::ID => {
120 let args: FloatingIpStatusInput =
121 serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
122 let out = provider.floating_ip_status(args).await?;
123 Ok(serde_json::to_value(out)?)
124 }
125 other => bail!(
126 "{} floating-ip envoy does not support verb {other:?}",
127 provider.id()
128 ),
129 }
130}
131
132const FLOATING_IP_VERBS: [&str; 2] = [FloatingIpAssign::ID, FloatingIpStatus::ID];
135
136#[async_trait]
137impl EnvoyAdapter for HetznerFloatingIp {
138 fn id(&self) -> &str {
139 FloatingIpProvider::id(self)
140 }
141 fn tier(&self) -> Tier {
142 Tier::S
143 }
144 fn flavor(&self) -> AdapterFlavor {
145 AdapterFlavor::Native
146 }
147 fn supported_verb_ids(&self) -> Vec<&'static str> {
148 FLOATING_IP_VERBS.to_vec()
149 }
150 async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
151 dispatch_floating_ip_verb(self, verb_id, input).await
152 }
153}
154
155#[async_trait]
156impl EnvoyAdapter for OvhFloatingIp {
157 fn id(&self) -> &str {
158 FloatingIpProvider::id(self)
159 }
160 fn tier(&self) -> Tier {
164 Tier::A
165 }
166 fn flavor(&self) -> AdapterFlavor {
167 AdapterFlavor::Native
168 }
169 fn supported_verb_ids(&self) -> Vec<&'static str> {
170 FLOATING_IP_VERBS.to_vec()
171 }
172 async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
173 dispatch_floating_ip_verb(self, verb_id, input).await
174 }
175}
176
177#[async_trait]
178impl EnvoyAdapter for VultrFloatingIp {
179 fn id(&self) -> &str {
180 FloatingIpProvider::id(self)
181 }
182 fn tier(&self) -> Tier {
183 Tier::S
184 }
185 fn flavor(&self) -> AdapterFlavor {
186 AdapterFlavor::Native
187 }
188 fn supported_verb_ids(&self) -> Vec<&'static str> {
189 FLOATING_IP_VERBS.to_vec()
190 }
191 async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
192 dispatch_floating_ip_verb(self, verb_id, input).await
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
204 fn the_envoy_id_is_the_provider_id_for_every_adapter() {
205 let cases: [(&dyn EnvoyAdapter, &str); 3] = [
206 (&HetznerFloatingIp::new("t"), "hetzner"),
207 (&OvhFloatingIp::new("t"), "ovh"),
208 (&VultrFloatingIp::new("t"), "vultr"),
209 ];
210 for (adapter, expected) in cases {
211 assert_eq!(EnvoyAdapter::id(adapter), expected);
212 assert_eq!(
213 adapter.supported_verb_ids(),
214 vec!["floating_ip.assign", "floating_ip.status"]
215 );
216 }
217 }
218
219 #[tokio::test]
223 async fn an_unknown_verb_is_refused_and_names_the_provider() {
224 let err = dispatch_floating_ip_verb(
225 &VultrFloatingIp::new("t"),
226 "floating_ip.detach",
227 serde_json::json!({}),
228 )
229 .await
230 .unwrap_err();
231 let msg = format!("{err:#}");
232 assert!(msg.contains("vultr"), "{msg}");
233 assert!(msg.contains("floating_ip.detach"), "{msg}");
234 }
235}