Skip to content

Commit 02674ca

Browse files
committed
wip: for 3.x stabilisation changes
1 parent ce469d2 commit 02674ca

11 files changed

Lines changed: 1175 additions & 49 deletions

File tree

deploy/helm/airflow-operator/crds/crds.yaml

Lines changed: 830 additions & 0 deletions
Large diffs are not rendered by default.

rust/operator-binary/src/airflow_controller.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,7 +1168,10 @@ fn build_server_rolegroup_statefulset(
11681168
AirflowRole::Scheduler => {
11691169
"OrderedReady" // Scheduler pods should start after another, since part of their startup phase is initializing the database, see crd/src/lib.rs
11701170
}
1171-
AirflowRole::Webserver | AirflowRole::Worker => "Parallel",
1171+
AirflowRole::Webserver
1172+
| AirflowRole::Worker
1173+
| AirflowRole::Processor
1174+
| AirflowRole::Triggerer => "Parallel",
11721175
}
11731176
.to_string(),
11741177
),

rust/operator-binary/src/crd/mod.rs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ pub mod versioned {
217217

218218
#[serde(flatten)]
219219
pub executor: AirflowExecutor,
220+
221+
/// The `processor` role runs the DAG processor routine for DAG preparation.
222+
#[serde(default, skip_serializing_if = "Option::is_none")]
223+
pub processors: Option<Role<AirflowConfigFragment>>,
224+
225+
/// The `triggerer` role runs the triggerer process for use with deferrable DAG operators.
226+
#[serde(default, skip_serializing_if = "Option::is_none")]
227+
pub triggerers: Option<Role<AirflowConfigFragment>>,
220228
}
221229

