Skip to content

Commit 4e76ec9

Browse files
committed
chore: Merge branch 'main' into feat/stackable-telemetry-tuples
2 parents 269549e + af0d1f1 commit 4e76ec9

File tree

12 files changed

+683
-148
lines changed

12 files changed

+683
-148
lines changed

Cargo.lock

Lines changed: 106 additions & 102 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ chrono = { version = "0.4.38", default-features = false }
1616
clap = { version = "4.5.17", features = ["derive", "cargo", "env"] }
1717
const_format = "0.2.33"
1818
const-oid = "0.9.6"
19-
convert_case = "0.6.0"
19+
convert_case = "0.7.1"
2020
darling = "0.20.10"
2121
delegate = "0.13.0"
2222
dockerfile-parser = "0.9.0"

crates/stackable-operator/CHANGELOG.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,30 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [0.84.0] - 2025-01-16
8+
79
### Added
810

911
- BREAKING: Aggregate emitted Kubernetes events on the CustomResources thanks to the new
1012
[kube feature](https://github.com/kube-rs/controller-rs/pull/116). Instead of reporting the same
1113
event multiple times it now uses `EventSeries` to aggregate these events to single entry with an
12-
age like `3s (x11 over 53s)` ([#867]):
14+
age like `3s (x11 over 53s)` ([#938]):
1315
- The `report_controller_error` function now needs to be async.
1416
- It now takes `Recorder` as a parameter instead of a `Client`.
1517
- The `Recorder` instance needs to be available across all `reconcile` invocations, to ensure
1618
aggregation works correctly.
1719
- The operator needs permission to `patch` events (previously only `create` was needed).
20+
- Add `ProductSpecificCommonConfig`, so that product operators can have custom fields within `commonConfig`.
21+
Also add a `JavaCommonConfig`, which can be used by JVM-based tools to offer `jvmArgumentOverrides` with this mechanism ([#931])
1822

1923
### Changed
2024

21-
- BREAKING: Bump Rust dependencies to enable Kubernetes 1.32 (via `kube` 0.98.0 and `k8s-openapi`
22-
0.23.0) ([#867]).
25+
- BREAKING: Bump Rust dependencies to enable Kubernetes 1.32 (via `kube` 0.98.0 and `k8s-openapi` 0.23.0) ([#938]).
26+
- BREAKING: Append a dot to the default cluster domain to make it a FQDN and allow FQDNs when validating a `DomainName` ([#939]).
27+
28+
[#931]: https://github.com/stackabletech/operator-rs/pull/931
29+
[#938]: https://github.com/stackabletech/operator-rs/pull/938
30+
[#939]: https://github.com/stackabletech/operator-rs/pull/939
2331

2432
## [0.83.0] - 2024-12-03
2533

@@ -316,7 +324,7 @@ All notable changes to this project will be documented in this file.
316324

317325
[#808]: https://github.com/stackabletech/operator-rs/pull/808
318326

319-
## [0.69.1] 2024-06-10
327+
## [0.69.1] - 2024-06-10
320328

321329
### Added
322330

crates/stackable-operator/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "stackable-operator"
33
description = "Stackable Operator Framework"
4-
version = "0.83.0"
4+
version = "0.84.0"
55
authors.workspace = true
66
license.workspace = true
77
edition.workspace = true

crates/stackable-operator/src/commons/networking.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ use crate::validation;
1111
Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, JsonSchema,
1212
)]
1313
#[serde(try_from = "String", into = "String")]
14-
pub struct DomainName(#[validate(regex(path = "validation::RFC_1123_SUBDOMAIN_REGEX"))] String);
14+
pub struct DomainName(#[validate(regex(path = "validation::DOMAIN_REGEX"))] String);
1515

1616
impl FromStr for DomainName {
1717
type Err = validation::Errors;
1818

1919
fn from_str(value: &str) -> Result<Self, Self::Err> {
20-
validation::is_rfc_1123_subdomain(value)?;
20+
validation::is_domain(value)?;
2121
Ok(DomainName(value.to_owned()))
2222
}
2323
}

crates/stackable-operator/src/config/fragment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pub trait FromFragment: Sized {
109109
/// For complex structs, this should be a variant of `Self` where each field is replaced by its respective `Fragment` type. This can be derived using
110110
/// [`Fragment`].
111111
type Fragment;
112-
/// A variant of [`Self::Fragment`] that is used when the container already provides a to indicate that a value is optional.
112+
/// A variant of [`Self::Fragment`] that is used when the container already provides a way to indicate that a value is optional.
113113
///
114114
/// For example, there's no use marking a value as [`Option`]al again if the value is already contained in an `Option`.
115115
///

crates/stackable-operator/src/config/merge.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Automatically merges objects *deeply*, especially fragments.
2+
13
use k8s_openapi::{
24
api::core::v1::{NodeAffinity, PodAffinity, PodAntiAffinity, PodTemplateSpec},
35
apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::LabelSelector},
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,173 @@
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+
1165
pub mod fragment;
2166
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};

crates/stackable-operator/src/product_config_utils.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,21 @@ pub fn config_for_role_and_group<'a>(
167167
/// - `resource` - Not used directly. It's passed on to the `Configuration::compute_*` calls.
168168
/// - `roles` - A map keyed by role names. The value is a tuple of a vector of `PropertyNameKind`
169169
/// like (Cli, Env or Files) and [`crate::role_utils::Role`] with a boxed [`Configuration`].
170-
pub fn transform_all_roles_to_config<T, U>(
170+
#[allow(clippy::type_complexity)]
171+
pub fn transform_all_roles_to_config<T, U, ProductSpecificCommonConfig>(
171172
resource: &T::Configurable,
172-
roles: HashMap<String, (Vec<PropertyNameKind>, Role<T, U>)>,
173+
roles: HashMap<
174+
String,
175+
(
176+
Vec<PropertyNameKind>,
177+
Role<T, U, ProductSpecificCommonConfig>,
178+
),
179+
>,
173180
) -> Result<RoleConfigByPropertyKind>
174181
where
175182
T: Configuration,
176183
U: Default + JsonSchema + Serialize,
184+
ProductSpecificCommonConfig: Default + JsonSchema + Serialize,
177185
{
178186
let mut result = HashMap::new();
179187

@@ -359,15 +367,16 @@ fn process_validation_result(
359367
/// - `role_name` - The name of the role.
360368
/// - `role` - The role for which to transform the configuration parameters.
361369
/// - `property_kinds` - Used as "buckets" to partition the configuration properties by.
362-
fn transform_role_to_config<T, U>(
370+
fn transform_role_to_config<T, U, ProductSpecificCommonConfig>(
363371
resource: &T::Configurable,
364372
role_name: &str,
365-
role: &Role<T, U>,
373+
role: &Role<T, U, ProductSpecificCommonConfig>,
366374
property_kinds: &[PropertyNameKind],
367375
) -> Result<RoleGroupConfigByPropertyKind>
368376
where
369377
T: Configuration,
370378
U: Default + JsonSchema + Serialize,
379+
ProductSpecificCommonConfig: Default + JsonSchema + Serialize,
371380
{
372381
let mut result = HashMap::new();
373382

@@ -422,10 +431,10 @@ where
422431
/// - `role_name` - Not used directly but passed on to the `Configuration::compute_*` calls.
423432
/// - `config` - The configuration properties to partition.
424433
/// - `property_kinds` - The "buckets" used to partition the configuration properties.
425-
fn parse_role_config<T>(
434+
fn parse_role_config<T, ProductSpecificCommonConfig>(
426435
resource: &<T as Configuration>::Configurable,
427436
role_name: &str,
428-
config: &CommonConfiguration<T>,
437+
config: &CommonConfiguration<T, ProductSpecificCommonConfig>,
429438
property_kinds: &[PropertyNameKind],
430439
) -> Result<HashMap<PropertyNameKind, BTreeMap<String, Option<String>>>>
431440
where
@@ -452,8 +461,8 @@ where
452461
Ok(result)
453462
}
454463

455-
fn parse_role_overrides<T>(
456-
config: &CommonConfiguration<T>,
464+
fn parse_role_overrides<T, ProductSpecificCommonConfig>(
465+
config: &CommonConfiguration<T, ProductSpecificCommonConfig>,
457466
property_kinds: &[PropertyNameKind],
458467
) -> Result<HashMap<PropertyNameKind, BTreeMap<String, Option<String>>>>
459468
where
@@ -489,8 +498,8 @@ where
489498
Ok(result)
490499
}
491500

492-
fn parse_file_overrides<T>(
493-
config: &CommonConfiguration<T>,
501+
fn parse_file_overrides<T, ProductSpecificCommonConfig>(
502+
config: &CommonConfiguration<T, ProductSpecificCommonConfig>,
494503
file: &str,
495504
) -> Result<BTreeMap<String, Option<String>>>
496505
where
@@ -522,7 +531,7 @@ mod tests {
522531
}
523532

524533
use super::*;
525-
use crate::role_utils::{Role, RoleGroup};
534+
use crate::role_utils::{GenericProductSpecificCommonConfig, Role, RoleGroup};
526535
use k8s_openapi::api::core::v1::PodTemplateSpec;
527536
use rstest::*;
528537
use std::collections::HashMap;
@@ -610,13 +619,14 @@ mod tests {
610619
config_overrides: Option<HashMap<String, HashMap<String, String>>>,
611620
env_overrides: Option<HashMap<String, String>>,
612621
cli_overrides: Option<BTreeMap<String, String>>,
613-
) -> CommonConfiguration<Box<TestConfig>> {
622+
) -> CommonConfiguration<Box<TestConfig>, GenericProductSpecificCommonConfig> {
614623
CommonConfiguration {
615624
config: test_config.unwrap_or_default(),
616625
config_overrides: config_overrides.unwrap_or_default(),
617626
env_overrides: env_overrides.unwrap_or_default(),
618627
cli_overrides: cli_overrides.unwrap_or_default(),
619628
pod_overrides: PodTemplateSpec::default(),
629+
product_specific_common_config: GenericProductSpecificCommonConfig::default(),
620630
}
621631
}
622632

0 commit comments

Comments
 (0)