chore: checkpoint before Python removal

This commit is contained in:
2026-03-26 22:33:59 +00:00
parent 683cec9307
commit e568ddf82a
29972 changed files with 11269302 additions and 2 deletions

View File

@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"7fc701b18a99a89babd57b88e28f0e2e1ca7b195d846063ac6e564824b3510ca","Cargo.lock":"3faa11292f6c1cce7ec3a02a59decab690442a9c2b18293ea00f0092f1de4a6c","Cargo.toml":"bec2cf5d5b87eae9a7f12f2bc6606b32ecd1092b0db3a284d7fac88952cfd744","Cargo.toml.orig":"1a43f74dbd524185c29c9a4ff8bc3c9c26d0270a7c78df250d94dd9beff97757","README.md":"ca6eb9085d22547a6d49ed29f9ee9fd0ac4c97d6af3d650665ccdffb601a1376","src/cel_schema.rs":"35abc497ac4fe745adc314aaeed9e7ef791bbefdc67358f456f49c24285d4b73","src/custom_resource.rs":"f4fe546bfdbe723f88d9c9203c62952ad2a471b6e7b2ca6cc46ec7bf05bb2dc2","src/lib.rs":"9a57546184ca9e4d8f58acad3c1a7cbf8e50996c9a6eb5042f4307f2dd3eaac1","src/resource.rs":"c3278421d0ff9094efbd5a51572e3fce43feb4f33fe92e597fd8998d634e1d74","tests/crd_enum_test.rs":"8aeac6d61b5fee5631731e02655e9654d05290b6f870b6472f4cd1fdd51ac247","tests/crd_schema_test.rs":"010b3145f4873ef21fb9df0f42d1cb6ca731aef0f7389c66e760a28f83dfa010","tests/resource.rs":"23996286a171be16310aaeef2c74384099522f5f1db5a43e6f841bf0b7c6daa0","tests/test_ui.rs":"61f8acf82eb005b427b40e2f99469548d7aeb296c76a3a150eb913a69de831e1","tests/ui/fail_with_suggestion.rs":"5a834802e8c4dcf1e921334929d6c758a68d5c4408a8f647fa28fc1b390747f2","tests/ui/fail_with_suggestion.stderr":"a93e5264f48932d670b31639ae00c6ac51a427ac7c7e0b945b24c668fb946af7","tests/ui/missing_required.rs":"bf83be67b45276009f256c4a0bcc44783358fef399eaf80cbe59242ce5a3c79f","tests/ui/missing_required.stderr":"80b0cfc8856e407c1eddef007f2c37e7260f26f18af19ba225c57c0f75d880a9","tests/ui/union_fails.rs":"59a95156cd4821ab74546966ddac02ec1fea3bf3b4c44408e6a5b8783fae29d9","tests/ui/union_fails.stderr":"f4777b87061a08d9d3168750429f01b00d3cef8ad5331080aaee33a86cb58a12"},"package":"c562f58dc9f7ca5feac8a6ee5850ca221edd6f04ce0dd2ee873202a88cd494c9"}

View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "c9b7b70f7fa0378ea1cd6ac697c1a0c0bb7b7dd3"
},
"path_in_vcs": "kube-derive"
}

1575
vendor/kube-derive/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

123
vendor/kube-derive/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,123 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.81.0"
name = "kube-derive"
version = "0.99.0"
authors = [
"clux <sszynrae@gmail.com>",
"Natalie Klestrup Röijezon <nat@nullable.se>",
"kazk <kazk.dev@gmail.com>",
]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Custom derives for the kube kubernetes crates"
readme = "README.md"
keywords = [
"kubernetes",
"macro",
"customresource",
"crd",
]
categories = [
"api-bindings",
"encoding",
]
license = "Apache-2.0"
repository = "https://github.com/kube-rs/kube"
resolver = "1"
[lib]
name = "kube_derive"
path = "src/lib.rs"
proc-macro = true
[[test]]
name = "crd_enum_test"
path = "tests/crd_enum_test.rs"
[[test]]
name = "crd_schema_test"
path = "tests/crd_schema_test.rs"
[[test]]
name = "resource"
path = "tests/resource.rs"
[[test]]
name = "test_ui"
path = "tests/test_ui.rs"
[dependencies.darling]
version = "0.20.3"
[dependencies.proc-macro2]
version = "1.0.29"
[dependencies.quote]
version = "1.0.10"
[dependencies.serde]
version = "1.0.130"
features = ["derive"]
[dependencies.serde_json]
version = "1.0.68"
[dependencies.syn]
version = "2.0.38"
features = ["extra-traits"]
[dev-dependencies.assert-json-diff]
version = "2.0.2"
[dev-dependencies.chrono]
version = "0.4.34"
default-features = false
[dev-dependencies.k8s-openapi]
version = "0.24.0"
features = ["latest"]
default-features = false
[dev-dependencies.kube]
version = "<2.0.0, >=0.98.0"
features = [
"derive",
"client",
]
[dev-dependencies.prettyplease]
version = "0.2.25"
[dev-dependencies.schemars]
version = "0.8.6"
features = ["chrono"]
[dev-dependencies.serde]
version = "1.0.130"
features = ["derive"]
[dev-dependencies.serde_yaml]
version = "0.9.19"
[dev-dependencies.trybuild]
version = "1.0.48"
[lints.rust]
missing_docs = "deny"
unsafe_code = "forbid"

19
vendor/kube-derive/README.md vendored Normal file
View File