222230
#[derive(Clone, Deserialize, Debug, JsonSchema, PartialEq, Serialize)]
@@ -343,7 +351,10 @@ impl v1alpha1::AirflowCluster {
343351
pub fn group_listener_name(&self, role: &AirflowRole) -> Option<String> {
344352
match role {
345353
AirflowRole::Webserver => Some(role_service_name(&self.name_any(), &role.to_string())),
346-
AirflowRole::Scheduler | AirflowRole::Worker => None,
354+
AirflowRole::Scheduler
355+
| AirflowRole::Worker
356+
| AirflowRole::Processor
357+
| AirflowRole::Triggerer => None,
347358
}
348359
}
349360

@@ -357,6 +368,8 @@ impl v1alpha1::AirflowCluster {
357368
.to_owned()
358369
.map(extract_role_from_webserver_config),
359370
AirflowRole::Scheduler => self.spec.schedulers.to_owned(),
371+
AirflowRole::Processor => self.spec.processors.to_owned(),
372+
AirflowRole::Triggerer => self.spec.triggerers.to_owned(),
360373
AirflowRole::Worker => {
361374
if let AirflowExecutor::CeleryExecutor { config } = &self.spec.executor {
362375
Some(config.clone())
@@ -413,6 +426,24 @@ impl v1alpha1::AirflowCluster {
413426
roles: AirflowRole::roles(),
414427
})?
415428
}
429+
AirflowRole::Processor => {
430+
self.spec
431+
.processors
432+
.as_ref()
433+
.context(UnknownAirflowRoleSnafu {
434+
role: role.to_string(),
435+
roles: AirflowRole::roles(),
436+
})?
437+
}
438+
AirflowRole::Triggerer => {
439+
self.spec
440+
.triggerers
441+
.as_ref()
442+
.context(UnknownAirflowRoleSnafu {
443+
role: role.to_string(),
444+
roles: AirflowRole::roles(),
445+
})?
446+
}
416447
};
417448

418449
// Retrieve role resource config
@@ -561,6 +592,12 @@ pub enum AirflowRole {
561592

562593
#[strum(serialize = "worker")]
563594
Worker,
595+
596+
#[strum(serialize = "processor")]
597+
Processor,
598+
599+
#[strum(serialize = "triggerer")]
600+
Triggerer,
564601
}
565602

566603
impl AirflowRole {
@@ -622,10 +659,27 @@ impl AirflowRole {
622659
command.extend(vec![
623660
"prepare_signal_handlers".to_string(),
624661
container_debug_command(),
625-
"airflow dag-processor &".to_string(),
626662
"airflow scheduler &".to_string(),
627663
]);
664+
if airflow.spec.processors.is_none() {
665+
// If no processors role has been specified, the
666+
// process needs to be included with the scheduler
667+
// (with 3.x there is no longer the possibility of
668+
// starting it as a subprocess, so it has to be
669+
// explicitly started *somewhere*)
670+
command.extend(vec!["airflow dag-processor &".to_string()]);
671+
}
628672
}
673+
AirflowRole::Processor => command.extend(vec![
674+
"prepare_signal_handlers".to_string(),
675+
container_debug_command(),
676+
"airflow dag-processor &".to_string(),
677+
]),
678+
AirflowRole::Triggerer => command.extend(vec![
679+
"prepare_signal_handlers".to_string(),
680+
container_debug_command(),
681+
"airflow triggerer &".to_string(),
682+
]),
629683
AirflowRole::Worker => command.extend(vec![
630684
"prepare_signal_handlers".to_string(),
631685
container_debug_command(),
@@ -668,6 +722,16 @@ impl AirflowRole {
668722
"airflow scheduler &".to_string(),
669723
]);
670724
}
725+
AirflowRole::Processor => command.extend(vec![
726+
"prepare_signal_handlers".to_string(),
727+
container_debug_command(),
728+
"airflow dag-processor &".to_string(),
729+
]),
730+
AirflowRole::Triggerer => command.extend(vec![
731+
"prepare_signal_handlers".to_string(),
732+
container_debug_command(),
733+
"airflow triggerer &".to_string(),
734+
]),
671735
AirflowRole::Worker => command.extend(vec![
672736
"prepare_signal_handlers".to_string(),
673737
container_debug_command(),
@@ -724,6 +788,8 @@ impl AirflowRole {
724788
AirflowRole::Webserver => Some(HTTP_PORT),
725789
AirflowRole::Scheduler => None,
726790
AirflowRole::Worker => None,
791+
AirflowRole::Processor => None,
792+
AirflowRole::Triggerer => None,
727793
}
728794
}
729795

@@ -742,7 +808,7 @@ impl AirflowRole {
742808
.webservers
743809
.to_owned()
744810
.map(|webserver| webserver.role_config.listener_class),
745-
Self::Worker | Self::Scheduler => None,
811+
Self::Worker | Self::Scheduler | Self::Processor | Self::Triggerer => None,
746812
}
747813
}
748814
}
@@ -878,9 +944,10 @@ impl AirflowConfig {
878944
logging: product_logging::spec::default_logging(),
879945
affinity: get_affinity(cluster_name, role),
880946
graceful_shutdown_timeout: Some(match role {
881-
AirflowRole::Webserver | AirflowRole::Scheduler => {
882-
DEFAULT_AIRFLOW_GRACEFUL_SHUTDOWN_TIMEOUT
883-
}
947+
AirflowRole::Webserver
948+
| AirflowRole::Scheduler
949+
| AirflowRole::Processor
950+
| AirflowRole::Triggerer => DEFAULT_AIRFLOW_GRACEFUL_SHUTDOWN_TIMEOUT,
884951
AirflowRole::Worker => DEFAULT_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT,
885952
}),
886953
}
@@ -953,6 +1020,26 @@ fn default_resources(role: &AirflowRole) -> ResourcesFragment<AirflowStorageConf
9531020
runtime_limits: NoRuntimeLimitsFragment {},
9541021
},
9551022
),
1023+
AirflowRole::Processor => (
1024+
CpuLimitsFragment {
1025+
min: Some(Quantity("1".to_owned())),
1026+
max: Some(Quantity("2".to_owned())),
1027+
},
1028+
MemoryLimitsFragment {
1029+
limit: Some(Quantity("1Gi".to_owned())),
1030+
runtime_limits: NoRuntimeLimitsFragment {},
1031+
},
1032+
),
1033+
AirflowRole::Triggerer => (
1034+
CpuLimitsFragment {
1035+
min: Some(Quantity("1".to_owned())),
1036+
max: Some(Quantity("2".to_owned())),
1037+
},
1038+
MemoryLimitsFragment {
1039+
limit: Some(Quantity("1Gi".to_owned())),
1040+
runtime_limits: NoRuntimeLimitsFragment {},
1041+
},
1042+
),
9561043
};
9571044

