Skip to content

Commit 1df7441

Browse files
maltesandersiegfriedweberadwk67
authored
refactor: Introduce build aggregator (#985)
* refactor: decouple statefulset builders from applied ServiceAccount * refactor: read cluster_domain from ValidatedCluster in listener config * refactor: introduce build() aggregator for Kubernetes resources * test: cover the build() aggregator's resource output * changelog --------- Co-authored-by: Siegfried Weber <mail@siegfriedweber.net> Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech>
1 parent eff2fad commit 1df7441

5 files changed

Lines changed: 393 additions & 188 deletions

File tree

CHANGELOG.md

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

55
## [Unreleased]
66

7+
### Changed
8+
9+
- Internal operator refactoring: introduce a build() step in the reconciler that
10+
assembles all relevant Kubernetes resources before anything is applied ([#985]).
11+
12+
[#985]: https://github.com/stackabletech/kafka-operator/pull/985
13+
714
## [26.7.0] - 2026-07-21
815

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

rust/operator-binary/src/controller.rs

Lines changed: 74 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ use stackable_operator::{
1919
cluster_resources::ClusterResourceApplyStrategy,
2020
commons::{networking::DomainName, product_image_selection::ResolvedProductImage},
2121
crd::listener,
22+
k8s_openapi::api::{
23+
apps::v1::StatefulSet,
24+
core::v1::{ConfigMap, Service},
25+
policy::v1::PodDisruptionBudget,
26+
},
2227
kube::{
23-
Resource,
28+
Resource, ResourceExt,
2429
api::{DynamicObject, ObjectMeta},
2530
core::{DeserializeGuard, error_boundary},
2631
runtime::{controller::Action, reflector::ObjectRef},
@@ -55,18 +60,7 @@ pub use stackable_operator::v2::types::operator::{RoleGroupName, RoleName};
5560

5661
use crate::{
5762
controller::{
58-
build::{
59-
properties::listener::get_kafka_listener_config,
60-
resource::{
61-
listener::build_broker_rolegroup_bootstrap_listener,
62-
pdb::build_pdb,
63-
rbac::{build_rbac_role_binding, build_rbac_service_account},
64-
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
65-
statefulset::{
66-
build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset,
67-
},
68-
},
69-
},
63+
build::resource::rbac::{build_rbac_role_binding, build_rbac_service_account},
7064
node_id_hasher::node_id_hash32_offset,
7165
security::ValidatedKafkaSecurity,
7266
},
@@ -94,6 +88,19 @@ pub enum PodDescriptorsError {
9488
},
9589
}
9690

91+
/// Every Kubernetes resource produced by the [`build`] step.
92+
///
93+
/// The discovery `ConfigMap` is not part of this: it depends on the applied bootstrap
94+
/// [`Listener`](listener)s' status and is therefore built in [`reconcile_kafka`] after they are
95+
/// applied.
96+
pub struct KubernetesResources {
97+
pub stateful_sets: Vec<StatefulSet>,
98+
pub services: Vec<Service>,
99+
pub listeners: Vec<listener::v1alpha1::Listener>,
100+
pub config_maps: Vec<ConfigMap>,
101+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
102+
}
103+
97104
/// The validated cluster. Carries everything the build steps need, resolved once
98105
/// here so downstream code never re-derives it or touches the raw spec.
99106
///
@@ -393,27 +400,12 @@ pub enum Error {
393400
#[snafu(display("failed to validate cluster"))]
394401
ValidateCluster { source: validate::Error },
395402

396-
#[snafu(display("failed to apply bootstrap Listener"))]
397-
ApplyBootstrapListener {
398-
source: stackable_operator::cluster_resources::Error,
399-
},
403+
#[snafu(display("failed to build the Kubernetes resources"))]
404+
BuildResources { source: build::Error },
400405

401-
#[snafu(display("failed to apply Service for role group {role_group}"))]
402-
ApplyRoleGroupService {
406+
#[snafu(display("failed to apply Kubernetes resource"))]
407+
ApplyResource {
403408
source: stackable_operator::cluster_resources::Error,
404-
role_group: RoleGroupName,
405-
},
406-
407-
#[snafu(display("failed to apply ConfigMap for role group {role_group}"))]
408-
ApplyRoleGroupConfig {
409-
source: stackable_operator::cluster_resources::Error,
410-
role_group: RoleGroupName,
411-
},
412-
413-
#[snafu(display("failed to apply StatefulSet for role group {role_group}"))]
414-
ApplyRoleGroupStatefulSet {
415-
source: stackable_operator::cluster_resources::Error,
416-
role_group: RoleGroupName,
417409
},
418410

419411
#[snafu(display("failed to build discovery ConfigMap"))]
@@ -446,11 +438,6 @@ pub enum Error {
446438
source: stackable_operator::client::Error,
447439
},
448440

449-
#[snafu(display("failed to apply PodDisruptionBudget"))]
450-
ApplyPdb {
451-
source: stackable_operator::cluster_resources::Error,
452-
},
453-
454441
#[snafu(display("failed to get required Labels"))]
455442
GetRequiredLabels {
456443
source:
@@ -461,16 +448,6 @@ pub enum Error {
461448
InvalidKafkaCluster {
462449
source: error_boundary::InvalidObject,
463450
},
464-
465-
#[snafu(display("failed to build statefulset"))]
466-
BuildStatefulset {
467-
source: crate::controller::build::resource::statefulset::Error,
468-
},
469-
470-
#[snafu(display("failed to build configmap"))]
471-
BuildConfigMap {
472-
source: crate::controller::build::resource::config_map::Error,
473-
},
474451
}
475452
type Result<T, E = Error> = std::result::Result<T, E>;
476453

@@ -483,21 +460,16 @@ impl ReconcilerError for Error {
483460
match self {
484461
Error::Dereference { .. } => None,
485462
Error::ValidateCluster { .. } => None,
486-
Error::ApplyBootstrapListener { .. } => None,
487-
Error::ApplyRoleGroupService { .. } => None,
488-
Error::ApplyRoleGroupConfig { .. } => None,
489-
Error::ApplyRoleGroupStatefulSet { .. } => None,
463+
Error::BuildResources { .. } => None,
464+
Error::ApplyResource { .. } => None,
490465
Error::BuildDiscoveryConfig { .. } => None,
491466
Error::ApplyDiscoveryConfig { .. } => None,
492467
Error::DeleteOrphans { .. } => None,
493468
Error::ApplyServiceAccount { .. } => None,
494469
Error::ApplyRoleBinding { .. } => None,
495470
Error::ApplyStatus { .. } => None,
496-
Error::ApplyPdb { .. } => None,
497471
Error::GetRequiredLabels { .. } => None,
498472
Error::InvalidKafkaCluster { .. } => None,
499-
Error::BuildStatefulset { .. } => None,
500-
Error::BuildConfigMap { .. } => None,
501473
}
502474
}
503475
}
@@ -557,127 +529,67 @@ pub async fn reconcile_kafka(
557529
.add(client, rbac_sa.clone())
558530
.await
559531
.context(ApplyServiceAccountSnafu)?;
532+
// The ServiceAccount name is deterministic, so the statefulset builders only need the name,
533+
// not the applied object.
534+
let service_account_name = rbac_sa.name_any();
560535
cluster_resources
561536
.add(client, rbac_rolebinding)
562537
.await
563538
.context(ApplyRoleBindingSnafu)?;
564539

565-
let mut bootstrap_listeners = Vec::<listener::v1alpha1::Listener>::new();
566-
567-
for (kafka_role, rg_map) in &validated_cluster.role_group_configs {
568-
for (rolegroup_name, validated_rg) in rg_map {
569-
// The Vector agent config is the static `vector.yaml`, added to the rolegroup
570-
// ConfigMap only when the Vector agent is enabled (resolved during validation).
571-
let vector_config = validated_rg
572-
.config
573-
.logging
574-
.vector_container
575-
.is_some()
576-
.then(build::properties::product_logging::vector_config_file_content);
577-
578-
let rg_headless_service = build_rolegroup_headless_service(
579-
&validated_cluster,
580-
kafka_role,
581-
rolegroup_name,
582-
&validated_cluster.cluster_config.kafka_security,
583-
);
584-
585-
let rg_metrics_service =
586-
build_rolegroup_metrics_service(&validated_cluster, kafka_role, rolegroup_name);
587-
588-
let kafka_listeners = get_kafka_listener_config(
589-
&validated_cluster,
590-
&validated_cluster.cluster_config.kafka_security,
591-
kafka_role,
592-
rolegroup_name,
593-
&client.kubernetes_cluster_info,
594-
);
595-
596-
let rg_configmap = build::resource::config_map::build_rolegroup_config_map(
597-
&validated_cluster,
598-
rolegroup_name,
599-
validated_rg,
600-
&kafka_listeners,
601-
vector_config,
602-
)
603-
.context(BuildConfigMapSnafu)?;
604-
605-
let rg_statefulset = match kafka_role {
606-
KafkaRole::Broker => build_broker_rolegroup_statefulset(
607-
kafka_role,
608-
rolegroup_name,
609-
&validated_cluster,
610-
validated_rg,
611-
&rbac_sa,
612-
)
613-
.context(BuildStatefulsetSnafu)?,
614-
KafkaRole::Controller => build_controller_rolegroup_statefulset(
615-
kafka_role,
616-
rolegroup_name,
617-
&validated_cluster,
618-
validated_rg,
619-
&rbac_sa,
620-
)
621-
.context(BuildStatefulsetSnafu)?,
622-
};
623-
624-
if let AnyConfig::Broker(broker_config) = &validated_rg.config.config {
625-
let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener(
626-
&validated_cluster,
627-
kafka_role,
628-
rolegroup_name,
629-
broker_config,
630-
);
631-
bootstrap_listeners.push(
632-
cluster_resources
633-
.add(client, rg_bootstrap_listener)
634-
.await
635-
.context(ApplyBootstrapListenerSnafu)?,
636-
);
637-
}
540+
// Build every Kubernetes resource up front (client-free). The discovery ConfigMap is not part
541+
// of this, as it depends on the applied bootstrap Listeners' status (see below).
542+
let resources =
543+
build::build(&validated_cluster, &service_account_name).context(BuildResourcesSnafu)?;
544+
545+
// Apply order: Services, then Listeners (collecting the applied bootstrap Listeners for the
546+
// discovery ConfigMap), then ConfigMaps, then PodDisruptionBudgets, and finally the
547+
// StatefulSets. The StatefulSets must be applied after all ConfigMaps and Secrets they mount to
548+
// prevent unnecessary Pod restarts.
549+
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
550+
for service in resources.services {
551+
cluster_resources
552+
.add(client, service)
553+
.await
554+
.context(ApplyResourceSnafu)?;
555+
}
638556

557+
let mut bootstrap_listeners = Vec::<listener::v1alpha1::Listener>::new();
558+
for rg_listener in resources.listeners {
559+
bootstrap_listeners.push(
639560
cluster_resources
640-
.add(client, rg_headless_service)
641-
.await
642-
.with_context(|_| ApplyRoleGroupServiceSnafu {
643-
role_group: rolegroup_name.clone(),
644-
})?;
645-
cluster_resources
646-
.add(client, rg_metrics_service)
647-
.await
648-
.with_context(|_| ApplyRoleGroupServiceSnafu {
649-
role_group: rolegroup_name.clone(),
650-
})?;
651-
cluster_resources
652-
.add(client, rg_configmap)
561+
.add(client, rg_listener)
653562
.await
654-
.with_context(|_| ApplyRoleGroupConfigSnafu {
655-
role_group: rolegroup_name.clone(),
656-
})?;
657-
658-
// Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts
659-
// to prevent unnecessary Pod restarts.
660-
// See https://github.com/stackabletech/commons-operator/issues/111 for details.
661-
ss_cond_builder.add(
662-
cluster_resources
663-
.add(client, rg_statefulset)
664-
.await
665-
.with_context(|_| ApplyRoleGroupStatefulSetSnafu {
666-
role_group: rolegroup_name.clone(),
667-
})?,
668-
);
669-
}
563+
.context(ApplyResourceSnafu)?,
564+
);
565+
}
566+
567+
for config_map in resources.config_maps {
568+
cluster_resources
569+
.add(client, config_map)
570+
.await
571+
.context(ApplyResourceSnafu)?;
572+
}
573+
574+
for pdb in resources.pod_disruption_budgets {
575+
cluster_resources
576+
.add(client, pdb)
577+
.await
578+
.context(ApplyResourceSnafu)?;
579+
}
670580

671-
if let Some(role_config) = validated_cluster.role_configs.get(kafka_role)
672-
&& let Some(pdb) = build_pdb(&role_config.pdb, &validated_cluster, kafka_role)
673-
{
581+
for stateful_set in resources.stateful_sets {
582+
ss_cond_builder.add(
674583
cluster_resources
675-
.add(client, pdb)
584+
.add(client, stateful_set)
676585
.await
677-
.context(ApplyPdbSnafu)?;
678-
}
586+
.context(ApplyResourceSnafu)?,
587+
);
679588
}
680589

590+
// The discovery ConfigMap reports the bootstrap Listeners' ingress addresses, which are only
591+
// populated on the applied Listener objects (by the Listener operator), so it is built here
592+
// rather than in the client-free build() step.
681593
let discovery_cm = build::resource::discovery::build_discovery_configmap(
682594
&validated_cluster,
683595
&bootstrap_listeners,

0 commit comments

Comments
 (0)