Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/crd_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use kube::{
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas"
))]
#[kube(printcolumn = r#"{"name":"Team", "jsonPath": ".spec.metadata.team", "type": "string"}"#)]
#[kube(printcolumn(name = "Team", json_path = ".spec.metadata.team", type_ = "string"))]
pub struct FooSpec {
#[schemars(length(min = 3))]
#[garde(length(min = 3))]
Expand Down
7 changes: 6 additions & 1 deletion examples/crd_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ use serde::{Deserialize, Serialize};
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas"
),
printcolumn = r#"{"name":"Spec", "type":"string", "description":"name of foo", "jsonPath":".spec.name"}"#,
printcolumn(
name = "Spec",
type_ = "string",
description = "name of foo",
json_path = ".spec.name",
),
Comment thread
doxxx93 marked this conversation as resolved.
selectable = "spec.name"
)]
pub struct MyFoo {
Expand Down
173 changes: 169 additions & 4 deletions kube-derive/src/custom_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ struct KubeAttrs {
categories: Vec<String>,
#[darling(multiple, rename = "shortname")]
shortnames: Vec<String>,

/// Add additional print columns, see [Kubernetes docs][1].
///
/// [1]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns
#[darling(multiple, rename = "printcolumn")]
printcolums: Vec<String>,
printcolumns: Vec<PrintColumn>,
#[darling(multiple)]
selectable: Vec<String>,

Expand Down Expand Up @@ -206,6 +210,164 @@ impl FromMeta for SchemaMode {
}
}

/// This struct mirrors the fields of `k8s_openapi::CustomResourceColumnDefinition` to support
/// parsing from the `#[kube]` attribute.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PrintColumn {
description: Option<String>,
format: Option<String>,
json_path: String,
name: String,
priority: Option<i32>,
type_: String,
Comment thread
clux marked this conversation as resolved.
}

// The reasoning for this custom FromMeta implementation is parallel to the one for the
// scale subresource. The two reasons are:
//
// - For backwards-compatibility by keeping the option to supply a JSON string.
// - To be able to declare the printcolumn as a list of typed fields.
impl FromMeta for PrintColumn {
Comment thread
doxxx93 marked this conversation as resolved.
/// This is implemented for backwards-compatibility. It allows that the printcolumn can be
/// deserialized from a JSON string.
fn from_string(value: &str) -> darling::Result<Self> {
serde_json::from_str(value).map_err(darling::Error::custom)
}
Comment thread
doxxx93 marked this conversation as resolved.

fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
let mut errors = darling::Error::accumulator();

let mut description: (bool, Option<Option<String>>) = (false, None);
let mut format: (bool, Option<Option<String>>) = (false, None);
let mut json_path: (bool, Option<String>) = (false, None);
let mut column_name: (bool, Option<String>) = (false, None);
let mut priority: (bool, Option<Option<i32>>) = (false, None);
let mut type_: (bool, Option<String>) = (false, None);

for item in items {
match item {
darling::ast::NestedMeta::Meta(meta) => {
let name = darling::util::path_to_string(meta.path());

match name.as_str() {
"description" => {
if !description.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
description = (true, Some(path))
} else {
errors.push(darling::Error::duplicate_field("description").with_span(&meta));
}
}
"format" => {
if !format.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
format = (true, Some(path))
} else {
errors.push(darling::Error::duplicate_field("format").with_span(&meta));
}
}
"json_path" => {
if !json_path.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
json_path = (true, path)
} else {
errors.push(darling::Error::duplicate_field("json_path").with_span(&meta));
}
}
"name" => {
if !column_name.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
column_name = (true, path)
} else {
errors.push(darling::Error::duplicate_field("name").with_span(&meta));
}
}
"priority" => {
if !priority.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
priority = (true, Some(path))
} else {
errors.push(darling::Error::duplicate_field("priority").with_span(&meta));
}
}
"type_" => {
if !type_.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
type_ = (true, path)
} else {
errors.push(darling::Error::duplicate_field("type_").with_span(&meta));
}
}
other => errors.push(darling::Error::unknown_field(other)),
}
}
darling::ast::NestedMeta::Lit(lit) => {
errors.push(darling::Error::unsupported_format("literal").with_span(&lit.span()))
}
}
}

if !json_path.0 && json_path.1.is_none() {
errors.push(darling::Error::missing_field("json_path"));
}

if !column_name.0 && column_name.1.is_none() {
errors.push(darling::Error::missing_field("name"));
}

if !type_.0 && type_.1.is_none() {
errors.push(darling::Error::missing_field("type"));
}

errors.finish()?;

Ok(Self {
description: description.1.unwrap_or_default(),
format: format.1.unwrap_or_default(),
json_path: json_path.1.unwrap(),
name: column_name.1.unwrap(),
priority: priority.1.unwrap_or_default(),
type_: type_.1.unwrap(),
})
}
}

