Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
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]).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I think we should add a small explanation how this works/what this does.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a bit of content in 9f5019b


### 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]).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Whats the reason those two trait implementations are removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because Vec is now stored in the ClusterResources, and DynamicObject does not implement Eq. I was at least able to derive PartialEq again: bf1d753


[#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);
},
);
Comment on lines 24 to 39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Please add two dev comments to quickly explain what this code does and why we need this specialized merge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I struggle to find anything that is not already on the trait docs in https://docs.rs/k8s-openapi/latest/k8s_openapi/trait.DeepMerge.html
What are you thinking of?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was able to add two comments in e28d64e

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);
},
);
Comment on lines 50 to 65
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Same as above.

}
}

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);
},
);
Comment on lines +73 to +79
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Again, same as above.

}
}

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Can we please move those YAML contents into actual files (for example located at stackable-operator/fixtures/object-overrides or similar) instead of inline strings.

"
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Maybe object_overrides would be a better name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the "cleverness" of the code to apply arbitrary patches to any k8s resource.
To me personally these are Kubernetes resources (but it's also fine to call them objects) and a list of patches.
I see that we need to call it objectOverrides in the context of SDP because of consistency, but would prefer to keep the code generic, so that we (or technically someone else) might be able to use it for other stuff in the future.

And yeah, I also like the name ^^

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.
Comment on lines +10 to +11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// A list of generic Kubernetes objects, which are merged onto the objects that the operator
/// creates.
/// A list of generic Kubernetes objects, which are merged with objects created by the operator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this?

Suggested change
/// A list of generic Kubernetes objects, which are merged onto the objects that the operator
/// creates.
/// A list of generic Kubernetes objects, which are merged onto the objects created by the operator.

It makes it clear what is merged onto what

///
/// 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>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Seeing that this struct only contains a single field, this is make sense to turn this into a tuple struct instead? This could also enable us to implement Deref(Mut) on it for ergonomics.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we could have

#[derive(Clone, Debug, Deserialize, Default, JsonSchema, Serialize, PartialEq)]
pub struct ObjectOverrides2(
    /// 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.
    #[schemars(schema_with = "raw_object_list_schema")]
    Vec<DynamicObject>,
);

and than call them as

        #[serde(flatten)]
        object_overrides: ObjectOverrides,
        #[serde(default)]
        object_overrides_2: ObjectOverrides2,

I think we should optimize for ease of use, not sure which one that is.
The ObjectOverrides2 have the downside that users need to remember to #[serde(default)] it and that we can't add new fields. But it has the upside that one is not forced to name the field configIverrides.
Any opinions? Can we think of any field additions that might come?

}
Loading