koprs 0.7.0

A reusable, ergonomic library that streamlines Kubernetes operator development, allowing developers to build controllers with significantly less code.
Documentation
//! Resource relationship helpers.
//! Covers two kinds of relationships between Kubernetes resources:
//!
//! - **Ownership** — [`owner_ref`], [`controller_ref`], and [`set_owner_refs`]
//!   build and attach `metadata.ownerReferences` so Kubernetes can garbage
//!   collect child resources when their owner is deleted.
//!
//! - **Controller wiring** — [`make_object_refs`], [`make_object_refs_namespaced`],
//!   [`make_object_refs_cluster`], and [`make_object_ref_mapper`] build
//!   [`ObjectRef`] sets and mapper closures for cross-resource reconcile
//!   triggers in `kube-runtime` controllers.
//!
//! See: <https://kube.rs/controllers/relations/#watched-relations>

use std::sync::Arc;

use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
use kube::{Api, Client, Resource};
use kube_runtime::reflector::ObjectRef;
use tracing::info;

use crate::error::{KubeGenericError, Result};
use crate::scope::{ApiScope, Cluster, Namespaced};
use crate::traits::{ClusterResource, KubeResource, NamespacedResource};

// ---------------------------------------------------------------------------
// Private core helpers
// ---------------------------------------------------------------------------

fn build_owner_ref<T: KubeResource>(owner: &T, controller: bool) -> Result<OwnerReference> {
    let meta = owner.meta();
    let name = meta
        .name
        .clone()
        .ok_or_else(|| KubeGenericError::MissingMetadata("name".into()))?;
    let uid = meta
        .uid
        .clone()
        .ok_or_else(|| KubeGenericError::MissingMetadata("uid".into()))?;

    Ok(OwnerReference {
        api_version: T::api_version(&()).to_string(),
        kind: T::kind(&()).to_string(),
        name,
        uid,
        controller: Some(controller),
        block_owner_deletion: Some(controller),
    })
}

async fn build_object_refs<T>(api: Api<T>) -> Result<Vec<ObjectRef<T>>>
where
    T: KubeResource,
{
    let resources = api.list(&Default::default()).await?;
    let mut refs = Vec::new();
    for resource in resources.items {
        let meta = resource.meta();
        let name = meta
            .name
            .clone()
            .ok_or_else(|| KubeGenericError::MissingMetadata("name".into()))?;
        info!(%name, "Building ObjectRef");
        refs.push(ObjectRef::new(&name));
    }
    Ok(refs)
}

// ---------------------------------------------------------------------------
// Ownership — public API
// ---------------------------------------------------------------------------

/// Build a non-controller `OwnerReference` pointing at `owner`.
///
/// The resulting reference has `controller: false` and
/// `block_owner_deletion: false`. Use [`controller_ref`] when the resource
/// is the sole managing owner and should block deletion.
///
/// Returns [`KubeGenericError::MissingMetadata`] if `name` or `uid` is absent
/// from the owner's metadata (both are required by the Kubernetes API).
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use koprs::owners::owner_ref;
/// use k8s_openapi::api::core::v1::ConfigMap;
///
/// # fn example(parent: &ConfigMap) -> Result<(), KubeGenericError> {
/// let oref = owner_ref(parent)?;
/// # Ok(())
/// # }
/// ```
pub fn owner_ref<T: KubeResource>(owner: &T) -> Result<OwnerReference> {
    build_owner_ref(owner, false)
}

/// Build a controller `OwnerReference` pointing at `owner`.
///
/// Sets `controller: true` and `block_owner_deletion: true`. Use this when
/// `owner` is the single controlling owner of a child resource — Kubernetes
/// enforces that at most one owner has `controller: true`.
///
/// Returns [`KubeGenericError::MissingMetadata`] if `name` or `uid` is absent
/// from the owner's metadata.
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use koprs::owners::controller_ref;
/// use k8s_openapi::api::core::v1::ConfigMap;
///
/// # fn example(parent: &ConfigMap) -> Result<(), KubeGenericError> {
/// let oref = controller_ref(parent)?;
/// # Ok(())
/// # }
/// ```
pub fn controller_ref<T: KubeResource>(owner: &T) -> Result<OwnerReference> {
    build_owner_ref(owner, true)
}

/// Overwrite `metadata.ownerReferences` on `child` with `refs`.
///
/// Replaces any existing owner references. To add a single owner reference
/// produced by [`owner_ref`] or [`controller_ref`], pass a `vec![oref]`.
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use koprs::owners::{controller_ref, set_owner_refs};
/// use k8s_openapi::api::apps::v1::Deployment;
/// use k8s_openapi::api::core::v1::ConfigMap;
///
/// # fn example(parent: &ConfigMap, child: &mut Deployment) -> Result<(), KubeGenericError> {
/// let oref = controller_ref(parent)?;
/// set_owner_refs(child, vec![oref]);
/// # Ok(())
/// # }
/// ```
pub fn set_owner_refs<T: KubeResource>(child: &mut T, refs: Vec<OwnerReference>) {
    child.meta_mut().owner_references = Some(refs);
}

