stackable_versioned/
lib.rs

1//! This crate enables versioning of structs and enums through procedural macros.
2//!
3//! It is part of the project DeLorean and converts between different CRD versions by using 1.21 GW
4//! of power, 142km/h and time travel.
5//!
6//! Currently supported versioning schemes:
7//!
8//! - Kubernetes API versions (eg: `v1alpha1`, `v1beta1`, `v1`, `v2`), with optional support for
9//!   generating CRDs.
10//!
11//! Support will be extended to SemVer versions, as well as custom version formats in the future.
12//!
13//! See [`versioned`] for an in-depth usage guide and a list of supported arguments.
14//!
15//! ```text
16//!   __---~~~~--__                      __--~~~~---__
17//!  `\---~~~~~~~~\\                    //~~~~~~~~---/'
18//!    \/~~~~~~~~~\||                  ||/~~~~~~~~~\/
19//!                `\\                //'
20//!                  `\\            //'
21//!                    ||          ||
22//!          ______--~~~~~~~~~~~~~~~~~~--______
23//!     ___ // _-~                        ~-_ \\ ___
24//!    `\__)\/~                              ~\/(__/'
25//!     _--`-___                            ___-'--_
26//!   /~     `\ ~~~~~~~~------------~~~~~~~~ /'     ~\
27//!  /|        `\         ________         /'        |\
28//! | `\   ______`\_      \------/      _/'______   /' |
29//! |   `\_~-_____\ ~-________________-~ /_____-~_/'   |
30//! `.     ~-__________________________________-~     .'
31//!  `.      [_______/------|~~|------\_______]      .'
32//!   `\--___((____)(________\/________)(____))___--/'
33//!    |>>>>>>||</                        \>||<<<<<<|
34//!    `\<<<<</'                            `\>>>>>/'
35//! ```
36//!
37//! Credit: Tua Xiong in <https://asciiart.website/art/4323>
38use std::collections::BTreeMap;
39
40use schemars::{Schema, json_schema};
41use snafu::{ErrorCompat, Snafu};
42// Re-export
43pub use stackable_versioned_macros::versioned;
44
45/// A value-to-value conversion that consumes the input value while tracking changes via a
46/// Kubernetes status.
47///
48/// This allows nested sub structs to bubble up their tracked changes.
49pub trait TrackingFrom<T, S>
50where
51    Self: Sized,
52    S: TrackingStatus + Default,
53{
54    /// Convert `T` into `Self`.
55    fn tracking_from(value: T, status: &mut S, parent: &str) -> Self;
56}
57
58/// A value-to-value conversion that consumes the input value while tracking changes via a
59/// Kubernetes status. The opposite of [`TrackingFrom`].
60///
61/// One should avoid implementing [`TrackingInto`] as it is automatically implemented via a
62/// blanket implementation.
63pub trait TrackingInto<T, S>
64where
65    Self: Sized,
66    S: TrackingStatus + Default,
67{
68    /// Convert `Self` into `T`.
69    fn tracking_into(self, status: &mut S, parent: &str) -> T;
70}
71
72impl<T, U, S> TrackingInto<U, S> for T
73where
74    S: TrackingStatus + Default,
75    U: TrackingFrom<T, S>,
76{
77    fn tracking_into(self, status: &mut S, parent: &str) -> U {
78        U::tracking_from(self, status, parent)
79    }
80}
81
82/// Used to access [`ChangedValues`] from any status.
83pub trait TrackingStatus {
84    fn changes(&mut self) -> &mut ChangedValues;
85}
86
87// NOTE (@Techassi): This struct represents a rough first draft of how tracking values across
88// CRD versions can be achieved. It might change down the line.
89// FIXME (@Techassi): Ideally we don't serialize empty maps. Further, we shouldn't serialize the
90// changedValues field in the status, if there it is empty. This currently "pollutes" the status
91// with empty JSON objects.
92/// Contains changed values during upgrades and downgrades of CRDs.
93#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
94pub struct ChangedValues {
95    /// List of values needed when downgrading to a particular version.
96    pub downgrades: BTreeMap<String, Vec<ChangedValue>>,
97
98    /// List of values needed when upgrading to a particular version.
99    pub upgrades: BTreeMap<String, Vec<ChangedValue>>,
100    // TODO (@Techassi): Add a version indicator here if we ever decide to change the tracking
101    // mechanism.
102}
103
104/// Contains a changed value for a single field of the CRD.
105#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
106#[serde(rename_all = "camelCase")]
107pub struct ChangedValue {
108    /// The name of the field of the custom resource this value is for.
109    pub json_path: String,
110
111    /// The value to be used when upgrading or downgrading the custom resource.
112    #[schemars(schema_with = "raw_object_schema")]
113    pub value: serde_yaml::Value,
114}
115
116// TODO (@Techassi): Think about where this should live. Basically this already exists in
117// stackable-operator, but we cannot use it without depending on it which I would like to
118// avoid.
119fn raw_object_schema(_: &mut schemars::generate::SchemaGenerator) -> Schema {
120    json_schema!({
121        "type": "object",
122        "x-kubernetes-preserve-unknown-fields": true,
123    })
124}
125
126/// This error indicates that parsing an object from a conversion review failed.
127#[derive(Debug, Snafu)]
128pub enum ParseObjectError {
129    #[snafu(display("the field {field:?} is missing"))]
130    FieldNotPresent { field: String },
131
132    #[snafu(display("the field {field:?} must be a string"))]
133    FieldNotStr { field: String },
134
135    #[snafu(display("encountered unknown object API version {api_version:?}"))]
136    UnknownApiVersion { api_version: String },
137
138    #[snafu(display("failed to deserialize object from JSON"))]
139    Deserialize { source: serde_json::Error },
140
141    #[snafu(display("unexpected object kind {kind:?}, expected {expected:?}"))]
142    UnexpectedKind { kind: String, expected: String },
143}
144
145/// This error indicates that converting an object from a conversion review to the desired
146/// version failed.
147#[derive(Debug, Snafu)]
148pub enum ConvertObjectError {
149    #[snafu(display("failed to parse object"))]
150    Parse { source: ParseObjectError },
151
152    #[snafu(display("failed to serialize object into json"))]
153    Serialize { source: serde_json::Error },
154
155    #[snafu(display("failed to parse desired API version"))]
156    ParseDesiredApiVersion {
157        source: UnknownDesiredApiVersionError,
158    },
159}
160
161impl ConvertObjectError {
162    /// Joins the error and its sources using colons.
163    pub fn join_errors(&self) -> String {
164        // NOTE (@Techassi): This can be done with itertools in a way shorter
165        // fashion but obviously brings in another dependency. Which of those
166        // two solutions performs better needs to evaluated.
167        // self.iter_chain().join(": ")
168        self.iter_chain()
169            .map(|err| err.to_string())
170            .collect::<Vec<String>>()
171            .join(": ")
172    }
173
174    /// Returns a HTTP status code based on the underlying error.
175    pub fn http_status_code(&self) -> u16 {
176        match self {
177            ConvertObjectError::Parse { .. } => 400,
178            ConvertObjectError::Serialize { .. } => 500,
179
180            // This is likely the clients fault, as it is requesting a unsupported version
181            ConvertObjectError::ParseDesiredApiVersion {
182                source: UnknownDesiredApiVersionError { .. },
183            } => 400,
184        }
185    }
186}
187
188#[derive(Debug, Snafu)]
189#[snafu(display("unknown API version {api_version:?}"))]
190pub struct UnknownDesiredApiVersionError {
191    pub api_version: String,
192}
193
194pub fn jthong_path(parent: &str, child: &str) -> String {
195    format!("{parent}.{child}")
196}