@@ -0,0 +1,19 @@
# kube-derive
Add `#[derive(CustomResource)]` to your custom resource struct.
## Installation
Add the `derive` feature to `kube`:
```toml
[dependencies]
kube = { version = "0.99.0", feature = ["derive"] }
```
## Usage
See the **[kube-derive API Docs](https://docs.rs/kube-derive/)**
## Examples
See the `crd_` prefixed [examples](../examples) for more.
## Development
Help very welcome! Kubebuilder like features, testing improvement, openapi feature. See https://github.com/kube-rs/kube/labels/derive

235
vendor/kube-derive/src/cel_schema.rs vendored Normal file
View File

@@ -0,0 +1,235 @@
use darling::{FromDeriveInput, FromField, FromMeta};
use proc_macro2::TokenStream;
use syn::{parse_quote, Attribute, DeriveInput, Expr, Ident, Path};
#[derive(FromField)]
#[darling(attributes(cel_validate))]
struct Rule {
#[darling(multiple, rename = "rule")]
rules: Vec<Expr>,
}
#[derive(FromDeriveInput)]
#[darling(attributes(cel_validate), supports(struct_named))]
struct CELSchema {
#[darling(default)]
crates: Crates,
ident: Ident,
#[darling(multiple, rename = "rule")]
rules: Vec<Expr>,
}
#[derive(Debug, FromMeta)]
struct Crates {
#[darling(default = "Self::default_kube_core")]
kube_core: Path,
#[darling(default = "Self::default_schemars")]
schemars: Path,
#[darling(default = "Self::default_serde")]
serde: Path,
}
// Default is required when the subattribute isn't mentioned at all
// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
impl Default for Crates {
fn default() -> Self {
Self::from_list(&[]).unwrap()
}
}
impl Crates {
fn default_kube_core() -> Path {
parse_quote! { ::kube::core } // by default must work well with people using facade crate
}
fn default_schemars() -> Path {
parse_quote! { ::schemars }
}
fn default_serde() -> Path {
parse_quote! { ::serde }
}
}
pub(crate) fn derive_validated_schema(input: TokenStream) -> TokenStream {
let mut ast: DeriveInput = match syn::parse2(input) {
Err(err) => return err.to_compile_error(),
Ok(di) => di,
};
let CELSchema {
crates: Crates {
kube_core,
schemars,
serde,
},
ident,
rules,
} = match CELSchema::from_derive_input(&ast) {
Err(err) => return err.write_errors(),
Ok(attrs) => attrs,
};
// Collect global structure validation rules
let struct_name = ident.to_string();
let struct_rules: Vec<TokenStream> = rules.iter().map(|r| quote! {#r,}).collect();
// Remove all unknown attributes from the original structure copy
// Has to happen on the original definition at all times, as we don't have #[derive] stanzes.
let attribute_whitelist = ["serde", "schemars", "doc"];
ast.attrs = remove_attributes(&ast.attrs, &attribute_whitelist);
let struct_data = match ast.data {
syn::Data::Struct(ref mut struct_data) => struct_data,
_ => return quote! {},
};
// Preserve all serde attributes, to allow #[serde(rename_all = "camelCase")] or similar
let struct_attrs: Vec<TokenStream> = ast.attrs.iter().map(|attr| quote! {#attr}).collect();
let mut property_modifications = vec![];
if let syn::Fields::Named(fields) = &mut struct_data.fields {
for field in &mut fields.named {
let Rule { rules, .. } = match Rule::from_field(field) {
Ok(rule) => rule,
Err(err) => return err.write_errors(),
};
// Remove all unknown attributes from each field
// Has to happen on the original definition at all times, as we don't have #[derive] stanzes.
field.attrs = remove_attributes(&field.attrs, &attribute_whitelist);
if rules.is_empty() {
continue;
}
let rules: Vec<TokenStream> = rules.iter().map(|r| quote! {#r,}).collect();
// We need to prepend derive macros, as they were consumed by this macro processing, being a derive by itself.
property_modifications.push(quote! {
{
#[derive(#serde::Serialize, #schemars::JsonSchema)]
#(#struct_attrs)*
#[automatically_derived]
#[allow(missing_docs)]
struct Validated {
#field
}
let merge = &mut Validated::json_schema(gen);
#kube_core::validate_property(merge, 0, &[#(#rules)*]).unwrap();
#kube_core::merge_properties(s, merge);
}
});
}
}
quote! {
impl #schemars::JsonSchema for #ident {
fn is_referenceable() -> bool {
false
}
fn schema_name() -> String {
#struct_name.to_string() + "_kube_validation".into()
}
fn json_schema(gen: &mut #schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
#[derive(#serde::Serialize, #schemars::JsonSchema)]
#[automatically_derived]
#[allow(missing_docs)]
#ast
use #kube_core::{Rule, Message, Reason};
let s = &mut #ident::json_schema(gen);
#kube_core::validate(s, &[#(#struct_rules)*]).unwrap();
#(#property_modifications)*
s.clone()
}
}
}
}
// Remove all unknown attributes from the list
fn remove_attributes(attrs: &[Attribute], witelist: &[&str]) -> Vec<Attribute> {
attrs
.iter()
.filter(|attr| witelist.iter().any(|i| attr.path().is_ident(i)))
.cloned()
.collect()
}
#[test]
fn test_derive_validated() {
let input = quote! {
#[derive(CustomResource, CELSchema, Serialize, Deserialize, Debug, PartialEq, Clone)]
#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
#[cel_validate(rule = "self != ''".into())]
struct FooSpec {
#[cel_validate(rule = "self != ''".into())]
foo: String
}
};
let input = syn::parse2(input).unwrap();
let v = CELSchema::from_derive_input(&input).unwrap();
assert_eq!(v.rules.len(), 1);
}
#[cfg(test)]
mod tests {
use prettyplease::unparse;
use syn::parse::{Parse as _, Parser as _};
use super::*;
#[test]
fn test_derive_validated_full() {
let input = quote! {
#[derive(CELSchema)]
#[cel_validate(rule = "true".into())]
struct FooSpec {
#[cel_validate(rule = "true".into())]
foo: String
}
};
let expected = quote! {
impl ::schemars::JsonSchema for FooSpec {
fn is_referenceable() -> bool {
false
}
fn schema_name() -> String {
"FooSpec".to_string() + "_kube_validation".into()
}
fn json_schema(
gen: &mut ::schemars::gen::SchemaGenerator,
) -> schemars::schema::Schema {
#[derive(::serde::Serialize, ::schemars::JsonSchema)]
#[automatically_derived]
#[allow(missing_docs)]
struct FooSpec {
foo: String,
}
use ::kube::core::{Rule, Message, Reason};
let s = &mut FooSpec::json_schema(gen);
::kube::core::validate(s, &["true".into()]).unwrap();
{
#[derive(::serde::Serialize, ::schemars::JsonSchema)]
#[automatically_derived]
#[allow(missing_docs)]
struct Validated {
foo: String,
}
let merge = &mut Validated::json_schema(gen);
::kube::core::validate_property(merge, 0, &["true".into()]).unwrap();
::kube::core::merge_properties(s, merge);
}
s.clone()
}
}
};
let output = derive_validated_schema(input);
let output = unparse(&syn::File::parse.parse2(output).unwrap());
let expected = unparse(&syn::File::parse.parse2(expected).unwrap());
assert_eq!(output, expected);
}
}

View File

@@ -0,0 +1,912 @@
// Generated by darling macros, out of our control
#![allow(clippy::manual_unwrap_or_default)]
use darling::{util::Override, FromDeriveInput, FromMeta};
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::{ToTokens, TokenStreamExt as _};
use serde::Deserialize;
use syn::{parse_quote, Data, DeriveInput, Expr, Path, Visibility};
/// Values we can parse from #[kube(attrs)]
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(kube))]
struct KubeAttrs {
group: String,
version: String,
kind: String,
doc: Option<String>,
#[darling(rename = "root")]
kind_struct: Option<String>,
/// lowercase plural of kind (inferred if omitted)
plural: Option<String>,
/// singular defaults to lowercased kind
singular: Option<String>,
#[darling(default)]
namespaced: bool,
#[darling(multiple, rename = "derive")]
derives: Vec<String>,
schema: Option<SchemaMode>,
status: Option<Path>,
#[darling(multiple, rename = "category")]
categories: Vec<String>,
#[darling(multiple, rename = "shortname")]
shortnames: Vec<String>,
#[darling(multiple, rename = "printcolumn")]
printcolums: Vec<String>,
#[darling(multiple)]
selectable: Vec<String>,
/// Customize the scale subresource, see [Kubernetes docs][1].
///
/// [1]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
scale: Option<Scale>,
#[darling(default)]
crates: Crates,
#[darling(multiple, rename = "annotation")]
annotations: Vec<KVTuple>,
#[darling(multiple, rename = "label")]
labels: Vec<KVTuple>,
#[darling(multiple, rename = "rule")]
rules: Vec<Expr>,
/// Sets the `storage` property to `true` or `false`.
///
/// Defaults to `true`.
#[darling(default = default_storage_arg)]
storage: bool,
/// Sets the `served` property to `true` or `false`.
///
/// Defaults to `true`.
#[darling(default = default_served_arg)]
served: bool,
/// Sets the `deprecated` and optionally the `deprecationWarning` property.
///
/// See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-deprecation
deprecated: Option<Override<String>>,
}
#[derive(Debug)]
struct KVTuple(String, String);
impl FromMeta for KVTuple {
fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
if items.len() == 2 {
if let (
darling::ast::NestedMeta::Lit(syn::Lit::Str(key)),
darling::ast::NestedMeta::Lit(syn::Lit::Str(value)),
) = (&items[0], &items[1])
{
return Ok(KVTuple(key.value(), value.value()));
}
}
Err(darling::Error::unsupported_format(
"expected `\"key\", \"value\"` format",
))
}
}
impl From<(&'static str, &'static str)> for KVTuple {
fn from((key, value): (&'static str, &'static str)) -> Self {
Self(key.to_string(), value.to_string())
}
}
impl ToTokens for KVTuple {
fn to_tokens(&self, tokens: &mut TokenStream) {
let (k, v) = (&self.0, &self.1);
tokens.append_all(quote! { (#k, #v) });
}
}
fn default_storage_arg() -> bool {
// This defaults to true to be backwards compatible.
true
}
fn default_served_arg() -> bool {
// This defaults to true to be backwards compatible.
true
}
#[derive(Debug, FromMeta)]
struct Crates {
#[darling(default = "Self::default_kube")]
kube: Path,
#[darling(default = "Self::default_kube_core")]
kube_core: Path,
#[darling(default = "Self::default_k8s_openapi")]
k8s_openapi: Path,
#[darling(default = "Self::default_schemars")]
schemars: Path,
#[darling(default = "Self::default_serde")]
serde: Path,
#[darling(default = "Self::default_serde_json")]
serde_json: Path,
#[darling(default = "Self::default_std")]
std: Path,
}
// Default is required when the subattribute isn't mentioned at all
// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
impl Default for Crates {
fn default() -> Self {
Self::from_list(&[]).unwrap()
}
}
impl Crates {
fn default_kube_core() -> Path {
parse_quote! { ::kube::core } // by default must work well with people using facade crate
}
fn default_kube() -> Path {
parse_quote! { ::kube }
}
fn default_k8s_openapi() -> Path {
parse_quote! { ::k8s_openapi }
}
fn default_schemars() -> Path {
parse_quote! { ::schemars }
}
fn default_serde() -> Path {
parse_quote! { ::serde }
}
fn default_serde_json() -> Path {
parse_quote! { ::serde_json }
}
fn default_std() -> Path {
parse_quote! { ::std }
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum SchemaMode {
Disabled,
Manual,
Derived,
}
impl SchemaMode {
fn derive(self) -> bool {
match self {
SchemaMode::Disabled => false,
SchemaMode::Manual => false,
SchemaMode::Derived => true,
}
}
fn use_in_crd(self) -> bool {
match self {
SchemaMode::Disabled => false,
SchemaMode::Manual => true,
SchemaMode::Derived => true,
}
}
}
impl FromMeta for SchemaMode {
fn from_string(value: &str) -> darling::Result<Self> {
match value {
"disabled" => Ok(SchemaMode::Disabled),
"manual" => Ok(SchemaMode::Manual),
"derived" => Ok(SchemaMode::Derived),
x => Err(darling::Error::unknown_value(x)),
}
}
}
/// This struct mirrors the fields of `k8s_openapi::CustomResourceSubresourceScale` to support
/// parsing from the `#[kube]` attribute.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Scale {
pub(crate) label_selector_path: Option<String>,
pub(crate) spec_replicas_path: String,
pub(crate) status_replicas_path: String,
}
// This custom FromMeta implementation is needed for two reasons:
//
// - To enable backwards-compatibility. Up to version 0.97.0 it was only possible to set scale
// subresource values as a JSON string.
// - To be able to declare the scale sub-resource as a list of typed fields. The from_list impl uses
// the derived implementation as inspiration.
impl FromMeta for Scale {
/// This is implemented for backwards-compatibility. It allows that the scale subresource 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)
}
fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
let mut errors = darling::Error::accumulator();
let mut label_selector_path: (bool, Option<Option<String>>) = (false, None);
let mut spec_replicas_path: (bool, Option<String>) = (false, None);
let mut status_replicas_path: (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() {
"label_selector_path" => {
if !label_selector_path.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
label_selector_path = (true, Some(path))
} else {
errors.push(
darling::Error::duplicate_field("label_selector_path").with_span(&meta),
);
}
}
"spec_replicas_path" => {
if !spec_replicas_path.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
spec_replicas_path = (true, path)
} else {
errors.push(
darling::Error::duplicate_field("spec_replicas_path").with_span(&meta),
);
}
}
"status_replicas_path" => {
if !status_replicas_path.0 {
let path = errors.handle(darling::FromMeta::from_meta(meta));
status_replicas_path = (true, path)
} else {
errors.push(
darling::Error::duplicate_field("status_replicas_path").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 !spec_replicas_path.0 && spec_replicas_path.1.is_none() {
errors.push(darling::Error::missing_field("spec_replicas_path"));
}
if !status_replicas_path.0 && status_replicas_path.1.is_none() {
errors.push(darling::Error::missing_field("status_replicas_path"));
}
errors.finish()?;
Ok(Self {
label_selector_path: label_selector_path.1.unwrap_or_default(),
spec_replicas_path: spec_replicas_path.1.unwrap(),
status_replicas_path: status_replicas_path.1.unwrap(),
})
}
}
impl Scale {
fn to_tokens(&self, k8s_openapi: &Path) -> TokenStream {
let apiext = quote! {
#k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1
};
let label_selector_path = self
.label_selector_path
.as_ref()
.map_or_else(|| quote! { None }, |p| quote! { Some(#p.into()) });
let spec_replicas_path = &self.spec_replicas_path;
let status_replicas_path = &self.status_replicas_path;
quote! {
#apiext::CustomResourceSubresourceScale {
label_selector_path: #label_selector_path,
spec_replicas_path: #spec_replicas_path.into(),
status_replicas_path: #status_replicas_path.into()
}
}
}
}
pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let derive_input: DeriveInput = match syn::parse2(input) {
Err(err) => return err.to_compile_error(),
Ok(di) => di,
};
// Limit derive to structs
match derive_input.data {
Data::Struct(_) | Data::Enum(_) => {}
_ => {
return syn::Error::new_spanned(
&derive_input.ident,
r#"Unions can not #[derive(CustomResource)]"#,
)
.to_compile_error()
}
}
let kube_attrs = match KubeAttrs::from_derive_input(&derive_input) {
Err(err) => return err.write_errors(),
Ok(attrs) => attrs,
};
let KubeAttrs {
group,
kind,
kind_struct,
version,
doc,
namespaced,
derives,
schema: schema_mode,
status,
plural,
singular,
categories,
shortnames,
printcolums,
selectable,
scale,
rules,
storage,
served,
deprecated,
crates:
Crates {
kube_core,
kube,
k8s_openapi,
schemars,
serde,
serde_json,
std,
},
annotations,
labels,
} = kube_attrs;
let struct_name = kind_struct.unwrap_or_else(|| kind.clone());
if derive_input.ident == struct_name {
return syn::Error::new_spanned(
derive_input.ident,
r#"#[derive(CustomResource)] `kind = "..."` must not equal the struct name (this is generated)"#,
)
.to_compile_error();
}
let visibility = derive_input.vis;
let ident = derive_input.ident;
// 1. Create root object Foo and truncate name from FooSpec
// Default visibility is `pub(crate)`
// Default generics is no generics (makes little sense to re-use CRD kind?)
// We enforce metadata + spec's existence (always there)
// => No default impl
let rootident = Ident::new(&struct_name, Span::call_site());
let rootident_str = rootident.to_string();
// if status set, also add that
let StatusInformation {
field: status_field,
default: status_default,
impl_hasstatus,
} = process_status(&rootident, &status, &visibility, &kube_core);
let has_status = status.is_some();
let serialize_status = if has_status {
quote! {
if let Some(status) = &self.status {
obj.serialize_field("status", &status)?;
}
}
} else {
quote! {}
};
let has_status_value = if has_status {
quote! { self.status.is_some() }
} else {
quote! { false }
};
let mut derive_paths: Vec<Path> = vec![
syn::parse_quote! { #serde::Deserialize },
syn::parse_quote! { Clone },
syn::parse_quote! { Debug },
];
let mut has_default = false;
for d in &derives {
if d == "Default" {
has_default = true; // overridden manually to avoid confusion
} else {
match syn::parse_str(d) {
Err(err) => return err.to_compile_error(),
Ok(d) => derive_paths.push(d),
}
}
}
// Enable schema generation by default as in v1 it is mandatory.
let schema_mode = schema_mode.unwrap_or(SchemaMode::Derived);
// We exclude fields `apiVersion`, `kind`, and `metadata` from our schema because
// these are validated by the API server implicitly. Also, we can't generate the
// schema for `metadata` (`ObjectMeta`) because it doesn't implement `JsonSchema`.
let schemars_skip = schema_mode.derive().then_some(quote! { #[schemars(skip)] });
if schema_mode.derive() && !rules.is_empty() {
derive_paths.push(syn::parse_quote! { #kube::CELSchema });
} else if schema_mode.derive() {
derive_paths.push(syn::parse_quote! { #schemars::JsonSchema });
}
let struct_rules: Option<Vec<TokenStream>> =
(!rules.is_empty()).then(|| rules.iter().map(|r| quote! {rule = #r,}).collect());
let struct_rules = struct_rules.map(|r| quote! { #[cel_validate(#(#r)*)]});
let meta_annotations = if !annotations.is_empty() {
quote! { Some(std::collections::BTreeMap::from([#((#annotations.0.to_string(), #annotations.1.to_string()),)*])) }
} else {
quote! { None }
};
let meta_labels = if !labels.is_empty() {
quote! { Some(std::collections::BTreeMap::from([#((#labels.0.to_string(), #labels.1.to_string()),)*])) }
} else {
quote! { None }
};
let docstr =
doc.unwrap_or_else(|| format!(" Auto-generated derived type for {ident} via `CustomResource`"));
let quoted_serde = Literal::string(&serde.to_token_stream().to_string());
let root_obj = quote! {
#[doc = #docstr]
#[automatically_derived]
#[allow(missing_docs)]
#[derive(#(#derive_paths),*)]
#[serde(rename_all = "camelCase")]
#[serde(crate = #quoted_serde)]
#struct_rules
#visibility struct #rootident {
#schemars_skip
#visibility metadata: #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta,
#visibility spec: #ident,
#status_field
}
impl #rootident {
/// Spec based constructor for derived custom resource
pub fn new(name: &str, spec: #ident) -> Self {
Self {
metadata: #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
annotations: #meta_annotations,
labels: #meta_labels,
name: Some(name.to_string()),
..Default::default()
},
spec: spec,
#status_default
}
}
}
impl #serde::Serialize for #rootident {
fn serialize<S: #serde::Serializer>(&self, ser: S) -> #std::result::Result<S::Ok, S::Error> {
use #serde::ser::SerializeStruct;
let mut obj = ser.serialize_struct(#rootident_str, 4 + usize::from(#has_status_value))?;
obj.serialize_field("apiVersion", &<#rootident as #kube_core::Resource>::api_version(&()))?;
obj.serialize_field("kind", &<#rootident as #kube_core::Resource>::kind(&()))?;
obj.serialize_field("metadata", &self.metadata)?;
obj.serialize_field("spec", &self.spec)?;
#serialize_status
obj.end()
}
}
};
// 2. Implement Resource trait
let name = singular.unwrap_or_else(|| kind.to_ascii_lowercase());
let plural = plural.unwrap_or_else(|| to_plural(&name));
let (scope, scope_quote) = if namespaced {
("Namespaced", quote! { #kube_core::NamespaceResourceScope })
} else {
("Cluster", quote! { #kube_core::ClusterResourceScope })
};
let api_ver = format!("{group}/{version}");
let impl_resource = quote! {
impl #kube_core::Resource for #rootident {
type DynamicType = ();
type Scope = #scope_quote;
fn group(_: &()) -> std::borrow::Cow<'_, str> {
#group.into()
}
fn kind(_: &()) -> std::borrow::Cow<'_, str> {
#kind.into()
}
fn version(_: &()) -> std::borrow::Cow<'_, str> {
#version.into()
}
fn api_version(_: &()) -> std::borrow::Cow<'_, str> {
#api_ver.into()
}
fn plural(_: &()) -> std::borrow::Cow<'_, str> {
#plural.into()
}
fn meta(&self) -> &#k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
&self.metadata
}
fn meta_mut(&mut self) -> &mut #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
&mut self.metadata
}
}
};
// 3. Implement Default if requested
let impl_default = if has_default {
quote! {
impl Default for #rootident {
fn default() -> Self {
Self {
metadata: #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta::default(),
spec: Default::default(),
#status_default
}
}
}
}
} else {
quote! {}
};
// 4. Implement CustomResource
// Compute a bunch of crd props
let printers = format!("[ {} ]", printcolums.join(",")); // hacksss
let fields: Vec<String> = selectable
.iter()
.map(|s| format!(r#"{{ "jsonPath": "{s}" }}"#))
.collect();
let fields = format!("[ {} ]", fields.join(","));
let scale = scale.map_or_else(
|| quote! { None },
|s| {
let scale = s.to_tokens(&k8s_openapi);
quote! { Some(#scale) }
},
);
// Ensure it generates for the correct CRD version (only v1 supported now)
let apiext = quote! {
#k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1
};
let extver = quote! {
#kube_core::crd::v1
};
let shortnames_slice = {
let names = shortnames
.iter()
.map(|name| quote! { #name, })
.collect::<TokenStream>();
quote! { &[#names] }
};
let categories_json = serde_json::to_string(&categories).unwrap();
let short_json = serde_json::to_string(&shortnames).unwrap();
let crd_meta_name = format!("{plural}.{group}");
let mut crd_meta = TokenStream::new();
crd_meta.extend(quote! { "name": #crd_meta_name });
if !annotations.is_empty() {
crd_meta.extend(quote! { , "annotations": #meta_annotations });
}
if !labels.is_empty() {
crd_meta.extend(quote! { , "labels": #meta_labels });
}
let schemagen = if schema_mode.use_in_crd() {
quote! {
// Don't use definitions and don't include `$schema` because these are not allowed.
let gen = #schemars::gen::SchemaSettings::openapi3()
.with(|s| {
s.inline_subschemas = true;
s.meta_schema = None;
})
.with_visitor(#kube_core::schema::StructuralSchemaRewriter)
.into_generator();
let schema = gen.into_root_schema_for::<Self>();
}
} else {
// we could issue a compile time warning for this, but it would hit EVERY compile, which would be noisy
// eprintln!("warning: kube-derive configured with manual schema generation");
// users must manually set a valid schema in crd.spec.versions[*].schema - see examples: crd_derive_no_schema
quote! {
let schema: Option<#k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::JSONSchemaProps> = None;
}
};
let selectable = if !selectable.is_empty() {
quote! { "selectableFields": fields, }
} else {
quote! {}
};
let deprecation = if let Some(deprecation) = deprecated {
match deprecation {
Override::Inherit => quote! { "deprecated": true, },
Override::Explicit(warning) => quote! {
"deprecated": true,
"deprecationWarning": #warning,
},
}
} else {
quote! {}
};
// Known constraints that are hard to enforce elsewhere
let compile_constraints = if !selectable.is_empty() {
quote! {
#k8s_openapi::k8s_if_le_1_29! {
compile_error!("selectable fields require Kubernetes >= 1.30");
}
}
} else {
quote! {}
};
let jsondata = quote! {
#schemagen
let jsondata = #serde_json::json!({
"metadata": {
#crd_meta
},
"spec": {
"group": #group,
"scope": #scope,
"names": {
"categories": categories,
"plural": #plural,
"singular": #name,
"kind": #kind,
"shortNames": shorts
},
"versions": [{
"name": #version,
"served": #served,
"storage": #storage,
#deprecation
"schema": {
"openAPIV3Schema": schema,
},
"additionalPrinterColumns": columns,
#selectable
"subresources": subres,
}],
}
});
};
// Implement the CustomResourceExt trait to allow users writing generic logic on top of them
let impl_crd = quote! {
impl #extver::CustomResourceExt for #rootident {
fn crd() -> #apiext::CustomResourceDefinition {
let columns : Vec<#apiext::CustomResourceColumnDefinition> = #serde_json::from_str(#printers).expect("valid printer column json");
#k8s_openapi::k8s_if_ge_1_30! {
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");
let shorts : Vec<String> = #serde_json::from_str(#short_json).expect("valid shortnames");
let subres = if #has_status {
if let Some(s) = &scale {
#serde_json::json!({
"status": {},
"scale": scale
})
} else {
#serde_json::json!({"status": {} })
}
} else {
#serde_json::json!({})
};
#jsondata
#serde_json::from_value(jsondata)
.expect("valid custom resource from #[kube(attrs..)]")
}
fn crd_name() -> &'static str {
#crd_meta_name
}
fn api_resource() -> #kube_core::dynamic::ApiResource {
#kube_core::dynamic::ApiResource::erase::<Self>(&())
}
fn shortnames() -> &'static [&'static str] {
#shortnames_slice
}
}
};
let impl_hasspec = generate_hasspec(&ident, &rootident, &kube_core);
// Concat output
quote! {
#compile_constraints
#root_obj
#impl_resource
#impl_default
#impl_crd
#impl_hasspec
#impl_hasstatus
}
}
/// This generates the code for the `#kube_core::object::HasSpec` trait implementation.
///
/// All CRDs have a spec so it is implemented for all of them.
///
/// # Arguments
///
/// * `ident`: The identity (name) of the spec struct
/// * `root ident`: The identity (name) of the main CRD struct (the one we generate in this macro)
/// * `kube_core`: The path stream for the analagous kube::core import location from users POV
fn generate_hasspec(spec_ident: &Ident, root_ident: &Ident, kube_core: &Path) -> TokenStream {
quote! {
impl #kube_core::object::HasSpec for #root_ident {
type Spec = #spec_ident;
fn spec(&self) -> &#spec_ident {
&self.spec
}
fn spec_mut(&mut self) -> &mut #spec_ident {
&mut self.spec
}
}
}
}
struct StatusInformation {
/// The code to be used for the field in the main struct
field: TokenStream,
/// The initialization code to use in a `Default` and `::new()` implementation
default: TokenStream,
/// The implementation code for the `HasStatus` trait
impl_hasstatus: TokenStream,
}
/// This processes the `status` field of a CRD.
///
/// As it is optional some features will be turned on or off depending on whether it's available or not.
///
/// # Arguments
///
/// * `root ident`: The identity (name) of the main CRD struct (the one we generate in this macro)
/// * `status`: The optional name of the `status` struct to use
/// * `visibility`: Desired visibility of the generated field
/// * `kube_core`: The path stream for the analagous kube::core import location from users POV
///
/// returns: A `StatusInformation` struct
fn process_status(
root_ident: &Ident,
status: &Option<Path>,
visibility: &Visibility,
kube_core: &Path,
) -> StatusInformation {
if let Some(pth) = &status {
StatusInformation {
field: quote! {
#[serde(skip_serializing_if = "Option::is_none")]
#visibility status: Option<#pth>,
},
default: quote! { status: None, },
impl_hasstatus: quote! {
impl #kube_core::object::HasStatus for #root_ident {
type Status = #pth;
fn status(&self) -> Option<&#pth> {
self.status.as_ref()
}
fn status_mut(&mut self) -> &mut Option<#pth> {
&mut self.status
}
}
},
}
} else {
let empty_quote = quote! {};
StatusInformation {
field: empty_quote.clone(),
default: empty_quote.clone(),
impl_hasstatus: empty_quote,
}
}
}
// Simple pluralizer.
// Duplicating the code from kube (without special casing) because it's simple enough.
// Irregular plurals must be explicitly specified.
fn to_plural(word: &str) -> String {
// Words ending in s, x, z, ch, sh will be pluralized with -es (eg. foxes).
if word.ends_with('s')
|| word.ends_with('x')
|| word.ends_with('z')
|| word.ends_with("ch")
|| word.ends_with("sh")
{
return format!("{word}es");
}
// Words ending in y that are preceded by a consonant will be pluralized by
// replacing y with -ies (eg. puppies).
if word.ends_with('y') {
if let Some(c) = word.chars().nth(word.len() - 2) {
if !matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
// Remove 'y' and add `ies`
let mut chars = word.chars();
chars.next_back();
return format!("{}ies", chars.as_str());
}
}
}
// All other words will have "s" added to the end (eg. days).
format!("{word}s")
}
#[cfg(test)]
mod tests {
use std::{env, fs};
use super::*;
#[test]
fn test_parse_default() {
let input = quote! {
#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
struct FooSpec { foo: String }
};
let input = syn::parse2(input).unwrap();
let kube_attrs = KubeAttrs::from_derive_input(&input).unwrap();
assert_eq!(kube_attrs.group, "clux.dev".to_string());
assert_eq!(kube_attrs.version, "v1".to_string());
assert_eq!(kube_attrs.kind, "Foo".to_string());
assert!(kube_attrs.namespaced);
}
#[test]
fn test_derive_crd() {
let path = env::current_dir().unwrap().join("tests").join("crd_enum_test.rs");
let file = fs::File::open(path).unwrap();
runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap();
let path = env::current_dir()
.unwrap()
.join("tests")
.join("crd_schema_test.rs");
let file = fs::File::open(path).unwrap();
runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap();
}
}

450
vendor/kube-derive/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,450 @@
//! A crate for kube's derive macros.
#![recursion_limit = "1024"]
extern crate proc_macro;
#[macro_use] extern crate quote;
mod cel_schema;
mod custom_resource;
mod resource;
/// A custom derive for kubernetes custom resource definitions.
///
/// This will generate a **root object** containing your spec and metadata.
/// This root object will implement the [`kube::Resource`] trait
/// so it can be used with [`kube::Api`].
///
/// The generated type will also implement kube's [`kube::CustomResourceExt`] trait to generate the crd
/// and generate [`kube::core::ApiResource`] information for use with the dynamic api.
///
/// # Example
///
/// ```rust
/// use serde::{Serialize, Deserialize};
/// use kube::core::{Resource, CustomResourceExt};
/// use kube_derive::CustomResource;
/// use schemars::JsonSchema;
///
/// #[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
/// #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
/// struct FooSpec {
/// info: String,
/// }
///
/// println!("kind = {}", Foo::kind(&())); // impl kube::Resource
/// let f = Foo::new("foo-1", FooSpec {
/// info: "informative info".into(),
/// });
/// println!("foo: {:?}", f); // debug print on root type
/// println!("crd: {}", serde_yaml::to_string(&Foo::crd()).unwrap()); // crd yaml
/// ```
///
/// This example generates a `struct Foo` containing metadata, the spec,
/// and optionally status. The **root** struct `Foo` can be used with the [`kube`] crate
/// as an `Api<Foo>` object (`FooSpec` can not be used with [`Api`][`kube::Api`]).
///
/// ```no_run
/// # use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
/// # use kube_derive::CustomResource;
/// # use kube::{api::{Api, Patch, PatchParams}, Client, CustomResourceExt};
/// # use serde::{Deserialize, Serialize};
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
/// # #[derive(CustomResource, Clone, Debug, Deserialize, Serialize, schemars::JsonSchema)]
/// # #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
/// # struct FooSpec {}
/// # let client: Client = todo!();
/// let foos: Api<Foo> = Api::default_namespaced(client.clone());
/// let crds: Api<CustomResourceDefinition> = Api::all(client.clone());
/// crds.patch("foos.clux.dev", &PatchParams::apply("myapp"), &Patch::Apply(Foo::crd())).await;
/// # Ok(())
/// # }
/// ```
///
/// This example posts the generated `::crd` to the `CustomResourceDefinition` API.
/// After this has been accepted (few secs max), you can start using `foos` as a normal
/// kube `Api` object. See the `crd_` prefixed [examples](https://github.com/kube-rs/kube/blob/main/examples/)
/// for details on this.
///
/// # Required properties
///
/// ## `#[kube(group = "mygroup.tld")]`
/// Your cr api group. The part before the slash in the top level `apiVersion` key.
///
/// ## `#[kube(version = "v1")]`
/// Your cr api version. The part after the slash in the top level `apiVersion` key.
///
/// ## `#[kube(kind = "Kind")]`
/// Name of your kind, and implied default for your generated root type.
///
/// # Optional `#[kube]` attributes
///
/// ## `#[kube(singular = "nonstandard-singular")]`
/// To specify the singular name. Defaults to lowercased `.kind` value.
///
/// ## `#[kube(plural = "nonstandard-plural")]`
/// To specify the plural name. Defaults to inferring from singular.
///
/// ## `#[kube(namespaced)]`
/// To specify that this is a namespaced resource rather than cluster level.
///
/// ## `#[kube(root = "StructName")]`
/// Customize the name of the generated root struct (defaults to `.kind` value).
///
/// ## `#[kube(crates(kube_core = "::kube::core"))]`
/// Customize the crate name the generated code will reach into (defaults to `::kube::core`).
/// Should be one of `kube::core`, `kube_client::core` or `kube_core`.
///
/// ## `#[kube(crates(k8s_openapi = "::k8s_openapi"))]`
/// Customize the crate name the generated code will use for [`k8s_openapi`](https://docs.rs/k8s-openapi/) (defaults to `::k8s_openapi`).
///
/// ## `#[kube(crates(schemars = "::schemars"))]`
/// Customize the crate name the generated code will use for [`schemars`](https://docs.rs/schemars/) (defaults to `::schemars`).
///
/// ## `#[kube(crates(serde = "::serde"))]`
/// Customize the crate name the generated code will use for [`serde`](https://docs.rs/serde/) (defaults to `::serde`).
///
/// ## `#[kube(crates(serde_json = "::serde_json"))]`
/// Customize the crate name the generated code will use for [`serde_json`](https://docs.rs/serde_json/) (defaults to `::serde_json`).
///
/// ## `#[kube(status = "StatusStructName")]`
/// Adds a status struct to the top level generated type and enables the status
/// subresource in your crd.
///
/// ## `#[kube(derive = "Trait")]`
/// Adding `#[kube(derive = "PartialEq")]` is required if you want your generated
/// top level type to be able to `#[derive(PartialEq)]`
///
/// ## `#[kube(schema = "mode")]`
/// Defines whether the `JsonSchema` of the top level generated type should be used when generating a `CustomResourceDefinition`.
///
/// Legal values:
/// - `"derived"`: A `JsonSchema` implementation is automatically derived
/// - `"manual"`: `JsonSchema` is not derived, but used when creating the `CustomResourceDefinition` object
/// - `"disabled"`: No `JsonSchema` is used
///
/// This can be used to provide a completely custom schema, or to interact with third-party custom resources
/// where you are not responsible for installing the `CustomResourceDefinition`.
///
/// Defaults to `"derived"`.
///
/// NOTE: `CustomResourceDefinition`s require a schema. If `schema = "disabled"` then
/// `Self::crd()` will not be installable into the cluster as-is.
///
/// ## `#[kube(scale(...))]`
///
/// Allow customizing the scale struct for the [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#subresources).
/// It should be noted, that the status subresource must also be enabled to use the scale subresource. This is because
/// the `statusReplicasPath` only accepts JSONPaths under `.status`.
///
/// ```ignore
/// #[kube(scale(
/// specReplicasPath = ".spec.replicas",
/// statusReplicaPath = ".status.replicas",
/// labelSelectorPath = ".spec.labelSelector"
/// ))]
/// ```
///
/// 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(shortname = "sn")]`
/// Add a single shortname to the generated crd.
///
/// ## `#[kube(category = "apps")]`
/// Add a single category to `crd.spec.names.categories`.
///
/// ## `#[kube(selectable = "fieldSelectorPath")]`
/// Adds a Kubernetes >=1.30 `selectableFields` property ([KEP-4358](https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/4358-custom-resource-field-selectors/README.md)) to the schema.
/// Unlocks `kubectl get kind --field-selector fieldSelectorPath`.
///
/// ## `#[kube(doc = "description")]`
/// Sets the description of the schema in the generated CRD. If not specified
/// `Auto-generated derived type for {customResourceName} via CustomResource` will be used instead.
///
/// ## `#[kube(annotation("ANNOTATION_KEY", "ANNOTATION_VALUE"))]`
/// Add a single annotation to the generated CRD.
///
/// ## `#[kube(label("LABEL_KEY", "LABEL_VALUE"))]`
/// Add a single label to the generated CRD.
///
/// ## `#[kube(storage = true)]`
/// Sets the `storage` property to `true` or `false`.
///
/// ## `#[kube(served = true)]`
/// Sets the `served` property to `true` or `false`.
///
/// ## `#[kube(deprecated [= "warning"])]`
/// Sets the `deprecated` property to `true`.
///
/// ```ignore
/// #[kube(deprecated)]
/// ```
///
/// Aditionally, you can provide a `deprecationWarning` using the following example.
///
/// ```ignore
/// #[kube(deprecated = "Replaced by other CRD")]
/// ```
///
/// ## `#[kube(rule = Rule::new("self == oldSelf").message("field is immutable"))]`
/// Inject a top level CEL validation rule for the top level generated struct.
/// This attribute is for resources deriving [`CELSchema`] instead of [`schemars::JsonSchema`].
///
/// ## Example with all properties
///
/// ```rust
/// use serde::{Serialize, Deserialize};
/// use kube_derive::CustomResource;
/// use schemars::JsonSchema;
///
/// #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
/// #[kube(
/// group = "clux.dev",
/// version = "v1",
/// kind = "Foo",
/// root = "FooCrd",
/// namespaced,
/// doc = "Custom resource representing a Foo",
/// status = "FooStatus",
/// derive = "PartialEq",
/// singular = "foot",
/// 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"}"#,
/// selectable = "spec.replicasCount"
/// )]
/// #[serde(rename_all = "camelCase")]
/// struct FooSpec {
/// #[schemars(length(min = 3))]
/// data: String,
/// replicas_count: i32
/// }
///
/// #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
/// struct FooStatus {
/// replicas: i32
/// }
/// ```
///
/// # Enums
///
/// Kubernetes requires that the generated [schema is "structural"](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema).
/// This means that the structure of the schema must not depend on the particular values. For enums this imposes a few limitations:
///
/// - Only [externally tagged enums](https://serde.rs/enum-representations.html#externally-tagged) are supported
/// - Unit variants may not be mixed with struct or tuple variants (`enum Foo { Bar, Baz {}, Qux() }` is invalid, for example)
///
/// If these restrictions are not followed then `YourCrd::crd()` may panic, or the Kubernetes API may reject the CRD definition.
///
/// # Generated code
///
/// The example above will **roughly** generate:
/// ```compile_fail
/// #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
/// #[serde(rename_all = "camelCase")]
/// pub struct FooCrd {
/// api_version: String,
/// kind: String,
/// metadata: ObjectMeta,
/// spec: FooSpec,
/// status: Option<FooStatus>,
/// }
/// impl kube::Resource for FooCrd { .. }
///
/// impl FooCrd {
/// pub fn new(name: &str, spec: FooSpec) -> Self { .. }
/// pub fn crd() -> CustomResourceDefinition { .. }
/// }
/// ```
///
/// # Customizing Schemas
/// Should you need to customize the schemas, you can use:
/// - [Serde/Schemars Attributes](https://graham.cool/schemars/examples/3-schemars_attrs/) (no need to duplicate serde renames)
/// - [`#[schemars(schema_with = "func")]`](https://graham.cool/schemars/examples/7-custom_serialization/) (e.g. like in the [`crd_derive` example](https://github.com/kube-rs/kube/blob/main/examples/crd_derive.rs))
/// - `impl JsonSchema` on a type / newtype around external type. See [#129](https://github.com/kube-rs/kube/issues/129#issuecomment-750852916)
/// - [`#[garde(...)]` field attributes for client-side validation](https://github.com/jprochazk/garde) (see [`crd_api` example](https://github.com/kube-rs/kube/blob/main/examples/crd_api.rs))
///
/// You might need to override parts of the schemas (for fields in question) when you are:
/// - **using complex enums**: enums do not currently generate [structural schemas](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema), so kubernetes won't support them by default
/// - **customizing [merge-strategies](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy)** (e.g. like in the [`crd_derive_schema` example](https://github.com/kube-rs/kube/blob/main/examples/crd_derive_schema.rs))
///
/// See [kubernetes openapi validation](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation) for the format of the OpenAPI v3 schemas.
///
/// If you have to override a lot, [you can opt-out of schema-generation entirely](#kubeschema--mode)
///
/// # Advanced Features
///
/// - **embedding k8s-openapi types** can be done by enabling the `schemars` feature of `k8s-openapi` from [`0.13.0`](https://github.com/Arnavion/k8s-openapi/blob/master/CHANGELOG.md#v0130-2021-08-09)
/// - **adding validation** via [validator crate](https://github.com/Keats/validator) is supported from `schemars` >= [`0.8.5`](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md#085---2021-09-20)
/// - **generating rust code from schemas** can be done via [kopium](https://github.com/kube-rs/kopium) and is supported on stable crds (> 1.16 kubernetes)
///
/// ## Schema Validation
/// There are two main ways of doing validation; **server-side** (embedding validation attributes into the schema for the apiserver to respect), and **client-side** (provides `validate()` methods in your code).
///
/// Client side validation of structs can be achieved by hooking up `#[garde]` attributes in your struct and is a replacement of the now unmaintained [`validator`](https://github.com/Keats/validator/issues/201) crate.
/// Server-side validation require mutation of your generated schema, and can in the basic cases be achieved through the use of `schemars`'s [validation attributes](https://graham.cool/schemars/deriving/attributes/#supported-validator-attributes).
/// For complete control, [parts of the schema can be overridden](https://github.com/kube-rs/kube/blob/e01187e13ba364ccecec452e023316a62fb13e04/examples/crd_derive.rs#L37-L38) to support more advanced [Kubernetes specific validation rules](https://kubernetes.io/blog/2022/09/23/crd-validation-rules-beta/).
///
/// When using `garde` directly, you must add it to your dependencies (with the `derive` feature).
///
/// ### Validation Caveats
/// Make sure your validation rules are static and handled by `schemars`:
/// - validations from `#[garde(custom(my_func))]` will not show up in the schema.
/// - similarly; [nested / must_match / credit_card were unhandled by schemars at time of writing](https://github.com/GREsau/schemars/pull/78)
/// - encoding validations specified through garde (i.e. #[garde(ascii)]), are currently not supported by schemars
/// - to validate required attributes client-side, garde requires a custom validation function (`#[garde(custom(my_required_check))]`)
/// - when using garde, fields that should not be validated need to be explictly skipped through the `#[garde(skip)]` attr
///
/// For sanity, you should review the generated schema before sending it to kubernetes.
///
/// ## Versioning
/// Note that any changes to your struct / validation rules / serialization attributes will require you to re-apply the
/// generated schema to kubernetes, so that the apiserver can validate against the right version of your structs.
///
/// **Backwards compatibility** between schema versions is **recommended** unless you are in a controlled environment
/// where you can migrate manually. I.e. if you add new properties behind options, and simply mark old fields as deprecated,
/// then you can safely roll schema out changes **without bumping** the version.
///
/// If you need **multiple versions**, then you need:
///
/// - one **module** for **each version** of your types (e.g. `v1::MyCrd` and `v2::MyCrd`)
/// - use the [`merge_crds`](https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html) fn to combine crds
/// - roll out new schemas utilizing conversion webhooks / manual conversions / or allow kubectl to do its best
///
/// See the [crd_derive_multi](https://github.com/kube-rs/kube/blob/main/examples/crd_derive_multi.rs) example to see
/// how this upgrade flow works without special logic.
///
/// The **upgrade flow** with **breaking changes** involves:
///
/// 1. upgrade version marked as `storage` (from v1 to v2)
/// 2. read instances from the older `Api<v1::MyCrd>`
/// 3. perform conversion in memory and write them to the new `Api<v2::MyCrd>`.
/// 4. remove support for old version
///
/// If you need to maintain support for the old version for some time, then you have to repeat or continuously
/// run steps 2 and 3. I.e. you probably need a **conversion webhook**.
///
/// **NB**: kube does currently [not implement conversion webhooks yet](https://github.com/kube-rs/kube/issues/865).
///
/// ## Debugging
/// Try `cargo-expand` to see your own macro expansion.
///
/// # Installation
/// Enable the `derive` feature on the `kube` crate:
///
/// ```toml
/// kube = { version = "...", features = ["derive"] }
/// ```
///
/// ## Runtime dependencies
/// Due to [rust-lang/rust#54363](https://github.com/rust-lang/rust/issues/54363), we cannot be resilient against crate renames within our generated code.
/// It's therefore **required** that you have the following crates in scope, not renamed:
///
/// - `serde_json`
/// - `k8s_openapi`
/// - `schemars` (by default, unless `schema` feature disabled)
///
/// You are ultimately responsible for maintaining the versions and feature flags of these libraries.
///
/// [`kube`]: https://docs.rs/kube
/// [`kube::Api`]: https://docs.rs/kube/*/kube/struct.Api.html
/// [`kube::Resource`]: https://docs.rs/kube/*/kube/trait.Resource.html
/// [`kube::core::ApiResource`]: https://docs.rs/kube/*/kube/core/struct.ApiResource.html
/// [`kube::CustomResourceExt`]: https://docs.rs/kube/*/kube/trait.CustomResourceExt.html
#[proc_macro_derive(CustomResource, attributes(kube))]
pub fn derive_custom_resource(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
custom_resource::derive(proc_macro2::TokenStream::from(input)).into()
}
/// Generates a JsonSchema implementation a set of CEL validation rules applied on the CRD.
///
/// ```rust
/// use kube::CELSchema;
/// use kube::CustomResource;
/// use serde::Deserialize;
/// use serde::Serialize;
/// use kube::core::crd::CustomResourceExt;
///
/// #[derive(CustomResource, CELSchema, Serialize, Deserialize, Clone, Debug)]
/// #[kube(
/// group = "kube.rs",
/// version = "v1",
/// kind = "Struct",
/// rule = Rule::new("self.matadata.name == 'singleton'"),
/// )]
/// #[cel_validate(rule = Rule::new("self == oldSelf"))]
/// struct MyStruct {
/// #[serde(default = "default")]
/// #[cel_validate(rule = Rule::new("self != ''").message("failure message"))]
/// field: String,
/// }
///
/// fn default() -> String {
/// "value".into()
/// }
///
/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains("x-kubernetes-validations"));
/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""rule":"self == oldSelf""#));
/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""rule":"self != ''""#));
/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""message":"failure message""#));
/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""default":"value""#));
/// assert!(serde_json::to_string(&Struct::crd()).unwrap().contains(r#""rule":"self.matadata.name == 'singleton'""#));
/// ```
#[proc_macro_derive(CELSchema, attributes(cel_validate, schemars))]
pub fn derive_schema_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
cel_schema::derive_validated_schema(input.into()).into()
}
/// A custom derive for inheriting Resource impl for the type.
///
/// This will generate a [`kube::Resource`] trait implementation,
/// inheriting from a specified resource trait implementation.
///
/// This allows strict typing to some typical resources like `Secret` or `ConfigMap`,
/// in cases when implementing CRD is not desirable or it does not fit the use-case.
///
/// Once derived, the type can be used with [`kube::Api`].
///
/// # Example
///
/// ```rust,no_run
/// use kube::api::ObjectMeta;
/// use k8s_openapi::api::core::v1::ConfigMap;
/// use kube_derive::Resource;
/// use kube::Client;
/// use kube::Api;
/// use serde::Deserialize;
///
/// #[derive(Resource, Clone, Debug, Deserialize)]
/// #[resource(inherit = "ConfigMap")]
/// struct FooMap {
/// metadata: ObjectMeta,
/// data: Option<FooMapSpec>,
/// }
///
/// #[derive(Clone, Debug, Deserialize)]
/// struct FooMapSpec {
/// field: String,
/// }
///
/// let client: Client = todo!();
/// let api: Api<FooMap> = Api::default_namespaced(client);
/// let config_map = api.get("with-field");
/// ```
///
/// The example above will generate:
/// ```
/// // impl kube::Resource for FooMap { .. }
/// ```
/// [`kube`]: https://docs.rs/kube
/// [`kube::Api`]: https://docs.rs/kube/*/kube/struct.Api.html
/// [`kube::Resource`]: https://docs.rs/kube/*/kube/trait.Resource.html
/// [`kube::core::ApiResource`]: https://docs.rs/kube/*/kube/core/struct.ApiResource.html
/// [`kube::CustomResourceExt`]: https://docs.rs/kube/*/kube/trait.CustomResourceExt.html
#[proc_macro_derive(Resource, attributes(resource))]
pub fn derive_resource(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
resource::derive(proc_macro2::TokenStream::from(input)).into()
}

127
vendor/kube-derive/src/resource.rs vendored Normal file
View File

@@ -0,0 +1,127 @@
// Generated by darling macros, out of our control
#![allow(clippy::manual_unwrap_or_default)]
use darling::{FromDeriveInput, FromMeta};
use syn::{parse_quote, Data, DeriveInput, Path};
/// Values we can parse from #[kube(attrs)]
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(resource))]
struct InheritAttrs {
inherit: syn::Path,
#[darling(default)]
crates: Crates,
}
#[derive(Debug, FromMeta)]
struct Crates {
#[darling(default = "Self::default_kube_core")]
kube_core: Path,
#[darling(default = "Self::default_k8s_openapi")]
k8s_openapi: Path,
}
// Default is required when the subattribute isn't mentioned at all
// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
impl Default for Crates {
fn default() -> Self {
Self::from_list(&[]).unwrap()
}
}
impl Crates {
fn default_kube_core() -> Path {
parse_quote! { ::kube::core } // by default must work well with people using facade crate
}
fn default_k8s_openapi() -> Path {
parse_quote! { ::k8s_openapi }
}
}
pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let derive_input: DeriveInput = match syn::parse2(input) {
Err(err) => return err.to_compile_error(),
Ok(di) => di,
};
// Limit derive to structs
match derive_input.data {
Data::Struct(_) | Data::Enum(_) => {}
_ => {
return syn::Error::new_spanned(&derive_input.ident, r#"Unions can not #[derive(Resource)]"#)
.to_compile_error()
}
}
let kube_attrs = match InheritAttrs::from_derive_input(&derive_input) {
Err(err) => return err.write_errors(),
Ok(attrs) => attrs,
};
let InheritAttrs {
inherit: resource,
crates: Crates {
kube_core,
k8s_openapi,
},
..
} = kube_attrs;
let rootident = derive_input.ident;
let inherit_resource = quote! {
impl #kube_core::Resource for #rootident {
type DynamicType = <#resource as #kube_core::Resource>::DynamicType;
type Scope = <#resource as #kube_core::Resource>::Scope;
fn group(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> {
#resource::group(&Default::default()).into_owned().into()
}
fn kind(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> {
#resource::kind(&Default::default()).into_owned().into()
}
fn version(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> {
#resource::version(&Default::default()).into_owned().into()
}
fn api_version(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> {
#resource::api_version(&Default::default()).into_owned().into()
}
fn plural(_: &<#resource as #kube_core::Resource>::DynamicType) -> std::borrow::Cow<'_, str> {
#resource::plural(&Default::default()).into_owned().into()
}
fn meta(&self) -> &#k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
&self.metadata
}
fn meta_mut(&mut self) -> &mut #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
&mut self.metadata
}
}
};
// Concat output
quote! {
#inherit_resource
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_inherit() {
let input = quote! {
#[derive(Resource)]
#[resource(inherit = "ConfigMap")]
struct Foo { metadata: ObjectMeta }
};
let input = syn::parse2(input).unwrap();
InheritAttrs::from_derive_input(&input).unwrap();
}
}

Binary file not shown.

View File

@@ -0,0 +1 @@
{"name":"kube-derive","vers":"0.99.0","deps":[{"name":"darling","req":"^0.20.3","features":[],"optional":false,"default_features":true,"target":null,"kind":"normal","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"proc-macro2","req":"^1.0.29","features":[],"optional":false,"default_features":true,"target":null,"kind":"normal","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"quote","req":"^1.0.10","features":[],"optional":false,"default_features":true,"target":null,"kind":"normal","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"serde","req":"^1.0.130","features":["derive"],"optional":false,"default_features":true,"target":null,"kind":"normal","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"serde_json","req":"^1.0.68","features":[],"optional":false,"default_features":true,"target":null,"kind":"normal","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"syn","req":"^2.0.38","features":["extra-traits"],"optional":false,"default_features":true,"target":null,"kind":"normal","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"assert-json-diff","req":"^2.0.2","features":[],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"chrono","req":"^0.4.34","features":[],"optional":false,"default_features":false,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"k8s-openapi","req":"^0.24.0","features":["latest"],"optional":false,"default_features":false,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"kube","req":"<2.0.0, >=0.98.0","features":["derive","client"],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"prettyplease","req":"^0.2.25","features":[],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"schemars","req":"^0.8.6","features":["chrono"],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"serde","req":"^1.0.130","features":["derive"],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"serde_yaml","req":"^0.9.19","features":[],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false},{"name":"trybuild","req":"^1.0.48","features":[],"optional":false,"default_features":true,"target":null,"kind":"dev","registry":"https://github.com/rust-lang/crates.io-index","package":null,"public":null,"artifact":null,"bindep_target":null,"lib":false}],"features":{},"features2":null,"cksum":"1cb701e0fc477064cffdaa87eb1398d42a37885200d623ac96775331822c3d7f","yanked":null,"links":null,"rust_version":null,"v":2}

View File

@@ -0,0 +1,155 @@
use kube_derive::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "FooEnum")]
#[serde(rename_all = "camelCase")]
#[allow(clippy::enum_variant_names)]
enum FooEnumSpec {
/// First variant with an int
VariantOne { int: i32 },
/// Second variant with an String
VariantTwo { str: String },
/// Third variant which doesn't has an attribute
VariantThree {},
}
#[test]
fn test_crd_name() {
use kube::core::CustomResourceExt;
assert_eq!("fooenums.clux.dev", FooEnum::crd_name());
}
#[test]
fn test_serialized_matches_expected() {
assert_eq!(
serde_json::to_value(FooEnum::new("bar", FooEnumSpec::VariantOne { int: 42 })).unwrap(),
serde_json::json!({
"apiVersion": "clux.dev/v1",
"kind": "FooEnum",
"metadata": {
"name": "bar",
},
"spec": {
"variantOne": {
"int": 42
}
}
})
);
assert_eq!(
serde_json::to_value(FooEnum::new("bar", FooEnumSpec::VariantThree {})).unwrap(),
serde_json::json!({
"apiVersion": "clux.dev/v1",
"kind": "FooEnum",
"metadata": {
"name": "bar",
},
"spec": {
"variantThree": {}
}
})
);
}
#[test]
fn test_crd_schema_matches_expected() {
use kube::core::CustomResourceExt;
assert_eq!(
FooEnum::crd(),
serde_json::from_value(serde_json::json!({
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {
"name": "fooenums.clux.dev"
},
"spec": {
"group": "clux.dev",
"names": {
"categories": [],
"kind": "FooEnum",
"plural": "fooenums",
"shortNames": [],
"singular": "fooenum"
},
"scope": "Cluster",
"versions": [
{
"additionalPrinterColumns": [],
"name": "v1",
"schema": {
"openAPIV3Schema": {
"description": "Auto-generated derived type for FooEnumSpec via `CustomResource`",
"properties": {
"spec": {
"oneOf": [
{
"required": [
"variantOne"
]
},
{
"required": [
"variantTwo"
]
},
{
"required": [
"variantThree"
]
}
],
"properties": {
"variantOne": {
"description": "First variant with an int",
"properties": {
"int": {
"format": "int32",
"type": "integer"
}
},
"required": [
"int"
],
"type": "object"
},
"variantThree": {
"description": "Third variant which doesn't has an attribute",
"type": "object"
},
"variantTwo": {
"description": "Second variant with an String",
"properties": {
"str": {
"type": "string"
}
},
"required": [
"str"
],
"type": "object"
}
},
"type": "object"
}
},
"required": [
"spec"
],
"title": "FooEnum",
"type": "object"
}
},
"served": true,
"storage": true,
"subresources": {}
}
]
}
}
))
.unwrap()
);
}

View File

@@ -0,0 +1,436 @@
#![allow(missing_docs)]
#![recursion_limit = "256"]
use assert_json_diff::assert_json_eq;
use chrono::{DateTime, Utc};
use kube::CELSchema;
use kube_derive::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
// See `crd_derive_schema` example for how the schema generated from this struct affects defaulting and validation.
#[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, CELSchema)]
#[kube(
group = "clux.dev",
version = "v1",
kind = "Foo",
category = "clux",
namespaced,
doc = "Custom resource representing a Foo",
derive = "PartialEq",
shortname = "fo",
shortname = "f",
served = false,
storage = false,
deprecated = "my warning",
selectable = ".spec.nonNullable",
selectable = ".spec.nullable",
annotation("clux.dev", "cluxingv1"),
annotation("clux.dev/firewall", "enabled"),
label("clux.dev", "cluxingv1"),
label("clux.dev/persistence", "disabled"),
rule = Rule::new("self.metadata.name == 'singleton'"),
status = "Status",
scale(
spec_replicas_path = ".spec.replicas",
status_replicas_path = ".status.replicas",
label_selector_path = ".status.labelSelector"
),
)]
#[cel_validate(rule = Rule::new("has(self.nonNullable)"))]
#[serde(rename_all = "camelCase")]
struct FooSpec {
non_nullable: String,
#[serde(default = "default_value")]
non_nullable_with_default: String,
#[serde(skip_serializing_if = "Option::is_none")]
nullable_skipped: Option<String>,
nullable: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default = "default_nullable")]
nullable_skipped_with_default: Option<String>,
#[serde(default = "default_nullable")]
nullable_with_default: Option<String>,
// Using feature `chrono`
timestamp: DateTime<Utc>,
/// This is a complex enum with a description
#[cel_validate(rule = Rule::new("!has(self.variantOne) || self.variantOne.int > 22"))]
complex_enum: ComplexEnum,
/// This is a untagged enum with a description
untagged_enum_person: UntaggedEnumPerson,
set: HashSet<String>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Status {
replicas: usize,
label_selector: String,
}
fn default_value() -> String {
"default_value".into()
}
fn default_nullable() -> Option<String> {
Some("default_nullable".into())
}
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "Flattening")]
pub struct FlatteningSpec {
foo: String,
#[serde(flatten)]
arbitrary: HashMap<String, serde_json::Value>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::enum_variant_names)]
enum ComplexEnum {
/// First variant with an int
VariantOne { int: i32 },
/// Second variant with an String
VariantTwo { str: String },
/// Third variant which doesn't has an attribute
VariantThree {},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
enum UntaggedEnumPerson {
GenderAndAge(GenderAndAge),
GenderAndDateOfBirth(GenderAndDateOfBirth),
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct GenderAndAge {
/// Gender of the person
gender: Gender,
/// Age of the person in years
age: i32,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct GenderAndDateOfBirth {
/// Gender of the person
gender: Gender,
/// Date of birth of the person as ISO 8601 date
date_of_birth: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
#[serde(rename_all = "PascalCase")]
enum Gender {
Female,
Male,
/// This variant has a comment!
Other,
}
#[test]
fn test_crd_name() {
use kube::core::CustomResourceExt;
assert_eq!("foos.clux.dev", Foo::crd_name());
}
#[test]
fn test_shortnames() {
use kube::core::CustomResourceExt;
assert_eq!(&["fo", "f"], Foo::shortnames());
}
#[test]
fn test_serialized_matches_expected() {
assert_json_eq!(
serde_json::to_value(Foo::new("bar", FooSpec {
non_nullable: "asdf".to_string(),
non_nullable_with_default: "asdf".to_string(),
nullable_skipped: None,
nullable: None,
nullable_skipped_with_default: None,
nullable_with_default: None,
timestamp: DateTime::from_timestamp(0, 0).unwrap(),
complex_enum: ComplexEnum::VariantOne { int: 23 },
untagged_enum_person: UntaggedEnumPerson::GenderAndAge(GenderAndAge {
age: 42,
gender: Gender::Male,
}),
set: HashSet::from(["foo".to_owned()])
}))
.unwrap(),
serde_json::json!({
"apiVersion": "clux.dev/v1",
"kind": "Foo",
"metadata": {
"annotations": {
"clux.dev": "cluxingv1",
"clux.dev/firewall": "enabled",
},
"labels": {
"clux.dev": "cluxingv1",
"clux.dev/persistence": "disabled",
},
"name": "bar",
},
"spec": {
"nonNullable": "asdf",
"nonNullableWithDefault": "asdf",
"nullable": null,
"nullableWithDefault": null,
"timestamp": "1970-01-01T00:00:00Z",
"complexEnum": {
"variantOne": {
"int": 23
}
},
"untaggedEnumPerson": {
"age": 42,
"gender": "Male"
},
"set": ["foo"]
}
})
)
}
#[test]
fn test_crd_schema_matches_expected() {
use kube::core::CustomResourceExt;
assert_json_eq!(
Foo::crd(),
serde_json::json!({
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {
"annotations": {
"clux.dev": "cluxingv1",
"clux.dev/firewall": "enabled",
},
"labels": {
"clux.dev": "cluxingv1",
"clux.dev/persistence": "disabled",
},
"name": "foos.clux.dev"
},
"spec": {
"group": "clux.dev",
"names": {
"categories": ["clux"],
"kind": "Foo",
"plural": "foos",
"shortNames": ["fo", "f"],
"singular": "foo"
},
"scope": "Namespaced",
"versions": [
{
"name": "v1",
"served": false,
"storage": false,
"deprecated": true,
"deprecationWarning": "my warning",
"additionalPrinterColumns": [],
"selectableFields": [{
"jsonPath": ".spec.nonNullable"
}, {
"jsonPath": ".spec.nullable"
}],
"subresources": {
"status": {},
"scale": {
"specReplicasPath": ".spec.replicas",
"labelSelectorPath": ".status.labelSelector",
"statusReplicasPath": ".status.replicas"
}
},
"schema": {
"openAPIV3Schema": {
"description": "Custom resource representing a Foo",
"properties": {
"spec": {
"properties": {
"nonNullable": {
"type": "string"
},
"nonNullableWithDefault": {
"default": "default_value",
"type": "string"
},
"nullableSkipped": {
"nullable": true,
"type": "string"
},
"nullable": {
"nullable": true,
"type": "string"
},
"nullableSkippedWithDefault": {
"default": "default_nullable",
"nullable": true,
"type": "string"
},
"nullableWithDefault": {
"default": "default_nullable",
"nullable": true,
"type": "string"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"complexEnum": {
"type": "object",
"properties": {
"variantOne": {
"type": "object",
"properties": {
"int": {
"type": "integer",
"format": "int32"
}
},
"required": ["int"],
"description": "First variant with an int"
},
"variantTwo": {
"type": "object",
"properties": {
"str": {
"type": "string"
}
},
"required": ["str"],
"description": "Second variant with an String"
},
"variantThree": {
"type": "object",
"description": "Third variant which doesn't has an attribute"
}
},
"oneOf": [
{
"required": ["variantOne"]
},
{
"required": ["variantTwo"]
},
{
"required": ["variantThree"]
}
],
"x-kubernetes-validations": [{
"rule": "!has(self.variantOne) || self.variantOne.int > 22",
}],
"description": "This is a complex enum with a description"
},
"untaggedEnumPerson": {
"type": "object",
"properties": {
"age": {
"type": "integer",
"format": "int32",
"description": "Age of the person in years"
},
"dateOfBirth": {
"type": "string",
"description": "Date of birth of the person as ISO 8601 date"
},
"gender": {
"type": "string",
"enum": ["Female", "Male", "Other"],
"description": "Gender of the person"
}
},
"anyOf": [
{
"required": ["age", "gender"]
},
{
"required": ["dateOfBirth", "gender"]
}
],
"description": "This is a untagged enum with a description"
},
"set": {
"type": "array",
"items": {
"type": "string"
},
},
},
"required": [
"complexEnum",
"nonNullable",
"set",
"timestamp",
"untaggedEnumPerson"
],
"x-kubernetes-validations": [{
"rule": "has(self.nonNullable)",
}],
"type": "object"
},
"status": {
"properties": {
"replicas": {
"type": "integer",
"format": "uint",
"minimum": 0.0,
},
"labelSelector": {
"type": "string"
}
},
"required": [
"labelSelector",
"replicas"
],
"nullable": true,
"type": "object"
}
},
"required": [
"spec"
],
"x-kubernetes-validations": [{
"rule": "self.metadata.name == 'singleton'",
}],
"title": "Foo_kube_validation",
"type": "object"
}
},
}
]
}
})
);
}
#[test]
fn flattening() {
use kube::core::CustomResourceExt;
let spec = &Flattening::crd().spec.versions[0]
.schema
.clone()
.unwrap()
.open_api_v3_schema
.unwrap()
.properties
.unwrap()["spec"];
assert_eq!(spec.x_kubernetes_preserve_unknown_fields, Some(true));
assert_eq!(spec.additional_properties, None);
}

55
vendor/kube-derive/tests/resource.rs vendored Normal file
View File

@@ -0,0 +1,55 @@
use k8s_openapi::{
api::core::v1::{ConfigMap, Secret},
ByteString,
};
use kube::api::ObjectMeta;
use kube_derive::Resource;
#[derive(Resource, Default)]
#[resource(inherit = "ConfigMap")]
struct TypedMap {
metadata: ObjectMeta,
data: Option<TypedData>,
}
#[derive(Default)]
struct TypedData {
field: String,
}
#[derive(Resource, Default)]
#[resource(inherit = "Secret")]
struct TypedSecret {
metadata: ObjectMeta,
data: Option<TypedSecretData>,
}
#[derive(Default)]
struct TypedSecretData {
field: ByteString,
}
#[cfg(test)]
mod tests {
use kube::Resource;
use crate::{TypedMap, TypedSecret};
#[test]
fn test_parse_config_map_default() {
TypedMap::default();
assert_eq!(TypedMap::kind(&()), "ConfigMap");
assert_eq!(TypedMap::api_version(&()), "v1");
assert_eq!(TypedMap::group(&()), "");
assert_eq!(TypedMap::plural(&()), "configmaps");
}
#[test]
fn test_parse_secret_default() {
TypedSecret::default();
assert_eq!(TypedSecret::kind(&()), "Secret");
assert_eq!(TypedSecret::api_version(&()), "v1");
assert_eq!(TypedSecret::group(&()), "");
assert_eq!(TypedSecret::plural(&()), "secrets");
}
}

10
vendor/kube-derive/tests/test_ui.rs vendored Normal file
View File

@@ -0,0 +1,10 @@
// Test that `kube-derive` outputs helpful error messages.
// If you make a change, remove `tests/ui/*.stderr` and run `cargo test`.
// Then copy the files that appear under `wip/` if it's what you expected.
// Alternatively, run `TRYBUILD=overwrite cargo test`.
// See https://github.com/dtolnay/trybuild
#[test]
fn test_failures() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}

View File

@@ -0,0 +1,11 @@
use kube_derive::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "Foo", shortnames = "foo")]
struct FooSpec {
foo: String,
}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: Unknown field: `shortnames`. Did you mean `shortname`?
--> $DIR/fail_with_suggestion.rs:6:58
|
6 | #[kube(group = "clux.dev", version = "v1", kind = "Foo", shortnames = "foo")]
| ^^^^^^^^^^

View File

@@ -0,0 +1,10 @@
use kube_derive::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
struct FooSpec {
foo: String,
}
fn main() {}

View File

@@ -0,0 +1,23 @@
error: Missing field `group`
--> $DIR/missing_required.rs:5:10
|
5 | #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
| ^^^^^^^^^^^^^^
|
= note: this error originates in the derive macro `CustomResource` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Missing field `version`
--> $DIR/missing_required.rs:5:10
|
5 | #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
| ^^^^^^^^^^^^^^
|
= note: this error originates in the derive macro `CustomResource` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Missing field `kind`
--> $DIR/missing_required.rs:5:10
|
5 | #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
| ^^^^^^^^^^^^^^
|
= note: this error originates in the derive macro `CustomResource` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@@ -0,0 +1,10 @@
use kube_derive::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Serialize, Deserialize, JsonSchema)]
union FooSpec {
int: u32,
}
fn main() {}

View File

@@ -0,0 +1,13 @@
error: Unions can not #[derive(CustomResource)]
--> tests/ui/union_fails.rs:6:7
|
6 | union FooSpec {
| ^^^^^^^
error: Serde does not support derive for unions
--> tests/ui/union_fails.rs:6:1
|
6 | / union FooSpec {
7 | | int: u32,
8 | | }
| |_^