Skip to main content

Resource Trait and Type System

At the heart of kube is the Resource trait. This single trait allows static types (k8s-openapi) and dynamic types (DynamicObject) to be handled through the same interface, while using Scope to prevent invalid API calls at compile time.

Anatomy of the Resource Trait

kube-core/src/resource.rs (simplified)
pub trait Resource {
type DynamicType: Send + Sync + 'static;
type Scope;

fn kind(dt: &Self::DynamicType) -> Cow<'_, str>;
fn group(dt: &Self::DynamicType) -> Cow<'_, str>;
fn version(dt: &Self::DynamicType) -> Cow<'_, str>;
fn api_version(dt: &Self::DynamicType) -> Cow<'_, str>;
fn plural(dt: &Self::DynamicType) -> Cow<'_, str>;
fn url_path(dt: &Self::DynamicType, namespace: Option<&str>) -> String;

fn meta(&self) -> &ObjectMeta;
fn meta_mut(&mut self) -> &mut ObjectMeta;

fn object_ref(&self, dt: &Self::DynamicType) -> ObjectReference;
fn controller_owner_ref(&self, dt: &Self::DynamicType) -> Option<OwnerReference>;
fn owner_ref(&self, dt: &Self::DynamicType) -> Option<OwnerReference>;
}

Two associated types form the core of this trait.

DynamicType — Where Metadata Lives

DynamicType determines where the GVK (Group/Version/Kind) information for a resource comes from.

  • () — Static type. The kind/group/version are embedded in the type itself at compile time. Zero runtime cost.
  • ApiResource — Dynamic type. GVK information is carried at runtime.

This is why methods like kind(), group(), and version() take &Self::DynamicType as a parameter. Static types receive (), ignore it, and return constants, while dynamic types retrieve values from the ApiResource.

Scope — Resource Scope

Expresses at the type level whether a resource belongs to a namespace or is cluster-wide.

Scope TypeMeaningExamples
NamespaceResourceScopeResource scoped to a namespacePod, Service, ConfigMap
ClusterResourceScopeCluster-wide resourceNode, Namespace, ClusterRole
DynamicResourceScopeDetermined at runtimeDynamicObject

blanket impl — Automatic k8s-openapi Integration

kube-core automatically implements the Resource trait for all k8s-openapi types.

kube-core/src/resource.rs (simplified)
impl<K, S> Resource for K
where
K: k8s_openapi::Metadata<Ty = ObjectMeta>,
K: k8s_openapi::Resource<Scope = S>,
{
type DynamicType = ();
type Scope = S;

fn kind(_: &()) -> Cow<'_, str> {
K::KIND.into()
}
fn group(_: &()) -> Cow<'_, str> {
K::GROUP.into()
}
fn version(_: &()) -> Cow<'_, str> {
K::VERSION.into()
}
// ...
}

This blanket impl is the bridge connecting two crates (k8s-openapi and kube-core). All k8s-openapi types like Pod, Deployment, and Service automatically implement kube's Resource, so users never need to write their own impl.

Since DynamicType = (), all metadata (KIND, GROUP, VERSION) comes from constants. There is zero runtime overhead.

Scope — Compile-Time Safety

The Api<K> constructors check K::Scope to reject invalid combinations at compile time.

use k8s_openapi::api::core::v1::{Pod, Node, Namespace};

let client = Client::try_default().await?;

// Pod has NamespaceResourceScope -> Api::namespaced() is allowed
let pods: Api<Pod> = Api::namespaced(client.clone(), "default");

// Node has ClusterResourceScope -> only Api::all() is allowed
let nodes: Api<Node> = Api::all(client.clone());

// Creating a cluster-scoped resource with Api::namespaced() causes a compile error
// let ns: Api<Namespace> = Api::namespaced(client.clone(), "default");
// error: Namespace: Resource<Scope = ClusterResourceScope>
// but expected NamespaceResourceScope