// ---------------------------------------------------------------------------
// Controller wiring — generic public API
// ---------------------------------------------------------------------------

/// Generate [`ObjectRef`]s for all live instances of a resource type.
///
/// Queries the Kubernetes API and returns one `ObjectRef` per resource found.
/// Pass [`Cluster`] or [`Namespaced`] as the `scope` argument to select the
/// correct API surface at compile time. Prefer [`make_object_refs_namespaced`]
/// or [`make_object_refs_cluster`] for the common cases.
///
/// Useful for setting up watched relations in `kube-runtime` controllers.
/// See: <https://kube.rs/controllers/relations/#watched-relations>
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use kube::Client;
/// use koprs::owners::make_object_refs;
/// use koprs::scope::Namespaced;
/// use koprs::traits::NamespacedResource;
///
/// # async fn example<MyCR: NamespacedResource>(client: Client) -> Result<(), KubeGenericError> {
/// let refs = make_object_refs::<MyCR, _>(client, Namespaced("my-namespace")).await?;
/// # Ok(())
/// # }
/// ```
pub async fn make_object_refs<T, Scope>(client: Client, scope: Scope) -> Result<Vec<ObjectRef<T>>>
where
    T: KubeResource,
    Scope: ApiScope<T>,
{
    build_object_refs(scope.into_api(client)).await
}

// ---------------------------------------------------------------------------
// Controller wiring — convenience wrappers
// ---------------------------------------------------------------------------

/// Generate [`ObjectRef`]s for all live instances of a **namespace-scoped**
/// resource type.
///
/// Delegates to [`make_object_refs`] with [`Namespaced`] as the scope.
///
/// See: <https://kube.rs/controllers/relations/#watched-relations>
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use kube::Client;
/// use koprs::owners::make_object_refs_namespaced;
/// use koprs::traits::NamespacedResource;
///
/// # async fn example<MyCR: NamespacedResource>(client: Client) -> Result<(), KubeGenericError> {
/// let refs = make_object_refs_namespaced::<MyCR>(client, "my-namespace").await?;
/// # Ok(())
/// # }
/// ```
pub async fn make_object_refs_namespaced<T>(
    client: Client,
    namespace: &str,
) -> Result<Vec<ObjectRef<T>>>
where
    T: NamespacedResource,
{
    make_object_refs::<T, _>(client, Namespaced(namespace)).await
}

/// Generate [`ObjectRef`]s for all live instances of a **cluster-scoped**
/// resource type.
///
/// Delegates to [`make_object_refs`] with [`Cluster`] as the scope.
///
/// See: <https://kube.rs/controllers/relations/#watched-relations>
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use kube::Client;
/// use koprs::owners::make_object_refs_cluster;
/// use koprs::traits::ClusterResource;
///
/// # async fn example<MyCR: ClusterResource>(client: Client) -> Result<(), KubeGenericError> {
/// let refs = make_object_refs_cluster::<MyCR>(client).await?;
/// # Ok(())
/// # }
/// ```
pub async fn make_object_refs_cluster<T>(client: Client) -> Result<Vec<ObjectRef<T>>>
where
    T: ClusterResource,
{
    make_object_refs::<T, _>(client, Cluster).await
}

/// Build a mapper closure that returns a fixed set of [`ObjectRef`]s for any
/// triggering resource `T`.
///
/// The returned closure ignores the concrete value of `T` and always yields a
/// clone of `refs`. Pass it to `kube_runtime::Controller::watches` to trigger
/// reconciliation of a set of CRs whenever any `T` changes.
///
/// The triggering type `T` is unconstrained beyond `'static` — any resource
/// type (including those without a `.spec`) may be used as a trigger.
///
/// See: <https://kube.rs/controllers/relations/#watched-relations>
///
/// # Examples
///
/// ```no_run
/// use koprs::error::KubeGenericError;
/// use kube::Client;
/// use koprs::owners::{make_object_refs_namespaced, make_object_ref_mapper};
/// use koprs::traits::NamespacedResource;
/// use k8s_openapi::api::core::v1::ConfigMap;
/// use std::sync::Arc;
///
/// # async fn example<MyCR: NamespacedResource>(client: Client) -> Result<(), KubeGenericError> {
/// let refs = make_object_refs_namespaced::<MyCR>(client, "my-namespace").await?;
/// let mapper = make_object_ref_mapper::<ConfigMap, MyCR>(Arc::new(refs));
/// # Ok(())
/// # }
/// ```
pub fn make_object_ref_mapper<T, CR>(
    refs: Arc<Vec<ObjectRef<CR>>>,
) -> impl Fn(T) -> Vec<ObjectRef<CR>>
where
    CR: Clone + Resource<DynamicType = ()> + 'static,
    T: 'static,
{
    move |_: T| (*refs).clone()
}