Skip to content

Commit 8759c68

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor/uif-sidecar
2 parents a25da2b + 103efe1 commit 8759c68

5 files changed

Lines changed: 236 additions & 150 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ 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+
14+
## [26.7.0] - 2026-07-21
15+
16+
## [26.7.0-rc1] - 2026-07-16
17+
718
### Added
819

920
- Added support for OPA `1.16.2`. `1.12.3` is now deprecated ([#838]).

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/mod.rs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@ use stackable_operator::{
2424
apps::v1::{DaemonSet, DaemonSetSpec, DaemonSetUpdateStrategy, RollingUpdateDaemonSet},
2525
core::v1::{
2626
EmptyDirVolumeSource, EnvVarSource, HTTPGetAction, ObjectFieldSelector, Probe,
27-
ResourceRequirements, ServiceAccount,
27+
ResourceRequirements,
2828
},
2929
},
3030
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
3131
},
32-
kube::ResourceExt,
3332
memory::{BinaryMultiple, MemoryQuantity},
3433
product_logging::{
3534
self,
@@ -222,7 +221,7 @@ pub fn build_server_rolegroup_daemonset(
222221
role_group: &OpaRoleGroupConfig,
223222
opa_bundle_builder_image: &str,
224223
user_info_fetcher_image: &str,
225-
service_account: &ServiceAccount,
224+
service_account_name: &str,
226225
cluster_info: &KubernetesClusterInfo,
227226
) -> Result<DaemonSet> {
228227
let resolved_product_image = &cluster.image;
@@ -382,7 +381,7 @@ pub fn build_server_rolegroup_daemonset(
382381
.build(),
383382
)
384383
.context(AddVolumeSnafu)?
385-
.service_account_name(service_account.name_any())
384+
.service_account_name(service_account_name)
386385
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());
387386

388387
if let Some(tls) = &cluster.cluster_config.tls {
@@ -710,11 +709,7 @@ fn build_prepare_start_command(
710709
mod tests {
711710
use serde_json::json;
712711
use stackable_operator::{
713-
commons::networking::DomainName,
714-
k8s_openapi::{
715-
api::core::v1::{Container, ServiceAccount},
716-
apimachinery::pkg::apis::meta::v1::ObjectMeta,
717-
},
712+
commons::networking::DomainName, k8s_openapi::api::core::v1::Container,
718713
};
719714

720715
use super::*;
@@ -728,16 +723,6 @@ mod tests {
728723
}
729724
}
730725

731-
fn service_account() -> ServiceAccount {
732-
ServiceAccount {
733-
metadata: ObjectMeta {
734-
name: Some("test-opa-serviceaccount".to_owned()),
735-
..Default::default()
736-
},
737-
..Default::default()
738-
}
739-
}
740-
741726
fn build(cluster: &ValidatedCluster) -> DaemonSet {
742727
let (role_group_name, role_group) = cluster.role_group_configs[&OpaRole::Server]
743728
.iter()
@@ -749,7 +734,7 @@ mod tests {
749734
role_group,
750735
"bundle-builder-image",
751736
"user-info-fetcher-image",
752-
&service_account(),
737+
"test-opa-serviceaccount",
753738
&cluster_info(),
754739
)
755740
.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)