Skip to content

Commit 103efe1

Browse files
maltesanderadwk67siegfriedweber
authored
refactor: Introduce build aggregator (#852)
* refactor: Make the DaemonSet builder take a ServiceAccount name * refactor: Introduce build aggregator * changelog --------- Co-authored-by: Andrew Kenworthy <andrew.kenworthy@stackable.tech> Co-authored-by: Siegfried Weber <mail@siegfriedweber.net>
1 parent 0d0c0cb commit 103efe1

5 files changed

Lines changed: 232 additions & 151 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
9+
- Internal operator refactoring: introduce a build() step in the reconciler that
10+
assembles all relevant Kubernetes resources before anything is applied ([#852]).
11+
12+
[#852]: https://github.com/stackabletech/opa-operator/pull/852
13+
714
## [26.7.0] - 2026-07-21
815

916
## [26.7.0-rc1] - 2026-07-16

rust/operator-binary/src/controller/build.rs

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,104 @@
33
44
use std::str::FromStr;
55

6-
use stackable_operator::v2::types::common::Port;
6+
use snafu::{ResultExt, Snafu};
7+
use stackable_operator::{utils::cluster_info::KubernetesClusterInfo, v2::types::common::Port};
78

8-
use crate::controller::RoleGroupName;
9+
use crate::controller::{
10+
KubernetesResources, RoleGroupName, ValidatedCluster,
11+
build::resource::{
12+
config_map::build_rolegroup_config_map,
13+
daemonset::build_server_rolegroup_daemonset,
14+
discovery::build_discovery_config_map,
15+
service::{
16+
build_rolegroup_headless_service, build_rolegroup_metrics_service,
17+
build_server_role_service,
18+
},
19+
},
20+
};
921

1022
pub mod properties;
1123
pub mod resource;
1224

25+
#[derive(Snafu, Debug)]
26+
pub enum Error {
27+
#[snafu(display("failed to build ConfigMap for role group {role_group}"))]
28+
ConfigMap {
29+
source: resource::config_map::Error,
30+
role_group: RoleGroupName,
31+
},
32+
33+
#[snafu(display("failed to build DaemonSet for role group {role_group}"))]
34+
DaemonSet {
35+
source: resource::daemonset::Error,
36+
role_group: RoleGroupName,
37+
},
38+
39+
#[snafu(display("failed to build the discovery ConfigMap"))]
40+
Discovery { source: resource::discovery::Error },
41+
}
42+
43+
/// Builds every Kubernetes resource for the given validated cluster.
44+
///
45+
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
46+
/// dereferenced and validated by this point, so the errors returned here are resource-assembly
47+
/// failures only.
48+
///
49+
/// `service_account_name` is the name of the RBAC `ServiceAccount` the Pods run under (the RBAC
50+
/// resources are built and applied separately, in the reconcile step). `cluster_info` supplies the
51+
/// Kubernetes cluster domain used in the discovery URL and the sidecar environment.
52+
pub fn build(
53+
cluster: &ValidatedCluster,
54+
service_account_name: &str,
55+
opa_bundle_builder_image: &str,
56+
user_info_fetcher_image: &str,
57+
cluster_info: &KubernetesClusterInfo,
58+
) -> Result<KubernetesResources, Error> {
59+
let mut daemon_sets = vec![];
60+
let mut services = vec![];
61+
let mut config_maps = vec![];
62+
63+
// The role-level load-balanced Service, which is not bound to a single role group.
64+
services.push(build_server_role_service(cluster));
65+
66+
for role_group_configs in cluster.role_group_configs.values() {
67+
for (role_group_name, role_group) in role_group_configs {
68+
config_maps.push(
69+
build_rolegroup_config_map(cluster, role_group_name, role_group).context(
70+
ConfigMapSnafu {
71+
role_group: role_group_name.clone(),
72+
},
73+
)?,
74+
);
75+
services.push(build_rolegroup_headless_service(cluster, role_group_name));
76+
services.push(build_rolegroup_metrics_service(cluster, role_group_name));
77+
daemon_sets.push(
78+
build_server_rolegroup_daemonset(
79+
cluster,
80+
role_group_name,
81+
role_group,
82+
opa_bundle_builder_image,
83+
user_info_fetcher_image,
84+
service_account_name,
85+
cluster_info,
86+
)
87+
.context(DaemonSetSnafu {
88+
role_group: role_group_name.clone(),
89+
})?,
90+
);
91+
}
92+
}
93+
94+
// The cluster-level discovery ConfigMap.
95+
config_maps.push(build_discovery_config_map(cluster, cluster_info).context(DiscoverySnafu)?);
96+
97+
Ok(KubernetesResources {
98+
daemon_sets,
99+
services,
100+
config_maps,
101+
})
102+
}
103+
13104
/// The port the bundle-builder sidecar listens on, and which OPA connects to for bundle polling.
14105
pub(crate) const BUNDLE_BUILDER_PORT: Port = Port(3030);
15106

@@ -21,3 +112,71 @@ stackable_operator::constant!(pub(crate) PLACEHOLDER_ROLE_LEVEL_ROLE_GROUP: Role
21112
// Placeholder role-group name for the recommended labels of the discovery `ConfigMap`, which is a
22113
// cluster-level object not bound to a single role group.
23114
stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleGroupName = "discovery");
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use serde_json::json;
119+
use stackable_operator::{
120+
commons::networking::DomainName, kube::Resource, utils::cluster_info::KubernetesClusterInfo,
121+
};
122+
123+
use super::build;
124+
use crate::controller::{
125+
ValidatedCluster, build::properties::test_support::validated_cluster_from_spec,
126+
};
127+
128+
fn cluster_info() -> KubernetesClusterInfo {
129+
KubernetesClusterInfo {
130+
cluster_domain: DomainName::try_from("cluster.local").unwrap(),
131+
}
132+
}
133+
134+
fn sorted_names(resources: &[impl Resource]) -> Vec<&str> {
135+
let mut names: Vec<&str> = resources
136+
.iter()
137+
.filter_map(|resource| resource.meta().name.as_deref())
138+
.collect();
139+
names.sort();
140+
names
141+
}
142+
143+
/// A validated single-role-group cluster with no TLS or extra sidecars.
144+
fn cluster() -> ValidatedCluster {
145+
validated_cluster_from_spec(json!({
146+
"image": { "productVersion": "1.2.3" },
147+
"servers": { "roleGroups": { "default": {} } },
148+
}))
149+
}
150+
151+
#[test]
152+
fn build_produces_expected_resource_names() {
153+
let resources = build(
154+
&cluster(),
155+
"test-opa-serviceaccount",
156+
"bundle-builder-image",
157+
"user-info-fetcher-image",
158+
&cluster_info(),
159+
)
160+
.expect("build succeeds");
161+
162+
// One DaemonSet per role group.
163+
assert_eq!(
164+
sorted_names(&resources.daemon_sets),
165+
["test-opa-server-default"]
166+
);
167+
// The role-level Service plus a headless and a metrics Service per role group.
168+
assert_eq!(
169+
sorted_names(&resources.services),
170+
[
171+
"test-opa-server",
172+
"test-opa-server-default-headless",
173+
"test-opa-server-default-metrics",
174+
]
175+
);
176+
// The per-role-group ConfigMap plus the cluster-level discovery ConfigMap (`test-opa`).
177+
assert_eq!(
178+
sorted_names(&resources.config_maps),
179+
["test-opa", "test-opa-server-default"]
180+
);
181+
}
182+
}

rust/operator-binary/src/controller/build/resource/daemonset.rs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@ use stackable_operator::{
3030
apps::v1::{DaemonSet, DaemonSetSpec, DaemonSetUpdateStrategy, RollingUpdateDaemonSet},
3131
core::v1::{
3232
EmptyDirVolumeSource, EnvVarSource, HTTPGetAction, ObjectFieldSelector, Probe,
33-
ResourceRequirements, SecretVolumeSource, ServiceAccount,
33+
ResourceRequirements, SecretVolumeSource,
3434
},
3535
},
3636
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
3737
},
38-
kube::ResourceExt,
3938
memory::{BinaryMultiple, MemoryQuantity},
4039
product_logging::{
4140
self,
@@ -245,7 +244,7 @@ pub fn build_server_rolegroup_daemonset(
245244
role_group: &OpaRoleGroupConfig,
246245
opa_bundle_builder_image: &str,
247246
user_info_fetcher_image: &str,
248-
service_account: &ServiceAccount,
247+
service_account_name: &str,
249248
cluster_info: &KubernetesClusterInfo,
250249
) -> Result<DaemonSet> {
251250
let resolved_product_image = &cluster.image;
@@ -405,7 +404,7 @@ pub fn build_server_rolegroup_daemonset(
405404
.build(),
406405
)
407406
.context(AddVolumeSnafu)?
408-
.service_account_name(service_account.name_any())
407+
.service_account_name(service_account_name)
409408
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
410409

411410
if let Some(tls) = &cluster.cluster_config.tls {
@@ -847,12 +846,7 @@ fn build_prepare_start_command(
847846
#[cfg(test)]
848847
mod tests {
849848
use serde_json::json;
850-
use stackable_operator::{
851-
commons::networking::DomainName,
852-
k8s_openapi::{
853-
api::core::v1::ServiceAccount, apimachinery::pkg::apis::meta::v1::ObjectMeta,
854-
},
855-
};
849+
use stackable_operator::commons::networking::DomainName;
856850

857851
use super::*;
858852
use crate::{
@@ -865,16 +859,6 @@ mod tests {
865859
}
866860
}
867861

868-
fn service_account() -> ServiceAccount {
869-
ServiceAccount {
870-
metadata: ObjectMeta {
871-
name: Some("test-opa-serviceaccount".to_owned()),
872-
..Default::default()
873-
},
874-
..Default::default()
875-
}
876-
}
877-
878862
fn build(cluster: &ValidatedCluster) -> DaemonSet {
879863
let (role_group_name, role_group) = cluster.role_group_configs[&OpaRole::Server]
880864
.iter()
@@ -886,7 +870,7 @@ mod tests {
886870
role_group,
887871
"bundle-builder-image",
888872
"user-info-fetcher-image",
889-
&service_account(),
873+
"test-opa-serviceaccount",
890874
&cluster_info(),
891875
)
892876
.expect("the daemonset should build")

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ use stackable_operator::{
1111
product_image_selection::ResolvedProductImage,
1212
resources::{NoRuntimeLimits, Resources},
1313
},
14+
k8s_openapi::api::{
15+
apps::v1::DaemonSet,
16+
core::v1::{ConfigMap, Service},
17+
},
1418
kube::{Resource as KubeResource, api::ObjectMeta},
1519
kvp::Labels,
1620
shared::time::Duration,
@@ -232,6 +236,18 @@ impl KubeResource for ValidatedCluster {
232236
}
233237
}
234238

239+
/// Every Kubernetes resource produced by the [`build`](build::build) step.
240+
///
241+
/// OPA runs as a `DaemonSet` (one Pod per node), so there are no `StatefulSet`s, PDBs or
242+
/// `Listener`s. `services` holds the role-level `Service` and the per-role-group headless and
243+
/// metrics `Service`s; `config_maps` holds the per-role-group `ConfigMap`s and the cluster-level
244+
/// discovery `ConfigMap`.
245+
pub struct KubernetesResources {
246+
pub daemon_sets: Vec<DaemonSet>,
247+
pub services: Vec<Service>,
248+
pub config_maps: Vec<ConfigMap>,
249+
}
250+
235251
/// Cluster-wide settings resolved once during validation, so the build steps no longer need the
236252
/// raw `OpaCluster` to render config (except for owner references).
237253
pub struct ValidatedClusterConfig {

0 commit comments

Comments
 (0)