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}