stackable_versioned_macros/lib.rs
1use darling::{FromMeta, ast::NestedMeta};
2#[cfg(doc)]
3use kube::core::conversion::ConversionReview;
4use proc_macro::TokenStream;
5use syn::{Error, Item, spanned::Spanned};
6
7use crate::{attrs::module::ModuleAttributes, codegen::module::Module};
8
9#[cfg(test)]
10mod test_utils;
11
12mod attrs;
13mod codegen;
14mod utils;
15
16/// This macro enables generating versioned structs and enums.
17///
18/// In this guide, code blocks usually come in pairs. The first code block
19/// describes how the macro is used. The second expandable block displays the
20/// generated piece of code for explanation purposes. It should be noted, that
21/// the exact code can diverge from what is being depicted in this guide. Most
22/// code is heavily simplified. For example, `#[automatically_derived]` and
23/// `#[allow(deprecated)]` are removed in most examples to reduce visual clutter.
24///
25/// <div class="warning">
26///
27/// It is **important** to note that this macro must be placed before any other
28/// (derive) macros and attributes. Macros supplied before the versioned macro
29/// will be erased, because the original struct, enum or module (container) is
30/// erased, and new containers are generated. This ensures that the macros and
31/// attributes are applied to the generated versioned instances of the
32/// container.
33///
34/// </div>
35///
36/// # Version Declarations
37///
38/// Before any of the fields or variants can be versioned, versions need to be
39/// declared at the module level. Each version currently supports two
40/// parameters: `name` and the `deprecated` flag. The `name` must be a valid
41/// (and supported) format.
42///
43/// <div class="warning">
44///
45/// Currently, only [Kubernetes API versions][k8s-version-format] are supported.
46/// The macro checks each declared version and reports any error encountered
47/// during parsing.
48///
49/// </div>
50///
51/// It should be noted that the defined struct always represents the **latest**
52/// version, eg: when defining three versions `v1alpha1`, `v1beta1`, and `v1`,
53/// the struct will describe the structure of the data in `v1`. This behaviour
54/// is especially noticeable in the [`changed()`](#changed-action) action which
55/// works "backwards" by describing how a field looked before the current
56/// (latest) version.
57///
58/// TODO: Version declarations should eventually be moved back to containers.
59///
60/// ```
61/// # use stackable_versioned_macros::versioned;
62/// #[versioned(version(name = "v1alpha1"))]
63/// mod versioned {
64/// struct Foo {
65/// bar: usize,
66/// }
67/// }
68/// ```
69///
70/// <details>
71/// <summary>Generated code</summary>
72///
73/// 1. The `#[automatically_derived]` attribute indicates that the following
74/// piece of code is automatically generated by a macro instead of being
75/// handwritten by a developer. This information is used by cargo and rustc.
76/// 2. For each declared version, a new module containing the containers is
77/// generated. This enables you to reference the container by versions via
78/// `v1alpha1::Foo`.
79/// 3. This `use` statement gives the generated containers access to the imports
80/// at the top of the file. This is a convenience, because otherwise you
81/// would need to prefix used items with `super::`. Additionally, other
82/// macros can have trouble using items referred to with `super::`.
83///
84/// ```ignore
85/// #[automatically_derived] // 1
86/// mod v1alpha1 { // 2
87/// use super::*; // 3
88/// pub struct Foo {
89/// bar: usize,
90/// }
91/// }
92/// ```
93/// </details>
94///
95/// ## Version Deprecation
96///
97/// The `deprecated` flag marks the version as deprecated. This currently adds
98/// the `#[deprecated]` attribute to the appropriate piece of code. In the
99/// future, this will additionally mark the CRD version with `deprecated: true`.
100/// See the official docs on [version deprecation][k8s-crd-ver-deprecation].
101///
102/// ```
103/// # use stackable_versioned_macros::versioned;
104/// #[versioned(version(name = "v1alpha1", deprecated))]
105/// mod versioned {
106/// struct Foo {
107/// bar: usize,
108/// }
109/// }
110/// ```
111///
112/// <details>
113/// <summary>Generated code</summary>
114///
115/// 1. The `deprecated` flag will generate a `#[deprecated]` attribute and the
116/// note is automatically generated.
117///
118/// ```ignore
119/// #[automatically_derived]
120/// #[deprecated = "Version v1alpha1 is deprecated"] // 1
121/// mod v1alpha1 {
122/// use super::*;
123/// pub struct Foo {
124/// pub bar: usize,
125/// }
126/// }
127/// ```
128/// </details>
129///
130/// ## Version Sorting
131///
132/// Additionally, it is ensured that each version is unique. Declaring the same
133/// version multiple times will result in an error. Furthermore, declaring the
134/// versions out-of-order is prohibited by default. It is possible to opt-out
135/// of this check by setting `options(allow_unsorted)`.
136///
137/// <div class="warning">
138///
139/// It is **not** recommended to use this setting and instead use sorted versions
140/// across all versioned items.
141///
142/// </div>
143///
144/// ```
145/// # use stackable_versioned_macros::versioned;
146/// #[versioned(
147/// version(name = "v1beta1"),
148/// version(name = "v1alpha1"),
149/// options(allow_unsorted)
150/// )]
151/// mod versioned {
152/// struct Foo {
153/// bar: usize,
154/// }
155/// }
156/// ```
157///
158/// # Versioning Module
159///
160/// The purpose of the macro is to version Kubernetes CustomResourceDefinitions
161/// (CRDs). As such, the design and how it works is focused on defining and
162/// versioning these CRDs. These CRDs are defined as a top-level struct which
163/// can them self contain many sub structs.
164///
165/// To be able to maximize the visibility on items comprising the CRD, the macro
166/// needs to be applied to module blocks. The name of the module can be freely
167/// chosen. Throughout this guide, the name `versioned` is used. The module is
168/// erased in the generated code. This behaviour can however be
169/// [customized](#preserve-module).
170///
171/// TODO: Mention visibility of module
172///
173/// ## Preserve Module
174///
175/// The previous examples completely replaced the `versioned` module with
176/// top-level version modules. This is the default behaviour. Preserving the
177/// module can however be enabled by setting the `preserve_module` flag.
178///
179/// ```
180/// # use stackable_versioned_macros::versioned;
181/// #[versioned(
182/// version(name = "v1alpha1"),
183/// version(name = "v1"),
184/// options(preserve_module)
185/// )]
186/// mod versioned {
187/// struct Foo {
188/// bar: usize,
189/// }
190///
191/// struct Bar {
192/// baz: String,
193/// }
194/// }
195/// ```
196///
197/// ## Crate Overrides
198///
199/// Override the import path of specific crates which is especially useful if
200/// the crates are brought into scope through re-exports. The following code
201/// block depicts supported overrides and their default values.
202///
203/// ```ignore
204/// # use stackable_versioned_macros::versioned;
205/// #[versioned(
206/// version(name = "v1alpha1"),
207/// version(name = "v1beta1"),
208/// crates(
209/// versioned = "::stackable_versioned",
210/// kube_client = "::kube::client",
211/// k8s_openapi = "::k8s_openapi",
212/// serde_json = "::serde_json",
213/// kube_core = "::kube::core",
214/// schemars = "::schemars",
215/// serde = "::serde",
216/// // Mutually exclusive with kube_core and kube_client
217/// kube = "::kube",
218/// )
219/// )]
220/// mod versioned {
221/// // ...
222/// }
223/// ```
224///
225/// ## Additional Options
226///
227/// This section contains optional options which influence parts of the code
228/// generation.
229///
230/// ```
231/// # use stackable_versioned_macros::versioned;
232/// #[versioned(
233/// version(name = "v1alpha1"),
234/// version(name = "v1beta1"),
235/// options(k8s(
236/// // Highly experimental conversion tracking. Opting into this feature will
237/// // introduce frequent breaking changes.
238/// experimental_conversion_tracking,
239/// // Enables instrumentation and log events via the tracing crate.
240/// enable_tracing,
241/// ))
242/// )]
243/// mod versioned {
244/// // ...
245/// }
246/// ```
247///
248/// ## Merging Submodules
249///
250/// Modules defined in the versioned module will be re-emitted. This allows for
251/// composition of re-exports to compose easier to use imports for downstream
252/// consumers of versioned containers. The following rules apply:
253///
254/// 1. Only modules named the same like defined versions will be re-emitted.
255/// Using modules with invalid names will return an error.
256/// 2. Only `use` statements defined in the module will be emitted. Declaring
257/// other items will return an error.
258///
259/// ```
260/// # use stackable_versioned_macros::versioned;
261/// # mod a {
262/// # pub mod v1alpha1 {}
263/// # }
264/// # mod b {
265/// # pub mod v1alpha1 {}
266/// # }
267/// #[versioned(version(name = "v1alpha1"), version(name = "v1"))]
268/// mod versioned {
269/// mod v1alpha1 {
270/// pub use a::v1alpha1::*;
271/// pub use b::v1alpha1::*;
272/// }
273///
274/// struct Foo {
275/// bar: usize,
276/// }
277/// }
278/// # fn main() {}
279/// ```
280///
281/// <details>
282/// <summary>Expand Generated Code</summary>
283///
284/// ```ignore
285/// mod v1alpha1 {
286/// use super::*;
287/// pub use a::v1alpha1::*;
288/// pub use b::v1alpha1::*;
289/// pub struct Foo {
290/// pub bar: usize,
291/// }
292/// }
293///
294/// mod v1 {
295/// use super::*;
296/// pub struct Foo {
297/// pub bar: usize,
298/// }
299/// }
300/// ```
301///
302/// </details>
303///
304/// # CRD Spec Definition
305///
306/// ## Arguments
307///
308/// <div class="warning">
309///
310/// It should be noted that not every `#[kube]` argument is supported or
311/// forwarded without changes.
312///
313/// </div>
314///
315/// Structs annotated with `#[versioned(crd()]` are treated as top-level CRD
316/// spec definitions. This section lists all currently supported arguments.
317/// Most of these arguments are directly forwarded to the underlying `#[kube]`
318/// attribute. Some arguments are specific to this macro and don't exist in the
319/// upstream [`kube`] crate.
320///
321/// ```
322/// # use kube::CustomResource;
323/// # use schemars::JsonSchema;
324/// # use serde::{Deserialize, Serialize};
325/// # use stackable_versioned_macros::versioned;
326/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
327/// mod versioned {
328/// #[versioned(crd(
329/// // **Required.** Set the group of the CRD, usually the domain of the
330/// // company, like `example.com`.
331/// group = "example.com",
332/// // Override the kind field of the CRD. This defaults to the struct
333/// // name (without the `Spec` suffix). Overriding this value will also
334/// // influence the names of other generated items, like the status
335/// // struct (if used) or the version enum.
336/// kind = "CustomKind",
337/// // Set the singular name. Defaults to lowercased `kind` value.
338/// singular = "...",
339/// // Set the plural name. Defaults to inferring from singular.
340/// plural = "...",
341/// // Indicate that this is a namespaced scoped resource rather than a
342/// // cluster scoped resource.
343/// namespaced,
344/// // Set the specified struct as the status subresource. If conversion
345/// // tracking is enabled, this struct will be automatically merged into
346/// // the generated tracking status struct.
347/// status = "FooStatus",
348/// // Set a shortname. This can be specified multiple times.
349/// shortname = "..."
350/// ))]
351/// # #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
352/// pub struct FooSpec {
353/// #[versioned(deprecated(since = "v1beta1"))]
354/// deprecated_bar: usize,
355/// baz: bool,
356/// }
357/// }
358/// # #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
359/// # pub struct FooStatus {}
360/// # fn main() {}
361/// ```
362///
363/// ## Field Actions
364///
365/// This crate currently supports three different item actions. Items can
366/// be added, changed, and deprecated. The macro ensures that these actions
367/// adhere to the following set of rules:
368///
369/// 1. Items cannot be added and deprecated in the same version.
370/// 2. Items cannot be added and changed in the same version.
371/// 3. Items cannot be changed and deprecated in the same version.
372/// 4. Items added in version _a_, renamed _0...n_ times in versions
373/// b<sub>1</sub>, ..., b<sub>n</sub> and deprecated in
374/// version _c_ must ensure _a < b<sub>1</sub>, ..., b<sub>n</sub> < c_.
375/// 5. All item actions must use previously declared versions. Using versions
376/// not present at the container level will result in an error.
377///
378/// For items marked as deprecated, one additional rule applies:
379///
380/// - Fields must start with the `deprecated_` and variants with the
381/// `Deprecated` prefix. This is enforced because Kubernetes doesn't allow
382/// removing fields in CRDs entirely. Instead, they should be marked as
383/// deprecated. By convention this is done with the `deprecated` prefix.
384///
385/// ### Added Action
386///
387/// This action indicates that an item is added in a particular version.
388/// Available arguments are:
389///
390/// - `since` to indicate since which version the item is present.
391/// - `default` to customize the default function used to populate the item
392/// in auto-generated conversion implementations.
393///
394/// ```
395/// # use stackable_versioned_macros::versioned;
396/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
397/// mod versioned {
398/// pub struct Foo {
399/// #[versioned(added(since = "v1beta1"))]
400/// bar: usize,
401/// baz: bool,
402/// }
403/// }
404/// ```
405///
406/// <details>
407/// <summary>Expand Generated Code</summary>
408///
409/// 1. The field `bar` is not yet present in version `v1alpha1` and is therefore
410/// not generated.
411/// 2. Now the field `bar` is present and uses `Default::default()` to populate
412/// the field during conversion. This function can be customized as shown
413/// later in this guide.
414///
415/// ```ignore
416/// pub mod v1alpha1 {
417/// use super::*;
418/// pub struct Foo { // 1
419/// pub baz: bool,
420/// }
421/// }
422///
423/// impl From<v1alpha1::Foo> for v1beta1::Foo {
424/// fn from(foo: v1alpha1::Foo) -> Self {
425/// Self {
426/// bar: Default::default(), // 2
427/// baz: foo.baz,
428/// }
429/// }
430/// }
431///
432/// pub mod v1beta1 {
433/// use super::*;
434/// pub struct Foo {
435/// pub bar: usize, // 2
436/// pub baz: bool,
437/// }
438/// }
439/// ```
440/// </details>
441///
442/// #### Custom Default Function
443///
444/// To customize the default function used in the generated conversion
445/// implementations the `added` action provides the `default` argument. It
446/// expects a path to a function without braces. This path can for example point
447/// at free-standing or associated functions.
448///
449/// ```
450/// # use stackable_versioned_macros::versioned;
451/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
452/// mod versioned {
453/// pub struct Foo {
454/// #[versioned(added(since = "v1beta1", default = "default_bar"))]
455/// bar: usize,
456/// baz: bool,
457/// }
458/// }
459///
460/// fn default_bar() -> usize {
461/// 42
462/// }
463/// ```
464///
465/// <details>
466/// <summary>Expand Generated Code</summary>
467///
468/// 1. Instead of `Default::default()`, the provided function `default_bar()` is
469/// used. It is of course fully type checked and needs to return the expected
470/// type (`usize` in this case).
471///
472/// ```ignore
473/// // Snip
474///
475/// impl From<v1alpha1::Foo> for v1beta1::Foo {
476/// fn from(foo: v1alpha1::Foo) -> Self {
477/// Self {
478/// bar: default_bar(), // 1
479/// baz: foo.baz,
480/// }
481/// }
482/// }
483///
484/// // Snip
485/// ```
486/// </details>
487///
488/// ### Changed Action
489///
490/// This action indicates that an item is changed in a particular version. It
491/// combines renames and type changes into a single action. You can choose to
492/// change the name, change the type or do both. Available arguments are:
493///
494/// - `since` to indicate since which version the item is changed.
495/// - `from_name` to indicate from which previous name the field is renamed.
496/// - `from_type` to indicate from which previous type the field is changed.
497/// - `upgrade_with` to provide a custom upgrade function. This argument can
498/// only be used in combination with the `from_type` argument. The expected
499/// function signature is: `fn (OLD_TYPE) -> NEW_TYPE`. This function must
500/// not fail.
501/// - `downgrade_with` to provide a custom downgrade function. This argument can
502/// only be used in combination with the `from_type` argument. The expected
503/// function signature is: `fn (NEW_TYPE) -> OLD_TYPE`. This function must
504/// not fail.
505/// ```
506/// # use stackable_versioned_macros::versioned;
507/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
508/// mod versioned {
509/// pub struct Foo {
510/// #[versioned(changed(
511/// since = "v1beta1",
512/// from_name = "prev_bar",
513/// from_type = "u16",
514/// downgrade_with = usize_to_u16
515/// ))]
516/// bar: usize,
517/// baz: bool,
518/// }
519/// }
520///
521/// fn usize_to_u16(input: usize) -> u16 {
522/// input.try_into().unwrap()
523/// }
524/// ```
525///
526/// <details>
527/// <summary>Expand Generated Code</summary>
528///
529/// 1. In version `v1alpha1` the field is named `prev_bar` and uses a `u16`.
530/// 2. In the next version, `v1beta1`, the field is now named `bar` and uses
531/// `usize` instead of a `u16`. The conversion implementations transforms the
532/// type automatically.
533///
534/// ```ignore
535/// pub mod v1alpha1 {
536/// use super::*;
537/// pub struct Foo {
538/// pub prev_bar: u16, // 1
539/// pub baz: bool,
540/// }
541/// }
542///
543/// impl From<v1alpha1::Foo> for v1beta1::Foo {
544/// fn from(foo: v1alpha1::Foo) -> Self {
545/// Self {
546/// bar: foo.prev_bar.into(), // 2
547/// baz: foo.baz,
548/// }
549/// }
550/// }
551///
552/// pub mod v1beta1 {
553/// use super::*;
554/// pub struct Foo {
555/// pub bar: usize, // 2
556/// pub baz: bool,
557/// }
558/// }
559/// ```
560/// </details>
561///
562/// ### Deprecated Action
563///
564/// This action indicates that an item is deprecated in a particular version.
565/// Deprecated items are not removed. Available arguments are:
566///
567/// - `since` to indicate since which version the item is deprecated.
568/// - `note` to specify an optional deprecation note.
569///
570/// ```
571/// # use stackable_versioned_macros::versioned;
572/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
573/// mod versioned {
574/// pub struct Foo {
575/// #[versioned(deprecated(since = "v1beta1"))]
576/// deprecated_bar: usize,
577/// baz: bool,
578/// }
579/// }
580/// ```
581///
582/// <details>
583/// <summary>Expand Generated Code</summary>
584///
585/// 1. In version `v1alpha1` the field `bar` is not yet deprecated and thus uses
586/// the name without the `deprecated_` prefix.
587/// 2. In version `v1beta1` the field is deprecated and now includes the
588/// `deprecated_` prefix. It also uses the `#[deprecated]` attribute to
589/// indicate to Clippy this part of Rust code is deprecated. Therefore, the
590/// conversion implementations include `#[allow(deprecated)]` to allow the
591/// usage of deprecated items in automatically generated code.
592///
593/// ```ignore
594/// pub mod v1alpha1 {
595/// use super::*;
596/// pub struct Foo {
597/// pub bar: usize, // 1
598/// pub baz: bool,
599/// }
600/// }
601///
602/// #[allow(deprecated)] // 2
603/// impl From<v1alpha1::Foo> for v1beta1::Foo {
604/// fn from(foo: v1alpha1::Foo) -> Self {
605/// Self {
606/// deprecated_bar: foo.bar, // 2
607/// baz: foo.baz,
608/// }
609/// }
610/// }
611///
612/// pub mod v1beta1 {
613/// use super::*;
614/// pub struct Foo {
615/// #[deprecated] // 2
616/// pub deprecated_bar: usize,
617/// pub baz: bool,
618/// }
619/// }
620/// ```
621/// </details>
622///
623/// ## Additional Arguments
624///
625/// In addition to the field actions, the following top-level field arguments
626/// are available:
627///
628/// ### Hinting Wrapper Types
629///
630/// With `#[versioned(hint(...))]` it is possible to give hints to the macro
631/// that the field contains a wrapped type. Currently, these following hints
632/// are supported:
633///
634/// - `hint(option)`: Indicates that the field contains an `Option<T>`.
635/// - `hint(vec)`: Indicates that the field contains a `Vec<T>`.
636///
637/// These hints are especially useful for generated conversion functions. With
638/// these hints in place, the types are correctly mapped using `Into::into`
639/// (assuming the necessary `From` trait methods are implemented on the target
640/// types for the conversion to be done correctly).
641///
642/// ```
643/// # use stackable_versioned_macros::versioned;
644/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
645/// mod versioned {
646/// pub struct Foo {
647/// #[versioned(changed(since = "v1beta1", from_type = "Vec<usize>"), hint(vec))]
648/// bar: Vec<usize>,
649/// baz: bool,
650/// }
651/// }
652/// ```
653///
654/// # Generated Helpers
655///
656/// This macro generates a few different helpers to enable different operations
657/// around CRD versioning and conversion. The following sections explain these
658/// helpers and (some) of the code behind them in detail.
659///
660/// All these helpers are generated as associated functions on what this macro
661/// calls an entry enum. When defining the following three versions: `v1alpha1`,
662/// `v1beta1`, and `v1` the following entry enum will be generated:
663///
664/// ```ignore
665/// pub enum Foo {
666/// V1Alpha1(v1alpha1::Foo),
667/// V1Beta1(v1beta1::Foo),
668/// V1(v1::Foo),
669/// }
670/// ```
671///
672/// ## Merge CRD Versions
673///
674/// The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2]
675/// function. It automatically calls the `crd` methods of the CRD in all of its
676/// versions and additionally provides a strongly typed selector for the stored
677/// API version.
678///
679/// ```
680/// # use stackable_versioned_macros::versioned;
681/// # use kube::CustomResource;
682/// # use schemars::JsonSchema;
683/// # use serde::{Deserialize, Serialize};
684/// #[versioned(version(name = "v1alpha1"), version(name = "v1beta1"))]
685/// mod versioned {
686/// #[versioned(crd(group = "example.com"))]
687/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
688/// pub struct FooSpec {
689/// #[versioned(added(since = "v1beta1"))]
690/// bar: usize,
691/// baz: bool,
692/// }
693/// }
694///
695/// # fn main() {
696/// let merged_crd = Foo::merged_crd(FooVersion::V1Beta1).unwrap();
697/// println!("{yaml}", yaml = serde_yaml::to_string(&merged_crd).unwrap());
698/// # }
699/// ```
700///
701/// The strongly typed version enum looks very similar to the entry enum
702/// described above. It additionally provides various associated functions used
703/// for parsing and string representations.
704///
705/// ```
706/// #[derive(Copy, Clone, Debug)]
707/// pub enum FooVersion {
708/// V1Alpha1,
709/// V1Beta1,
710/// }
711/// ```
712///
713/// ---
714///
715/// The generation of merging helpers can be skipped if manual implementation
716/// is desired. The following piece of code lists all possible locations where
717/// this skip flag can be provided.
718///
719/// ```
720/// # use stackable_versioned_macros::versioned;
721/// # use kube::CustomResource;
722/// # use schemars::JsonSchema;
723/// # use serde::{Deserialize, Serialize};
724/// #
725/// # #[versioned(version(name = "v1alpha1"))]
726/// #[versioned(skip(merged_crd))] // Skip generation for ALL specs
727/// mod versioned {
728/// #[versioned(skip(merged_crd))] // Skip generation for specific specs
729///
730/// # #[versioned(crd(group = "example.com"))]
731/// # #[derive(Clone, Debug, CustomResource, Deserialize, Serialize, JsonSchema)]
732/// pub struct FooSpec {}
733/// }
734/// #
735/// # fn main() {}
736/// ```
737///
738/// ## Convert CustomResources
739///
740/// The conversion of CRs is tightly integrated with [`ConversionReview`]s, the
741/// payload which a conversion webhook receives from the Kubernetes apiserver.
742/// Naturally, the `try_convert` function takes in [`ConversionReview`] as a
743/// parameter and also returns a [`ConversionReview`] indicating success or
744/// failure.
745///
746/// ```ignore
747/// # use stackable_versioned_macros::versioned;
748/// # use kube::CustomResource;
749/// # use schemars::JsonSchema;
750/// # use serde::{Deserialize, Serialize};
751/// #[versioned(
752/// version(name = "v1alpha1"),
753/// version(name = "v1beta1"),
754/// version(name = "v1")
755/// )]
756/// mod versioned {
757/// #[versioned(crd(group = "example.com"))]
758/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
759/// pub struct FooSpec {
760/// #[versioned(added(since = "v1beta1"))]
761/// bar: usize,
762///
763/// #[versioned(added(since = "v1"))]
764/// baz: bool,
765///
766/// quox: String,
767/// }
768/// }
769///
770/// # fn main() {
771/// let conversion_review = Foo::try_convert(conversion_review);
772/// # }
773/// ```
774///
775/// The generation of conversion helpers can be skipped if manual implementation
776/// is desired. The following piece of code lists all possible locations where
777/// this skip flag can be provided:
778///
779/// ```
780/// # use stackable_versioned_macros::versioned;
781/// # use kube::CustomResource;
782/// # use schemars::JsonSchema;
783/// # use serde::{Deserialize, Serialize};
784/// #
785/// # #[versioned(version(name = "v1alpha1"))]
786/// #[versioned(skip(try_convert))] // Skip generation for ALL specs
787/// mod versioned {
788/// #[versioned(skip(try_convert))] // Skip generation for specific specs
789///
790/// # #[versioned(crd(group = "example.com"))]
791/// # #[derive(Clone, Debug, CustomResource, Deserialize, Serialize, JsonSchema)]
792/// pub struct FooSpec {}
793/// }
794/// #
795/// # fn main() {}
796/// ```
797///
798/// ### Conversion Tracking
799///
800/// <div class="warning">
801///
802/// This is a highly experimental feature. To enable it, provide the
803/// `experimental_conversion_tracking` flag. See the
804/// [additional options](#additional-options) section for more information.
805///
806/// </div>
807///
808/// As per recommendation by the Kubernetes project, conversions should aim to
809/// be infallible and lossless. The above example perfectly illustrates that
810/// achieving this is not as easy as it looks on the surface. Let's assume the
811/// following conditions:
812///
813/// - The CRD's latest version `v1` is marked as the stored version.
814/// - A client requests a CR in an earlier version, in this case `v1alpha1`.
815///
816/// The Kubernetes apiserver retrieves the stored object in `v1` from etcd.
817/// It needs to be downgraded to `v1alpha1` to be able to serve the client the
818/// correct requested version. As defined above, the field `baz` was only added
819/// in `v1` and `bar` was added in `v1beta1` and as such both don't exist in
820/// `v1alpha1`. During the downgrade, the conversion would lose these pieces of
821/// data when upgrading to `v1` again after the client did it's changes. This
822/// macro however provides a mechanism to automatically track values across
823/// conversations without data loss.
824///
825/// <div class="warning">
826///
827/// Currently, only tracking of **added** fields is supported. This will be
828/// expanded to removed fields, field type changes, and fields containing
829/// collections in the future.
830///
831/// </div>
832///
833/// There are many moving parts to enable this mechanism to work automatically
834/// with minimal manual developer input. Pretty much all of the required code
835/// can be generated based on a simple CRD definition described above. The
836/// following paragraphs explain various parts of the system in more detail.
837/// For a complete overview, it is however advised to look at the source code
838/// of the macro and the code it produces.
839///
840/// #### Tracking Values in the Status
841///
842/// Tracking changed values across conversions requires state. In this context,
843/// storing this state as close to the CustomResource as possible is essential.
844/// This is due to various factors such as avoiding external calls (which are
845/// susceptible to network errors), reducing the risk of state drift, and
846/// making the use of conversions as easy as possible straight out of the box;
847/// for both users and cluster administrators.
848///
849/// As such, values are tracked using the CustomResource's status via the
850/// `changedValues` field. This field contains two sections, one for upgrades
851/// and one for downgrades. Both of these sections contain version keys which
852/// list all tracked values for a particular version.
853///
854/// ```yaml
855/// status:
856/// changedValues:
857/// downgrades: null
858/// upgrades:
859/// v1beta1:
860/// - fieldName: "bar"
861/// value: 42
862/// v1:
863/// - fieldName: "baz"
864/// value: true
865/// ```
866///
867/// To continue the above example, upgrading the CustomResource from `v1alpha1`
868/// back to `v1` requires us to re-hydrate the resource with the tracked values.
869/// First, the resource is upgraded to `v1beta1`. During this step, the tracked
870/// value for the `bar` field is applied and removed from the status afterwards.
871/// The final upgrade to `v1` will apply the tracked value for the field `baz`.
872/// Again, it is removed from the status afterwards.
873///
874/// ### Tracking Nested Changes
875///
876/// To be able to automatically track values of changed fields in nested sub
877/// structs of specs, the fields needs to be marked with `#[versioned(nested)]`.
878/// This will indicate the macro to generate the appropriate conversion
879/// functions.
880///
881/// ```
882/// # use stackable_versioned_macros::versioned;
883/// # use kube::CustomResource;
884/// # use schemars::JsonSchema;
885/// # use serde::{Deserialize, Serialize};
886/// #[versioned(
887/// version(name = "v1alpha1"),
888/// version(name = "v1beta1"),
889/// options(k8s(experimental_conversion_tracking))
890/// )]
891/// mod versioned {
892/// #[versioned(crd(group = "example.com"))]
893/// #[derive(Clone, Debug, Deserialize, Serialize, CustomResource, JsonSchema)]
894/// struct FooSpec {
895/// bar: usize,
896///
897/// // TODO: This technically needs to be combined with a change, but
898/// // we want proper, per-container versioning before we add the correct
899/// // attributes here.
900/// #[versioned(nested)]
901/// baz: Baz,
902/// }
903///
904/// #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
905/// struct Baz {
906/// quax: String,
907///
908/// #[versioned(added(since = "v1beta1"))]
909/// quox: bool,
910/// }
911/// }
912/// # fn main() {}
913/// ```
914///
915/// # OpenTelemetry Semantic Conventions
916///
917/// If tracing is enabled, various traces and events are emitted. The fields of
918/// these signals follow the general rules of OpenTelemetry semantic conventions.
919/// There are currently no agreed-upon semantic conventions for CRD conversions.
920/// In the meantime these fields are used:
921///
922/// | Field | Type (Example) | Description |
923/// | :---- | :------------- | :---------- |
924/// | `k8s.crd.conversion.converted_object_count` | usize (6) | The number of successfully converted objects sent back in a conversion review |
925/// | `k8s.crd.conversion.desired_api_version` | String (v1alpha1) | The desired api version received via a conversion review |
926/// | `k8s.crd.conversion.api_version` | String (v1beta1) | The current api version of an object received via a conversion review |
927/// | `k8s.crd.conversion.steps` | usize (2) | The number of steps required to convert a single object from the current to the desired version |
928/// | `k8s.crd.conversion.kind` | String (Foo) | The kind of the CRD |
929///
930/// [1]: https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html
931/// [2]: https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html
932/// [k8s-version-format]: https://kubernetes.io/docs/reference/using-api/#api-versioning
933/// [k8s-crd-ver-deprecation]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-deprecation
934#[proc_macro_attribute]
935pub fn versioned(attrs: TokenStream, input: TokenStream) -> TokenStream {
936 let input = syn::parse_macro_input!(input as Item);
937 versioned_impl(attrs.into(), input).into()
938}
939
940fn versioned_impl(attrs: proc_macro2::TokenStream, input: Item) -> proc_macro2::TokenStream {
941 // TODO (@Techassi): Think about how we can handle nested structs / enums which
942 // are also versioned.
943
944 match input {
945 Item::Mod(item_mod) => {
946 let module_attributes: ModuleAttributes = match parse_outer_attributes(attrs) {
947 Ok(ma) => ma,
948 Err(err) => return err.write_errors(),
949 };
950
951 let module = match Module::new(item_mod, module_attributes) {
952 Ok(module) => module,
953 Err(err) => return err.write_errors(),
954 };
955
956 module.generate_tokens()
957 }
958 _ => Error::new(
959 input.span(),
960 "attribute macro `versioned` can be only be applied to modules",
961 )
962 .into_compile_error(),
963 }
964}
965
966fn parse_outer_attributes<T>(attrs: proc_macro2::TokenStream) -> Result<T, darling::Error>
967where
968 T: FromMeta,
969{
970 let nm = NestedMeta::parse_meta_list(attrs)?;
971 T::from_list(&nm)
972}
973
974#[cfg(test)]
975mod snapshots {
976 use insta::{assert_snapshot, glob};
977
978 use super::*;
979
980 #[test]
981 fn pass() {
982 // TODO (@Techassi): Re-add skip tests
983 let _settings_guard = test_utils::set_snapshot_path().bind_to_scope();
984
985 glob!("../tests/inputs/pass", "*.rs", |path| {
986 let formatted = test_utils::expand_from_file(path)
987 .inspect_err(|err| eprintln!("{err}"))
988 .unwrap();
989 assert_snapshot!(formatted);
990 });
991 }
992}