Skip to content

Commit a1a62e6

Browse files
committed
chore: Listener class as webserver role level only
1 parent 8a6e886 commit a1a62e6

28 files changed

Lines changed: 152 additions & 205 deletions

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,10 +1332,6 @@ spec:
13321332
description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details.
13331333
nullable: true
13341334
type: string
1335-
listenerClass:
1336-
description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose the webserver.
1337-
nullable: true
1338-
type: string
13391335
logging:
13401336
default:
13411337
containers: {}
@@ -1480,11 +1476,16 @@ spec:
14801476
x-kubernetes-preserve-unknown-fields: true
14811477
roleConfig:
14821478
default:
1479+
listenerClass: cluster-internal
14831480
podDisruptionBudget:
14841481
enabled: true
14851482
maxUnavailable: null
14861483
description: This is a product-agnostic RoleConfig, which is sufficient for most of the products.
14871484
properties:
1485+
listenerClass:
1486+
default: cluster-internal
1487+
description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose the webserver.
1488+
type: string
14881489
podDisruptionBudget:
14891490
default:
14901491
enabled: true
@@ -1553,10 +1554,6 @@ spec:
15531554
description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details.
15541555
nullable: true
15551556
type: string
1556-
listenerClass:
1557-
description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose the webserver.
1558-
nullable: true
1559-
type: string
15601557
logging:
15611558
default:
15621559
containers: {}

docs/modules/airflow/pages/usage-guide/listenerclass.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ The listener defaults to only being accessible from within the Kubernetes cluste
88
----
99
spec:
1010
webservers:
11-
config:
11+
roleConfig:
1212
listenerClass: external-unstable # <1>
13+
config:
14+
...
1315
schedulers:
1416
...
1517
celeryExecutors:
1618
...
1719
----
18-
<1> Specify a ListenerClass, such as `external-stable`, `external-unstable`, or `cluster-internal` (the default setting is `cluster-internal`).
20+
<1> Specify a ListenerClass, such as `external-stable`, `external-unstable`, or `cluster-internal` (the default setting is `cluster-internal`) at role-level.
1921
This can be set only for the webservers role.