9581045
ResourcesFragment {

rust/operator-binary/src/env_vars.rs

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use stackable_operator::{
1515

1616
use crate::{
1717
crd::{
18-
AirflowConfig, AirflowExecutor, AirflowRole, ExecutorConfig, LOG_CONFIG_DIR,
19-
STACKABLE_LOG_DIR, TEMPLATE_LOCATION, TEMPLATE_NAME,
18+
AirflowExecutor, AirflowRole, ExecutorConfig, LOG_CONFIG_DIR, STACKABLE_LOG_DIR,
19+
TEMPLATE_LOCATION, TEMPLATE_NAME,
2020
authentication::{
2121
AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetailsResolved,
2222
},
@@ -61,7 +61,7 @@ const PYTHONPATH: &str = "PYTHONPATH";
6161
/// This key is only intended for use during experimental support and will
6262
/// be replaced with a secret at a later stage. See the issue covering
6363
/// this at <https://github.com/stackabletech/airflow-operator/issues/639>.
64-
const JWT_KEY: &str = "ThisKeyIsNotIntendedForProduction!";
64+
//const JWT_KEY: &str = "ThisKeyIsNotIntendedForProduction!";
6565

6666
#[derive(Snafu, Debug)]
6767
pub enum Error {
@@ -86,57 +86,53 @@ pub fn build_airflow_statefulset_envs(
8686
resolved_product_image: &ResolvedProductImage,
8787
) -> Result<Vec<EnvVar>, Error> {
8888
let mut env: BTreeMap<String, EnvVar> = BTreeMap::new();
89+
let secret = airflow.spec.cluster_config.credentials_secret.as_str();
8990

9091
env.extend(static_envs(git_sync_resources));
9192

92-
add_version_specific_env_vars(airflow, airflow_role, resolved_product_image, &mut env);
93-
9493
// environment variables
9594
let env_vars = rolegroup_config.get(&PropertyNameKind::Env);
9695

97-
let secret_prop =
98-
env_vars.and_then(|vars| vars.get(AirflowConfig::CREDENTIALS_SECRET_PROPERTY));
96+
add_version_specific_env_vars(airflow, airflow_role, resolved_product_image, &mut env);
9997

100-
if let Some(secret) = secret_prop {
98+
env.insert(
99+
AIRFLOW_WEBSERVER_SECRET_KEY.into(),
100+
// The secret key is used to run the webserver flask app and also used to authorize
101+
// requests to Celery workers when logs are retrieved.
102+
env_var_from_secret(
103+
AIRFLOW_WEBSERVER_SECRET_KEY,
104+
secret,
105+
"connections.secretKey",
106+
),
107+
);
108+
env.insert(
109+
AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(),
110+
env_var_from_secret(
111+
AIRFLOW_DATABASE_SQL_ALCHEMY_CONN,
112+
secret,
113+
"connections.sqlalchemyDatabaseUri",
114+
),
115+
);
116+
117+
// Redis is only needed when celery executors are used
118+
// see https://github.com/stackabletech/airflow-operator/issues/424 for details
119+
if matches!(executor, AirflowExecutor::CeleryExecutor { .. }) {
101120
env.insert(
102-
AIRFLOW_WEBSERVER_SECRET_KEY.into(),
103-
// The secret key is used to run the webserver flask app and also used to authorize
104-
// requests to Celery workers when logs are retrieved.
121+
AIRFLOW_CELERY_RESULT_BACKEND.into(),
105122
env_var_from_secret(
106-
AIRFLOW_WEBSERVER_SECRET_KEY,
123+
AIRFLOW_CELERY_RESULT_BACKEND,
107124
secret,
108-
"connections.secretKey",
125+
"connections.celeryResultBackend",
109126
),
110127
);
111128
env.insert(
112-
AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(),
129+
AIRFLOW_CELERY_BROKER_URL.into(),
113130
env_var_from_secret(
114-
AIRFLOW_DATABASE_SQL_ALCHEMY_CONN,
131+
AIRFLOW_CELERY_BROKER_URL,
115132
secret,
116-
"connections.sqlalchemyDatabaseUri",
133+
"connections.celeryBrokerUrl",
117134
),
118135
);
119-
120-
// Redis is only needed when celery executors are used
121-
// see https://github.com/stackabletech/airflow-operator/issues/424 for details
122-
if matches!(executor, AirflowExecutor::CeleryExecutor { .. }) {
123-
env.insert(
124-
AIRFLOW_CELERY_RESULT_BACKEND.into(),
125-
env_var_from_secret(
126-
AIRFLOW_CELERY_RESULT_BACKEND,
127-
secret,
128-
"connections.celeryResultBackend",
129-
),
130-
);
131-
env.insert(
132-
AIRFLOW_CELERY_BROKER_URL.into(),
133-
env_var_from_secret(
134-
AIRFLOW_CELERY_BROKER_URL,
135-
secret,
136-
"connections.celeryBrokerUrl",
137-
),
138-
);
139-
}
140136
}
141137

142138
let dags_folder = get_dags_folder(git_sync_resources);
@@ -454,6 +450,8 @@ fn add_version_specific_env_vars(
454450
resolved_product_image: &ResolvedProductImage,
455451
env: &mut BTreeMap<String, EnvVar>,
456452
) {
453+
let secret = airflow.spec.cluster_config.credentials_secret.as_str();
454+
457455
if resolved_product_image.product_version.starts_with("3.") {
458456
env.extend(execution_server_env_vars(airflow));
459457
env.insert(
@@ -483,13 +481,17 @@ fn add_version_specific_env_vars(
483481
// See issue <https://github.com/stackabletech/airflow-operator/issues/639>:
484482
// later it will be accessed from a secret to avoid cluster restarts
485483
// being triggered by an operator restart.
484+
// env.insert(
485+
// "AIRFLOW__API_AUTH__JWT_SECRET".into(),
486+
// EnvVar {
487+
// name: "AIRFLOW__API_AUTH__JWT_SECRET".into(),
488+
// value: Some(JWT_KEY.into()),
489+
// ..Default::default()
490+
// },
491+
// );
486492
env.insert(
487493
"AIRFLOW__API_AUTH__JWT_SECRET".into(),
488-
EnvVar {
489-
name: "AIRFLOW__API_AUTH__JWT_SECRET".into(),
490-
value: Some(JWT_KEY.into()),
491-
..Default::default()
492-
},
494+
env_var_from_secret("AIRFLOW__API_AUTH__JWT_SECRET", secret, "jwt.key"),
493495
);
494496
if airflow_role == &AirflowRole::Webserver {
495497
// Sometimes a race condition can arise when both scheduler and
@@ -527,6 +529,19 @@ fn add_version_specific_env_vars(
527529
..Default::default()
528530
},
529531
);
532+
if airflow.spec.processors.is_some() {
533+
// In airflow 2.x the dag-processor can optionally be started as a
534+
// standalone process (rather then as a scheduler subprocess),
535+
// governed by this env-var
536+
env.insert(
537+
"AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR".into(),
538+
EnvVar {
539+
name: "AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR".into(),
540+
value: Some("True".into()),
541+
..Default::default()
542+
},
543+
);
544+
}
530545
}
531546
}
532547

rust/operator-binary/src/operations/pdb.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub async fn add_pdbs(
3737
let max_unavailable = pdb.max_unavailable.unwrap_or(match role {
3838
AirflowRole::Scheduler => max_unavailable_schedulers(),
3939
AirflowRole::Webserver => max_unavailable_webservers(),
40+
AirflowRole::Processor => max_unavailable_processors(),
41+
AirflowRole::Triggerer => max_unavailable_triggerers(),
4042
AirflowRole::Worker => match airflow.spec.executor {
4143
AirflowExecutor::CeleryExecutor { .. } => max_unavailable_workers(),
4244
AirflowExecutor::KubernetesExecutor { .. } => {
@@ -77,3 +79,11 @@ fn max_unavailable_workers() -> u16 {
7779
fn max_unavailable_webservers() -> u16 {
7880
1
7981
}
82+
83+
fn max_unavailable_processors() -> u16 {
84+
1
85+
}
86+
87+
fn max_unavailable_triggerers() -> u16 {
88+
1
89+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
apiVersion: v1
3+
kind: Secret
4+
metadata:
5+
name: minio-credentials
6+
labels:
7+
secrets.stackable.tech/class: spark-pi-private-s3-credentials-class
8+
timeout: 240
9+
stringData:
10+
accessKey: minioAccessKey
11+
secretKey: minioSecretKey
12+
# The following two entries are used by the Bitnami chart for MinIO to
13+
# set up credentials for accessing buckets managed by the MinIO tenant.
14+
root-user: minioAccessKey
15+
root-password: minioSecretKey
16+
---
17+
apiVersion: secrets.stackable.tech/v1alpha1
18+
kind: SecretClass
19+
metadata:
20+
name: spark-pi-private-s3-credentials-class
21+
spec:
22+
backend:
23+
k8sSearch:
24+
searchNamespace:
25+
pod: {}

0 commit comments

Comments
 (0)