Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Support floating tags for product images via the new `spec.image.stackableVersionPolicy` field
([#790]).
- Add `/ready` endpoint to the operator Deployment, which reports the CRD installation status ([#795]).
- Nodes now have a default affinity to the OPA Pods when the role mapping from OPA is configured ([#796]).

### Changed

Expand Down Expand Up @@ -67,6 +68,7 @@
[#790]: https://github.com/stackabletech/superset-operator/pull/790
[#791]: https://github.com/stackabletech/superset-operator/pull/791
[#795]: https://github.com/stackabletech/superset-operator/pull/795
[#796]: https://github.com/stackabletech/superset-operator/pull/796

## [26.7.0] - 2026-07-21

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ You can configure the Pod placement of the Superset pods as described in xref:co
The default affinities created by the operator are:

1. Distribute all the Superset Pods (weight 70)
2. If the role mapping from OPA is configured: co-locate the Superset nodes with the OPA Pods (weight 50).
Workers and beat do not get this affinity.
6 changes: 5 additions & 1 deletion rust/operator-binary/src/controller/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ fn validate_role_groups(
let Some(resolved_role) = superset.get_role(role) else {
return Ok(BTreeMap::new());
};
let default_config = SupersetConfig::default_config(&superset.name_any(), role);
let default_config = SupersetConfig::default_config(
&superset.name_any(),
role,
superset.get_opa_config().map(|opa_config| &opa_config.opa),
);

resolved_role
.role_groups
Expand Down
93 changes: 80 additions & 13 deletions rust/operator-binary/src/crd/affinity.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
use stackable_operator::{
commons::affinity::{StackableAffinityFragment, affinity_between_role_pods},
k8s_openapi::api::core::v1::PodAntiAffinity,
commons::{
affinity::{StackableAffinityFragment, affinity_between_role_pods},
opa::OpaConfig,
},
k8s_openapi::api::core::v1::{PodAffinity, PodAntiAffinity},
};

use crate::crd::{APP_NAME, SupersetRole};

pub fn get_affinity(cluster_name: &str, role: &SupersetRole) -> StackableAffinityFragment {
/// `opa_config` is only passed for roles that send requests to OPA.
pub fn get_affinity(
cluster_name: &str,
role: &SupersetRole,
opa_config: Option<&OpaConfig>,
) -> StackableAffinityFragment {
// With the role mapping from OPA configured, the role sends its requests to OPA, so prefer to
// place it next to the OPA Pods.
let pod_affinity = opa_config.map(|opa_config| PodAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
affinity_between_role_pods(
"opa",
&opa_config.config_map_name, // The discovery cm has the same name as the OpaCluster itself
"server",
50,
),
]),
required_during_scheduling_ignored_during_execution: None,
});

StackableAffinityFragment {
pod_affinity: None,
pod_affinity,
pod_anti_affinity: Some(PodAntiAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
affinity_between_role_pods(APP_NAME, cluster_name, &role.to_string(), 70),
Expand All @@ -23,11 +45,14 @@ pub fn get_affinity(cluster_name: &str, role: &SupersetRole) -> StackableAffinit
mod tests {
use std::collections::BTreeMap;

use rstest::rstest;
use stackable_operator::{
commons::affinity::StackableAffinity,
config::fragment,
k8s_openapi::{
api::core::v1::{PodAffinityTerm, PodAntiAffinity, WeightedPodAffinityTerm},
api::core::v1::{
PodAffinity, PodAffinityTerm, PodAntiAffinity, WeightedPodAffinityTerm,
},
apimachinery::pkg::apis::meta::v1::LabelSelector,
},
kube::ResourceExt,
Expand All @@ -37,8 +62,11 @@ mod tests {
use super::*;
use crate::crd::v1alpha1;

#[test]
fn test_affinity_defaults() {
#[rstest]
#[case(SupersetRole::Node)]
#[case(SupersetRole::Worker)]
#[case(SupersetRole::Beat)]
fn test_affinity_defaults(#[case] role: SupersetRole) {
let input = r#"
apiVersion: superset.stackable.tech/v1alpha1
kind: SupersetCluster
Expand All @@ -54,6 +82,10 @@ mod tests {
host: superset-postgresql
database: superset
credentialsSecretName: superset-postgresql-credentials
authorization:
roleMappingFromOpa:
configMapName: simple-opa
package: superset
nodes:
roleGroups:
default:
Expand All @@ -63,15 +95,50 @@ mod tests {
yaml_from_str_singleton_map(input).expect("illegal test input");
// The role group carries no resource/affinity overrides, so the merged config is just the
// validated default config.
let merged_config: v1alpha1::SupersetConfig = fragment::validate(
v1alpha1::SupersetConfig::default_config(&superset.name_any(), &SupersetRole::Node),
)
.expect("default config should validate");
let merged_config: v1alpha1::SupersetConfig =
fragment::validate(v1alpha1::SupersetConfig::default_config(
&superset.name_any(),
&role,
superset.get_opa_config().map(|opa_config| &opa_config.opa),
))
.expect("default config should validate");

assert_eq!(
merged_config.affinity,
StackableAffinity {
pod_affinity: None,
pod_affinity: match role {
// Only the web server (node) is known to request the role mapping from OPA.
SupersetRole::Node => Some(PodAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
WeightedPodAffinityTerm {
pod_affinity_term: PodAffinityTerm {
label_selector: Some(LabelSelector {
match_expressions: None,
match_labels: Some(BTreeMap::from([
(
"app.kubernetes.io/name".to_string(),
"opa".to_string()
),
(
"app.kubernetes.io/instance".to_string(),
"simple-opa".to_string(),
),
(
"app.kubernetes.io/component".to_string(),
"server".to_string(),
),
])),
}),
topology_key: "kubernetes.io/hostname".to_string(),
..PodAffinityTerm::default()
},
weight: 50,
}
]),
required_during_scheduling_ignored_during_execution: None,
}),
SupersetRole::Worker | SupersetRole::Beat => None,
},
pod_anti_affinity: Some(PodAntiAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
WeightedPodAffinityTerm {
Expand All @@ -89,7 +156,7 @@ mod tests {
),
(
"app.kubernetes.io/component".to_string(),
"node".to_string(),
role.to_string(),
)
]))
}),
Expand Down
11 changes: 8 additions & 3 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ impl v1alpha1::SupersetConfig {
pub(crate) fn default_config(
cluster_name: &str,
role: &SupersetRole,
opa_config: Option<&OpaConfig>,
) -> v1alpha1::SupersetConfigFragment {
match role {
SupersetRole::Node => v1alpha1::SupersetConfigFragment {
Expand All @@ -525,7 +526,7 @@ impl v1alpha1::SupersetConfig {
storage: v1alpha1::SupersetStorageConfigFragment {},
},
logging: product_logging::spec::default_logging(),
affinity: affinity::get_affinity(cluster_name, role),
affinity: affinity::get_affinity(cluster_name, role, opa_config),
graceful_shutdown_timeout: Some(DEFAULT_NODE_GRACEFUL_SHUTDOWN_TIMEOUT),
row_limit: None,
webserver_timeout: None,
Expand All @@ -543,7 +544,9 @@ impl v1alpha1::SupersetConfig {
storage: v1alpha1::SupersetStorageConfigFragment {},
},
logging: product_logging::spec::default_logging(),
affinity: affinity::get_affinity(cluster_name, role),
// Only the web server (node) is known to request the role mapping from OPA, so
// workers and beat get no affinity to the OPA Pods.
affinity: affinity::get_affinity(cluster_name, role, None),
graceful_shutdown_timeout: Some(DEFAULT_NODE_GRACEFUL_SHUTDOWN_TIMEOUT),
row_limit: None,
webserver_timeout: None,
Expand All @@ -561,7 +564,9 @@ impl v1alpha1::SupersetConfig {
storage: v1alpha1::SupersetStorageConfigFragment {},
},
logging: product_logging::spec::default_logging(),
affinity: affinity::get_affinity(cluster_name, role),
// Only the web server (node) is known to request the role mapping from OPA, so
// workers and beat get no affinity to the OPA Pods.
affinity: affinity::get_affinity(cluster_name, role, None),
graceful_shutdown_timeout: Some(DEFAULT_NODE_GRACEFUL_SHUTDOWN_TIMEOUT),
row_limit: None,
webserver_timeout: None,
Expand Down
Loading