k8s_openapi_derive/
custom_resource_definition.rs

1use k8s_openapi_codegen_common::swagger20;
2
3use super::ResultExt;
4
5pub(super) struct CustomResourceDefinition {
6    ident: proc_macro2::Ident,
7    vis: syn::Visibility,
8    tokens: proc_macro2::TokenStream,
9
10    group: String,
11    version: String,
12    plural: String,
13    generate_schema: bool,
14    namespaced: bool,
15    has_subresources: Option<String>,
16    impl_deep_merge: bool,
17}
18
19impl super::CustomDerive for CustomResourceDefinition {
20    fn parse(input: syn::DeriveInput, tokens: proc_macro2::TokenStream) -> Result<Self, syn::Error> {
21        let ident = input.ident;
22        let vis = input.vis;
23
24        let mut group = None;
25        let mut version = None;
26        let mut plural = None;
27        let mut generate_schema = false;
28        let mut namespaced = false;
29        let mut has_subresources = None;
30        let mut impl_deep_merge = false;
31
32        for attr in &input.attrs {
33            let syn::AttrStyle::Outer = attr.style else { continue; };
34
35            if !attr.path().is_ident("custom_resource_definition") {
36                continue;
37            }
38
39            let metas = attr.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)?;
40            for meta in metas {
41                let meta: &dyn quote::ToTokens = match &meta {
42                    syn::Meta::NameValue(meta) =>
43                        if meta.path.is_ident("group") {
44                            let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }) = &meta.value else {
45                                return Err(r#"#[custom_resource_definition(group = "...")] expects a string literal value"#).spanning(meta);
46                            };
47                            group = Some(lit.value());
48                            continue;
49                        }
50                        else if meta.path.is_ident("version") {
51                            let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }) = &meta.value else {
52                                return Err(r#"#[custom_resource_definition(version = "...")] expects a string literal value"#).spanning(meta);
53                            };
54                            version = Some(lit.value());
55                            continue;
56                        }
57                        else if meta.path.is_ident("plural") {
58                            let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }) = &meta.value else {
59                                return Err(r#"#[custom_resource_definition(plural = "...")] expects a string literal value"#).spanning(meta);
60                            };
61                            plural = Some(lit.value());
62                            continue;
63                        }
64                        else if meta.path.is_ident("has_subresources") {
65                            let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }) = &meta.value else {
66                                return Err(r#"#[custom_resource_definition(has_subresources = "...")] expects a string literal value"#).spanning(meta);
67                            };
68                            has_subresources = Some(lit.value());
69                            continue;
70                        }
71                        else {
72                            meta
73                        },
74
75                    syn::Meta::Path(path) =>
76                        if path.is_ident("generate_schema") {
77                            generate_schema = true;
78                            continue;
79                        }
80                        else if path.is_ident("namespaced") {
81                            namespaced = true;
82                            continue;
83                        }
84                        else if path.is_ident("impl_deep_merge") {
85                            impl_deep_merge = true;
86                            continue;
87                        }
88                        else {
89                            &meta
90                        },
91
92                    meta @ syn::Meta::List(_) => meta,
93                };
94
95                return
96                    Err(r#"\
97                        #[derive(CustomResourceDefinition)] found unexpected meta. \
98                        Expected `group = "..."`, `namespaced`, `plural = "..."`, `version = "..." or `has_subresources` = "..."`"#)
99                    .spanning(meta);
100            }
101        }
102
103        let group =
104            group
105            .ok_or(r#"#[derive(CustomResourceDefinition)] did not find a #[custom_resource_definition(group = "...")] attribute on the struct"#)
106            .spanning(&tokens)?;
107        let version =
108            version
109            .ok_or(r#"#[derive(CustomResourceDefinition)] did not find a #[custom_resource_definition(version = "...")] attribute on the struct"#)
110            .spanning(&tokens)?;
111        let plural =
112            plural
113            .ok_or(r#"#[derive(CustomResourceDefinition)] did not find a #[custom_resource_definition(plural = "...")] attribute on the struct"#)
114            .spanning(&tokens)?;
115
116        Ok(CustomResourceDefinition {
117            ident,
118            vis,
119            tokens,
120
121            group,
122            version,
123            plural,
124            generate_schema,
125            namespaced,
126            has_subresources,
127            impl_deep_merge,
128        })
129    }
130
131    fn emit(self) -> Result<proc_macro2::TokenStream, syn::Error> {
132        let CustomResourceDefinition { ident: cr_spec_name, vis, tokens, group, version, plural, generate_schema, namespaced, has_subresources, impl_deep_merge } = self;
133
134        let vis: std::borrow::Cow<'_, str> = match vis {
135            syn::Visibility::Inherited => "".into(),
136            vis => format!("{} ", quote::ToTokens::into_token_stream(vis)).into(),
137        };
138
139        let (cr_spec_name, cr_name) = {
140            let cr_spec_name_string = cr_spec_name.to_string();
141            if !cr_spec_name_string.ends_with("Spec") {
142                return Err("#[derive(CustomResourceDefinition)] requires the name of the struct to end with `Spec`").spanning(cr_spec_name);
143            }
144            let cr_name_string = cr_spec_name_string[..(cr_spec_name_string.len() - 4)].to_owned();
145            (cr_spec_name_string, cr_name_string)
146        };
147
148        let (namespace_operation_id_component, namespace_path_component) =
149            if namespaced {
150                ("Namespaced", "/namespaces/{namespace}")
151            }
152            else {
153                ("", "")
154            };
155
156        let mut spec = swagger20::Spec {
157            info: swagger20::Info {
158                title: String::new(),
159                version: String::new(),
160            },
161            definitions: [
162                (swagger20::DefinitionPath(cr_name.clone()), swagger20::Schema {
163                    description: Some(format!("Custom resource for `{cr_spec_name}`")),
164                    kind: swagger20::SchemaKind::Properties([
165                        (swagger20::PropertyName("apiVersion".to_owned()), (swagger20::Schema {
166                            description: Some("APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: <https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources>".to_owned()),
167                            kind: swagger20::SchemaKind::Ty(swagger20::Type::String { format: None }),
168                            kubernetes_group_kind_versions: vec![],
169                            list_kind: None,
170                            merge_type: swagger20::MergeType::Default,
171                            impl_deep_merge: true,
172                        }, false)),
173                        (swagger20::PropertyName("kind".to_owned()), (swagger20::Schema {
174                            description: Some("Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: <https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds>".to_owned()),
175                            kind: swagger20::SchemaKind::Ty(swagger20::Type::String { format: None }),
176                            kubernetes_group_kind_versions: vec![],
177                            list_kind: None,
178                            merge_type: swagger20::MergeType::Default,
179                            impl_deep_merge: true,
180                        }, false)),
181                        (swagger20::PropertyName("metadata".to_owned()), (swagger20::Schema {
182                            description: Some("Standard object's metadata. More info: <https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata>".to_owned()),
183                            kind: swagger20::SchemaKind::Ref(swagger20::RefPath {
184                                path: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta".to_owned(),
185                                can_be_default: None,
186                            }),
187                            kubernetes_group_kind_versions: vec![],
188                            list_kind: None,
189                            merge_type: swagger20::MergeType::Default,
190                            impl_deep_merge: true,
191                        }, true)),
192                        (swagger20::PropertyName("spec".to_owned()), (swagger20::Schema {
193                            description: Some(format!("Specification of the `{cr_name}` custom resource")),
194                            kind: swagger20::SchemaKind::Ref(swagger20::RefPath {
195                                path: cr_spec_name,
196                                can_be_default: None,
197                            }),
198                            kubernetes_group_kind_versions: vec![],
199                            list_kind: None,
200                            merge_type: swagger20::MergeType::Default,
201                            impl_deep_merge: true,
202                        }, false)),
203                    ].into_iter().chain(
204                        has_subresources.map(|has_subresources|
205                            (swagger20::PropertyName("subresources".to_owned()), (swagger20::Schema {
206                                description: Some(format!("Subresources of the `{cr_name}` custom resource")),
207                                kind: swagger20::SchemaKind::Ty(swagger20::Type::CustomResourceSubresources(has_subresources)),
208                                kubernetes_group_kind_versions: vec![],
209                                list_kind: None,
210                                merge_type: swagger20::MergeType::Default,
211                                impl_deep_merge: true,
212                            }, true)))
213                    ).collect()),
214                    kubernetes_group_kind_versions: vec![
215                        swagger20::KubernetesGroupKindVersion {
216                            group: group.clone(),
217                            kind: cr_name.clone(),
218                            version: version.clone(),
219                        },
220                    ],
221                    list_kind: Some(format!("{cr_name}List")),
222                    merge_type: swagger20::MergeType::Default,
223                    impl_deep_merge,
224                }),
225            ].into(),
226            operations: vec![
227                swagger20::Operation {
228                    id: format!("create{namespace_operation_id_component}{cr_name}"),
229                    kubernetes_action: Some(swagger20::KubernetesAction::Post),
230                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
231                        group: group.clone(),
232                        kind: cr_name.clone(),
233                        version: version.clone(),
234                    }),
235                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}")),
236                },
237
238                swagger20::Operation {
239                    id: format!("delete{namespace_operation_id_component}{cr_name}"),
240                    kubernetes_action: Some(swagger20::KubernetesAction::Delete),
241                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
242                        group: group.clone(),
243                        kind: cr_name.clone(),
244                        version: version.clone(),
245                    }),
246                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}")),
247                },
248
249                swagger20::Operation {
250                    id: format!("deleteCollection{namespace_operation_id_component}{cr_name}"),
251                    kubernetes_action: Some(swagger20::KubernetesAction::DeleteCollection),
252                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
253                        group: group.clone(),
254                        kind: cr_name.clone(),
255                        version: version.clone(),
256                    }),
257                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}")),
258                },
259
260                swagger20::Operation {
261                    id: format!("list{namespace_operation_id_component}{cr_name}"),
262                    kubernetes_action: Some(swagger20::KubernetesAction::List),
263                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
264                        group: group.clone(),
265                        kind: cr_name.clone(),
266                        version: version.clone(),
267                    }),
268                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}")),
269                },
270
271                swagger20::Operation {
272                    id: format!("patch{namespace_operation_id_component}{cr_name}"),
273                    kubernetes_action: Some(swagger20::KubernetesAction::Patch),
274                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
275                        group: group.clone(),
276                        kind: cr_name.clone(),
277                        version: version.clone(),
278                    }),
279                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}")),
280                },
281
282                swagger20::Operation {
283                    id: format!("patch{namespace_operation_id_component}{cr_name}Status"),
284                    kubernetes_action: Some(swagger20::KubernetesAction::Patch),
285                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
286                        group: group.clone(),
287                        kind: cr_name.clone(),
288                        version: version.clone(),
289                    }),
290                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}/status")),
291                },
292
293                swagger20::Operation {
294                    id: format!("read{namespace_operation_id_component}{cr_name}"),
295                    kubernetes_action: Some(swagger20::KubernetesAction::Get),
296                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
297                        group: group.clone(),
298                        kind: cr_name.clone(),
299                        version: version.clone(),
300                    }),
301                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}")),
302                },
303
304                swagger20::Operation {
305                    id: format!("read{namespace_operation_id_component}{cr_name}Status"),
306                    kubernetes_action: Some(swagger20::KubernetesAction::Get),
307                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
308                        group: group.clone(),
309                        kind: cr_name.clone(),
310                        version: version.clone(),
311                    }),
312                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}/status")),
313                },
314
315                swagger20::Operation {
316                    id: format!("replace{namespace_operation_id_component}{cr_name}"),
317                    kubernetes_action: Some(swagger20::KubernetesAction::Put),
318                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
319                        group: group.clone(),
320                        kind: cr_name.clone(),
321                        version: version.clone(),
322                    }),
323                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}")),
324                },
325
326                swagger20::Operation {
327                    id: format!("replace{namespace_operation_id_component}{cr_name}Status"),
328                    kubernetes_action: Some(swagger20::KubernetesAction::Put),
329                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
330                        group: group.clone(),
331                        kind: cr_name.clone(),
332                        version: version.clone(),
333                    }),
334                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}/{{name}}/status")),
335                },
336
337                swagger20::Operation {
338                    id: format!("watch{namespace_operation_id_component}{cr_name}"),
339                    kubernetes_action: Some(swagger20::KubernetesAction::Watch),
340                    kubernetes_group_kind_version: Some(swagger20::KubernetesGroupKindVersion {
341                        group: group.clone(),
342                        kind: cr_name.clone(),
343                        version: version.clone(),
344                    }),
345                    path: swagger20::Path(format!("/apis/{group}/{version}{namespace_path_component}/{plural}")),
346                },
347            ],
348        };
349
350        let mut run_state = RunState {
351            writer: vec![],
352        };
353
354        let _ =
355            k8s_openapi_codegen_common::run(
356                &spec.definitions,
357                &mut spec.operations,
358                &swagger20::DefinitionPath(cr_name),
359                &MapNamespace,
360                &vis,
361                if generate_schema { k8s_openapi_codegen_common::GenerateSchema::Yes { feature: None } } else { k8s_openapi_codegen_common::GenerateSchema::No },
362                &mut run_state,
363            )
364            .map_err(|err| format!("#[derive(CustomResourceDefinition)] failed: {err}"))
365            .spanning(&tokens)?;
366
367        assert!(spec.operations.is_empty());
368
369        let out = String::from_utf8(run_state.writer).map_err(|err| format!("#[derive(CustomResourceDefinition)] failed: {err}")).spanning(&tokens)?;
370        let result = out.parse().map_err(|err| format!("#[derive(CustomResourceDefinition)] failed: {err:?}")).spanning(&tokens)?;
371        Ok(result)
372    }
373}
374
375struct MapNamespace;
376
377impl k8s_openapi_codegen_common::MapNamespace for MapNamespace {
378    fn map_namespace<'a>(&self, path_parts: &[&'a str]) -> Option<Vec<&'a str>> {
379        match path_parts {
380            ["io", "k8s", rest @ ..] => Some(std::iter::once("k8s_openapi").chain(rest.iter().copied()).collect()),
381            path_parts => Some(path_parts.to_owned()),
382        }
383    }
384}
385
386struct RunState {
387    writer: Vec<u8>,
388}
389
390impl k8s_openapi_codegen_common::RunState for RunState {
391    type Writer = Vec<u8>;
392
393    fn make_writer(&mut self, _parts: &[&str]) -> std::io::Result<Self::Writer> {
394        Ok(std::mem::take(&mut self.writer))
395    }
396
397    fn finish(&mut self, writer: Self::Writer) {
398        self.writer = writer;
399    }
400}