#![cfg(feature = "local-docker")]
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use serde_json::Value;
use super::local_docker::LocalDockerProvider;
use super::{Location, MachineProvider, ServerId, ServerSpec};
use crate::envoy::cloud_object::{
CloudObjectBucketCreate, CloudObjectBucketCreateInput, CloudObjectBucketCreateOutput,
CloudObjectBucketDelete, CloudObjectBucketDeleteInput, CloudObjectBucketDeleteOutput,
CloudObjectBucketExists, CloudObjectBucketExistsInput, CloudObjectBucketExistsOutput,
};
use crate::envoy::cloud_vps::{
server_status_to_output, CloudVpsCreate, CloudVpsCreateInput, CloudVpsCreateOutput,
CloudVpsDestroy, CloudVpsDestroyInput, CloudVpsDestroyOutput, CloudVpsStatus,
CloudVpsStatusInput, CloudVpsStatusOutput,
};
use crate::envoy::{AdapterFlavor, EnvoyAdapter, InternalVerb, Tier};
pub struct LocalDockerEnvoy {
provider: Arc<LocalDockerProvider>,
}
impl LocalDockerEnvoy {
pub fn new(provider: LocalDockerProvider) -> Self {
Self {
provider: Arc::new(provider),
}
}
pub fn from_arc(provider: Arc<LocalDockerProvider>) -> Self {
Self { provider }
}
pub async fn cloud_vps_create(
&self,
input: CloudVpsCreateInput,
) -> Result<CloudVpsCreateOutput> {
Location::try_from(input.location.as_str())
.with_context(|| format!("cloud.vps.create: unknown location {:?}", input.location))?;
let project = self
.provider
.ensure_project("")
.await
.context("cloud.vps.create: ensure_project")?;
let spec = ServerSpec {
name: input.name,
server_type: input.server_type,
image: input.image,
location: Location::Pdx,
ssh_keys: vec![],
};
let id = self
.provider
.create_server(&project, &spec, &input.user_data)
.await
.context("cloud.vps.create: create_server")?;
Ok(CloudVpsCreateOutput { id: id.0 })
}
pub async fn cloud_vps_destroy(
&self,
input: CloudVpsDestroyInput,
) -> Result<CloudVpsDestroyOutput> {
self.provider
.destroy_server(&ServerId(input.id))
.await
.context("cloud.vps.destroy: destroy_server")?;
Ok(CloudVpsDestroyOutput::default())
}
pub async fn cloud_vps_status(
&self,
input: CloudVpsStatusInput,
) -> Result<CloudVpsStatusOutput> {
let status = self
.provider
.server_status(&ServerId(input.id))
.await
.context("cloud.vps.status: server_status")?;
Ok(server_status_to_output(status))
}
pub async fn cloud_object_bucket_create(
&self,
input: CloudObjectBucketCreateInput,
) -> Result<CloudObjectBucketCreateOutput> {
let bucket = self
.provider
.create_bucket(&input.name, Location::Pdx)
.await
.with_context(|| format!("cloud.object.bucket.create: create {:?}", input.name))?;
Ok(CloudObjectBucketCreateOutput {
endpoint: bucket.endpoint,
})
}
pub async fn cloud_object_bucket_delete(
&self,
input: CloudObjectBucketDeleteInput,
) -> Result<CloudObjectBucketDeleteOutput> {
self.provider
.delete_bucket(&input.name, Location::Pdx)
.await
.with_context(|| format!("cloud.object.bucket.delete: delete {:?}", input.name))?;
Ok(CloudObjectBucketDeleteOutput::default())
}
pub async fn cloud_object_bucket_exists(
&self,
input: CloudObjectBucketExistsInput,
) -> Result<CloudObjectBucketExistsOutput> {
use super::MachineProvider;
let exists = self
.provider
.bucket_exists(&input.name, Location::Pdx)
.await
.with_context(|| format!("cloud.object.bucket.exists: check {:?}", input.name))?;
Ok(CloudObjectBucketExistsOutput {
exists,
endpoint: if exists {
Some(self.provider.minio_endpoint().to_string())
} else {
None
},
})
}
}
#[async_trait]
impl EnvoyAdapter for LocalDockerEnvoy {
fn id(&self) -> &str {
"local-docker"
}
fn tier(&self) -> Tier {
Tier::S
}
fn flavor(&self) -> AdapterFlavor {
AdapterFlavor::Synthetic
}
fn supported_verb_ids(&self) -> Vec<&'static str> {
vec![
CloudVpsCreate::ID,
CloudVpsDestroy::ID,
CloudVpsStatus::ID,
CloudObjectBucketCreate::ID,
CloudObjectBucketDelete::ID,
CloudObjectBucketExists::ID,
]
}
async fn dispatch(&self, verb_id: &str, input: Value) -> Result<Value> {
match verb_id {
id if id == CloudVpsCreate::ID => {
let args: CloudVpsCreateInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = self.cloud_vps_create(args).await?;
Ok(serde_json::to_value(out)?)
}
id if id == CloudVpsDestroy::ID => {
let args: CloudVpsDestroyInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = self.cloud_vps_destroy(args).await?;
Ok(serde_json::to_value(out)?)
}
id if id == CloudVpsStatus::ID => {
let args: CloudVpsStatusInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = self.cloud_vps_status(args).await?;
Ok(serde_json::to_value(out)?)
}
id if id == CloudObjectBucketCreate::ID => {
let args: CloudObjectBucketCreateInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = self.cloud_object_bucket_create(args).await?;
Ok(serde_json::to_value(out)?)
}
id if id == CloudObjectBucketDelete::ID => {
let args: CloudObjectBucketDeleteInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = self.cloud_object_bucket_delete(args).await?;
Ok(serde_json::to_value(out)?)
}
id if id == CloudObjectBucketExists::ID => {
let args: CloudObjectBucketExistsInput =
serde_json::from_value(input).with_context(|| format!("{id}: decode input"))?;
let out = self.cloud_object_bucket_exists(args).await?;
Ok(serde_json::to_value(out)?)
}
other => bail!("local-docker envoy does not support verb {other:?}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envoy_verb_ids_cover_both_categories() {
let ids = vec![
CloudVpsCreate::ID,
CloudVpsDestroy::ID,
CloudVpsStatus::ID,
CloudObjectBucketCreate::ID,
CloudObjectBucketDelete::ID,
CloudObjectBucketExists::ID,
];
for id in &ids {
assert!(
id.starts_with("cloud."),
"expected cloud.* prefix, got {id}"
);
}
assert_eq!(ids.len(), 6);
}
#[test]
fn envoy_id_and_tier_are_constants() {
assert_eq!("local-docker", "local-docker"); assert_eq!(Tier::S, Tier::S); assert_eq!(AdapterFlavor::Synthetic, AdapterFlavor::Synthetic); }
}