Skip to content

Commit 8b97621

Browse files
adwk67claude
andcommitted
refactor: extract controller into submodules
Split the monolithic airflow_controller.rs into controller/ with submodules: dereference, validate, build (+ build/role_group_builder), apply, and update_status. This is a purely structural refactor — no logic changes. Each pipeline stage now lives in its own file for easier navigation and review. Rename airflow_controller module to controller in main.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0caca38 commit 8b97621

9 files changed

Lines changed: 2885 additions & 2775 deletions

File tree

rust/operator-binary/src/airflow_controller.rs

Lines changed: 0 additions & 2770 deletions
This file was deleted.
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
//! Ensures that `Pod`s are configured and running for each [`v1alpha2::AirflowCluster`]
2+
//!
3+
//! Pipeline architecture: dereference -> validate -> build -> apply -> update_status
4+
5+
pub mod apply;
6+
pub mod build;
7+
pub mod dereference;
8+
pub mod update_status;
9+
pub mod validate;
10+
11+
// ---------------------------------------------------------------------------
12+
// Imports
13+
// ---------------------------------------------------------------------------
14+
15+
use std::{
16+
collections::BTreeMap,
17+
marker::PhantomData,
18+
sync::Arc,
19+
};
20+
21+
use const_format::concatcp;
22+
use product_config::ProductConfigManager;
23+
use snafu::{ResultExt, Snafu};
24+
use stackable_operator::{
25+
cli::OperatorEnvironmentOptions,
26+
cluster_resources::{ClusterResourceApplyStrategy, ClusterResources},
27+
commons::{
28+
affinity::StackableAffinity,
29+
product_image_selection::ResolvedProductImage,
30+
resources::{NoRuntimeLimits, Resources},
31+
},
32+
crd::listener,
33+
k8s_openapi::api::{
34+
apps::v1::StatefulSet,
35+
core::v1::{
36+
ConfigMap, Container as K8sContainer, EnvVar, PersistentVolumeClaim, PodTemplateSpec,
37+
Service, ServiceAccount, Volume, VolumeMount,
38+
},
39+
policy::v1::PodDisruptionBudget,
40+
rbac::v1::RoleBinding,
41+
},
42+
kube::{
43+
Resource,
44+
api::ObjectMeta,
45+
core::{DeserializeGuard, error_boundary},
46+
runtime::{controller::Action, reflector::ObjectRef},
47+
},
48+
logging::controller::ReconcilerError,
49+
product_logging::spec::ContainerLogConfig,
50+
role_utils::RoleGroupRef,
51+
shared::time::Duration,
52+
};
53+
use strum::{EnumDiscriminants, IntoStaticStr};
54+
55+
use crate::{
56+
crd::{
57+
APP_NAME, AirflowExecutor, AirflowRole, AirflowStorageConfig, OPERATOR_NAME,
58+
v1alpha2,
59+
},
60+
framework::{
61+
HasName, HasUid, NameIsValidLabelValue,
62+
product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig},
63+
types::{
64+
kubernetes::{NamespaceName, Uid},
65+
operator::ClusterName,
66+
},
67+
},
68+
};
69+
70+
use self::{
71+
apply::Applier,
72+
build::build,
73+
dereference::dereference,
74+
update_status::update_status,
75+
validate::validate_cluster,
76+
};
77+
78+
// ---------------------------------------------------------------------------
79+
// Constants and context
80+
// ---------------------------------------------------------------------------
81+
82+
pub const AIRFLOW_CONTROLLER_NAME: &str = "airflowcluster";
83+
pub const CONTAINER_IMAGE_BASE_NAME: &str = "airflow";
84+
pub const AIRFLOW_FULL_CONTROLLER_NAME: &str =
85+
concatcp!(AIRFLOW_CONTROLLER_NAME, '.', OPERATOR_NAME);
86+
87+
pub struct Ctx {
88+
pub client: stackable_operator::client::Client,
89+
pub product_config: ProductConfigManager,
90+
pub operator_environment: OperatorEnvironmentOptions,
91+
}
92+
93+
// ---------------------------------------------------------------------------
94+
// Validated types
95+
// ---------------------------------------------------------------------------
96+
97+
pub(crate) struct Prepared;
98+
pub(crate) struct Applied;
99+
100+
pub(crate) struct KubernetesResources<T> {
101+
pub stateful_sets: Vec<StatefulSet>,
102+
pub config_maps: Vec<ConfigMap>,
103+
pub services: Vec<Service>,
104+
pub service_accounts: Vec<ServiceAccount>,
105+
pub role_bindings: Vec<RoleBinding>,
106+
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
107+
pub listeners: Vec<listener::v1alpha1::Listener>,
108+
pub _status: PhantomData<T>,
109+
}
110+
111+
#[derive(Clone, Debug)]
112+
pub struct ValidatedRoleConfig {
113+
pub pdb_enabled: bool,
114+
pub pdb_max_unavailable: Option<u16>,
115+
pub listener_class: Option<String>,
116+
pub group_listener_name: Option<String>,
117+
}
118+
119+
#[derive(Clone, Debug)]
120+
pub struct ValidatedRoleGroupConfig {
121+
pub resources: Resources<AirflowStorageConfig, NoRuntimeLimits>,
122+
pub logging: ValidatedLogging,
123+
pub affinity: StackableAffinity,
124+
pub graceful_shutdown_timeout: Duration,
125+
pub config_file_content: String,
126+
}
127+
128+
#[derive(Clone)]
129+
pub struct PrecomputedPodData {
130+
pub env_vars: Vec<EnvVar>,
131+
pub airflow_commands: Vec<String>,
132+
pub auth_volumes: Vec<Volume>,
133+
pub auth_volume_mounts: Vec<VolumeMount>,
134+
pub extra_volumes: Vec<Volume>,
135+
pub extra_volume_mounts: Vec<VolumeMount>,
136+
pub git_sync_containers: Vec<K8sContainer>,
137+
pub git_sync_init_containers: Vec<K8sContainer>,
138+
pub git_sync_volumes: Vec<Volume>,
139+
pub git_sync_volume_mounts: Vec<VolumeMount>,
140+
pub vector_container: Option<K8sContainer>,
141+
pub service_account_name: String,
142+
pub replicas: Option<u16>,
143+
pub pod_overrides: PodTemplateSpec,
144+
pub executor: AirflowExecutor,
145+
pub executor_template_configmap_name: Option<String>,
146+
pub listener_volume_claim_template: Option<PersistentVolumeClaim>,
147+
}
148+
149+
#[derive(Clone, Debug)]
150+
pub struct ValidatedLogging {
151+
pub airflow_container: ValidatedContainerLogConfigChoice,
152+
pub vector_container: Option<VectorContainerLogConfig>,
153+
pub git_sync_container_log_config: ContainerLogConfig,
154+
}
155+
156+
impl ValidatedLogging {
157+
pub fn is_vector_agent_enabled(&self) -> bool {
158+
self.vector_container.is_some()
159+
}
160+
}
161+
162+
// REVIEW: ValidatedAirflowCluster is the central validated type. It holds all data needed
163+
// by the build stage so that build() can be infallible. All optional-after-merge fields
164+
// are unwrapped during validation, and logging is pre-validated into ValidatedLogging.
165+
#[derive(Clone)]
166+
pub struct ValidatedAirflowCluster {
167+
metadata: ObjectMeta,
168+
pub image: ResolvedProductImage,
169+
pub name: ClusterName,
170+
pub namespace: NamespaceName,
171+
pub uid: Uid,
172+
pub role_groups: BTreeMap<AirflowRole, BTreeMap<String, ValidatedRoleGroupConfig>>,
173+
pub precomputed_pod_data: BTreeMap<AirflowRole, BTreeMap<String, PrecomputedPodData>>,
174+
pub executor_template_config_maps: Vec<ConfigMap>,
175+
pub role_configs: BTreeMap<AirflowRole, ValidatedRoleConfig>,
176+
pub executor: AirflowExecutor,
177+
}
178+
179+
impl ValidatedAirflowCluster {
180+
#[allow(clippy::too_many_arguments)]
181+
pub fn new(
182+
image: ResolvedProductImage,
183+
name: ClusterName,
184+
namespace: NamespaceName,
185+
uid: Uid,
186+
role_groups: BTreeMap<AirflowRole, BTreeMap<String, ValidatedRoleGroupConfig>>,
187+
precomputed_pod_data: BTreeMap<AirflowRole, BTreeMap<String, PrecomputedPodData>>,
188+
executor_template_config_maps: Vec<ConfigMap>,
189+
role_configs: BTreeMap<AirflowRole, ValidatedRoleConfig>,
190+
executor: AirflowExecutor,
191+
) -> Self {
192+
Self {
193+
metadata: ObjectMeta {
194+
name: Some(name.to_string()),
195+
namespace: Some(namespace.to_string()),
196+
uid: Some(uid.to_string()),
197+
..ObjectMeta::default()
198+
},
199+
image,
200+
name,
201+
namespace,
202+
uid,
203+
role_groups,
204+
precomputed_pod_data,
205+
executor_template_config_maps,
206+
role_configs,
207+
executor,
208+
}
209+
}
210+
211+
pub fn rolegroup_ref(&self, role: &AirflowRole, role_group: &str) -> RoleGroupRef<Self> {
212+
RoleGroupRef {
213+
cluster: ObjectRef::from_obj(self),
214+
role: role.to_string(),
215+
role_group: role_group.to_string(),
216+
}
217+
}
218+
}
219+
220+
impl HasName for ValidatedAirflowCluster {
221+
fn to_name(&self) -> String {
222+
self.name.to_string()
223+
}
224+
}
225+
226+
impl HasUid for ValidatedAirflowCluster {
227+
fn to_uid(&self) -> Uid {
228+
self.uid.clone()
229+
}
230+
}
231+
232+
impl Resource for ValidatedAirflowCluster {
233+
type DynamicType =
234+
<v1alpha2::AirflowCluster as stackable_operator::kube::Resource>::DynamicType;
235+
type Scope = <v1alpha2::AirflowCluster as stackable_operator::kube::Resource>::Scope;
236+
237+
fn kind(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> {
238+
v1alpha2::AirflowCluster::kind(dt)
239+
}
240+
241+
fn group(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> {
242+
v1alpha2::AirflowCluster::group(dt)
243+
}
244+
245+
fn version(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> {
246+
v1alpha2::AirflowCluster::version(dt)
247+
}
248+
249+
fn plural(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> {
250+
v1alpha2::AirflowCluster::plural(dt)
251+
}
252+
253+
fn meta(&self) -> &ObjectMeta {
254+
&self.metadata
255+
}
256+
257+
fn meta_mut(&mut self) -> &mut ObjectMeta {
258+
&mut self.metadata
259+
}
260+
}
261+
262+
impl NameIsValidLabelValue for ValidatedAirflowCluster {
263+
fn to_label_value(&self) -> String {
264+
self.name.to_label_value()
265+
}
266+
}
267+
268+
// ---------------------------------------------------------------------------
269+
// Error types and reconcile
270+
// ---------------------------------------------------------------------------
271+
272+
#[derive(Snafu, Debug, EnumDiscriminants)]
273+
#[strum_discriminants(derive(IntoStaticStr))]
274+
pub enum Error {
275+
#[snafu(display("AirflowCluster object is invalid"))]
276+
InvalidAirflowCluster {
277+
source: error_boundary::InvalidObject,
278+
},
279+
280+
#[snafu(display("failed to dereference resources"))]
281+
Dereference { source: dereference::Error },
282+
283+
#[snafu(display("failed to validate cluster"))]
284+
Validate { source: validate::Error },
285+
286+
#[snafu(display("failed to create cluster resources"))]
287+
CreateClusterResources {
288+
source: stackable_operator::cluster_resources::Error,
289+
},
290+
291+
#[snafu(display("failed to apply resources"))]
292+
Apply { source: apply::Error },
293+
294+
#[snafu(display("failed to update status"))]
295+
UpdateStatus { source: update_status::Error },
296+
}
297+
298+
type Result<T, E = Error> = std::result::Result<T, E>;
299+
300+
impl ReconcilerError for Error {
301+
fn category(&self) -> &'static str {
302+
ErrorDiscriminants::from(self).into()
303+
}
304+
}
305+
306+
// REVIEW: The reconcile pipeline is structured as five sequential stages:
307+
// 1. dereference — async, fallible: resolve external references
308+
// 2. validate — sync, fallible: validate and merge configs
309+
// 3. build — sync, infallible: construct Kubernetes resources
310+
// 4. apply — async, fallible: apply resources to the cluster
311+
// 5. update_status — async, fallible: patch status on the CRD
312+
pub async fn reconcile(
313+
airflow: Arc<DeserializeGuard<v1alpha2::AirflowCluster>>,
314+
ctx: Arc<Ctx>,
315+
) -> Result<Action> {
316+
tracing::info!("Starting reconcile");
317+
318+
let airflow = airflow
319+
.0
320+
.as_ref()
321+
.map_err(error_boundary::InvalidObject::clone)
322+
.context(InvalidAirflowClusterSnafu)?;
323+
324+
// --- dereference (async, fallible) ---
325+
let dereferenced = dereference(
326+
&ctx.client,
327+
airflow,
328+
CONTAINER_IMAGE_BASE_NAME,
329+
&ctx.operator_environment.image_repository,
330+
crate::built_info::PKG_VERSION,
331+
)
332+
.await
333+
.context(DereferenceSnafu)?;
334+
335+
// --- validate (sync, fallible) ---
336+
let validated =
337+
validate_cluster(airflow, &dereferenced, &ctx.product_config).context(ValidateSnafu)?;
338+
339+
// REVIEW: build() is infallible — all validation and fallible operations (config
340+
// generation, PodBuilder/ContainerBuilder usage, logging validation) happen in the
341+
// validate stage. The build stage purely assembles Kubernetes resource structs.
342+
// --- build (sync, infallible) ---
343+
let prepared = build(&validated);
344+
345+
// --- apply (async, fallible) ---
346+
let cluster_resources = ClusterResources::new(
347+
APP_NAME,
348+
OPERATOR_NAME,
349+
AIRFLOW_CONTROLLER_NAME,
350+
&airflow.object_ref(&()),
351+
ClusterResourceApplyStrategy::from(&airflow.spec.cluster_operation),
352+
&airflow.spec.object_overrides,
353+
)
354+
.context(CreateClusterResourcesSnafu)?;
355+
356+
let applied = Applier::new(&ctx.client, cluster_resources)
357+
.apply(prepared)
358+
.await
359+
.context(ApplySnafu)?;
360+
361+
// --- update status (async, fallible) ---
362+
update_status(&ctx.client, airflow, applied)
363+
.await
364+
.context(UpdateStatusSnafu)?;
365+
366+
Ok(Action::await_change())
367+
}
368+
369+
pub fn error_policy(
370+
_obj: Arc<DeserializeGuard<v1alpha2::AirflowCluster>>,
371+
error: &Error,
372+
_ctx: Arc<Ctx>,
373+
) -> Action {
374+
match error {
375+
Error::InvalidAirflowCluster { .. } => Action::await_change(),
376+
_ => Action::requeue(*Duration::from_secs(10)),
377+
}
378+
}

0 commit comments

Comments
 (0)