Skip to content

Commit 60a87ce

Browse files
adwk67claude
andcommitted
feat: rewrite controller pipeline with validated config types
Replace the monolithic controller with a structured pipeline of validate → build → apply → update_status stages. Each stage operates on validated, type-safe data rather than raw CRD types. Remove discovery.rs, operations/, and service.rs modules whose logic has been absorbed into the new pipeline. Trim listener.rs and kerberos.rs to their shared helpers now that pod-building logic lives in controller/build.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b7e9c56 commit 60a87ce

21 files changed

Lines changed: 2512 additions & 1677 deletions

rust/operator-binary/src/controller.rs

Lines changed: 225 additions & 1105 deletions
Large diffs are not rendered by default.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use std::{hash::Hasher, marker::PhantomData};
2+
3+
use fnv::FnvHasher;
4+
use snafu::{ResultExt, Snafu};
5+
use stackable_operator::{
6+
builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder},
7+
cluster_resources::ClusterResources,
8+
crd::listener::v1alpha1::Listener,
9+
k8s_openapi::api::core::v1::ConfigMap,
10+
kvp::{Labels, ObjectLabels},
11+
};
12+
use strum::{EnumDiscriminants, IntoStaticStr};
13+
14+
use super::{Applied, HIVE_CONTROLLER_NAME, KubernetesResources, Prepared, ValidatedHiveCluster};
15+
use crate::{OPERATOR_NAME, crd::APP_NAME, listener::build_listener_connection_string};
16+
17+
#[derive(Snafu, Debug, EnumDiscriminants)]
18+
#[strum_discriminants(derive(IntoStaticStr))]
19+
pub enum Error {
20+
#[snafu(display("failed to apply service account"))]
21+
ApplyServiceAccount {
22+
source: stackable_operator::cluster_resources::Error,
23+
},
24+
25+
#[snafu(display("failed to apply role binding"))]
26+
ApplyRoleBinding {
27+
source: stackable_operator::cluster_resources::Error,
28+
},
29+
30+
#[snafu(display("failed to apply ConfigMap for {name}"))]
31+
ApplyConfigMap {
32+
source: stackable_operator::cluster_resources::Error,
33+
name: String,
34+
},
35+
36+
#[snafu(display("failed to apply Service for {name}"))]
37+
ApplyService {
38+
source: stackable_operator::cluster_resources::Error,
39+
name: String,
40+
},
41+
42+
#[snafu(display("failed to apply StatefulSet for {name}"))]
43+
ApplyStatefulSet {
44+
source: stackable_operator::cluster_resources::Error,
45+
name: String,
46+
},
47+
48+
#[snafu(display("failed to apply PodDisruptionBudget"))]
49+
ApplyPdb {
50+
source: stackable_operator::cluster_resources::Error,
51+
},
52+
53+
#[snafu(display("failed to apply listener for {role}"))]
54+
ApplyListener {
55+
source: stackable_operator::cluster_resources::Error,
56+
role: String,
57+
},
58+
59+
#[snafu(display("failed to build discovery ConfigMap"))]
60+
BuildDiscoveryConfigMap {
61+
source: stackable_operator::builder::configmap::Error,
62+
},
63+
64+
#[snafu(display("failed to apply discovery ConfigMap"))]
65+
ApplyDiscoveryConfigMap {
66+
source: stackable_operator::cluster_resources::Error,
67+
},
68+
69+
#[snafu(display("failed to build discovery ConfigMap metadata"))]
70+
DiscoveryMetadataBuild {
71+
source: stackable_operator::builder::meta::Error,
72+
},
73+
74+
#[snafu(display("failed to configure listener connection string"))]
75+
ListenerConfiguration { source: crate::listener::Error },
76+
77+
#[snafu(display("failed to delete orphaned resources"))]
78+
DeleteOrphanedResources {
79+
source: stackable_operator::cluster_resources::Error,
80+
},
81+
}
82+
83+
pub struct Applier<'a> {
84+
client: &'a stackable_operator::client::Client,
85+
cluster_resources: ClusterResources<'a>,
86+
}
87+
88+
impl<'a> Applier<'a> {
89+
pub fn new(
90+
client: &'a stackable_operator::client::Client,
91+
cluster_resources: ClusterResources<'a>,
92+
) -> Self {
93+
Self {
94+
client,
95+
cluster_resources,
96+
}
97+
}
98+
99+
pub async fn apply(
100+
mut self,
101+
prepared: KubernetesResources<Prepared>,
102+
validated: &ValidatedHiveCluster,
103+
) -> Result<KubernetesResources<Applied>, Error> {
104+
// RBAC
105+
for sa in prepared.service_accounts {
106+
self.cluster_resources
107+
.add(self.client, sa)
108+
.await
109+
.context(ApplyServiceAccountSnafu)?;
110+
}
111+
for rb in prepared.role_bindings {
112+
self.cluster_resources
113+
.add(self.client, rb)
114+
.await
115+
.context(ApplyRoleBindingSnafu)?;
116+
}
117+
118+
// ConfigMaps
119+
for cm in prepared.config_maps {
120+
let name = cm.metadata.name.clone().unwrap_or_default();
121+
self.cluster_resources
122+
.add(self.client, cm)
123+
.await
124+
.context(ApplyConfigMapSnafu { name })?;
125+
}
126+
127+
// Services
128+
for svc in prepared.services {
129+
let name = svc.metadata.name.clone().unwrap_or_default();
130+
self.cluster_resources
131+
.add(self.client, svc)
132+
.await
133+
.context(ApplyServiceSnafu { name })?;
134+
}
135+
136+
// StatefulSets — applied after ConfigMaps to prevent unnecessary Pod restarts
137+
let mut applied_stateful_sets = Vec::new();
138+
for ss in prepared.stateful_sets {
139+
let name = ss.metadata.name.clone().unwrap_or_default();
140+
let applied = self
141+
.cluster_resources
142+
.add(self.client, ss)
143+
.await
144+
.context(ApplyStatefulSetSnafu { name })?;
145+
applied_stateful_sets.push(applied);
146+
}
147+
148+
// PDBs
149+
for pdb in prepared.pod_disruption_budgets {
150+
self.cluster_resources
151+
.add(self.client, pdb)
152+
.await
153+
.context(ApplyPdbSnafu)?;
154+
}
155+
156+
// Listeners + discovery ConfigMaps
157+
// Discovery ConfigMaps depend on applied Listener status (for connection string building).
158+
// This is hive-specific: the Listener must be applied first, then we read its status
159+
// to build the connection string in the discovery ConfigMap.
160+
let mut discovery_hash = FnvHasher::with_key(0);
161+
162+
for listener in prepared.listeners {
163+
let role = listener
164+
.metadata
165+
.labels
166+
.as_ref()
167+
.and_then(|l| l.get("app.kubernetes.io/component"))
168+
.cloned()
169+
.unwrap_or_else(|| "unknown".to_string());
170+
171+
let applied_listener: Listener = self
172+
.cluster_resources
173+
.add(self.client, listener)
174+
.await
175+
.context(ApplyListenerSnafu { role: role.clone() })?;
176+
177+
for discovery_cm in build_discovery_configmaps(validated, &role, applied_listener)? {
178+
let applied_cm = self
179+
.cluster_resources
180+
.add(self.client, discovery_cm)
181+
.await
182+
.context(ApplyDiscoveryConfigMapSnafu)?;
183+
if let Some(generation) = applied_cm.metadata.resource_version {
184+
discovery_hash.write(generation.as_bytes());
185+
}
186+
}
187+
}
188+
189+
self.cluster_resources
190+
.delete_orphaned_resources(self.client)
191+
.await
192+
.context(DeleteOrphanedResourcesSnafu)?;
193+
194+
Ok(KubernetesResources {
195+
stateful_sets: applied_stateful_sets,
196+
config_maps: vec![],
197+
services: vec![],
198+
service_accounts: vec![],
199+
role_bindings: vec![],
200+
pod_disruption_budgets: vec![],
201+
listeners: vec![],
202+
discovery_hash: Some(discovery_hash.finish().to_string()),
203+
_status: PhantomData,
204+
})
205+
}
206+
}
207+
208+
fn build_discovery_configmaps(
209+
validated: &ValidatedHiveCluster,
210+
role: &str,
211+
listener: Listener,
212+
) -> Result<Vec<ConfigMap>, Error> {
213+
let name = validated.name.to_string();
214+
215+
let recommended_object_labels = ObjectLabels {
216+
owner: validated,
217+
app_name: APP_NAME,
218+
app_version: &validated.image.app_version_label_value,
219+
operator_name: OPERATOR_NAME,
220+
controller_name: HIVE_CONTROLLER_NAME,
221+
role,
222+
role_group: "discovery",
223+
};
224+
225+
let labels = Labels::recommended(&recommended_object_labels)
226+
.expect("Labels should be created because the cluster name is a valid label value");
227+
228+
let connection_string = build_listener_connection_string(listener, &role.to_string(), None)
229+
.context(ListenerConfigurationSnafu)?;
230+
231+
let mut cm_builder = ConfigMapBuilder::new();
232+
cm_builder.metadata(
233+
ObjectMetaBuilder::new()
234+
.name(&name)
235+
.namespace(&validated.namespace)
236+
.ownerreference_from_resource(validated, None, Some(true))
237+
.context(DiscoveryMetadataBuildSnafu)?
238+
.with_labels(labels)
239+
.build(),
240+
);
241+
cm_builder.add_data("HIVE", connection_string);
242+
243+
let discovery_cm = cm_builder.build().context(BuildDiscoveryConfigMapSnafu)?;
244+
245+
Ok(vec![discovery_cm])
246+
}

0 commit comments

Comments
 (0)