Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
00fd1b7
feat: Support `objectOverrides`
sbernauer Nov 10, 2025
38a0996
refactor: Switch to a lis of objects (as opposed to a big string field)
sbernauer Nov 11, 2025
d87791f
changelog
sbernauer Nov 11, 2025
aaf74c2
Add TODO for docs
sbernauer Nov 11, 2025
22b1732
Add a test for Listener merging
sbernauer Nov 11, 2025
ec5b882
Fix doctests
sbernauer Nov 11, 2025
a46cada
Improve CRD docs
sbernauer Nov 20, 2025
058a828
Remove unused error variant
sbernauer Nov 20, 2025
58ab5ca
Add to DummyCluster
sbernauer Nov 20, 2025
45a2ec5
Improve CRD docs
sbernauer Nov 20, 2025
7cb1b89
Link to concepts page
sbernauer Nov 21, 2025
bf1d753
Derive PartialEq again
sbernauer Nov 23, 2025
cb0de44
Move import
sbernauer Nov 23, 2025
4873ac2
Take owned value
sbernauer Nov 23, 2025
5071be8
Update crates/stackable-operator/src/cluster_resources.rs
sbernauer Nov 23, 2025
713ea10
PartialEq again
sbernauer Nov 23, 2025
9f5019b
Improve changelog
sbernauer Nov 23, 2025
e28d64e
Add a comment in DeepMerge impl
sbernauer Nov 23, 2025
0d6c134
Add some rustdocs
sbernauer Nov 23, 2025
c573b7a
patchinator -> deep_merger
sbernauer Nov 24, 2025
7fde5b3
Fix remaining "patch" mentions
sbernauer Nov 24, 2025
d8274d0
Fixup accidential change
sbernauer Nov 24, 2025
956d523
Rename patch -> merge
sbernauer Nov 24, 2025
b4f98e0
Use indoc for tests
sbernauer Nov 24, 2025
fc38a7c
refactor: Move stuff into ObjectOverrides::apply_to
sbernauer Nov 27, 2025
fe29dcb
refactor: Switch ObjectOverrides to unit struct
sbernauer Nov 27, 2025
cdbde7d
fix tests
sbernauer Nov 27, 2025
22d1dc6
Update crates/stackable-operator/CHANGELOG.md
sbernauer Nov 27, 2025
53d9af5
Merge the merge into the onto
sbernauer Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Support `objectOverrides` ([#1118]).

### Changed

- BREAKING: `ClusterResources` now requires the objects added to implement `DeepMerge`.
This is very likely a stackable-operator internal change, but technically breaking ([#1118]).

### Removed

- BREAKING: `ClusterResources` no longer derives `Eq` and `PartialEq` ([#1118]).

[#1118]: https://github.com/stackabletech/operator-rs/pull/1118

## [0.100.3] - 2025-10-31

### Changed
Expand Down
14 changes: 14 additions & 0 deletions crates/stackable-operator/crds/DummyCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,20 @@ spec:
required:
- roleGroups
type: object
objectOverrides:
default: []
description: |-
A list of generic Kubernetes objects, which are merged onto the objects that the operator
creates.

List entries are arbitrary YAML objects, which need to be valid Kubernetes objects.

Read the [Object overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#object-overrides)
for more information.
items:
type: object
x-kubernetes-preserve-unknown-fields: true
type: array
opaConfig:
description: |-
Configure the OPA stacklet [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery)
Expand Down
30 changes: 24 additions & 6 deletions crates/stackable-operator/src/cluster_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
#[cfg(doc)]
use k8s_openapi::api::core::v1::{NodeSelector, Pod};
use k8s_openapi::{
NamespaceResourceScope,
DeepMerge, NamespaceResourceScope,
api::{
apps::v1::{
DaemonSet, DaemonSetSpec, Deployment, DeploymentSpec, StatefulSet, StatefulSetSpec,
Expand Down Expand Up @@ -42,6 +42,7 @@ use crate::{
Label, LabelError, Labels,
consts::{K8S_APP_INSTANCE_KEY, K8S_APP_MANAGED_BY_KEY, K8S_APP_NAME_KEY},
},
patchinator::{self, ObjectOverrides, apply_patches},
utils::format_full_controller_name,
};

Expand Down Expand Up @@ -87,6 +88,9 @@ pub enum Error {
#[snafu(source(from(crate::client::Error, Box::new)))]
source: Box<crate::client::Error>,
},

#[snafu(display("failed to apply user-provided object overrides"))]
ApplyObjectOverrides { source: patchinator::Error },
}

/// A cluster resource handled by [`ClusterResources`].
Expand All @@ -97,6 +101,7 @@ pub enum Error {
/// it must be added to [`ClusterResources::delete_orphaned_resources`] as well.
pub trait ClusterResource:
Clone
+ DeepMerge
+ Debug
+ DeserializeOwned
+ Resource<DynamicType = (), Scope = NamespaceResourceScope>
Expand Down Expand Up @@ -332,6 +337,7 @@ impl ClusterResource for Deployment {
/// use serde::{Deserialize, Serialize};
/// use stackable_operator::client::Client;
/// use stackable_operator::cluster_resources::{self, ClusterResourceApplyStrategy, ClusterResources};
/// use stackable_operator::patchinator::ObjectOverrides;
/// use stackable_operator::product_config_utils::ValidatedRoleConfigByPropertyKind;
/// use stackable_operator::role_utils::Role;
/// use std::sync::Arc;
Expand All @@ -348,7 +354,10 @@ impl ClusterResource for Deployment {
/// plural = "AppClusters",
/// namespaced,
/// )]
/// struct AppClusterSpec {}
/// struct AppClusterSpec {
/// #[serde(flatten)]
/// pub object_overrides: ObjectOverrides,
/// }
///
/// enum Error {
/// CreateClusterResources {
Expand All @@ -371,6 +380,7 @@ impl ClusterResource for Deployment {
/// CONTROLLER_NAME,
/// &app.object_ref(&()),
/// ClusterResourceApplyStrategy::Default,
/// &app.spec.object_overrides,
/// )
/// .map_err(|source| Error::CreateClusterResources { source })?;
///
Expand Down Expand Up @@ -413,8 +423,8 @@ impl ClusterResource for Deployment {
/// Ok(Action::await_change())
/// }
/// ```
#[derive(Debug, Eq, PartialEq)]
pub struct ClusterResources {
#[derive(Debug)]
pub struct ClusterResources<'a> {
/// The namespace of the cluster
namespace: String,

Expand Down Expand Up @@ -442,9 +452,12 @@ pub struct ClusterResources {
/// Strategy to manage how cluster resources are applied. Resources could be patched, merged
/// or not applied at all depending on the strategy.
apply_strategy: ClusterResourceApplyStrategy,

/// Arbitrary Kubernetes object overrides specified by the user via the CRD.
object_overrides: &'a ObjectOverrides,
}

impl ClusterResources {
impl<'a> ClusterResources<'a> {
/// Constructs new `ClusterResources`.
///
/// # Arguments
Expand All @@ -470,6 +483,7 @@ impl ClusterResources {
controller_name: &str,
cluster: &ObjectReference,
apply_strategy: ClusterResourceApplyStrategy,
object_overrides: &'a ObjectOverrides,
) -> Result<Self> {
let namespace = cluster
.namespace
Expand All @@ -494,6 +508,7 @@ impl ClusterResources {
manager: format_full_controller_name(operator_name, controller_name),
resource_ids: Default::default(),
apply_strategy,
object_overrides,
})
}

Expand Down Expand Up @@ -563,7 +578,10 @@ impl ClusterResources {
.unwrap_or_else(|err| warn!("{}", err));
}

let mutated = resource.maybe_mutate(&self.apply_strategy);
let mut mutated = resource.maybe_mutate(&self.apply_strategy);

// We apply the object overrides of the user at the very last to offer maximum flexibility.
apply_patches(&mut mutated, self.object_overrides).context(ApplyObjectOverridesSnafu)?;

let patched_resource = self
.apply_strategy
Expand Down
143 changes: 142 additions & 1 deletion crates/stackable-operator/src/crd/listener/listeners/v1alpha1_impl.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,148 @@
use crate::crd::listener::listeners::v1alpha1::ListenerSpec;
use k8s_openapi::{DeepMerge, merge_strategies};

use crate::crd::listener::listeners::v1alpha1::{
Listener, ListenerIngress, ListenerPort, ListenerSpec, ListenerStatus,
};

impl ListenerSpec {
pub(super) const fn default_publish_not_ready_addresses() -> Option<bool> {
Some(true)
}
}

impl DeepMerge for Listener {
fn merge_from(&mut self, other: Self) {
DeepMerge::merge_from(&mut self.metadata, other.metadata);
DeepMerge::merge_from(&mut self.spec, other.spec);
DeepMerge::merge_from(&mut self.status, other.status);
}
}

impl DeepMerge for ListenerSpec {
fn merge_from(&mut self, other: Self) {
DeepMerge::merge_from(&mut self.class_name, other.class_name);
merge_strategies::map::granular(
&mut self.extra_pod_selector_labels,
other.extra_pod_selector_labels,
|current_item, other_item| {
DeepMerge::merge_from(current_item, other_item);
},
);
merge_strategies::list::map(
&mut self.ports,
other.ports,
&[|lhs, rhs| lhs.name == rhs.name],
|current_item, other_item| {
DeepMerge::merge_from(current_item, other_item);
},
);
DeepMerge::merge_from(
&mut self.publish_not_ready_addresses,
other.publish_not_ready_addresses,
);
}
}

impl DeepMerge for ListenerStatus {
fn merge_from(&mut self, other: Self) {
DeepMerge::merge_from(&mut self.service_name, other.service_name);
merge_strategies::list::map(
&mut self.ingress_addresses,
other.ingress_addresses,
&[|lhs, rhs| lhs.address == rhs.address],
|current_item, other_item| {
DeepMerge::merge_from(current_item, other_item);
},
);
merge_strategies::map::granular(
&mut self.node_ports,
other.node_ports,
|current_item, other_item| {
DeepMerge::merge_from(current_item, other_item);
},
);
}
}

impl DeepMerge for ListenerIngress {
fn merge_from(&mut self, other: Self) {
DeepMerge::merge_from(&mut self.address, other.address);
self.address_type = other.address_type;
merge_strategies::map::granular(
&mut self.ports,
other.ports,
|current_item, other_item| {
DeepMerge::merge_from(current_item, other_item);
},
);
}
}

impl DeepMerge for ListenerPort {
fn merge_from(&mut self, other: Self) {
DeepMerge::merge_from(&mut self.name, other.name);
DeepMerge::merge_from(&mut self.port, other.port);
DeepMerge::merge_from(&mut self.protocol, other.protocol);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deep_merge_listener() {
let mut base: ListenerSpec = serde_yaml::from_str(
"
className: my-listener-class
extraPodSelectorLabels:
foo: bar
ports:
- name: http
port: 8080
protocol: http
- name: https
port: 8080
protocol: https
# publishNotReadyAddresses defaults to true
",
)
.unwrap();

let patch: ListenerSpec = serde_yaml::from_str(
"
className: custom-listener-class
extraPodSelectorLabels:
foo: overridden
extra: label
ports:
- name: https
port: 8443
publishNotReadyAddresses: false
",
)
.unwrap();

base.merge_from(patch);

let expected: ListenerSpec = serde_yaml::from_str(
"
className: custom-listener-class
extraPodSelectorLabels:
foo: overridden
extra: label
ports:
- name: http
port: 8080
protocol: http
- name: https
port: 8443 # overridden
protocol: https
publishNotReadyAddresses: false
",
)
.unwrap();

assert_eq!(base, expected);
}
}
1 change: 1 addition & 0 deletions crates/stackable-operator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod kvp;
pub mod logging;
pub mod memory;
pub mod namespace;
pub mod patchinator;
pub mod pod_utils;
pub mod product_config_utils;
pub mod product_logging;
Expand Down
20 changes: 20 additions & 0 deletions crates/stackable-operator/src/patchinator/crd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use kube::api::DynamicObject;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::utils::crds::raw_object_list_schema;

#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ObjectOverrides {
/// A list of generic Kubernetes objects, which are merged onto the objects that the operator
/// creates.
///
/// List entries are arbitrary YAML objects, which need to be valid Kubernetes objects.
///
/// Read the [Object overrides documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/overrides#object-overrides)
/// for more information.
#[serde(default)]
#[schemars(schema_with = "raw_object_list_schema")]
pub object_overrides: Vec<DynamicObject>,
}
Loading