kube_derive/
custom_resource.rs

1// Generated by darling macros, out of our control
2#![allow(clippy::manual_unwrap_or_default)]
3use darling::{FromDeriveInput, FromMeta};
4use proc_macro2::{Ident, Literal, Span, TokenStream};
5use quote::{ToTokens, TokenStreamExt as _};
6use syn::{parse_quote, Data, DeriveInput, Expr, Path, Visibility};
7
8/// Values we can parse from #[kube(attrs)]
9#[derive(Debug, FromDeriveInput)]
10#[darling(attributes(kube))]
11struct KubeAttrs {
12    group: String,
13    version: String,
14    kind: String,
15    doc: Option<String>,
16    #[darling(rename = "root")]
17    kind_struct: Option<String>,
18    /// lowercase plural of kind (inferred if omitted)
19    plural: Option<String>,
20    /// singular defaults to lowercased kind
21    singular: Option<String>,
22    #[darling(default)]
23    namespaced: bool,
24    #[darling(multiple, rename = "derive")]
25    derives: Vec<String>,
26    schema: Option<SchemaMode>,
27    status: Option<String>,
28    #[darling(multiple, rename = "category")]
29    categories: Vec<String>,
30    #[darling(multiple, rename = "shortname")]
31    shortnames: Vec<String>,
32    #[darling(multiple, rename = "printcolumn")]
33    printcolums: Vec<String>,
34    #[darling(multiple)]
35    selectable: Vec<String>,
36    scale: Option<String>,
37    #[darling(default)]
38    crates: Crates,
39    #[darling(multiple, rename = "annotation")]
40    annotations: Vec<KVTuple>,
41    #[darling(multiple, rename = "label")]
42    labels: Vec<KVTuple>,
43    #[darling(multiple, rename = "rule")]
44    rules: Vec<Expr>,
45
46    /// Sets the `storage` property to `true` or `false`.
47    ///
48    /// Defaults to `true`.
49    #[darling(default = default_storage_arg)]
50    storage: bool,
51
52    /// Sets the `served` property to `true` or `false`.
53    ///
54    /// Defaults to `true`.
55    #[darling(default = default_served_arg)]
56    served: bool,
57}
58
59#[derive(Debug)]
60struct KVTuple(String, String);
61
62impl FromMeta for KVTuple {
63    fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
64        if items.len() == 2 {
65            if let (
66                darling::ast::NestedMeta::Lit(syn::Lit::Str(key)),
67                darling::ast::NestedMeta::Lit(syn::Lit::Str(value)),
68            ) = (&items[0], &items[1])
69            {
70                return Ok(KVTuple(key.value(), value.value()));
71            }
72        }
73
74        Err(darling::Error::unsupported_format(
75            "expected `\"key\", \"value\"` format",
76        ))
77    }
78}
79
80impl From<(&'static str, &'static str)> for KVTuple {
81    fn from((key, value): (&'static str, &'static str)) -> Self {
82        Self(key.to_string(), value.to_string())
83    }
84}
85
86impl ToTokens for KVTuple {
87    fn to_tokens(&self, tokens: &mut TokenStream) {
88        let (k, v) = (&self.0, &self.1);
89        tokens.append_all(quote! { (#k, #v) });
90    }
91}
92
93fn default_storage_arg() -> bool {
94    // This defaults to true to be backwards compatible.
95    true
96}
97
98fn default_served_arg() -> bool {
99    // This defaults to true to be backwards compatible.
100    true
101}
102
103#[derive(Debug, FromMeta)]
104struct Crates {
105    #[darling(default = "Self::default_kube")]
106    kube: Path,
107    #[darling(default = "Self::default_kube_core")]
108    kube_core: Path,
109    #[darling(default = "Self::default_k8s_openapi")]
110    k8s_openapi: Path,
111    #[darling(default = "Self::default_schemars")]
112    schemars: Path,
113    #[darling(default = "Self::default_serde")]
114    serde: Path,
115    #[darling(default = "Self::default_serde_json")]
116    serde_json: Path,
117    #[darling(default = "Self::default_std")]
118    std: Path,
119}
120
121// Default is required when the subattribute isn't mentioned at all
122// Delegate to darling rather than deriving, so that we can piggyback off the `#[darling(default)]` clauses
123impl Default for Crates {
124    fn default() -> Self {
125        Self::from_list(&[]).unwrap()
126    }
127}
128
129impl Crates {
130    fn default_kube_core() -> Path {
131        parse_quote! { ::kube::core } // by default must work well with people using facade crate
132    }
133
134    fn default_kube() -> Path {
135        parse_quote! { ::kube }
136    }
137
138    fn default_k8s_openapi() -> Path {
139        parse_quote! { ::k8s_openapi }
140    }
141
142    fn default_schemars() -> Path {
143        parse_quote! { ::schemars }
144    }
145
146    fn default_serde() -> Path {
147        parse_quote! { ::serde }
148    }
149
150    fn default_serde_json() -> Path {
151        parse_quote! { ::serde_json }
152    }
153
154    fn default_std() -> Path {
155        parse_quote! { ::std }
156    }
157}
158
159#[derive(Debug, PartialEq, Eq, Clone, Copy)]
160enum SchemaMode {
161    Disabled,
162    Manual,
163    Derived,
164}
165
166impl SchemaMode {
167    fn derive(self) -> bool {
168        match self {
169            SchemaMode::Disabled => false,
170            SchemaMode::Manual => false,
171            SchemaMode::Derived => true,
172        }
173    }
174
175    fn use_in_crd(self) -> bool {
176        match self {
177            SchemaMode::Disabled => false,
178            SchemaMode::Manual => true,
179            SchemaMode::Derived => true,
180        }
181    }
182}
183
184impl FromMeta for SchemaMode {
185    fn from_string(value: &str) -> darling::Result<Self> {
186        match value {
187            "disabled" => Ok(SchemaMode::Disabled),
188            "manual" => Ok(SchemaMode::Manual),
189            "derived" => Ok(SchemaMode::Derived),
190            x => Err(darling::Error::unknown_value(x)),
191        }
192    }
193}
194
195pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
196    let derive_input: DeriveInput = match syn::parse2(input) {
197        Err(err) => return err.to_compile_error(),
198        Ok(di) => di,
199    };
200    // Limit derive to structs
201    match derive_input.data {
202        Data::Struct(_) | Data::Enum(_) => {}
203        _ => {
204            return syn::Error::new_spanned(
205                &derive_input.ident,
206                r#"Unions can not #[derive(CustomResource)]"#,
207            )
208            .to_compile_error()
209        }
210    }
211
212    let kube_attrs = match KubeAttrs::from_derive_input(&derive_input) {
213        Err(err) => return err.write_errors(),
214        Ok(attrs) => attrs,
215    };
216
217    let KubeAttrs {
218        group,
219        kind,
220        kind_struct,
221        version,
222        doc,
223        namespaced,
224        derives,
225        schema: schema_mode,
226        status,
227        plural,
228        singular,
229        categories,
230        shortnames,
231        printcolums,
232        selectable,
233        scale,
234        rules,
235        storage,
236        served,
237        crates:
238            Crates {
239                kube_core,
240                kube,
241                k8s_openapi,
242                schemars,
243                serde,
244                serde_json,
245                std,
246            },
247        annotations,
248        labels,
249    } = kube_attrs;
250
251    let struct_name = kind_struct.unwrap_or_else(|| kind.clone());
252    if derive_input.ident == struct_name {
253        return syn::Error::new_spanned(
254            derive_input.ident,
255            r#"#[derive(CustomResource)] `kind = "..."` must not equal the struct name (this is generated)"#,
256        )
257        .to_compile_error();
258    }
259    let visibility = derive_input.vis;
260    let ident = derive_input.ident;
261
262    // 1. Create root object Foo and truncate name from FooSpec
263
264    // Default visibility is `pub(crate)`
265    // Default generics is no generics (makes little sense to re-use CRD kind?)
266    // We enforce metadata + spec's existence (always there)
267    // => No default impl
268    let rootident = Ident::new(&struct_name, Span::call_site());
269    let rootident_str = rootident.to_string();
270
271    // if status set, also add that
272    let StatusInformation {
273        field: status_field,
274        default: status_default,
275        impl_hasstatus,
276    } = process_status(&rootident, &status, &visibility, &kube_core);
277    let has_status = status.is_some();
278    let serialize_status = if has_status {
279        quote! {
280            if let Some(status) = &self.status {
281                obj.serialize_field("status", &status)?;
282            }
283        }
284    } else {
285        quote! {}
286    };
287    let has_status_value = if has_status {
288        quote! { self.status.is_some() }
289    } else {
290        quote! { false }
291    };
292
293    let mut derive_paths: Vec<Path> = vec![
294        syn::parse_quote! { #serde::Deserialize },
295        syn::parse_quote! { Clone },
296        syn::parse_quote! { Debug },
297    ];
298    let mut has_default = false;
299    for d in &derives {
300        if d == "Default" {
301            has_default = true; // overridden manually to avoid confusion
302        } else {
303            match syn::parse_str(d) {
304                Err(err) => return err.to_compile_error(),
305                Ok(d) => derive_paths.push(d),
306            }
307        }
308    }
309
310    // Enable schema generation by default as in v1 it is mandatory.
311    let schema_mode = schema_mode.unwrap_or(SchemaMode::Derived);
312    // We exclude fields `apiVersion`, `kind`, and `metadata` from our schema because
313    // these are validated by the API server implicitly. Also, we can't generate the
314    // schema for `metadata` (`ObjectMeta`) because it doesn't implement `JsonSchema`.
315    let schemars_skip = schema_mode.derive().then_some(quote! { #[schemars(skip)] });
316    if schema_mode.derive() && !rules.is_empty() {
317        derive_paths.push(syn::parse_quote! { #kube::CELSchema });
318    } else if schema_mode.derive() {
319        derive_paths.push(syn::parse_quote! { #schemars::JsonSchema });
320    }
321
322    let struct_rules: Option<Vec<TokenStream>> =
323        (!rules.is_empty()).then(|| rules.iter().map(|r| quote! {rule = #r,}).collect());
324    let struct_rules = struct_rules.map(|r| quote! { #[cel_validate(#(#r)*)]});
325
326    let meta_annotations = if !annotations.is_empty() {
327        quote! { Some(std::collections::BTreeMap::from([#((#annotations.0.to_string(), #annotations.1.to_string()),)*])) }
328    } else {
329        quote! { None }
330    };
331
332    let meta_labels = if !labels.is_empty() {
333        quote! { Some(std::collections::BTreeMap::from([#((#labels.0.to_string(), #labels.1.to_string()),)*])) }
334    } else {
335        quote! { None }
336    };
337
338    let docstr =
339        doc.unwrap_or_else(|| format!(" Auto-generated derived type for {ident} via `CustomResource`"));
340    let quoted_serde = Literal::string(&serde.to_token_stream().to_string());
341    let root_obj = quote! {
342        #[doc = #docstr]
343        #[automatically_derived]
344        #[allow(missing_docs)]
345        #[derive(#(#derive_paths),*)]
346        #[serde(rename_all = "camelCase")]
347        #[serde(crate = #quoted_serde)]
348        #struct_rules
349        #visibility struct #rootident {
350            #schemars_skip
351            #visibility metadata: #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta,
352            #visibility spec: #ident,
353            #status_field
354        }
355        impl #rootident {
356            /// Spec based constructor for derived custom resource
357            pub fn new(name: &str, spec: #ident) -> Self {
358                Self {
359                    metadata: #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
360                        annotations: #meta_annotations,
361                        labels: #meta_labels,
362                        name: Some(name.to_string()),
363                        ..Default::default()
364                    },
365                    spec: spec,
366                    #status_default
367                }
368            }
369        }
370        impl #serde::Serialize for #rootident {
371            fn serialize<S: #serde::Serializer>(&self, ser: S) -> #std::result::Result<S::Ok, S::Error> {
372                use #serde::ser::SerializeStruct;
373                let mut obj = ser.serialize_struct(#rootident_str, 4 + usize::from(#has_status_value))?;
374                obj.serialize_field("apiVersion", &<#rootident as #kube_core::Resource>::api_version(&()))?;
375                obj.serialize_field("kind", &<#rootident as #kube_core::Resource>::kind(&()))?;
376                obj.serialize_field("metadata", &self.metadata)?;
377                obj.serialize_field("spec", &self.spec)?;
378                #serialize_status
379                obj.end()
380            }
381        }
382    };
383
384    // 2. Implement Resource trait
385    let name = singular.unwrap_or_else(|| kind.to_ascii_lowercase());
386    let plural = plural.unwrap_or_else(|| to_plural(&name));
387    let (scope, scope_quote) = if namespaced {
388        ("Namespaced", quote! { #kube_core::NamespaceResourceScope })
389    } else {
390        ("Cluster", quote! { #kube_core::ClusterResourceScope })
391    };
392
393    let api_ver = format!("{group}/{version}");
394    let impl_resource = quote! {
395        impl #kube_core::Resource for #rootident {
396            type DynamicType = ();
397            type Scope = #scope_quote;
398
399            fn group(_: &()) -> std::borrow::Cow<'_, str> {
400               #group.into()
401            }
402
403            fn kind(_: &()) -> std::borrow::Cow<'_, str> {
404                #kind.into()
405            }
406
407            fn version(_: &()) -> std::borrow::Cow<'_, str> {
408                #version.into()
409            }
410
411            fn api_version(_: &()) -> std::borrow::Cow<'_, str> {
412                #api_ver.into()
413            }
414
415            fn plural(_: &()) -> std::borrow::Cow<'_, str> {
416                #plural.into()
417            }
418
419            fn meta(&self) -> &#k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
420                &self.metadata
421            }
422
423            fn meta_mut(&mut self) -> &mut #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta {
424                &mut self.metadata
425            }
426        }
427    };
428
429    // 3. Implement Default if requested
430    let impl_default = if has_default {
431        quote! {
432            impl Default for #rootident {
433                fn default() -> Self {
434                    Self {
435                        metadata: #k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta::default(),
436                        spec: Default::default(),
437                        #status_default
438                    }
439                }
440            }
441        }
442    } else {
443        quote! {}
444    };
445
446    // 4. Implement CustomResource
447
448    // Compute a bunch of crd props
449    let printers = format!("[ {} ]", printcolums.join(",")); // hacksss
450    let fields: Vec<String> = selectable
451        .iter()
452        .map(|s| format!(r#"{{ "jsonPath": "{s}" }}"#))
453        .collect();
454    let fields = format!("[ {} ]", fields.join(","));
455    let scale_code = if let Some(s) = scale { s } else { "".to_string() };
456
457    // Ensure it generates for the correct CRD version (only v1 supported now)
458    let apiext = quote! {
459        #k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1
460    };
461    let extver = quote! {
462        #kube_core::crd::v1
463    };
464
465    let shortnames_slice = {
466        let names = shortnames
467            .iter()
468            .map(|name| quote! { #name, })
469            .collect::<TokenStream>();
470        quote! { &[#names] }
471    };
472
473    let categories_json = serde_json::to_string(&categories).unwrap();
474    let short_json = serde_json::to_string(&shortnames).unwrap();
475    let crd_meta_name = format!("{plural}.{group}");
476
477    let mut crd_meta = TokenStream::new();
478    crd_meta.extend(quote! { "name": #crd_meta_name });
479
480    if !annotations.is_empty() {
481        crd_meta.extend(quote! { , "annotations": #meta_annotations });
482    }
483
484    if !labels.is_empty() {
485        crd_meta.extend(quote! { , "labels": #meta_labels });
486    }
487
488    let schemagen = if schema_mode.use_in_crd() {
489        quote! {
490            // Don't use definitions and don't include `$schema` because these are not allowed.
491            let gen = #schemars::gen::SchemaSettings::openapi3()
492                .with(|s| {
493                    s.inline_subschemas = true;
494                    s.meta_schema = None;
495                })
496                .with_visitor(#kube_core::schema::StructuralSchemaRewriter)
497                .into_generator();
498            let schema = gen.into_root_schema_for::<Self>();
499        }
500    } else {
501        // we could issue a compile time warning for this, but it would hit EVERY compile, which would be noisy
502        // eprintln!("warning: kube-derive configured with manual schema generation");
503        // users must manually set a valid schema in crd.spec.versions[*].schema - see examples: crd_derive_no_schema
504        quote! {
505            let schema: Option<#k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::JSONSchemaProps> = None;
506        }
507    };
508
509    let selectable = if !selectable.is_empty() {
510        quote! { "selectableFields": fields, }
511    } else {
512        quote! {}
513    };
514
515    // Known constraints that are hard to enforce elsewhere
516    let compile_constraints = if !selectable.is_empty() {
517        quote! {
518            #k8s_openapi::k8s_if_le_1_29! {
519                compile_error!("selectable fields require Kubernetes >= 1.30");
520            }
521        }
522    } else {
523        quote! {}
524    };
525
526    let jsondata = quote! {
527        #schemagen
528
529        let jsondata = #serde_json::json!({
530            "metadata": {
531                #crd_meta
532            },
533            "spec": {
534                "group": #group,
535                "scope": #scope,
536                "names": {
537                    "categories": categories,
538                    "plural": #plural,
539                    "singular": #name,
540                    "kind": #kind,
541                    "shortNames": shorts
542                },
543                "versions": [{
544                    "name": #version,
545                    "served": #served,
546                    "storage": #storage,
547                    "schema": {
548                        "openAPIV3Schema": schema,
549                    },
550                    "additionalPrinterColumns": columns,
551                    #selectable
552                    "subresources": subres,
553                }],
554            }
555        });
556    };
557
558    // Implement the CustomResourceExt trait to allow users writing generic logic on top of them
559    let impl_crd = quote! {
560        impl #extver::CustomResourceExt for #rootident {
561
562            fn crd() -> #apiext::CustomResourceDefinition {
563                let columns : Vec<#apiext::CustomResourceColumnDefinition> = #serde_json::from_str(#printers).expect("valid printer column json");
564                #k8s_openapi::k8s_if_ge_1_30! {
565                    let fields : Vec<#apiext::SelectableField> = #serde_json::from_str(#fields).expect("valid selectableField column json");
566                }
567                let scale: Option<#apiext::CustomResourceSubresourceScale> = if #scale_code.is_empty() {
568                    None
569                } else {
570                    #serde_json::from_str(#scale_code).expect("valid scale subresource json")
571                };
572                let categories: Vec<String> = #serde_json::from_str(#categories_json).expect("valid categories");
573                let shorts : Vec<String> = #serde_json::from_str(#short_json).expect("valid shortnames");
574                let subres = if #has_status {
575                    if let Some(s) = &scale {
576                        #serde_json::json!({
577                            "status": {},
578                            "scale": scale
579                        })
580                    } else {
581                        #serde_json::json!({"status": {} })
582                    }
583                } else {
584                    #serde_json::json!({})
585                };
586
587                #jsondata
588                #serde_json::from_value(jsondata)
589                    .expect("valid custom resource from #[kube(attrs..)]")
590            }
591
592            fn crd_name() -> &'static str {
593                #crd_meta_name
594            }
595
596            fn api_resource() -> #kube_core::dynamic::ApiResource {
597                #kube_core::dynamic::ApiResource::erase::<Self>(&())
598            }
599
600            fn shortnames() -> &'static [&'static str] {
601                #shortnames_slice
602            }
603        }
604    };
605
606    let impl_hasspec = generate_hasspec(&ident, &rootident, &kube_core);
607
608    // Concat output
609    quote! {
610        #compile_constraints
611        #root_obj
612        #impl_resource
613        #impl_default
614        #impl_crd
615        #impl_hasspec
616        #impl_hasstatus
617    }
618}
619
620/// This generates the code for the `#kube_core::object::HasSpec` trait implementation.
621///
622/// All CRDs have a spec so it is implemented for all of them.
623///
624/// # Arguments
625///
626/// * `ident`: The identity (name) of the spec struct
627/// * `root ident`: The identity (name) of the main CRD struct (the one we generate in this macro)
628/// * `kube_core`: The path stream for the analagous kube::core import location from users POV
629fn generate_hasspec(spec_ident: &Ident, root_ident: &Ident, kube_core: &Path) -> TokenStream {
630    quote! {
631        impl #kube_core::object::HasSpec for #root_ident {
632            type Spec = #spec_ident;
633
634            fn spec(&self) -> &#spec_ident {
635                &self.spec
636            }
637
638            fn spec_mut(&mut self) -> &mut #spec_ident {
639                &mut self.spec
640            }
641        }
642    }
643}
644
645struct StatusInformation {
646    /// The code to be used for the field in the main struct
647    field: TokenStream,
648    /// The initialization code to use in a `Default` and `::new()` implementation
649    default: TokenStream,
650    /// The implementation code for the `HasStatus` trait
651    impl_hasstatus: TokenStream,
652}
653
654/// This processes the `status` field of a CRD.
655///
656/// As it is optional some features will be turned on or off depending on whether it's available or not.
657///
658/// # Arguments
659///
660/// * `root ident`: The identity (name) of the main CRD struct (the one we generate in this macro)
661/// * `status`: The optional name of the `status` struct to use
662/// * `visibility`: Desired visibility of the generated field
663/// * `kube_core`: The path stream for the analagous kube::core import location from users POV
664///
665/// returns: A `StatusInformation` struct
666fn process_status(
667    root_ident: &Ident,
668    status: &Option<String>,
669    visibility: &Visibility,
670    kube_core: &Path,
671) -> StatusInformation {
672    if let Some(status_name) = &status {
673        let ident = format_ident!("{}", status_name);
674        StatusInformation {
675            field: quote! {
676                #[serde(skip_serializing_if = "Option::is_none")]
677                #visibility status: Option<#ident>,
678            },
679            default: quote! { status: None, },
680            impl_hasstatus: quote! {
681                impl #kube_core::object::HasStatus for #root_ident {
682
683                    type Status = #ident;
684
685                    fn status(&self) -> Option<&#ident> {
686                        self.status.as_ref()
687                    }
688
689                    fn status_mut(&mut self) -> &mut Option<#ident> {
690                        &mut self.status
691                    }
692                }
693            },
694        }
695    } else {
696        let empty_quote = quote! {};
697        StatusInformation {
698            field: empty_quote.clone(),
699            default: empty_quote.clone(),
700            impl_hasstatus: empty_quote,
701        }
702    }
703}
704
705// Simple pluralizer.
706// Duplicating the code from kube (without special casing) because it's simple enough.
707// Irregular plurals must be explicitly specified.
708fn to_plural(word: &str) -> String {
709    // Words ending in s, x, z, ch, sh will be pluralized with -es (eg. foxes).
710    if word.ends_with('s')
711        || word.ends_with('x')
712        || word.ends_with('z')
713        || word.ends_with("ch")
714        || word.ends_with("sh")
715    {
716        return format!("{word}es");
717    }
718
719    // Words ending in y that are preceded by a consonant will be pluralized by
720    // replacing y with -ies (eg. puppies).
721    if word.ends_with('y') {
722        if let Some(c) = word.chars().nth(word.len() - 2) {
723            if !matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
724                // Remove 'y' and add `ies`
725                let mut chars = word.chars();
726                chars.next_back();
727                return format!("{}ies", chars.as_str());
728            }
729        }
730    }
731
732    // All other words will have "s" added to the end (eg. days).
733    format!("{word}s")
734}
735
736#[cfg(test)]
737mod tests {
738    use std::{env, fs};
739
740    use super::*;
741
742    #[test]
743    fn test_parse_default() {
744        let input = quote! {
745            #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)]
746            #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
747            struct FooSpec { foo: String }
748        };
749        let input = syn::parse2(input).unwrap();
750        let kube_attrs = KubeAttrs::from_derive_input(&input).unwrap();
751        assert_eq!(kube_attrs.group, "clux.dev".to_string());
752        assert_eq!(kube_attrs.version, "v1".to_string());
753        assert_eq!(kube_attrs.kind, "Foo".to_string());
754        assert!(kube_attrs.namespaced);
755    }
756
757    #[test]
758    fn test_derive_crd() {
759        let path = env::current_dir().unwrap().join("tests").join("crd_enum_test.rs");
760        let file = fs::File::open(path).unwrap();
761        runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap();
762
763        let path = env::current_dir()
764            .unwrap()
765            .join("tests")
766            .join("crd_schema_test.rs");
767        let file = fs::File::open(path).unwrap();
768        runtime_macros::emulate_derive_macro_expansion(file, &[("CustomResource", derive)]).unwrap();
769    }
770}