Skip to content

Commit 5dbc9f8

Browse files
maltesanderadwk67siegfriedweber
authored
refactor: Introduce build aggregator (#756)
* refactor: Introduce build aggregator * tests: add resource names test * Fix unit test * changelog --------- Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech> Co-authored-by: Siegfried Weber <mail@siegfriedweber.net>
1 parent 7e2dc13 commit 5dbc9f8

3 files changed

Lines changed: 319 additions & 170 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
7+
- Internal operator refactoring: introduce a build() step in the reconciler that
8+
assembles all relevant Kubernetes resources before anything is applied ([#756]).
9+
10+
[#756]: https://github.com/stackabletech/superset-operator/pull/756
11+
512
## [26.7.0] - 2026-07-21
613

714
## [26.7.0-rc1] - 2026-07-16

rust/operator-binary/src/controller.rs

Lines changed: 63 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ use stackable_operator::{
1818
rbac::build_rbac_resources,
1919
resources::{NoRuntimeLimits, Resources},
2020
},
21-
k8s_openapi::api::core::v1::Secret,
21+
crd::listener,
22+
k8s_openapi::api::{
23+
apps::v1::{Deployment, StatefulSet},
24+
core::v1::{ConfigMap, Secret, Service},
25+
policy::v1::PodDisruptionBudget,
26+
},
2227
kube::{
2328
Resource, ResourceExt,
2429
api::ObjectMeta,
@@ -54,14 +59,6 @@ use tracing::instrument;
5459

5560
use crate::{
5661
OPERATOR_NAME,
57-
controller::build::resource::{
58-
config_map::build_rolegroup_config_map,
59-
deployment::build_rolegroup_deployment,
60-
listener::build_group_listener,
61-
pdb::build_pdb,
62-
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
63-
statefulset::build_node_rolegroup_statefulset,
64-
},
6562
crd::{
6663
APP_NAME, INTERNAL_SECRET_SECRET_KEY, SupersetRole,
6764
authentication::SupersetClientAuthenticationDetailsResolved,
@@ -86,6 +83,19 @@ pub struct Ctx {
8683
pub operator_environment: OperatorEnvironmentOptions,
8784
}
8885

86+
/// Every Kubernetes resource produced by the build step.
87+
///
88+
/// The `Node` role is provisioned via a `StatefulSet` (it serves the Superset web UI), while the
89+
/// `Worker`/`Beat` Celery roles are provisioned via `Deployment`s; the build step collects both.
90+
pub struct KubernetesResources {
91+
pub stateful_sets: Vec<StatefulSet>,
92+
pub deployments: Vec<Deployment>,
93+
pub services: Vec<Service>,
94+
pub listeners: Vec<listener::v1alpha1::Listener>,
95+
pub config_maps: Vec<ConfigMap>,
96+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
97+
}
98+
8999
/// Per-role configuration extracted during validation.
90100
#[derive(Clone, Debug)]
91101
pub struct ValidatedRoleConfig {
@@ -386,28 +396,12 @@ pub enum Error {
386396
source: stackable_operator::cluster_resources::Error,
387397
},
388398

389-
#[snafu(display("failed to apply Service for role group {role_group_name}"))]
390-
ApplyRoleGroupService {
391-
source: stackable_operator::cluster_resources::Error,
392-
role_group_name: RoleGroupName,
393-
},
394-
395-
#[snafu(display("failed to apply ConfigMap for role group {role_group_name}"))]
396-
ApplyRoleGroupConfig {
397-
source: stackable_operator::cluster_resources::Error,
398-
role_group_name: RoleGroupName,
399-
},
399+
#[snafu(display("failed to build the Kubernetes resources"))]
400+
BuildResources { source: build::Error },
400401

401-
#[snafu(display("failed to apply StatefulSet for role group {role_group_name}"))]
402-
ApplyRoleGroupStatefulSet {
402+
#[snafu(display("failed to apply Kubernetes resource"))]
403+
ApplyResource {
403404
source: stackable_operator::cluster_resources::Error,
404-
role_group_name: RoleGroupName,
405-
},
406-
407-
#[snafu(display("failed to apply Deployment for role group {role_group_name}"))]
408-
ApplyRoleGroupDeployment {
409-
source: stackable_operator::cluster_resources::Error,
410-
role_group_name: RoleGroupName,
411405
},
412406

413407
#[snafu(display("failed to update status"))]
@@ -430,11 +424,6 @@ pub enum Error {
430424
source: stackable_operator::commons::rbac::Error,
431425
},
432426

433-
#[snafu(display("failed to apply PodDisruptionBudget"))]
434-
ApplyPdb {
435-
source: stackable_operator::cluster_resources::Error,
436-
},
437-
438427
#[snafu(display("failed to get required Labels"))]
439428
GetRequiredLabels {
440429
source:
@@ -446,26 +435,6 @@ pub enum Error {
446435
source: error_boundary::InvalidObject,
447436
},
448437

449-
#[snafu(display("failed to apply group listener"))]
450-
ApplyGroupListener {
451-
source: stackable_operator::cluster_resources::Error,
452-
},
453-
454-
#[snafu(display("failed to build statefulset"))]
455-
BuildStatefulSet {
456-
source: crate::controller::build::resource::statefulset::Error,
457-
},
458-
459-
#[snafu(display("failed to build deployment"))]
460-
BuildDeployment {
461-
source: crate::controller::build::resource::deployment::Error,
462-
},
463-
464-
#[snafu(display("failed to build configmap"))]
465-
BuildConfigMap {
466-
source: crate::controller::build::resource::config_map::Error,
467-
},
468-
469438
#[snafu(display("failed to create SECRET_KEY secret"))]
470439
CreateSecretKeySecret {
471440
source: random_secret_creation::Error,
@@ -567,129 +536,53 @@ pub async fn reconcile_superset(
567536
.await
568537
.context(CreateSecretKeySecretSnafu)?;
569538

539+
let resources = build::build(&validated, &rbac_sa.name_any()).context(BuildResourcesSnafu)?;
540+
570541
let mut statefulset_cond_builder = StatefulSetConditionBuilder::default();
571542
let mut deployment_cond_builder = DeploymentConditionBuilder::default();
572543

573-
for (superset_role, rolegroup_configs) in &validated.role_groups {
574-
for (rolegroup_name, validated_rolegroup) in rolegroup_configs {
575-
let config = &validated_rolegroup.config;
576-
577-
let rg_configmap = build_rolegroup_config_map(
578-
&validated,
579-
superset_role,
580-
rolegroup_name,
581-
config,
582-
&validated_rolegroup.config_overrides,
583-
)
584-
.context(BuildConfigMapSnafu)?;
585-
586-
// Every role exposes metrics via the statsd-exporter sidecar, so each rolegroup gets a
587-
// metrics Service. The headless Service is built per role below: only the `Node` role's
588-
// StatefulSet references one (as its `serviceName`); the `Worker`/`Beat` Deployments have
589-
// no `serviceName` and do not serve the HTTP port, so they get no headless Service.
590-
let rg_metrics_service =
591-
build_rolegroup_metrics_service(&validated, superset_role, rolegroup_name);
592-
544+
// The StatefulSets/Deployments are applied last, so every ConfigMap and Secret they mount
545+
// already exists — otherwise a changed mount would restart the Pods.
546+
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
547+
for service in resources.services {
548+
cluster_resources
549+
.add(client, service)
550+
.await
551+
.context(ApplyResourceSnafu)?;
552+
}
553+
for config_map in resources.config_maps {
554+
cluster_resources
555+
.add(client, config_map)
556+
.await
557+
.context(ApplyResourceSnafu)?;
558+
}
559+
for listener in resources.listeners {
560+
cluster_resources
561+
.add(client, listener)
562+
.await
563+
.context(ApplyResourceSnafu)?;
564+
}
565+
for pdb in resources.pod_disruption_budgets {
566+
cluster_resources
567+
.add(client, pdb)
568+
.await
569+
.context(ApplyResourceSnafu)?;
570+
}
571+
for statefulset in resources.stateful_sets {
572+
statefulset_cond_builder.add(
593573
cluster_resources
594-
.add(client, rg_metrics_service)
574+
.add(client, statefulset)
595575
.await
596-
.with_context(|_| ApplyRoleGroupServiceSnafu {
597-
role_group_name: rolegroup_name.clone(),
598-
})?;
599-
576+
.context(ApplyResourceSnafu)?,
577+
);
578+
}
579+
for deployment in resources.deployments {
580+
deployment_cond_builder.add(
600581
cluster_resources
601-
.add(client, rg_configmap)
582+
.add(client, deployment)
602583
.await
603-
.with_context(|_| ApplyRoleGroupConfigSnafu {
604-
role_group_name: rolegroup_name.clone(),
605-
})?;
606-
607-
match superset_role {
608-
SupersetRole::Node => {
609-
let rg_headless_service =
610-
build_rolegroup_headless_service(&validated, superset_role, rolegroup_name);
611-
612-
cluster_resources
613-
.add(client, rg_headless_service)
614-
.await
615-
.with_context(|_| ApplyRoleGroupServiceSnafu {
616-
role_group_name: rolegroup_name.clone(),
617-
})?;
618-
619-
let rg_statefulset = build_node_rolegroup_statefulset(
620-
&validated,
621-
superset_role,
622-
rolegroup_name,
623-
validated_rolegroup,
624-
&rbac_sa.name_any(),
625-
)
626-
.context(BuildStatefulSetSnafu)?;
627-
628-
// Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts
629-
// to prevent unnecessary Pod restarts.
630-
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
631-
statefulset_cond_builder.add(
632-
cluster_resources
633-
.add(client, rg_statefulset)
634-
.await
635-
.with_context(|_| ApplyRoleGroupStatefulSetSnafu {
636-
role_group_name: rolegroup_name.clone(),
637-
})?,
638-
);
639-
}
640-
SupersetRole::Worker | SupersetRole::Beat => {
641-
let rg_deployment = build_rolegroup_deployment(
642-
&validated,
643-
superset_role,
644-
rolegroup_name,
645-
validated_rolegroup,
646-
&rbac_sa.name_any(),
647-
)
648-
.context(BuildDeploymentSnafu)?;
649-
650-
// Note: The Deployment needs to be applied after all ConfigMaps and Secrets it mounts
651-
// to prevent unnecessary Pod restarts.
652-
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
653-
deployment_cond_builder.add(
654-
cluster_resources
655-
.add(client, rg_deployment)
656-
.await
657-
.with_context(|_| ApplyRoleGroupDeploymentSnafu {
658-
role_group_name: rolegroup_name.clone(),
659-
})?,
660-
);
661-
}
662-
}
663-
}
664-
665-
// Role-level resources (group listener, PDB) are built once per role, after
666-
// its role groups — not once per role group.
667-
if let Some(role_config) = validated.role_configs.get(superset_role) {
668-
if let (Some(listener_class), Some(listener_group_name)) = (
669-
&role_config.listener_class,
670-
&role_config.group_listener_name,
671-
) {
672-
let group_listener = build_group_listener(
673-
&validated,
674-
superset_role,
675-
listener_class,
676-
listener_group_name.to_string(),
677-
);
678-
cluster_resources
679-
.add(client, group_listener)
680-
.await
681-
.context(ApplyGroupListenerSnafu)?;
682-
}
683-
684-
if let Some(pdb) = &role_config.pdb
685-
&& let Some(pdb) = build_pdb(pdb, &validated, superset_role)
686-
{
687-
cluster_resources
688-
.add(client, pdb)
689-
.await
690-
.context(ApplyPdbSnafu)?;
691-
}
692-
}
584+
.context(ApplyResourceSnafu)?,
585+
);
693586
}
694587

695588
cluster_resources

0 commit comments

Comments
 (0)