rust/operator-binary/src/airflow_controller.rs

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ use stackable_operator::{
5353
core::{DeserializeGuard, error_boundary},
5454
runtime::{controller::Action, reflector::ObjectRef},
5555
},
56-
kvp::{Annotation, Label, LabelError, Labels},
56+
kvp::{Annotation, Label, LabelError, Labels, ObjectLabels},
5757
logging::controller::ReconcilerError,
5858
product_config_utils::{
5959
CONFIG_OVERRIDE_FILE_FOOTER_KEY, CONFIG_OVERRIDE_FILE_HEADER_KEY, env_vars_from,
@@ -512,26 +512,6 @@ pub async fn reconcile_airflow(
512512
&git_sync_resources,
513513
)?;
514514

515-
if let Some(listener_class) =
516-
airflow.merged_listener_class(&airflow_role, &rolegroup.role_group)
517-
{
518-
if let Some(listener_group_name) =
519-
airflow.group_listener_name(&airflow_role, &rolegroup)
520-
{
521-
let rg_group_listener = build_group_listener(
522-
airflow,
523-
&resolved_product_image,
524-
&rolegroup,
525-
listener_class.to_string(),
526-
listener_group_name,
527-
)?;
528-
cluster_resources
529-
.add(client, rg_group_listener)
530-
.await
531-
.context(ApplyGroupListenerSnafu)?;
532-
}
533-
}
534-
535515
ss_cond_builder.add(
536516
cluster_resources
537517
.add(client, rg_statefulset)
@@ -568,6 +548,27 @@ pub async fn reconcile_airflow(
568548
.await
569549
.context(FailedToCreatePdbSnafu)?;
570550
}
551+
552+
if let Some(listener_class) = airflow_role.listener_class_name(airflow) {
553+
if let Some(listener_group_name) = airflow.group_listener_name(&airflow_role) {
554+
let rg_group_listener = build_group_listener(
555+
airflow,
556+
build_recommended_labels(
557+
airflow,
558+
AIRFLOW_CONTROLLER_NAME,
559+
&resolved_product_image.app_version_label,
560+
role_name,
561+
"none",
562+
),
563+
listener_class.to_string(),
564+
listener_group_name,
565+
)?;
566+
cluster_resources
567+
.add(client, rg_group_listener)
568+
.await
569+
.context(ApplyGroupListenerSnafu)?;
570+
}
571+
}
571572
}
572573

573574
cluster_resources
@@ -840,8 +841,7 @@ fn build_rolegroup_metadata(
840841

841842
pub fn build_group_listener(
842843
airflow: &v1alpha1::AirflowCluster,
843-
resolved_product_image: &ResolvedProductImage,
844-
rolegroup: &RoleGroupRef<v1alpha1::AirflowCluster>,
844+
object_labels: ObjectLabels<v1alpha1::AirflowCluster>,
845845
listener_class: String,
846846
listener_group_name: String,
847847
) -> Result<listener::v1alpha1::Listener> {
@@ -851,13 +851,7 @@ pub fn build_group_listener(
851851
.name(listener_group_name)
852852
.ownerreference_from_resource(airflow, None, Some(true))
853853
.context(ObjectMissingMetadataForOwnerRefSnafu)?
854-
.with_recommended_labels(build_recommended_labels(
855-
airflow,
856-
AIRFLOW_CONTROLLER_NAME,
857-
&resolved_product_image.app_version_label,
858-
&rolegroup.role,
859-
&rolegroup.role_group,
860-
))
854+
.with_recommended_labels(object_labels)
861855
.context(ObjectMetaSnafu)?
862856
.build(),
863857
spec: listener::v1alpha1::ListenerSpec {
@@ -916,7 +910,7 @@ fn build_server_rolegroup_statefulset(
916910
// A version value is required, and we do want to use the "recommended" format for the other desired labels
917911
"none",
918912
&rolegroup_ref.role,
919-
&rolegroup_ref.role_group,
913+
"none",
920914
))
921915
.context(LabelBuildSnafu)?;
922916

@@ -1019,7 +1013,7 @@ fn build_server_rolegroup_statefulset(
10191013

10201014
let mut pvcs: Option<Vec<PersistentVolumeClaim>> = None;
10211015

1022-
if let Some(listener_group_name) = airflow.group_listener_name(airflow_role, rolegroup_ref) {
1016+
if let Some(listener_group_name) = airflow.group_listener_name(airflow_role) {
10231017
// Listener endpoints for the Webserver role will use persistent volumes
10241018
// so that load balancers can hard-code the target addresses. This will
10251019
// be the case even when no class is set (and the value defaults to

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

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::crd::{
5151
AirflowAuthenticationClassResolved, AirflowClientAuthenticationDetails,
5252
AirflowClientAuthenticationDetailsResolved,
5353
},
54+
v1alpha1::WebserverRoleConfig,
5455
};
5556

5657
pub mod affinity;
@@ -163,6 +164,8 @@ impl FlaskAppConfigOptions for AirflowConfigOptions {
163164

164165
#[versioned(version(name = "v1alpha1"))]
165166
pub mod versioned {
167+
use crate::crd::v1alpha1::WebserverRoleConfig;
168+
166169
/// An Airflow cluster stacklet. This resource is managed by the Stackable operator for Apache Airflow.
167170
/// Find more information on how to use it and the resources that the operator generates in the
168171
/// [operator documentation](DOCS_BASE_URL_PLACEHOLDER/airflow/).
@@ -198,7 +201,7 @@ pub mod versioned {
198201

199202
/// The `webserver` role provides the main UI for user interaction.
200203
#[serde(default, skip_serializing_if = "Option::is_none")]
201-
pub webservers: Option<Role<WebserverConfigFragment>>,
204+
pub webservers: Option<Role<AirflowConfigFragment, WebserverRoleConfig>>,
202205

203206
/// The `scheduler` is responsible for triggering jobs and persisting their metadata to the backend database.
204207
/// Jobs are scheduled on the workers/executors.
@@ -258,6 +261,31 @@ pub mod versioned {
258261
#[schemars(schema_with = "raw_object_list_schema")]
259262
pub volume_mounts: Vec<VolumeMount>,
260263
}
264+
265+
// TODO: move generic version to op-rs?
266+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
267+
#[serde(rename_all = "camelCase")]
268+
pub struct WebserverRoleConfig {
269+
#[serde(flatten)]
270+
pub common: GenericRoleConfig,
271+
272+
/// This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose the webserver.
273+
#[serde(default = "webserver_default_listener_class")]
274+
pub listener_class: String,
275+
}
276+
}
277+
278+
impl Default for v1alpha1::WebserverRoleConfig {
279+
fn default() -> Self {
280+
v1alpha1::WebserverRoleConfig {
281+
listener_class: webserver_default_listener_class(),
282+
common: Default::default(),
283+
}
284+
}
285+
}
286+
287+
fn webserver_default_listener_class() -> String {
288+
"cluster-internal".to_string()
261289
}
262290

263291
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Eq, Serialize)]
@@ -277,16 +305,15 @@ impl HasStatusCondition for v1alpha1::AirflowCluster {
277305
}
278306

279307
impl v1alpha1::AirflowCluster {
280-
/// The name of the group-listener provided for a specific role-group.
308+
/// The name of the group-listener provided for a specific role.
281309
/// Webservers will use this group listener so that only one load balancer
282-
/// is needed (per role group).
283-
pub fn group_listener_name(
284-
&self,
285-
role: &AirflowRole,
286-
rolegroup: &RoleGroupRef<Self>,
287-
) -> Option<String> {
310+
/// is needed for that role.
311+
pub fn group_listener_name(&self, role: &AirflowRole) -> Option<String> {
288312
match role {
289-
AirflowRole::Webserver => Some(rolegroup.object_name()),
313+
AirflowRole::Webserver => Some(format!(
314+
"{cluster_name}-{role}",
315+
cluster_name = self.name_any()
316+
)),
290317
AirflowRole::Scheduler | AirflowRole::Worker => None,
291318
}
292319
}
@@ -381,34 +408,6 @@ impl v1alpha1::AirflowCluster {
381408
fragment::validate(conf_rolegroup).context(FragmentValidationFailureSnafu)
382409
}
383410

384-
pub fn merged_listener_class(
385-
&self,
386-
role: &AirflowRole,
387-
rolegroup_name: &String,
388-
) -> Option<String> {
389-
if role == &AirflowRole::Webserver {
390-
if let Some(webservers) = self.spec.webservers.as_ref() {
391-
let conf_defaults = Some("cluster-internal".to_string());
392-
let mut conf_role = webservers.config.config.listener_class.to_owned();
393-
let mut conf_rolegroup = webservers
394-
.role_groups
395-
.get(rolegroup_name)
396-
.map(|rg| rg.config.config.listener_class.clone())
397-
.unwrap_or_default();
398-
399-
conf_role.merge(&conf_defaults);
400-
conf_rolegroup.merge(&conf_role);
401-
402-
tracing::debug!("Merged listener-class: {:?}", conf_rolegroup);
403-
conf_rolegroup
404-
} else {
405-
None
406-
}
407-
} else {
408-
None
409-
}
410-
}
411-
412411
/// Retrieve and merge resource configs for the executor template
413412
pub fn merged_executor_config(
414413
&self,
@@ -436,18 +435,18 @@ impl v1alpha1::AirflowCluster {
436435
}
437436

438437
fn extract_role_from_webserver_config(
439-
fragment: Role<WebserverConfigFragment>,
438+
fragment: Role<AirflowConfigFragment, WebserverRoleConfig>,
440439
) -> Role<AirflowConfigFragment> {
441440
Role {
442441
config: CommonConfiguration {
443-
config: fragment.config.config.airflow_config,
442+
config: fragment.config.config,
444443
config_overrides: fragment.config.config_overrides,
445444
env_overrides: fragment.config.env_overrides,
446445
cli_overrides: fragment.config.cli_overrides,
447446
pod_overrides: fragment.config.pod_overrides,
448447
product_specific_common_config: fragment.config.product_specific_common_config,
449448
},
450-
role_config: fragment.role_config,
449+
role_config: fragment.role_config.common,
451450
role_groups: fragment
452451
.role_groups
453452
.into_iter()
@@ -456,7 +455,7 @@ fn extract_role_from_webserver_config(
456455
k,
457456
RoleGroup {
458457
config: CommonConfiguration {
459-
config: v.config.config.airflow_config,
458+
config: v.config.config,
460459
config_overrides: v.config.config_overrides,
461460
env_overrides: v.config.env_overrides,
462461
cli_overrides: v.config.cli_overrides,
@@ -687,6 +686,17 @@ impl AirflowRole {
687686
}
688687
roles
689688
}
689+
690+
pub fn listener_class_name(&self, airflow: &v1alpha1::AirflowCluster) -> Option<String> {
691+
match self {
692+
Self::Webserver => airflow
693+
.spec
694+
.webservers
695+
.to_owned()
696+
.map(|webserver| webserver.role_config.listener_class),
697+
Self::Worker | Self::Scheduler => None,
698+
}
699+
}
690700
}
691701

692702
fn container_debug_command() -> String {
@@ -810,29 +820,6 @@ pub struct AirflowConfig {
810820
pub graceful_shutdown_timeout: Option<Duration>,
811821
}
812822

813-
#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)]
814-
#[fragment_attrs(
815-
derive(
816-
Clone,
817-
Debug,
818-
Default,
819-
Deserialize,
820-
Merge,
821-
JsonSchema,
822-
PartialEq,
823-
Serialize
824-
),
825-
serde(rename_all = "camelCase")
826-
)]
827-
pub struct WebserverConfig {
828-
#[fragment_attrs(serde(default, flatten))]
829-
pub airflow_config: AirflowConfig,
830-
831-
/// This field controls which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) is used to expose the webserver.
832-
#[serde(default)]
833-
pub listener_class: String,
834-
}
835-
836823
impl AirflowConfig {
837824
pub const CREDENTIALS_SECRET_PROPERTY: &'static str = "credentialsSecret";
838825
pub const GIT_CREDENTIALS_SECRET_PROPERTY: &'static str = "gitCredentialsSecret";

0 commit comments

Comments
 (0)