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::{InstanceType, Schema, SchemaObject, SingleOrVec};
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::r#gen::SchemaGenerator) -> Schema {
94    Schema::Object(SchemaObject {
95        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
96        extensions: [(
97            "x-kubernetes-preserve-unknown-fields".to_owned(),
98            serde_json::Value::Bool(true),
99        )]
100        .into(),
101        ..Default::default()
102    })
103}
104
105/// This error indicates that parsing an object from a conversion review failed.
106#[derive(Debug, Snafu)]
107pub enum ParseObjectError {
108    #[snafu(display("the field {field:?} is missing"))]
109    FieldNotPresent { field: String },
110
111    #[snafu(display("the field {field:?} must be a string"))]
112    FieldNotStr { field: String },
113
114    #[snafu(display("encountered unknown object API version {api_version:?}"))]
115    UnknownApiVersion { api_version: String },
116
117    #[snafu(display("failed to deserialize object from JSON"))]
118    Deserialize { source: serde_json::Error },
119
120    #[snafu(display("unexpected object kind {kind:?}, expected {expected:?}"))]
121    UnexpectedKind { kind: String, expected: String },
122}
123
124/// This error indicates that converting an object from a conversion review to the desired
125/// version failed.
126#[derive(Debug, Snafu)]
127pub enum ConvertObjectError {
128    #[snafu(display("failed to parse object"))]
129    Parse { source: ParseObjectError },
130
131    #[snafu(display("failed to serialize object into json"))]
132    Serialize { source: serde_json::Error },
133
134    #[snafu(display("failed to parse desired API version"))]
135    ParseDesiredApiVersion {
136        source: UnknownDesiredApiVersionError,
137    },
138}
139
140impl ConvertObjectError {
141    /// Joins the error and its sources using colons.
142    pub fn join_errors(&self) -> String {
143        // NOTE (@Techassi): This can be done with itertools in a way shorter
144        // fashion but obviously brings in another dependency. Which of those
145        // two solutions performs better needs to evaluated.
146        // self.iter_chain().join(": ")
147        self.iter_chain()
148            .map(|err| err.to_string())
149            .collect::<Vec<String>>()
150            .join(": ")
151    }
152
153    /// Returns a HTTP status code based on the underlying error.
154    pub fn http_status_code(&self) -> u16 {
155        match self {
156            ConvertObjectError::Parse { .. } => 400,
157            ConvertObjectError::Serialize { .. } => 500,
158
159            // This is likely the clients fault, as it is requesting a unsupported version
160            ConvertObjectError::ParseDesiredApiVersion {
161                source: UnknownDesiredApiVersionError { .. },
162            } => 400,
163        }
164    }
165}
166
167#[derive(Debug, Snafu)]
168#[snafu(display("unknown API version {api_version:?}"))]
169pub struct UnknownDesiredApiVersionError {
170    pub api_version: String,
171}
172
173pub fn jthong_path(parent: &str, child: &str) -> String {
174    format!("{parent}.{child}")
175}