Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EventRecorder abstraction. #653

Merged
merged 18 commits into from
Oct 17, 2021
22 changes: 22 additions & 0 deletions kube-runtime/src/event_recorder/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::event_recorder::EventType;

/// Required information to publish a new event via [`EventRecorder::publish`].
///
/// [`EventRecorder::publish`]: crate::event_recorder::EventRecorder::publish
pub struct NewEvent {
/// The action that was taken (either successfully or unsuccessfully) against
/// the references object.
///
/// `action` must be machine-readable.
pub action: String,
/// The reason explaining why the `action` was taken.
///
/// `reason` must be human-readable.
pub reason: String,
/// A optional description of the status of the `action`.
///
/// `note` must be human-readable.
pub note: Option<String>,
/// The event severity.
pub event_type: EventType,
}
24 changes: 24 additions & 0 deletions kube-runtime/src/event_recorder/event_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::event_recorder::InstanceName;

/// Details about the event emitter.
///
/// ```rust
/// use std::convert::TryInto;
/// use kube_runtime::event_recorder::EventSource;
///
/// let event_source = EventSource {
/// instance_name: "my-awesome-controller-abcdef".try_into().unwrap(),
/// controller_name: "my-awesome-controller".into(),
/// };
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct EventSource {
/// The name of the controller publishing the event.
///
/// E.g. `my-awesome-controller`.
pub controller_name: String,
/// The name of the controller instance publishing the event.
///
/// E.g. `my-awesome-controller-abcdef`.
pub instance_name: InstanceName,
}
8 changes: 8 additions & 0 deletions kube-runtime/src/event_recorder/event_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// The event severity or type.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum EventType {
/// An event took place - nothing to worry about.
Normal,
/// Something is not working as expected - it might be worth to have a look.
Warning,
}
68 changes: 68 additions & 0 deletions kube-runtime/src/event_recorder/instance_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::{convert::TryFrom, fmt::Formatter};

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
/// The name of the controller instance publishing the event.
///
/// ```rust
/// use std::convert::TryInto;
/// use kube_runtime::event_recorder::InstanceName;
///
/// let instance_name: InstanceName = "my-awesome-controller-abcdef".try_into().unwrap();
/// ```
///
/// It must be:
///
/// - shorter than 128 characters.
pub struct InstanceName(String);

impl TryFrom<&str> for InstanceName {
type Error = InstanceNameParsingError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_from(value.to_string())
}
}

impl TryFrom<String> for InstanceName {
type Error = InstanceNameParsingError;

fn try_from(v: String) -> Result<Self, Self::Error> {
// Limit imposed by Kubernetes' API
let n_chars = v.chars().count();
if n_chars > 128 {
Err(InstanceNameParsingError { instance_name: v })
} else {
Ok(Self(v))
}
}
}

impl AsRef<str> for InstanceName {
fn as_ref(&self) -> &str {
&self.0
}
}

impl Into<String> for InstanceName {
fn into(self) -> String {
self.0
}
}

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct InstanceNameParsingError {
instance_name: String,
}

impl std::fmt::Display for InstanceNameParsingError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let n_chars = self.instance_name.chars().count();
write!(
f,
"The reporting instance name must be shorter than 128 characters.\n{} is {} characters long.",
self.instance_name, n_chars
)
}
}

impl std::error::Error for InstanceNameParsingError {}
11 changes: 11 additions & 0 deletions kube-runtime/src/event_recorder/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub use event::NewEvent;
pub use event_source::EventSource;
pub use event_type::EventType;
pub use instance_name::InstanceName;
pub use recorder::EventRecorder;

mod event;
mod event_source;
mod event_type;
mod instance_name;
mod recorder;
105 changes: 105 additions & 0 deletions kube-runtime/src/event_recorder/recorder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::event_recorder::{event::NewEvent, EventSource, EventType};
use k8s_openapi::{
api::{core::v1::ObjectReference, events::v1::Event},
apimachinery::pkg::apis::meta::v1::{MicroTime, ObjectMeta},
chrono::Utc,
};
use kube::{api::PostParams, Api, Client};

#[derive(Clone)]
/// A publisher abstraction to emit Kubernetes' events.
///
/// All events emitted by an `EventRecorder` are attached to the [`ObjectReference`]
/// specified when building the recorder using [`EventRecorder::new`].
///
/// ```rust
/// use std::convert::TryInto;
/// use kube_runtime::event_recorder::{EventSource, EventRecorder, NewEvent, EventType};
/// use k8s_openapi::api::core::v1::ObjectReference;
///
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
/// # let k8s_client: kube::Client = todo!();
/// let event_source = EventSource {
/// instance_name: "my-awesome-controller-abcdef".try_into().unwrap(),
/// controller_name: "my-awesome-controller".into(),
/// };
///
/// // You can populate this using `ObjectMeta` and `ApiResource` information
/// // from the object you are working with.
/// let object_reference = ObjectReference {
/// // [...]
/// # ..Default::default()
/// };
///
/// let event_recorder = EventRecorder::new(k8s_client, event_source, object_reference);
/// event_recorder.publish(NewEvent {
/// action: "Scheduling".into(),
/// reason: "Pulling".into(),
/// note: Some("Pulling image `nginx`".into()),
/// event_type: EventType::Normal
/// }).await?;
/// # Ok(())
/// # }
/// ```
///
/// Events attached to an object will be shown in the `Events` section of the output of
/// of `kubectl describe` for that object.
pub struct EventRecorder {
event_client: Api<Event>,
event_source: EventSource,
object_reference: ObjectReference,
}

impl EventRecorder {
/// Build a new [`EventRecorder`] instance to emit events attached to the
/// specified [`ObjectReference`].
pub fn new(k8s_client: Client, event_source: EventSource, object_reference: ObjectReference) -> Self {
let event_client = match object_reference.namespace.as_ref() {
None => Api::all(k8s_client),
Some(namespace) => Api::namespaced(k8s_client, namespace),
};
Self {
event_client,
event_source,
object_reference,
}
}

/// Publish a new Kubernetes' event.
///
/// # Access control
///
/// The event object is created in the same namespace of the [`ObjectReference`]
/// you specified in [`EventRecorder::new`].
/// Make sure that your controller has `create` permissions in the required namespaces
/// for the `event` resource in the API group `events.k8s.io`.
pub async fn publish(&self, new_event: NewEvent) -> Result<(), kube::Error> {
self.event_client
.create(&PostParams::default(), &Event {
action: Some(new_event.action),
reason: Some(new_event.reason),
deprecated_count: None,
deprecated_first_timestamp: None,
deprecated_last_timestamp: None,
deprecated_source: None,
event_time: MicroTime(Utc::now()),
regarding: Some(self.object_reference.clone()),
note: new_event.note,
metadata: ObjectMeta {
namespace: Some(self.object_reference.namespace.clone().unwrap()),
generate_name: Some(format!("{}-", self.event_source.controller_name)),
..Default::default()
},
reporting_controller: Some(self.event_source.controller_name.clone()),
reporting_instance: Some(self.event_source.instance_name.clone().into()),
series: None,
type_: match new_event.event_type {
EventType::Normal => Some("Normal".into()),
EventType::Warning => Some("Warning".into()),
},
related: None,
})
.await?;
Ok(())
}
}
1 change: 1 addition & 0 deletions kube-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#![allow(clippy::semicolon_if_nothing_returned)]

pub mod controller;
pub mod event_recorder;
pub mod finalizer;
pub mod reflector;
pub mod scheduler;
Expand Down