stackable_versioned/
lib.rs

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