Skip to content

Commit bb4fe09

Browse files
adwk67claude
andcommitted
feat: rewrite controller pipeline with validated config types
Replace the monolithic airflow_controller with a structured pipeline of dereference → validate → build → apply → update_status stages. Each stage operates on validated, type-safe data rather than raw CRD types. Remove airflow_controller.rs and operations/ modules whose logic has been absorbed into the new pipeline. Update product_logging.rs, controller_commons.rs, and service.rs signatures for the new validated types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f2bcb5c commit bb4fe09

22 files changed

Lines changed: 2904 additions & 1758 deletions

rust/operator-binary/src/airflow_controller.rs

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

0 commit comments

Comments
 (0)