|
| 1 | +//! The Stacklet Configuration System™©®️️️ (SCS). |
| 2 | +//! |
| 3 | +//! # But oh god why is this monstrosity a thing? |
| 4 | +//! |
| 5 | +//! Products are complicated. They need to be supplied many kinds of configuration. |
| 6 | +//! Some of it applies to the whole installation (Stacklet). Some of it applies only to one [role](`Role`). |
| 7 | +//! Some of it applies only to a subset of the instances of that role (we call this a [`RoleGroup`]). |
| 8 | +//! |
| 9 | +//! We (usually) don't know at what level it makes sense to apply a given piece of configuration, but we also |
| 10 | +//! don't want to force users to repeat themselves constantly! Instead, we model the configuration as a tree: |
| 11 | +//! |
| 12 | +//! ```yaml |
| 13 | +//! stacklet1: |
| 14 | +//! role1: |
| 15 | +//! group1: |
| 16 | +//! group2: |
| 17 | +//! role2: |
| 18 | +//! group3: |
| 19 | +//! group4: |
| 20 | +//! stacklet2: |
| 21 | +//! role3: |
| 22 | +//! group5: |
| 23 | +//! ``` |
| 24 | +//! |
| 25 | +//! Where only the leaves (*groups*) are actually realized into running products, but every level inherits |
| 26 | +//! the configuration of its parents. So `group1` would inherit any keys from `role1` (and, transitively, `stacklet1`), |
| 27 | +//! unless it overrides them. |
| 28 | +//! |
| 29 | +//! We also want to *validate* that the configuration actually makes sense, but only once we have the fully realized |
| 30 | +//! configuration for a given rolegroup. |
| 31 | +//! |
| 32 | +//! However, in practice, living in a fully typed land like Rust makes this slightly awkward. We end up having to choose from |
| 33 | +//! a few awkward options: |
| 34 | +//! |
| 35 | +//! 1. Give up on type safety until we're done merging - Type safety is nice, and we still need to produce a schema for |
| 36 | +//! Kubernetes to validate against. |
| 37 | +//! 2. Give on distinguishing between pre- and post-validation types - Type safety is nice, and it gets error-prone having to memorize |
| 38 | +//! which [`Option::unwrap`]s are completely benign, and which are going to bring down the whole cluster. And, uh, good luck trying |
| 39 | +//! to *change* that in either direction. |
| 40 | +//! 3. Write *separate* types for the pre- and post-validation states - That's a lot of tedious code to have to write twice, and that's not |
| 41 | +//! even counting the validation ([parsing]) and inheritance routines! That's not really stuff you want to get wrong! |
| 42 | +//! |
| 43 | +//! So far, none of those options look particularly great. 3 would probably be the least unworkable path, but... |
| 44 | +//! But then again, uh, we have a compiler. What if we could just make it do the hard work? |
| 45 | +//! |
| 46 | +//! # Okay, but how does it work? |
| 47 | +//! |
| 48 | +//! The SCS™©®️️️ is split into two subsystems: [`fragment`] and [`merge`]. |
| 49 | +//! |
| 50 | +//! ## Uhhhh, fragments? |
| 51 | +//! |
| 52 | +//! The [`Fragment`] macro implements option 3 from above for you. You define the final validated type, |
| 53 | +//! and it generates a "Fragment mirror type", where all fields are replaced by [`Option`]al counterparts. |
| 54 | +//! |
| 55 | +//! For example, |
| 56 | +//! |
| 57 | +//! ``` |
| 58 | +//! # use stackable_operator::config::fragment::Fragment; |
| 59 | +//! #[derive(Fragment)] |
| 60 | +//! struct Foo { |
| 61 | +//! bar: String, |
| 62 | +//! baz: u8, |
| 63 | +//! } |
| 64 | +//! ``` |
| 65 | +//! |
| 66 | +//! generates this: |
| 67 | +//! |
| 68 | +//! ``` |
| 69 | +//! struct FooFragment { |
| 70 | +//! bar: Option<String>, |
| 71 | +//! baz: Option<u8>, |
| 72 | +//! } |
| 73 | +//! ``` |
| 74 | +//! |
| 75 | +//! Additionally, it provides the [`validate`] function, which lets you turn your `FooFragment` back into a `Foo` |
| 76 | +//! (while also making sure that the contents actually make sense). |
| 77 | +//! |
| 78 | +//! Fragments can also be *nested*, as long as the whole hierarchy has fragments. In this case, the fragment of the substruct will be used, |
| 79 | +//! instead of wrapping it in an Option. For example, this: |
| 80 | +//! |
| 81 | +//! ``` |
| 82 | +//! # use stackable_operator::config::fragment::Fragment; |
| 83 | +//! #[derive(Fragment)] |
| 84 | +//! struct Foo { |
| 85 | +//! bar: Bar, |
| 86 | +//! } |
| 87 | +//! |
| 88 | +//! #[derive(Fragment)] |
| 89 | +//! struct Bar { |
| 90 | +//! baz: String, |
| 91 | +//! } |
| 92 | +//! ``` |
| 93 | +//! |
| 94 | +//! generates this: |
| 95 | +//! |
| 96 | +//! ``` |
| 97 | +//! struct FooFragment { |
| 98 | +//! bar: BarFragment, |
| 99 | +//! } |
| 100 | +//! |
| 101 | +//! struct BarFragment { |
| 102 | +//! baz: Option<String>, |
| 103 | +//! } |
| 104 | +//! ``` |
| 105 | +//! |
| 106 | +//! rather than wrapping `Bar` as an option, like this: |
| 107 | +//! |
| 108 | +//! ``` |
| 109 | +//! struct FooFragment { |
| 110 | +//! bar: Option<Bar>, |
| 111 | +//! } |
| 112 | +//! |
| 113 | +//! struct Bar { |
| 114 | +//! baz: String, |
| 115 | +//! } |
| 116 | +//! // BarFragment would be irrelevant here |
| 117 | +//! ``` |
| 118 | +//! |
| 119 | +//! ### How does it actually know whether to use a subfragment or an [`Option`]? |
| 120 | +//! |
| 121 | +//! That's (kind of) a trick question! [`Fragment`] actually has no idea about what an [`Option`] even is! |
| 122 | +//! It always uses [`FromFragment::Fragment`]. A type can opt into the [`Option`] treatment by implementing |
| 123 | +//! [`Atomic`], which is a marker trait for leaf types that cannot be merged any further. |
| 124 | +//! |
| 125 | +//! ### And what about defaults? That seems like a pretty big oversight. |
| 126 | +//! |
| 127 | +//! The Fragment system doesn't natively support default values! Instead, this comes "for free" with the merge system (below). |
| 128 | +//! One benefit of this is that the same `Fragment` type can support different default values in different contexts |
| 129 | +//! (for example: different defaults in different rolegroups). |
| 130 | +//! |
| 131 | +//! ### Can I customize my `Fragment` types? |
| 132 | +//! |
| 133 | +//! Attributes can be applied to the generated types using the `#[fragment_attrs]` attribute. For example, |
| 134 | +//! `#[fragment_attrs(derive(Default))]` applies `#[derive(Default)]` to the `Fragment` type. |
| 135 | +//! |
| 136 | +//! ## And what about merging? So far, those fragments seem pretty useless... |
| 137 | +//! |
| 138 | +//! This is where the [`Merge`] macro (and trait) comes in! It is designed to be applied to the `Fragment` types (see above), |
| 139 | +//! and merges their contents field-by-field, deeply (as in: [`merge`] will recurse into substructs, and merge *their* keys in turn). |
| 140 | +//! |
| 141 | +//! Just like for `Fragment`s, types can opt out of being merged using the [`Atomic`] trait. This is useful both for "primitive" values |
| 142 | +//! (like [`String`], the recursion needs to end *somewhere*, after all), and for values that don't really make sense to merge |
| 143 | +//! (like a set of search query parameters). |
| 144 | +//! |
| 145 | +//! # Fine, how do I actually use it, then? |
| 146 | +//! |
| 147 | +//! For declarations (in CRDs): |
| 148 | +//! - Apply `#[derive(Fragment)] #[fragment_attrs(derive(Merge))]` for your product configuration (and any of its nested types). |
| 149 | +//! - DON'T: `#[derive(Fragment, Merge)]` |
| 150 | +//! - Pretty much always derive deserialization and defaulting on the `Fragment`, not the validated type: |
| 151 | +//! - DO: `#[fragment_attrs(derive(Serialize, Deserialize, Default, JsonSchema))]` |
| 152 | +//! - DON'T: `#[derive(Fragment, Serialize, Deserialize, Default, JsonSchema)]` |
| 153 | +//! - Refer to the `Fragment` type in CRDs, not the validated type. |
| 154 | +//! - Implementing [`Atomic`] if something doesn't make sense to merge. |
| 155 | +//! - Define the "validated form" of your configuration: only make fields [`Option`]al if [`None`] is actually a legal value. |
| 156 | +//! |
| 157 | +//! For runtime code: |
| 158 | +//! - Validate and merge with [`RoleGroup::validate_config`] for CRDs, otherwise [`merge`] manually and then validate with [`validate`]. |
| 159 | +//! - Validate as soon as possible, user code should never read the contents of `Fragment`s. |
| 160 | +//! - Defaults are just another layer to be [`merge`]d. |
| 161 | +//! |
| 162 | +//! [parsing]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ |
| 163 | +//! [`merge`]: Merge::merge |
| 164 | +
|
1 | 165 | pub mod fragment;
|
2 | 166 | pub mod merge;
|
| 167 | + |
| 168 | +#[cfg(doc)] |
| 169 | +use crate::role_utils::{Role, RoleGroup}; |
| 170 | +#[cfg(doc)] |
| 171 | +use fragment::{validate, Fragment, FromFragment}; |
| 172 | +#[cfg(doc)] |
| 173 | +use merge::{Atomic, Merge}; |
0 commit comments