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
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 Type | Meaning | Examples |
|---|---|---|
NamespaceResourceScope | Resource scoped to a namespace | Pod, Service, ConfigMap |
ClusterResourceScope | Cluster-wide resource | Node, Namespace, ClusterRole |
DynamicResourceScope | Determined at runtime | DynamicObject |
blanket impl — Automatic k8s-openapi Integration
kube-core automatically implements the Resource trait for all k8s-openapi types.
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.
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();
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() 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.
#[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
nameandnamespaceare compared.resourceVersionanduidare ignored. This means different versions of the same resource are treated as the sameObjectRef. - Deduplication: The Controller's scheduler uses
ObjectRefas a key, merging duplicate reconcile requests for the same resource into one. - Type erasure:
.erase()converts it toObjectRef<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.