//! The connection side of the egress allow-list KV bucket: opening it for //! writing or reading, and the hive's direct read. The bucket's names, subjects //! and stored type are `swarm_wire::egress_allow`. /// Open the allow-list bucket for writing, creating it if nothing has yet. /// /// **Controller-side only.** `history: 1`: a hive wants the current value. #[cfg(feature = "kv")] pub async fn open_or_create( client: &async_nats::Client, ) -> Result { let js = async_nats::jetstream::new(client.clone()); crate::kv::open_or_create( &js, swarm_wire::egress_allow::BUCKET, "egress-allow", "The destinations the swarm lets agents reach through their hive's proxy".to_owned(), ) .await } /// Open the allow-list bucket for reading, or `None` when nothing has created /// it yet, which reads the same as no list. /// /// **Controller-side**, for reads that must not create the bucket. /// /// # Errors /// /// [`crate::Error::OpenBucket`] for every failure other than the bucket not /// existing. #[cfg(feature = "kv")] pub async fn open( client: &async_nats::Client, ) -> Result, crate::Error> { let js = async_nats::jetstream::new(client.clone()); match js.get_key_value(swarm_wire::egress_allow::BUCKET).await { Ok(store) => Ok(Some(store)), Err(e) if crate::wanted::is_bucket_not_found(&e) => Ok(None), Err(source) => Err(crate::Error::OpenBucket { bucket: swarm_wire::egress_allow::BUCKET.to_owned(), source, }), } } /// What reading the list found. #[cfg(feature = "egress-allow")] #[derive(Debug)] pub enum Read { /// The controller has written a list, which may be empty. Current(swarm_wire::egress_allow::AllowList), /// There is no list to read: no bucket answers, or the key was never /// written or has been deleted. A controller that predates the bucket reads /// this way, and so does a queue that is not serving the stream right now, /// so it says nothing about what the swarm wants. NotSet, } /// Read the current list with one direct get. /// /// # Errors /// /// A failed request or an undecodable value. A request the queue refused for /// permissions reaches the client as a timeout, not as a refusal. #[cfg(feature = "egress-allow")] pub async fn read( client: &async_nats::Client, ) -> Result> { use async_nats::jetstream::stream::DirectGetErrorKind; use swarm_wire::egress_allow::{STREAM, key_subject}; let js = async_nats::jetstream::new(client.clone()); let stream = js.get_stream_no_info(STREAM).await?; let message = match stream.direct_get_last_for_subject(key_subject()).await { Ok(message) => message, Err(e) if matches!(e.kind(), DirectGetErrorKind::NotFound) => return Ok(Read::NotSet), Err(e) if is_no_responders(&e) => return Ok(Read::NotSet), Err(e) => return Err(e.into()), }; // A KV delete or purge is stored as an empty marker carrying this header. let removed = message .headers .get("KV-Operation") .is_some_and(|op| matches!(op.as_str(), "DEL" | "PURGE")); if removed { return Ok(Read::NotSet); } Ok(Read::Current(serde_json::from_slice(&message.payload)?)) } #[cfg(feature = "egress-allow")] fn is_no_responders(e: &async_nats::jetstream::stream::DirectGetError) -> bool { matches!( e.kind(), async_nats::jetstream::stream::DirectGetErrorKind::ErrorResponse(status, _) if status == async_nats::StatusCode::NO_RESPONDERS ) }