use crate::utils::ResetTimerBackoff;
use async_trait::async_trait;
use backoff::{backoff::Backoff, ExponentialBackoff};
use derivative::Derivative;
use futures::{stream::BoxStream, Stream, StreamExt};
use kube_client::{
api::{ListParams, Resource, ResourceExt, VersionMatch, WatchEvent, WatchParams},
core::{metadata::PartialObjectMeta, ObjectList},
error::ErrorResponse,
Api, Error as ClientErr,
};
use serde::de::DeserializeOwned;
use smallvec::SmallVec;
use std::{clone::Clone, fmt::Debug, time::Duration};
use thiserror::Error;
use tracing::{debug, error, warn};
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to perform initial object list: {0}")]
InitialListFailed(#[source] kube_client::Error),
#[error("failed to start watching object: {0}")]
WatchStartFailed(#[source] kube_client::Error),
#[error("error returned by apiserver during watch: {0}")]
WatchError(#[source] ErrorResponse),
#[error("watch stream failed: {0}")]
WatchFailed(#[source] kube_client::Error),
#[error("no metadata.resourceVersion in watch result (does resource support watch?)")]
NoResourceVersion,
#[error("too many objects matched search criteria")]
TooManyObjects,
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub enum Event<K> {
Applied(K),
Deleted(K),
Restarted(Vec<K>),
}
impl<K> Event<K> {
pub fn into_iter_applied(self) -> impl Iterator<Item = K> {
match self {
Event::Applied(obj) => SmallVec::from_buf([obj]),
Event::Deleted(_) => SmallVec::new(),
Event::Restarted(objs) => SmallVec::from_vec(objs),
}
.into_iter()
}
pub fn into_iter_touched(self) -> impl Iterator<Item = K> {
match self {
Event::Applied(obj) | Event::Deleted(obj) => SmallVec::from_buf([obj]),
Event::Restarted(objs) => SmallVec::from_vec(objs),
}
.into_iter()
}
#[must_use]
pub fn modify(mut self, mut f: impl FnMut(&mut K)) -> Self {
match &mut self {
Event::Applied(obj) | Event::Deleted(obj) => (f)(obj),
Event::Restarted(objs) => {
for k in objs {
(f)(k)
}
}
}
self
}
}
#[derive(Derivative)]
#[derivative(Debug)]
enum State<K> {
Empty {
continue_token: Option<String>,
objects: Vec<K>,
},
IntialWatch {
objects: Vec<K>,
#[derivative(Debug = "ignore")]
stream: BoxStream<'static, kube_client::Result<WatchEvent<K>>>,
},
InitListed { resource_version: String },
Watching {
resource_version: String,
#[derivative(Debug = "ignore")]
stream: BoxStream<'static, kube_client::Result<WatchEvent<K>>>,
},
}
impl<K: Resource + Clone> Default for State<K> {
fn default() -> Self {
Self::Empty {
continue_token: None,
objects: vec![],
}
}
}
#[async_trait]
trait ApiMode {
type Value: Clone;
async fn list(&self, lp: &ListParams) -> kube_client::Result<ObjectList<Self::Value>>;
async fn watch(
&self,
wp: &WatchParams,
version: &str,
) -> kube_client::Result<BoxStream<'static, kube_client::Result<WatchEvent<Self::Value>>>>;
}
struct FullObject<'a, K> {
api: &'a Api<K>,
}
#[derive(Clone, Default, Debug, PartialEq)]
pub enum ListSemantic {
#[default]
MostRecent,
Any,
}
#[derive(Clone, Default, Debug, PartialEq)]
pub enum InitialListStrategy {
#[default]
ListWatch,
StreamingList,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
pub label_selector: Option<String>,
pub field_selector: Option<String>,
pub timeout: Option<u32>,
pub list_semantic: ListSemantic,
pub initial_list_strategy: InitialListStrategy,
pub page_size: Option<u32>,
pub bookmarks: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
bookmarks: true,
label_selector: None,
field_selector: None,
timeout: None,
list_semantic: ListSemantic::default(),
page_size: Some(500),
initial_list_strategy: InitialListStrategy::ListWatch,
}
}
}
impl Config {
#[must_use]
pub fn timeout(mut self, timeout_secs: u32) -> Self {
self.timeout = Some(timeout_secs);
self
}
#[must_use]
pub fn fields(mut self, field_selector: &str) -> Self {
self.field_selector = Some(field_selector.to_string());
self
}
#[must_use]
pub fn labels(mut self, label_selector: &str) -> Self {
self.label_selector = Some(label_selector.to_string());
self
}
#[must_use]
pub fn list_semantic(mut self, semantic: ListSemantic) -> Self {
self.list_semantic = semantic;
self
}
#[must_use]
pub fn any_semantic(self) -> Self {
self.list_semantic(ListSemantic::Any)
}
#[must_use]
pub fn disable_bookmarks(mut self) -> Self {
self.bookmarks = false;
self
}
#[must_use]
pub fn page_size(mut self, page_size: u32) -> Self {
self.page_size = Some(page_size);
self
}
#[must_use]
pub fn streaming_lists(mut self) -> Self {
self.initial_list_strategy = InitialListStrategy::StreamingList;
self
}
fn to_list_params(&self) -> ListParams {
let (resource_version, version_match) = match self.list_semantic {
ListSemantic::Any => (Some("0".into()), Some(VersionMatch::NotOlderThan)),
ListSemantic::MostRecent => (None, None),
};
ListParams {
label_selector: self.label_selector.clone(),
field_selector: self.field_selector.clone(),
timeout: self.timeout,
version_match,
resource_version,
limit: self.page_size,
continue_token: None,
}
}
fn to_watch_params(&self) -> WatchParams {
WatchParams {
label_selector: self.label_selector.clone(),
field_selector: self.field_selector.clone(),
timeout: self.timeout,
bookmarks: self.bookmarks,
send_initial_events: self.initial_list_strategy == InitialListStrategy::StreamingList,
}
}
}
#[async_trait]
impl<K> ApiMode for FullObject<'_, K>
where
K: Clone + Debug + DeserializeOwned + Send + 'static,
{
type Value = K;
async fn list(&self, lp: &ListParams) -> kube_client::Result<ObjectList<Self::Value>> {
self.api.list(lp).await
}
async fn watch(
&self,
wp: &WatchParams,
version: &str,
) -> kube_client::Result<BoxStream<'static, kube_client::Result<WatchEvent<Self::Value>>>> {
self.api.watch(wp, version).await.map(StreamExt::boxed)
}
}
struct MetaOnly<'a, K> {
api: &'a Api<K>,
}
#[async_trait]
impl<K> ApiMode for MetaOnly<'_, K>
where
K: Clone + Debug + DeserializeOwned + Send + 'static,
{
type Value = PartialObjectMeta<K>;
async fn list(&self, lp: &ListParams) -> kube_client::Result<ObjectList<Self::Value>> {
self.api.list_metadata(lp).await
}
async fn watch(
&self,
wp: &WatchParams,
version: &str,
) -> kube_client::Result<BoxStream<'static, kube_client::Result<WatchEvent<Self::Value>>>> {
self.api.watch_metadata(wp, version).await.map(StreamExt::boxed)
}
}
#[allow(clippy::too_many_lines)] async fn step_trampolined<A>(
api: &A,
wc: &Config,
state: State<A::Value>,
) -> (Option<Result<Event<A::Value>>>, State<A::Value>)
where
A: ApiMode,
A::Value: Resource + 'static,
{
match state {
State::Empty {
continue_token,
mut objects,
} => match wc.initial_list_strategy {
InitialListStrategy::ListWatch => {
let mut lp = wc.to_list_params();
lp.continue_token = continue_token;
match api.list(&lp).await {
Ok(list) => {
objects.extend(list.items);
if let Some(continue_token) = list.metadata.continue_.filter(|s| !s.is_empty()) {
(None, State::Empty {
continue_token: Some(continue_token),
objects,
})
} else if let Some(resource_version) =
list.metadata.resource_version.filter(|s| !s.is_empty())
{
(Some(Ok(Event::Restarted(objects))), State::InitListed {
resource_version,
})
} else {
(Some(Err(Error::NoResourceVersion)), State::default())
}
}
Err(err) => {
if std::matches!(err, ClientErr::Api(ErrorResponse { code: 403, .. })) {
warn!("watch list error with 403: {err:?}");
} else {
debug!("watch list error: {err:?}");
}
(Some(Err(Error::InitialListFailed(err))), State::default())
}
}
}
InitialListStrategy::StreamingList => match api.watch(&wc.to_watch_params(), "0").await {
Ok(stream) => (None, State::IntialWatch { stream, objects }),
Err(err) => {
if std::matches!(err, ClientErr::Api(ErrorResponse { code: 403, .. })) {
warn!("watch initlist error with 403: {err:?}");
} else {
debug!("watch initlist error: {err:?}");
}
(Some(Err(Error::WatchStartFailed(err))), State::default())
}
},
},
State::IntialWatch {
mut objects,
mut stream,
} => {
match stream.next().await {
Some(Ok(WatchEvent::Added(obj) | WatchEvent::Modified(obj))) => {
objects.push(obj);
(None, State::IntialWatch { objects, stream })
}
Some(Ok(WatchEvent::Deleted(obj))) => {
objects.retain(|o| o.name_any() != obj.name_any() && o.namespace() != obj.namespace());
(None, State::IntialWatch { objects, stream })
}
Some(Ok(WatchEvent::Bookmark(bm))) => {
let marks_initial_end = bm.metadata.annotations.contains_key("k8s.io/initial-events-end");
if marks_initial_end {
(Some(Ok(Event::Restarted(objects))), State::Watching {
resource_version: bm.metadata.resource_version,
stream,
})
} else {
(None, State::Watching {
resource_version: bm.metadata.resource_version,
stream,
})
}
}
Some(Ok(WatchEvent::Error(err))) => {
let new_state = if err.code == 410 {
State::default()
} else {
State::IntialWatch { objects, stream }
};
if err.code == 403 {
warn!("watcher watchevent error 403: {err:?}");
} else {
debug!("error watchevent error: {err:?}");
}
(Some(Err(Error::WatchError(err))), new_state)
}
Some(Err(err)) => {
if std::matches!(err, ClientErr::Api(ErrorResponse { code: 403, .. })) {
warn!("watcher error 403: {err:?}");
} else {
debug!("watcher error: {err:?}");
}
(Some(Err(Error::WatchFailed(err))), State::IntialWatch {
objects,
stream,
})
}
None => (None, State::default()),
}
}
State::InitListed { resource_version } => {
match api.watch(&wc.to_watch_params(), &resource_version).await {
Ok(stream) => (None, State::Watching {
resource_version,
stream,
}),
Err(err) => {
if std::matches!(err, ClientErr::Api(ErrorResponse { code: 403, .. })) {
warn!("watch initlist error with 403: {err:?}");
} else {
debug!("watch initlist error: {err:?}");
}
(Some(Err(Error::WatchStartFailed(err))), State::InitListed {
resource_version,
})
}
}
}
State::Watching {
resource_version,
mut stream,
} => match stream.next().await {
Some(Ok(WatchEvent::Added(obj) | WatchEvent::Modified(obj))) => {
let resource_version = obj.resource_version().unwrap_or_default();
if resource_version.is_empty() {
(Some(Err(Error::NoResourceVersion)), State::default())
} else {
(Some(Ok(Event::Applied(obj))), State::Watching {
resource_version,
stream,
})
}
}
Some(Ok(WatchEvent::Deleted(obj))) => {
let resource_version = obj.resource_version().unwrap_or_default();
if resource_version.is_empty() {
(Some(Err(Error::NoResourceVersion)), State::default())
} else {
(Some(Ok(Event::Deleted(obj))), State::Watching {
resource_version,
stream,
})
}
}
Some(Ok(WatchEvent::Bookmark(bm))) => (None, State::Watching {
resource_version: bm.metadata.resource_version,
stream,
}),
Some(Ok(WatchEvent::Error(err))) => {
let new_state = if err.code == 410 {
State::default()
} else {
State::Watching {
resource_version,
stream,
}
};
if err.code == 403 {
warn!("watcher watchevent error 403: {err:?}");
} else {
debug!("error watchevent error: {err:?}");
}
(Some(Err(Error::WatchError(err))), new_state)
}
Some(Err(err)) => {
if std::matches!(err, ClientErr::Api(ErrorResponse { code: 403, .. })) {
warn!("watcher error 403: {err:?}");
} else {
debug!("watcher error: {err:?}");
}
(Some(Err(Error::WatchFailed(err))), State::Watching {
resource_version,
stream,
})
}
None => (None, State::InitListed { resource_version }),
},
}
}
async fn step<A>(
api: &A,
config: &Config,
mut state: State<A::Value>,
) -> (Result<Event<A::Value>>, State<A::Value>)
where
A: ApiMode,
A::Value: Resource + 'static,
{
loop {
match step_trampolined(api, config, state).await {
(Some(result), new_state) => return (result, new_state),
(None, new_state) => state = new_state,
}
}
}
pub fn watcher<K: Resource + Clone + DeserializeOwned + Debug + Send + 'static>(
api: Api<K>,
watcher_config: Config,
) -> impl Stream<Item = Result<Event<K>>> + Send {
futures::stream::unfold(
(api, watcher_config, State::default()),
|(api, watcher_config, state)| async {
let (event, state) = step(&FullObject { api: &api }, &watcher_config, state).await;
Some((event, (api, watcher_config, state)))
},
)
}
#[allow(clippy::module_name_repetitions)]
pub fn metadata_watcher<K: Resource + Clone + DeserializeOwned + Debug + Send + 'static>(
api: Api<K>,
watcher_config: Config,
) -> impl Stream<Item = Result<Event<PartialObjectMeta<K>>>> + Send {
futures::stream::unfold(
(api, watcher_config, State::default()),
|(api, watcher_config, state)| async {
let (event, state) = step(&MetaOnly { api: &api }, &watcher_config, state).await;
Some((event, (api, watcher_config, state)))
},
)
}
pub fn watch_object<K: Resource + Clone + DeserializeOwned + Debug + Send + 'static>(
api: Api<K>,
name: &str,
) -> impl Stream<Item = Result<Option<K>>> + Send {
watcher(api, Config::default().fields(&format!("metadata.name={name}"))).map(|event| match event? {
Event::Deleted(_) => Ok(None),
Event::Restarted(objs) if objs.len() > 1 => Err(Error::TooManyObjects),
Event::Restarted(mut objs) => Ok(objs.pop()),
Event::Applied(obj) => Ok(Some(obj)),
})
}
#[must_use]
#[deprecated(
since = "0.84.0",
note = "replaced by `watcher::DefaultBackoff`. This fn will be removed in 0.88.0."
)]
pub fn default_backoff() -> DefaultBackoff {
DefaultBackoff::default()
}
pub struct DefaultBackoff(Strategy);
type Strategy = ResetTimerBackoff<ExponentialBackoff>;
impl Default for DefaultBackoff {
fn default() -> Self {
Self(ResetTimerBackoff::new(
backoff::ExponentialBackoff {
initial_interval: Duration::from_millis(800),
max_interval: Duration::from_secs(30),
randomization_factor: 1.0,
multiplier: 2.0,
max_elapsed_time: None,
..ExponentialBackoff::default()
},
Duration::from_secs(120),
))
}
}
impl Backoff for DefaultBackoff {
fn next_backoff(&mut self) -> Option<Duration> {
self.0.next_backoff()
}
fn reset(&mut self) {
self.0.reset()
}
}