Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
- Internal operator refactoring: introduce a build() step in the reconciler that
assembles all relevant Kubernetes resources before anything is applied ([#756]).
- Bump stackable-operator to 0.114.0 ([#765]).
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
functions and carry the full set of recommended labels ([#761]).
- BREAKING: The `nodes` role is now required by the CRD; a SupersetCluster without it was
previously accepted by the API server but reconciled to no `nodes` resources ([#761]).

[#756]: https://github.com/stackabletech/superset-operator/pull/756
[#761]: https://github.com/stackabletech/superset-operator/pull/761
[#765]: https://github.com/stackabletech/superset-operator/pull/765

## [26.7.0] - 2026-07-21
Expand Down
2 changes: 1 addition & 1 deletion extra/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,6 @@ spec:
at role level, the `roleConfig`.
You can learn more about this in the
[Roles and role group concept documentation](https://docs.stackable.tech/home/nightly/concepts/roles-and-role-groups).
nullable: true
properties:
cliOverrides:
additionalProperties:
Expand Down Expand Up @@ -2669,6 +2668,7 @@ spec:
required:
- clusterConfig
- image
- nodes
type: object
status:
nullable: true
Expand Down
116 changes: 45 additions & 71 deletions rust/operator-binary/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ use stackable_operator::{
affinity::StackableAffinity,
product_image_selection::ResolvedProductImage,
random_secret_creation::{self, create_random_secret_if_not_exists},
rbac::build_rbac_resources,
resources::{NoRuntimeLimits, Resources},
},
crd::listener,
k8s_openapi::api::{
apps::v1::{Deployment, StatefulSet},
core::v1::{ConfigMap, Secret, Service},
core::v1::{ConfigMap, Secret, Service, ServiceAccount},
policy::v1::PodDisruptionBudget,
rbac::v1::RoleBinding,
},
kube::{
Resource, ResourceExt,
Resource,
api::ObjectMeta,
core::{DeserializeGuard, error_boundary},
runtime::controller::Action,
Expand All @@ -39,12 +39,11 @@ use stackable_operator::{
},
v2::{
HasName, HasUid, NameIsValidLabelValue,
builder::meta::ownerreference_from_resource,
cluster_resources::cluster_resources_new,
kvp::label::{recommended_labels, role_group_selector},
product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig},
role_group_utils::ResourceNames,
role_utils::{GenericCommonConfig, RoleGroupConfig},
role_utils::{self, GenericCommonConfig, RoleGroupConfig},
types::{
kubernetes::{ListenerClassName, ListenerName, NamespaceName, Uid},
operator::{
Expand Down Expand Up @@ -94,6 +93,8 @@ pub struct KubernetesResources {
pub listeners: Vec<listener::v1alpha1::Listener>,
pub config_maps: Vec<ConfigMap>,
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
pub service_accounts: Vec<ServiceAccount>,
pub role_bindings: Vec<RoleBinding>,
}

/// Per-role configuration extracted during validation.
Expand Down Expand Up @@ -226,24 +227,33 @@ impl ValidatedCluster {
}
}

pub fn resource_names(
pub fn role_group_resource_names(
&self,
role: &SupersetRole,
role_group_name: &RoleGroupName,
) -> ResourceNames {
ResourceNames {
cluster_name: self.name.clone(),
role_name: role.role_name(),
role_name: role.into(),
role_group_name: role_group_name.clone(),
}
}

/// Type-safe names for the per-cluster RBAC resources: the ServiceAccount shared by all
/// Pods, its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds.
pub fn cluster_resource_names(&self) -> role_utils::ResourceNames {
role_utils::ResourceNames {
cluster_name: self.name.clone(),
product_name: product_name(),
}
}

pub fn recommended_labels(
&self,
role: &SupersetRole,
role_group_name: &RoleGroupName,
) -> Labels {
self.recommended_labels_for(&role.role_name(), role_group_name)
self.recommended_labels_for(&role.into(), role_group_name)
}

pub fn recommended_labels_for(
Expand All @@ -263,7 +273,7 @@ impl ValidatedCluster {
) -> Labels {
self.recommended_labels_with(
&build::UNVERSIONED_PRODUCT_VERSION,
&role.role_name(),
&role.into(),
role_group_name,
)
}
Expand All @@ -290,28 +300,7 @@ impl ValidatedCluster {
role: &SupersetRole,
role_group_name: &RoleGroupName,
) -> Labels {
role_group_selector(self, &product_name(), &role.role_name(), role_group_name)
}

/// Returns an [`ObjectMetaBuilder`] pre-filled with the namespace, an owner reference back to
/// this cluster, and the recommended labels for a resource named `name` in `role`/
/// `role_group_name`.
///
/// Consolidates the metadata chain repeated by the role-group child-resource builders. Call
/// sites that need extra labels/annotations chain them onto the returned builder.
pub(crate) fn object_meta(
&self,
name: impl Into<String>,
role: &SupersetRole,
role_group_name: &RoleGroupName,
) -> ObjectMetaBuilder {
let mut builder = ObjectMetaBuilder::new();
builder
.name_and_namespace(self)
.name(name)
.ownerreference(ownerreference_from_resource(self, None, Some(true)))
.with_labels(self.recommended_labels(role, role_group_name));
builder
role_group_selector(self, &product_name(), &role.into(), role_group_name)
}
}

Expand Down Expand Up @@ -409,27 +398,6 @@ pub enum Error {
source: stackable_operator::client::Error,
},

#[snafu(display("failed to patch service account"))]
ApplyServiceAccount {
source: stackable_operator::cluster_resources::Error,
},

#[snafu(display("failed to patch role binding"))]
ApplyRoleBinding {
source: stackable_operator::cluster_resources::Error,
},

#[snafu(display("failed to build RBAC objects"))]
BuildRBACObjects {
source: stackable_operator::commons::rbac::Error,
},

#[snafu(display("failed to get required Labels"))]
GetRequiredLabels {
source:
stackable_operator::kvp::KeyValuePairError<stackable_operator::kvp::LabelValueError>,
},

#[snafu(display("SupersetCluster object is invalid"))]
InvalidSupersetCluster {
source: error_boundary::InvalidObject,
Expand Down Expand Up @@ -504,24 +472,6 @@ pub async fn reconcile_superset(
&superset.spec.object_overrides,
);

let (rbac_sa, rbac_rolebinding) = build_rbac_resources(
superset,
APP_NAME,
cluster_resources
.get_required_labels()
.context(GetRequiredLabelsSnafu)?,
)
.context(BuildRBACObjectsSnafu)?;

let rbac_sa = cluster_resources
.add(client, rbac_sa)
.await
.context(ApplyServiceAccountSnafu)?;
cluster_resources
.add(client, rbac_rolebinding)
.await
.context(ApplyRoleBindingSnafu)?;

// TODO: Can be removed after SDP 26.7 is released (it's only a migration from 26.3 - 26.7)
// (don't forget about the snafu Error variants).
// Removal is tracked in https://github.com/stackabletech/superset-operator/issues/755
Expand All @@ -536,14 +486,26 @@ pub async fn reconcile_superset(
.await
.context(CreateSecretKeySecretSnafu)?;

let resources = build::build(&validated, &rbac_sa.name_any()).context(BuildResourcesSnafu)?;
let resources = build::build(&validated).context(BuildResourcesSnafu)?;

let mut statefulset_cond_builder = StatefulSetConditionBuilder::default();
let mut deployment_cond_builder = DeploymentConditionBuilder::default();

// The StatefulSets/Deployments are applied last, so every ConfigMap and Secret they mount
// already exists — otherwise a changed mount would restart the Pods.
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
for service_account in resources.service_accounts {
cluster_resources
.add(client, service_account)
.await
.context(ApplyResourceSnafu)?;
}
for role_binding in resources.role_bindings {
cluster_resources
.add(client, role_binding)
.await
.context(ApplyResourceSnafu)?;
}
for service in resources.services {
cluster_resources
.add(client, service)
Expand Down Expand Up @@ -687,6 +649,18 @@ pub fn error_policy(

#[cfg(test)]
pub(crate) mod test_support {
/// The expected `app.kubernetes.io/version` label value for the given product version.
///
/// The `-stackable` suffix carries the operator's own version, which is `0.0.0-dev` on main
/// but rewritten by the release process — so tests must derive it rather than hardcode it,
/// or they fail on release branches.
pub fn app_version_label(product_version: &str) -> String {
format!(
"{product_version}-stackable{}",
crate::built_info::PKG_VERSION
)
}

use crate::{
controller::dereference::DereferencedObjects,
crd::authentication::{
Expand Down
Loading
Loading