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