@@ -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
5661use 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}
475452type 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