Api::all() works with any Scope. When used with a namespace-scoped resource, Api::all() queries resources across all namespaces.

default_namespaced

Api::default_namespaced(client) uses the default namespace inferred from the Config. This is the current context namespace from kubeconfig, or the namespace where the Pod is running if in-cluster.

Using DynamicType

There are three patterns for working with resource types.

1. Static Type — DynamicType = ()

These are k8s-openapi types or types generated by #[derive(CustomResource)]. This is the most common pattern.

// k8s-openapi type
let pods: Api<Pod> = Api::namespaced(client.clone(), "default");
let pod = pods.get("my-pod").await?;
println!("{}", pod.metadata.name.unwrap());

// CRD type generated via derive
let docs: Api<Document> = Api::namespaced(client, "default");

All GVK information is embedded in the type, so no additional arguments are needed.

2. Dynamic Type — DynamicType = ApiResource

Use DynamicObject when the type is not known at compile time. GVK information is passed at runtime via ApiResource.

use kube::core::{DynamicObject, ApiResource, GroupVersionKind};

let gvk = GroupVersionKind::gvk("example.com", "v1", "Document");
let ar = ApiResource::from_gvk(&gvk);
let api = Api::<DynamicObject>::namespaced_with(client, "default", &ar);

let obj = api.get("my-doc").await?;
// Field access goes through serde_json::Value
let title = obj.data["spec"]["title"].as_str();
No type safety

All field access on DynamicObject goes through serde_json::Value, so accessing a non-existent field returns None at runtime rather than causing a compile error.

3. Semi-Dynamic Type — Object<P, U>

Use this when you know the spec/status structure but the GVK must be determined at runtime. It sits between static and dynamic types.

use kube::core::Object;

#[derive(Deserialize, Serialize, Clone, Debug)]
struct MySpec {
replicas: i32,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
struct MyStatus {
ready: bool,
}

type MyResource = Object<MySpec, MyStatus>;
// spec and status can be accessed with type safety
// GVK is specified at runtime via ApiResource

This is a useful pattern when working with third-party CRDs. For more details, see Third-Party CRDs.

ResourceExt — Convenience Methods

ResourceExt is an extension trait that provides convenience methods for any type implementing Resource.

use kube::ResourceExt;

let pod: Pod = api.get("my-pod").await?;

// Name and namespace
let name = pod.name_any(); // Returns name or generateName
let ns = pod.namespace(); // Option<String>

// Metadata access
let labels = pod.labels(); // &BTreeMap
let annotations = pod.annotations();
let finalizers = pod.finalizers(); // &[String]
let owner_refs = pod.owner_references(); // &[OwnerReference]

// Identifiers
let uid = pod.uid(); // Option<String>
let rv = pod.resource_version(); // Option<String>
name_any vs name_unchecked

name_any() falls back to metadata.generateName if metadata.name is absent. name_unchecked() panics if metadata.name is absent. If the resource already exists on the API server (i.e., it was fetched via get()), using name_unchecked() is safe.

ObjectRef — Resource Reference

ObjectRef<K> is a lightweight reference that identifies a resource. It plays a central role in tracking reconcile targets within the Controller.

kube-runtime/src/reflector/object_ref.rs (simplified)
#[non_exhaustive]
pub struct ObjectRef<K: Lookup + ?Sized> {
pub dyntype: K::DynamicType,
pub name: String,
pub namespace: Option<String>,
pub extra: Extra, // resource_version, uid, etc.
}

Key characteristics:

  • Hash/Eq: Only name and namespace are compared. resourceVersion and uid are ignored. This means different versions of the same resource are treated as the same ObjectRef.
  • Deduplication: The Controller's scheduler uses ObjectRef as a key, merging duplicate reconcile requests for the same resource into one.
  • Type erasure: .erase() converts it to ObjectRef<DynamicObject>. This is used when you need to collect ObjectRefs of different types into a single collection.

How ObjectRef is used in the Controller pipeline is covered in Controller Pipeline.