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}