feat(kube): Utility function to convert kube_openapi Resource to DynamicObject. This will allow initializing resources strongly typed and then bundle various types into a list of DynamicObject
All checks were successful
Run Check Script / check (pull_request) Successful in 1m18s
All checks were successful
Run Check Script / check (pull_request) Successful in 1m18s
This commit is contained in:
parent
b2825ec1ef
commit
95cfc03518
182
harmony/src/infra/kube.rs
Normal file
182
harmony/src/infra/kube.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
use k8s_openapi::Resource as K8sResource;
|
||||||
|
use kube::api::{ApiResource, DynamicObject, GroupVersionKind};
|
||||||
|
use kube::core::TypeMeta;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// Convert a typed Kubernetes resource `K` into a `DynamicObject`.
|
||||||
|
///
|
||||||
|
/// Requirements:
|
||||||
|
/// - `K` must be a k8s_openapi resource (provides static GVK via `Resource`).
|
||||||
|
/// - `K` must have standard Kubernetes shape (metadata + payload fields).
|
||||||
|
///
|
||||||
|
/// Notes:
|
||||||
|
/// - We set `types` (apiVersion/kind) and copy `metadata`.
|
||||||
|
/// - We place the remaining top-level fields into `obj.data` as JSON.
|
||||||
|
/// - Scope is not encoded on the object itself; you still need the corresponding
|
||||||
|
/// `DynamicResource` (derived from K::group/version/kind) when constructing an Api.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// let dyn_obj = kube_resource_to_dynamic(secret)?;
|
||||||
|
/// let api: Api<DynamicObject> = Api::namespaced_with(client, "ns", &dr);
|
||||||
|
/// api.patch(&dyn_obj.name_any(), &PatchParams::apply("mgr"), &Patch::Apply(dyn_obj)).await?;
|
||||||
|
pub fn kube_resource_to_dynamic<K>(res: &K) -> Result<DynamicObject, String>
|
||||||
|
where
|
||||||
|
K: K8sResource + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
// Serialize the typed resource to JSON so we can split metadata and payload
|
||||||
|
let mut v = serde_json::to_value(res).map_err(|e| format!("Failed to serialize : {e}"))?;
|
||||||
|
let obj = v
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or_else(|| "expected object JSON".to_string())?;
|
||||||
|
|
||||||
|
// Extract and parse metadata into kube::core::ObjectMeta
|
||||||
|
let metadata_value = obj
|
||||||
|
.remove("metadata")
|
||||||
|
.ok_or_else(|| "missing metadata".to_string())?;
|
||||||
|
let metadata: kube::core::ObjectMeta = serde_json::from_value(metadata_value)
|
||||||
|
.map_err(|e| format!("Failed to deserialize : {e}"))?;
|
||||||
|
|
||||||
|
// Name is required for DynamicObject::new; prefer metadata.name
|
||||||
|
let name = metadata
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| "metadata.name is required".to_string())?;
|
||||||
|
|
||||||
|
// Remaining fields (spec/status/data/etc.) become the dynamic payload
|
||||||
|
let payload = Value::Object(obj.clone());
|
||||||
|
|
||||||
|
// Construct the DynamicObject
|
||||||
|
let mut dyn_obj = DynamicObject::new(
|
||||||
|
&name,
|
||||||
|
&ApiResource::from_gvk(&GroupVersionKind::gvk(K::GROUP, K::VERSION, K::KIND)),
|
||||||
|
);
|
||||||
|
dyn_obj.types = Some(TypeMeta {
|
||||||
|
api_version: api_version_for::<K>(),
|
||||||
|
kind: K::KIND.into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preserve namespace/labels/annotations/etc.
|
||||||
|
dyn_obj.metadata = metadata;
|
||||||
|
|
||||||
|
// Attach payload
|
||||||
|
dyn_obj.data = payload;
|
||||||
|
|
||||||
|
Ok(dyn_obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: compute apiVersion string ("group/version" or "v1" for core).
|
||||||
|
fn api_version_for<K>() -> String
|
||||||
|
where
|
||||||
|
K: K8sResource,
|
||||||
|
{
|
||||||
|
let group = K::GROUP;
|
||||||
|
let version = K::VERSION;
|
||||||
|
if group.is_empty() {
|
||||||
|
version.to_string() // core/v1 => "v1"
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", group, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use k8s_openapi::api::{
|
||||||
|
apps::v1::{Deployment, DeploymentSpec},
|
||||||
|
core::v1::{PodTemplateSpec, Secret},
|
||||||
|
};
|
||||||
|
use kube::api::ObjectMeta;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secret_to_dynamic_roundtrip() {
|
||||||
|
// Create a sample Secret resource
|
||||||
|
let mut secret = Secret {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("my-secret".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
type_: Some("kubernetes.io/service-account-token".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to DynamicResource
|
||||||
|
let dynamic: DynamicObject =
|
||||||
|
kube_resource_to_dynamic(&secret).expect("Failed to convert Secret to DynamicResource");
|
||||||
|
|
||||||
|
// Serialize both the original and dynamic resources to Value
|
||||||
|
let original_value = serde_json::to_value(&secret).expect("Failed to serialize Secret");
|
||||||
|
let dynamic_value =
|
||||||
|
serde_json::to_value(&dynamic).expect("Failed to serialize DynamicResource");
|
||||||
|
|
||||||
|
// Assert that they are identical
|
||||||
|
assert_eq!(original_value, dynamic_value);
|
||||||
|
|
||||||
|
secret.metadata.namespace = Some("false".to_string());
|
||||||
|
let modified_value = serde_json::to_value(&secret).expect("Failed to serialize Secret");
|
||||||
|
assert_ne!(modified_value, dynamic_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deployment_to_dynamic_roundtrip() {
|
||||||
|
// Create a sample Deployment with nested structures
|
||||||
|
let mut deployment = Deployment {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("my-deployment".to_string()),
|
||||||
|
labels: Some({
|
||||||
|
let mut map = std::collections::BTreeMap::new();
|
||||||
|
map.insert("app".to_string(), "nginx".to_string());
|
||||||
|
map
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(DeploymentSpec {
|
||||||
|
replicas: Some(3),
|
||||||
|
selector: Default::default(),
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some({
|
||||||
|
let mut map = std::collections::BTreeMap::new();
|
||||||
|
map.insert("app".to_string(), "nginx".to_string());
|
||||||
|
map
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(Default::default()), // PodSpec with empty containers for simplicity
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dynamic = kube_resource_to_dynamic(&deployment).expect("Failed to convert Deployment");
|
||||||
|
|
||||||
|
let original_value = serde_json::to_value(&deployment).unwrap();
|
||||||
|
let dynamic_value = serde_json::to_value(&dynamic).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(original_value, dynamic_value);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dynamic.data.get("spec").unwrap().get("replicas").unwrap(),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
dynamic
|
||||||
|
.data
|
||||||
|
.get("spec")
|
||||||
|
.unwrap()
|
||||||
|
.get("template")
|
||||||
|
.unwrap()
|
||||||
|
.get("metadata")
|
||||||
|
.unwrap()
|
||||||
|
.get("labels")
|
||||||
|
.unwrap()
|
||||||
|
.get("app")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap(),
|
||||||
|
"nginx".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,5 +3,6 @@ pub mod executors;
|
|||||||
pub mod hp_ilo;
|
pub mod hp_ilo;
|
||||||
pub mod intel_amt;
|
pub mod intel_amt;
|
||||||
pub mod inventory;
|
pub mod inventory;
|
||||||
|
pub mod kube;
|
||||||
pub mod opnsense;
|
pub mod opnsense;
|
||||||
mod sqlx;
|
mod sqlx;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user