impl PrintColumn {
fn to_tokens(&self, k8s_openapi: &Path) -> TokenStream {
let apiext = quote! {
#k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1
};

let description = self
.description
.as_ref()
.map_or_else(|| quote! { None }, |d| quote! { Some(#d.into()) });
let format = self
.format
.as_ref()
.map_or_else(|| quote! { None }, |f| quote! { Some(#f.into()) });
let priority = self
.priority
.as_ref()
.map_or_else(|| quote! { None }, |p| quote! { Some(#p.into()) });
let json_path = &self.json_path;
let name = &self.name;
let type_ = &self.type_;

quote! {
#apiext::CustomResourceColumnDefinition {
description: #description,
format: #format,
json_path: #json_path.into(),
name: #name.into(),
priority: #priority,
type_: #type_.into(),
}
}
}
}

/// This struct mirrors the fields of `k8s_openapi::CustomResourceSubresourceScale` to support
/// parsing from the `#[kube]` attribute.
#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -397,7 +559,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
singular,
categories,
shortnames,
printcolums,
printcolumns,
selectable,
scale,
validations,
Expand Down Expand Up @@ -620,7 +782,10 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
// 4. Implement CustomResource

// Compute a bunch of crd props
let printers = format!("[ {} ]", printcolums.join(",")); // hacksss
let printers = {
let printers: Vec<_> = printcolumns.iter().map(|p| p.to_tokens(&k8s_openapi)).collect();
quote! { vec![ #(#printers),* ] }
};
let fields: Vec<String> = selectable
.iter()
.map(|s| format!(r#"{{ "jsonPath": "{s}" }}"#))
Expand Down Expand Up @@ -748,7 +913,7 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea
impl #extver::CustomResourceExt for #rootident {

fn crd() -> #apiext::CustomResourceDefinition {
let columns : Vec<#apiext::CustomResourceColumnDefinition> = #serde_json::from_str(#printers).expect("valid printer column json");
let columns : Vec<#apiext::CustomResourceColumnDefinition> = #printers;
let fields : Vec<#apiext::SelectableField> = #serde_json::from_str(#fields).expect("valid selectableField column json");
let scale: Option<#apiext::CustomResourceSubresourceScale> = #scale;
let categories: Vec<String> = #serde_json::from_str(#categories_json).expect("valid categories");
Expand Down
29 changes: 26 additions & 3 deletions kube-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,26 @@ mod resource;
/// The deprecated way of customizing the scale subresource using a raw JSON string is still
/// support for backwards-compatibility.
///
/// ## `#[kube(printcolumn = r#"json"#)]`
/// Allows adding straight json to [printcolumns](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns).
/// ## `#[kube(printcolumn(...))]`
Comment thread
cchndl marked this conversation as resolved.
///
/// Allows adding a custom column to the [printcolumns](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns).
///
/// ```ignore
/// #[kube(printcolumn(
/// name = "CustomColumn",
/// type_ = "integer",
/// json_path = ".spec.someField",
/// description = "a custom column", // optional
/// format = "int32", // optional
/// priority = "1", // optional
Comment thread
cchndl marked this conversation as resolved.
/// ))]
/// ```
///
/// The older method of supplying raw json is still supported:
///
/// ```ignore
/// printcolumn = r#"{"name":"Spec", "type":"string", "description":"name of foo", "jsonPath":".spec.name"}"#,
/// ```
///
/// ## `#[kube(shortname = "sn")]`
/// Add a single shortname to the generated crd.
Expand Down Expand Up @@ -231,7 +249,12 @@ mod resource;
/// plural = "feetz",
/// shortname = "f",
/// scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#,
/// printcolumn = r#"{"name":"Spec", "type":"string", "description":"name of foo", "jsonPath":".spec.name"}"#,
/// printcolumn(
/// name = "Spec",
/// type_ = "string",
/// description = "name of foo",
/// json_path = ".spec.name"
/// ),
/// selectable = "spec.replicasCount"
/// )]
/// #[serde(rename_all = "camelCase")]
Expand Down
7 changes: 6 additions & 1 deletion kube-derive/tests/crd_schema_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use std::collections::{HashMap, HashSet};
label("clux.dev/persistence", "disabled"),
validation = "self.metadata.name == 'singleton'",
status = "Status",
printcolumn(json_path = ".spec.name", name = "Spec", type_ = "string"),
scale(
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas",
Expand Down Expand Up @@ -272,7 +273,11 @@ fn test_crd_schema_matches_expected() {
"storage": false,
"deprecated": true,
"deprecationWarning": "my warning",
"additionalPrinterColumns": [],
"additionalPrinterColumns": [{
"jsonPath": ".spec.name",
"name": "Spec",
"type": "string",
}],
"selectableFields": [{
"jsonPath": ".spec.nonNullable"
}, {
Expand Down
64 changes: 64 additions & 0 deletions kube-derive/tests/typed_subresource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#![allow(missing_docs)]

use kube_derive::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[kube(
group = "clux.dev",
version = "v1",
kind = "Foo",
status = "Status",
printcolumn(json_path = ".spec.name", name = "Spec", type_ = "string"),
scale(
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas",
label_selector_path = ".spec.labelSelector"
)
)]
struct FooSpec {}

#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[kube(
group = "clux.dev",
version = "v1",
kind = "Compat",
printcolumn = r#"{"jsonPath": ".spec.name", "name": "Spec", "type": "string"}"#,
status = "Status",
scale = r#"{"specReplicasPath": ".spec.replicas", "statusReplicasPath": ".status.replicas", "labelSelectorPath": ".spec.labelSelector"}"#
)]
struct CompatSpec {}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Status {
replicas: usize,
label_selector: String,
}

#[test]
fn test_printcolumns_backwards_compatible() {
use kube::core::CustomResourceExt;
// Check that the typed version of printcolumn matches the backwards compatible raw json string
assert_eq!(
Compat::crd().spec.versions[0].additional_printer_columns,
Foo::crd().spec.versions[0].additional_printer_columns
);
}

#[test]
fn test_scale_backwards_compatible() {
use kube::core::CustomResourceExt;
// Check that the typed version of scale matches the backwards compatible raw json string
assert_eq!(
Compat::crd().spec.versions[0]
.subresources
.as_ref()
.map(|sr| sr.scale.clone()),
Foo::crd().spec.versions[0]
.subresources
.as_ref()
.map(|sr| sr.scale.clone())
);
}