use std::sync::Arc;
use std::time::Duration;
use engenho_etcd::pb::mvccpb::{Event, KeyValue};
use engenho_etcd::server::{
EtcdReadStore, EtcdRevision, EtcdStatusStore, EtcdWatchStore, OpenedWatch, RangeAt, StoreGone,
WatchEnd, WatchFeed, WatchStart, WatchStep,
};
use engenho_store::resource::ResourceKey;
use engenho_store::watch_backend::{WATCH_CHANNEL_CAPACITY, WatchGone, WatchOpts, WatchSignal};
use engenho_store::{DEFAULT_HISTORY_CAPACITY, Revision, StoreMesh, WatchStream};
#[must_use]
pub fn registry_path(key: &ResourceKey) -> Option<String> {
let d = engenho_types::generated_v1_34::catalog::RESOURCE_CATALOG
.iter()
.find(|d| d.group == key.group && d.kind == key.kind)?;
Some(
engenho_etcd::keyspace::object_key(
d.group,
d.plural,
d.namespaced,
key.namespace.as_deref(),
&key.name,
)
.key,
)
}
fn registry_path_under(key: &ResourceKey, prefix: &str) -> Option<String> {
registry_path(key).filter(|path| path.starts_with(prefix))
}
fn to_kv(
path: String,
value: &serde_json::Value,
meta: &engenho_store::revision::VersionMeta,
) -> KeyValue {
KeyValue {
key: path.into_bytes(),
create_revision: i64::try_from(meta.create_revision.0).unwrap_or(i64::MAX),
mod_revision: i64::try_from(meta.mod_revision.0).unwrap_or(i64::MAX),
version: i64::try_from(meta.version).unwrap_or(i64::MAX),
value: serde_json::to_vec(value).unwrap_or_default(),
lease: 0,
}
}
#[derive(Clone)]
pub struct MeshEtcdStore {
store: std::sync::Weak<StoreMesh>,
}
impl std::fmt::Debug for MeshEtcdStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MeshEtcdStore").finish_non_exhaustive()
}
}
impl MeshEtcdStore {
#[must_use]
pub fn new(store: &Arc<StoreMesh>) -> Self {
Self {
store: Arc::downgrade(store),
}
}
fn live(&self) -> Result<Arc<StoreMesh>, StoreGone> {
self.store.upgrade().ok_or(StoreGone)
}
async fn range_under(&self, prefix: &str) -> Result<RangeAt, StoreGone> {
let store = self.live()?;
let mut hits = Vec::new();
let read_at = store
.for_each_resource(|key, value, meta| {
if let Some(path) = registry_path_under(key, prefix) {
hits.push((path, value.clone(), meta));
}
})
.await;
let mut kvs: Vec<KeyValue> = hits
.into_iter()
.map(|(path, value, meta)| to_kv(path, &value, &meta))
.collect();
kvs.sort_by(|a, b| a.key.cmp(&b.key));
Ok(RangeAt {
kvs,
revision: wire_revision(read_at),
})
}
}
fn wire_revision(revision: Revision) -> i64 {
i64::try_from(revision.0).unwrap_or(i64::MAX)
}
#[tonic::async_trait]
impl EtcdRevision for MeshEtcdStore {
async fn revision(&self) -> Result<i64, StoreGone> {
Ok(wire_revision(self.live()?.current_revision().await))
}
}
#[tonic::async_trait]
impl EtcdReadStore for MeshEtcdStore {
async fn range_at(&self, prefix: &str) -> Result<RangeAt, StoreGone> {
self.range_under(prefix).await
}
}
#[tonic::async_trait]
impl EtcdStatusStore for MeshEtcdStore {
async fn applied_index(&self) -> Result<u64, StoreGone> {
Ok(self.live()?.last_applied_index().await)
}
async fn db_size(&self) -> Result<Option<i64>, StoreGone> {
let store = self.live()?;
let mut served = Vec::new();
store
.for_each_resource(|key, value, _| {
if let Some(path) = registry_path(key) {
served.push((path.len(), value.clone()));
}
})
.await;
let bytes = served.iter().fold(0usize, |total, (path_len, value)| {
total
.saturating_add(*path_len)
.saturating_add(serialized_len(value))
});
Ok(Some(i64::try_from(bytes).unwrap_or(i64::MAX)))
}
}
const FEED_BUFFER: usize = DEFAULT_HISTORY_CAPACITY + WATCH_CHANNEL_CAPACITY;
#[tonic::async_trait]
impl EtcdWatchStore for MeshEtcdStore {
type Feed = MeshWatchFeed;
async fn watch_from(
&self,
prefix: &str,
start: WatchStart,
) -> Result<OpenedWatch<MeshWatchFeed>, WatchEnd> {
let store = self.live()?;
let current = store.current_revision().await;
let after = match start {
WatchStart::Now => current,
WatchStart::At(first) => Revision(first.get() - 1),
WatchStart::BeforeHistory => {
return Err(WatchEnd::Compacted {
compact_revision: watermark(store.compacted_revision().await),
});
}
};
let from = after.min(current);
let stream = store
.watch_from(WatchOpts {
from,
buffer: FEED_BUFFER,
bookmark_every: Duration::ZERO,
})
.await
.map_err(|gone| watch_end(&gone))?;
Ok(OpenedWatch {
revision: wire_revision(current),
feed: MeshWatchFeed {
stream,
prefix: prefix.to_owned(),
after,
},
})
}
}
#[derive(Debug)]
pub struct MeshWatchFeed {
stream: WatchStream,
prefix: String,
after: Revision,
}
#[tonic::async_trait]
impl WatchFeed for MeshWatchFeed {
async fn next(&mut self) -> WatchStep {
loop {
match self.stream.next().await {
Some(Ok(WatchSignal::Event(event))) => {
if event.resource_version <= self.after.0 {
continue;
}
if let Some(rendered) = watch_event_to_event(&event, &self.prefix) {
return WatchStep::Event(rendered);
}
}
Some(Ok(WatchSignal::Bookmark(_))) => {}
Some(Err(gone)) => return WatchStep::End(watch_end(&gone)),
None => return WatchStep::End(WatchEnd::StoreGone(StoreGone)),
}
}
}
}
fn watch_end(gone: &WatchGone) -> WatchEnd {
match *gone {
WatchGone::CompactedTooOld { compacted, .. } => WatchEnd::Compacted {
compact_revision: watermark(compacted),
},
WatchGone::Overflow { last_seen, .. } => WatchEnd::Overflow {
last_seen: wire_revision(last_seen),
},
WatchGone::AheadOfStore { current, .. } => WatchEnd::Overflow {
last_seen: wire_revision(current),
},
}
}
fn watermark(compacted: engenho_store::Revision) -> i64 {
i64::try_from(compacted.0).unwrap_or(0)
}
fn serialized_len(value: &serde_json::Value) -> usize {
struct Count(usize);
impl std::io::Write for Count {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0 = self.0.saturating_add(buf.len());
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut count = Count(0);
match serde_json::to_writer(&mut count, value) {
Ok(()) => count.0,
Err(_) => 0,
}
}
fn watch_event_to_event(event: &engenho_store::watch::WatchEvent, prefix: &str) -> Option<Event> {
use engenho_store::watch::WatchEventKind;
let path = registry_path_under(&event.key, prefix)?;
let rev = i64::try_from(event.resource_version).unwrap_or(i64::MAX);
let deleted = matches!(event.kind, WatchEventKind::Deleted);
Some(Event {
r#type: i32::from(deleted),
kv: Some(KeyValue {
key: path.into_bytes(),
create_revision: 0,
mod_revision: rev,
version: 0,
value: if deleted {
Vec::new()
} else {
serde_json::to_vec(&event.object).unwrap_or_default()
},
lease: 0,
}),
prev_kv: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_plural_comes_from_the_catalog_not_from_appending_an_s() {
let ep = registry_path(&ResourceKey::namespaced(
"",
"v1",
"Endpoints",
"default",
"kubernetes",
))
.expect("Endpoints is in the catalog");
assert_eq!(ep, "/registry/services/endpoints/default/kubernetes");
assert!(!ep.contains("endpointss"), "{ep}");
let np = registry_path(&ResourceKey::namespaced(
"networking.k8s.io",
"v1",
"NetworkPolicy",
"ns",
"deny",
))
.expect("NetworkPolicy is in the catalog");
assert!(np.ends_with("networkpolicies/ns/deny"), "{np}");
}
#[test]
fn a_node_lands_under_minions_the_pre_1_0_name() {
let p = registry_path(&ResourceKey::cluster_scoped("", "v1", "Node", "cid"))
.expect("Node is in the catalog");
assert_eq!(p, "/registry/minions/cid");
}
#[test]
fn a_kind_outside_the_catalog_has_no_path_rather_than_a_guessed_one() {
assert!(
registry_path(&ResourceKey::cluster_scoped(
"example.com",
"v1",
"Widget",
"w1"
))
.is_none()
);
}
#[test]
fn a_grouped_kind_carries_its_group_and_a_groupless_one_does_not() {
let crd = registry_path(&ResourceKey::cluster_scoped(
"apiextensions.k8s.io",
"v1",
"CustomResourceDefinition",
"widgets.example.com",
))
.unwrap();
assert!(crd.contains("apiextensions.k8s.io"), "{crd}");
let role = registry_path(&ResourceKey::namespaced(
"rbac.authorization.k8s.io",
"v1",
"Role",
"ns",
"r",
))
.unwrap();
assert!(
!role.contains("rbac.authorization.k8s.io"),
"rbac is groupless in the keyspace: {role}"
);
}
use engenho_store::command::{Reason, ResourceCommand};
use engenho_store::{InProcessRouter, default_config};
use serde_json::json;
async fn boot(cluster: &str) -> Arc<StoreMesh> {
let store = StoreMesh::start(
1,
"in-process://1".into(),
InProcessRouter::new(),
default_config(cluster).expect("config"),
)
.await
.expect("start");
store.initialize_singleton().await.expect("initialize");
assert!(
store
.wait_for_leadership(std::time::Duration::from_secs(5))
.await
);
Arc::new(store)
}
async fn put(store: &StoreMesh, key: ResourceKey, value: serde_json::Value) {
store
.propose(ResourceCommand::Put {
key,
value,
expected: None,
reason: Reason::Operator,
})
.await
.expect("propose");
}
fn pod(ns: &str, name: &str) -> ResourceKey {
ResourceKey::namespaced("", "v1", "Pod", ns, name)
}
async fn seed(store: &StoreMesh) {
put(store, pod("default", "web"), json!({ "spec": { "n": 1 } })).await;
put(
store,
pod("kube-system", "dns"),
json!({ "spec": { "n": "é\"" } }),
)
.await;
put(
store,
ResourceKey::namespaced("", "v1", "Secret", "default", "s"),
json!({ "data": { "k": "dg==" } }),
)
.await;
put(
store,
ResourceKey::cluster_scoped("example.com", "v1", "Widget", "w"),
json!({}),
)
.await;
}
#[test]
fn serialized_len_counts_exactly_what_to_vec_writes() {
for value in [
json!(null),
json!(-1.5e300),
json!("é\"\\\n\u{1F600}"),
json!([1, [2, { "k": "v" }]]),
json!({ "metadata": { "name": "web", "labels": { "a": "b" } },
"spec": { "containers": [{ "image": "alpine" }] } }),
] {
let bytes = serde_json::to_vec(&value).expect("a Value serializes");
assert_eq!(serialized_len(&value), bytes.len(), "{value}");
}
}
#[tokio::test]
async fn db_size_is_the_serialized_size_of_every_object_served() {
let store = boot("facade-db-size").await;
seed(&store).await;
let mut expected = 0usize;
for kind in ["Pod", "Secret"] {
for (key, value) in store.list("", "v1", kind, None).await {
let path = registry_path(&key).expect("a built-in kind");
expected += path.len() + serde_json::to_vec(&value).expect("json").len();
}
}
let facade = MeshEtcdStore::new(&store);
assert_eq!(
EtcdStatusStore::db_size(&facade).await,
Ok(Some(i64::try_from(expected).expect("small"))),
"the paths and bodies of the three served objects, and not the Widget"
);
assert_eq!(
EtcdStatusStore::applied_index(&facade).await,
Ok(store.last_applied_index().await)
);
}
#[tokio::test]
async fn a_range_is_exactly_the_prefix_in_byte_order() {
let store = boot("facade-range").await;
seed(&store).await;
let facade = MeshEtcdStore::new(&store);
let keys = |kvs: Vec<KeyValue>| -> Vec<String> {
kvs.into_iter()
.map(|kv| String::from_utf8(kv.key).expect("utf8"))
.collect()
};
assert_eq!(
keys(
EtcdReadStore::range(&facade, "/registry/pods/")
.await
.expect("live")
),
vec![
"/registry/pods/default/web".to_owned(),
"/registry/pods/kube-system/dns".to_owned(),
]
);
assert_eq!(
keys(
EtcdReadStore::range(&facade, "/registry/")
.await
.expect("live")
),
vec![
"/registry/pods/default/web".to_owned(),
"/registry/pods/kube-system/dns".to_owned(),
"/registry/secrets/default/s".to_owned(),
],
"every served object, byte-ordered; the Widget has no path"
);
let web = EtcdReadStore::range(&facade, "/registry/pods/default/")
.await
.expect("live");
let (value, meta) = store
.get_with_meta(&pod("default", "web"))
.await
.expect("stored");
assert_eq!(
web,
vec![to_kv(
"/registry/pods/default/web".to_owned(),
&value,
&meta
)]
);
let at = EtcdReadStore::range_at(&facade, "/registry/")
.await
.expect("live");
assert_eq!(at.revision, 4, "the revision the keys were read at");
assert_eq!(
at.revision,
wire_revision(store.current_revision().await),
"nothing has been written since"
);
}
use engenho_etcd::pb::etcdserverpb::kv_server::Kv as _;
use engenho_etcd::pb::etcdserverpb::maintenance_server::Maintenance as _;
use engenho_etcd::pb::etcdserverpb::{RangeRequest, StatusRequest};
use engenho_etcd::server::{MaintenanceSvc, ReadOnlyKv, ServerIdentity};
use std::num::NonZeroU64;
fn at(revision: u64) -> WatchStart {
WatchStart::At(NonZeroU64::new(revision).expect("a start revision is at least 1"))
}
async fn ready(feed: &mut MeshWatchFeed) -> Vec<i64> {
let mut revisions = Vec::new();
while let Ok(step) =
tokio::time::timeout(std::time::Duration::from_millis(200), feed.next()).await
{
match step {
WatchStep::Event(event) => {
revisions.push(event.kv.expect("an event carries its kv").mod_revision);
}
WatchStep::End(end) => panic!("the watch ended early: {end}"),
}
}
revisions
}
async fn open(facade: &MeshEtcdStore, prefix: &str, start: WatchStart) -> MeshWatchFeed {
EtcdWatchStore::watch_from(facade, prefix, start)
.await
.expect("the watch opens")
.feed
}
async fn stop(store: Arc<StoreMesh>) {
Arc::try_unwrap(store)
.map_err(|_| "the facade is holding a strong reference to the store")
.expect("reclaimable")
.terminate()
.await
.expect("terminate");
}
#[tokio::test]
async fn a_watch_replays_its_prefix_then_tails_live_with_nothing_lost_or_doubled() {
let store = boot("facade-watch").await;
seed(&store).await;
let facade = MeshEtcdStore::new(&store);
let mut everything = open(&facade, "/registry/", at(1)).await;
assert_eq!(ready(&mut everything).await, vec![1, 2, 3]);
let mut pods = open(&facade, "/registry/pods/", at(2)).await;
assert_eq!(
ready(&mut pods).await,
vec![2],
"from the start, and only under the prefix"
);
let now = EtcdWatchStore::watch_from(&facade, "/registry/", WatchStart::Now)
.await
.expect("opens");
assert_eq!(now.revision, 4, "acknowledged at the store's revision");
let mut now = now.feed;
assert_eq!(
ready(&mut now).await,
Vec::<i64>::new(),
"no history for Now"
);
put(&store, pod("default", "late"), json!({ "spec": {} })).await;
for feed in [&mut everything, &mut pods, &mut now] {
assert_eq!(ready(feed).await, vec![5], "the live change, exactly once");
}
assert_eq!(
EtcdWatchStore::watch_from(&facade, "/registry/", WatchStart::BeforeHistory)
.await
.err(),
Some(WatchEnd::Compacted {
compact_revision: 0
}),
);
}
#[tokio::test]
async fn a_start_ahead_of_the_store_delivers_nothing_before_it() {
let store = boot("facade-ahead").await;
seed(&store).await;
let facade = MeshEtcdStore::new(&store);
let mut ahead = open(&facade, "/registry/", at(7)).await;
assert_eq!(ready(&mut ahead).await, Vec::<i64>::new());
for name in ["a", "b", "c", "d"] {
put(&store, pod("default", name), json!({})).await;
}
assert_eq!(
ready(&mut ahead).await,
vec![7, 8],
"revisions 5 and 6 precede the requested start"
);
}
#[tokio::test]
async fn a_resume_point_below_the_watermark_is_refused_with_it() {
let dir = tempfile::tempdir().expect("tempdir");
{
let store = StoreMesh::start_durable(
1,
"in-process://1".into(),
InProcessRouter::new(),
default_config("facade-gone").expect("config"),
dir.path(),
)
.await
.expect("start");
store.initialize_singleton().await.expect("initialize");
assert!(
store
.wait_for_leadership(std::time::Duration::from_secs(5))
.await
);
seed(&store).await;
store.terminate().await.expect("terminate");
}
let (store, _) = StoreMesh::start_or_resume(
1,
"in-process://1".into(),
InProcessRouter::new(),
default_config("facade-gone").expect("config"),
dir.path(),
)
.await
.expect("resume");
assert!(
store
.wait_for_leadership(std::time::Duration::from_secs(5))
.await
);
assert_eq!(store.compacted_revision().await, Revision(4));
let store = Arc::new(store);
let facade = MeshEtcdStore::new(&store);
let facade = &facade;
let refused = |start| async move {
EtcdWatchStore::watch_from(facade, "/registry/", start)
.await
.err()
};
let compacted = Some(WatchEnd::Compacted {
compact_revision: 4,
});
assert_eq!(
refused(at(4)).await,
compacted,
"below the reloaded watermark"
);
assert_eq!(
refused(WatchStart::BeforeHistory).await,
compacted,
"a negative start is below every watermark"
);
let mut from_watermark = open(facade, "/registry/", at(5)).await;
assert_eq!(
ready(&mut from_watermark).await,
Vec::<i64>::new(),
"the watermark itself is servable"
);
}
#[tokio::test]
async fn the_facade_and_its_clones_hold_no_strong_reference_and_see_one_store() {
let store = boot("facade-ownership").await;
let baseline = Arc::strong_count(&store);
let facade = MeshEtcdStore::new(&store);
let clones = [facade.clone(), facade.clone(), facade];
assert_eq!(
Arc::strong_count(&store),
baseline,
"a façade or a clone of one holds the store strongly"
);
seed(&store).await;
for clone in &clones {
let keys: Vec<String> = EtcdReadStore::range(clone, "/registry/pods/")
.await
.expect("live")
.into_iter()
.map(|kv| String::from_utf8(kv.key).expect("utf8"))
.collect();
assert_eq!(
keys,
[
"/registry/pods/default/web",
"/registry/pods/kube-system/dns"
],
"every clone reads the one store"
);
}
assert_eq!(
Arc::strong_count(&store),
baseline,
"a Range that returned still holds the store"
);
stop(store).await;
assert_eq!(
EtcdReadStore::range(&clones[0], "/registry/").await,
Err(StoreGone),
"a clone outliving the store says so"
);
}
#[tokio::test]
async fn a_dropped_store_is_unavailable_never_an_empty_answer() {
let store = boot("facade-dropped").await;
seed(&store).await;
let facade = MeshEtcdStore::new(&store);
let identity = ServerIdentity::default();
let kv = ReadOnlyKv {
store: facade.clone(),
identity,
};
let maintenance = MaintenanceSvc {
store: facade.clone(),
identity,
};
stop(store).await;
let range = kv
.range(tonic::Request::new(RangeRequest {
key: b"/registry/".to_vec(),
range_end: engenho_etcd::keyspace::prefix_range_end(b"/registry/"),
..RangeRequest::default()
}))
.await
.map(tonic::Response::into_inner);
let range = range.expect_err("a Range from a dropped store must not succeed");
assert_eq!(range.code(), tonic::Code::Unavailable);
assert_eq!(range.message(), StoreGone.to_string());
let status = maintenance
.status(tonic::Request::new(StatusRequest::default()))
.await
.map(tonic::Response::into_inner);
assert_eq!(
status.expect_err("a dropped store has no status").code(),
tonic::Code::Unavailable
);
assert_eq!(EtcdRevision::revision(&facade).await, Err(StoreGone));
assert_eq!(
EtcdReadStore::range(&facade, "/registry/").await,
Err(StoreGone)
);
assert_eq!(
EtcdStatusStore::applied_index(&facade).await,
Err(StoreGone)
);
assert_eq!(EtcdStatusStore::db_size(&facade).await, Err(StoreGone));
assert_eq!(
EtcdWatchStore::watch_from(&facade, "/registry/", WatchStart::Now)
.await
.err(),
Some(WatchEnd::StoreGone(StoreGone))
);
}
#[tokio::test]
async fn a_watch_open_when_the_store_is_dropped_is_cancelled_with_the_reason() {
use engenho_etcd::pb::etcdserverpb::watch_request::RequestUnion;
use engenho_etcd::pb::etcdserverpb::{WatchCreateRequest, WatchRequest, WatchResponse};
async fn reply(
rx: &mut tokio::sync::mpsc::Receiver<Result<WatchResponse, tonic::Status>>,
) -> WatchResponse {
tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
.await
.expect("a reply within 5s — the watch fell silent")
.expect("the stream is open")
.expect("a response, not a status")
}
let store = boot("facade-watch-dropped").await;
seed(&store).await;
let facade = Arc::new(MeshEtcdStore::new(&store));
let (req_tx, req_rx) = tokio::sync::mpsc::channel(1);
req_tx
.send(Ok(WatchRequest {
request_union: Some(RequestUnion::CreateRequest(WatchCreateRequest {
key: b"/registry/".to_vec(),
range_end: engenho_etcd::keyspace::prefix_range_end(b"/registry/"),
..WatchCreateRequest::default()
})),
}))
.await
.expect("queue");
drop(req_tx);
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
tokio::spawn(engenho_etcd::server::run_watch_loop(
facade,
ServerIdentity::default(),
tokio_stream::wrappers::ReceiverStream::new(req_rx),
tx,
));
let ack = reply(&mut rx).await;
assert!(ack.created);
stop(store).await;
let end = reply(&mut rx).await;
assert!(end.canceled, "the end of a watch is said: {end:?}");
assert_eq!(end.watch_id, ack.watch_id);
assert_eq!(end.cancel_reason, StoreGone.to_string());
}
}