Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ All notable changes to this project will be documented in this file.
- Internal operator refactoring: introduce a build() step in the reconciler that
assembles all relevant Kubernetes resources before anything is applied ([#852]).
- Bump `stackable-operator` to 0.114.0 ([#867]).
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
functions and carry the full set of recommended labels ([#861]).

[#852]: https://github.com/stackabletech/opa-operator/pull/852
[#861]: https://github.com/stackabletech/opa-operator/pull/861
[#867]: https://github.com/stackabletech/opa-operator/pull/867

## [26.7.0] - 2026-07-21
Expand Down
62 changes: 59 additions & 3 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::controller::{
config_map::build_rolegroup_config_map,
daemonset::build_server_rolegroup_daemonset,
discovery::build_discovery_config_map,
rbac::{build_role_binding, build_service_account},
service::{
build_rolegroup_headless_service, build_rolegroup_metrics_service,
build_server_role_service,
Expand Down Expand Up @@ -51,7 +52,6 @@ pub enum Error {
/// Kubernetes cluster domain used in the discovery URL and the sidecar environment.
pub fn build(
cluster: &ValidatedCluster,
service_account_name: &str,
opa_bundle_builder_image: &str,
user_info_fetcher_image: &str,
cluster_info: &KubernetesClusterInfo,
Expand Down Expand Up @@ -81,7 +81,6 @@ pub fn build(
role_group,
opa_bundle_builder_image,
user_info_fetcher_image,
service_account_name,
cluster_info,
)
.context(DaemonSetSnafu {
Expand All @@ -98,6 +97,8 @@ pub fn build(
daemon_sets,
services,
config_maps,
service_accounts: vec![build_service_account(cluster)],
role_bindings: vec![build_role_binding(cluster)],
})
}

Expand All @@ -115,6 +116,8 @@ stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleG

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use serde_json::json;
use stackable_operator::{
commons::networking::DomainName, kube::Resource, utils::cluster_info::KubernetesClusterInfo,
Expand Down Expand Up @@ -152,7 +155,6 @@ mod tests {
fn build_produces_expected_resource_names() {
let resources = build(
&cluster(),
"test-opa-serviceaccount",
"bundle-builder-image",
"user-info-fetcher-image",
&cluster_info(),
Expand All @@ -179,4 +181,58 @@ mod tests {
["test-opa", "test-opa-server-default"]
);
}

/// Locks the RBAC resource names, the roleRef, and the recommended label set against
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
/// that swapped `name`/`instance` label values cannot pass unnoticed.
#[test]
fn build_produces_rbac() {
let resources = build(
&cluster(),
"bundle-builder-image",
"user-info-fetcher-image",
&cluster_info(),
)
.expect("build succeeds");

assert_eq!(
sorted_names(&resources.service_accounts),
["test-opa-serviceaccount"]
);
assert_eq!(
sorted_names(&resources.role_bindings),
["test-opa-rolebinding"]
);

let expected_labels = BTreeMap::from(
[
("app.kubernetes.io/component", "none"),
("app.kubernetes.io/instance", "test-opa"),
(
"app.kubernetes.io/managed-by",
"opa.stackable.tech_opacluster",
),
("app.kubernetes.io/name", "opa"),
("app.kubernetes.io/role-group", "none"),
("app.kubernetes.io/version", "1.2.3-stackable0.0.0-dev"),
("stackable.tech/vendor", "Stackable"),
]
.map(|(key, value)| (key.to_string(), value.to_string())),
);
let service_account = resources
.service_accounts
.first()
.expect("a ServiceAccount is built");
assert_eq!(
service_account.metadata.labels,
Some(expected_labels.clone())
);

let role_binding = resources
.role_bindings
.first()
.expect("a RoleBinding is built");
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
assert_eq!(role_binding.role_ref.name, "opa-clusterrole");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub fn build_rolegroup_config_map(
let metadata = cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.role_group_config_map()
.to_string(),
role_group_name,
Expand Down
19 changes: 11 additions & 8 deletions rust/operator-binary/src/controller/build/resource/daemonset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ pub fn build_server_rolegroup_daemonset(
role_group: &OpaRoleGroupConfig,
opa_bundle_builder_image: &str,
user_info_fetcher_image: &str,
service_account_name: &str,
cluster_info: &KubernetesClusterInfo,
) -> Result<DaemonSet> {
let resolved_product_image = &cluster.image;
Expand Down Expand Up @@ -376,7 +375,7 @@ pub fn build_server_rolegroup_daemonset(
VolumeBuilder::new(CONFIG_VOLUME_NAME.as_ref())
.with_config_map(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.role_group_config_map()
.to_string(),
)
Expand Down Expand Up @@ -404,7 +403,12 @@ pub fn build_server_rolegroup_daemonset(
.build(),
)
.context(AddVolumeSnafu)?
.service_account_name(service_account_name)
.service_account_name(
cluster
.cluster_resource_names()
.service_account_name()
.to_string(),
)
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());

if let Some(tls) = &cluster.cluster_config.tls {
Expand All @@ -419,13 +423,13 @@ pub fn build_server_rolegroup_daemonset(
.with_service_scope(cluster.server_role_service_name())
.with_service_scope(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.headless_service_name()
.to_string(),
)
.with_service_scope(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.metrics_service_name()
.to_string(),
)
Expand Down Expand Up @@ -568,7 +572,7 @@ pub fn build_server_rolegroup_daemonset(
&container_name(&Container::Vector),
resolved_product_image,
vector_log_config,
&cluster.resource_names(role_group_name),
&cluster.role_group_resource_names(role_group_name),
&CONFIG_VOLUME_NAME,
&LOG_VOLUME_NAME,
EnvVarSet::new(),
Expand All @@ -583,7 +587,7 @@ pub fn build_server_rolegroup_daemonset(
let metadata = cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.daemon_set_name()
.to_string(),
role_group_name,
Expand Down Expand Up @@ -870,7 +874,6 @@ mod tests {
role_group,
"bundle-builder-image",
"user-info-fetcher-image",
"test-opa-serviceaccount",
&cluster_info(),
)
.expect("the daemonset should build")
Expand Down
1 change: 1 addition & 0 deletions rust/operator-binary/src/controller/build/resource/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
pub mod config_map;
pub mod daemonset;
pub mod discovery;
pub mod rbac;
pub mod service;
42 changes: 42 additions & 0 deletions rust/operator-binary/src/controller/build/resource/rbac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Builds the RBAC resources (ServiceAccount + RoleBinding) shared by all role groups.

use std::str::FromStr;

use stackable_operator::{
k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding},
kvp::Labels,
v2::{
rbac,
types::operator::{RoleGroupName, RoleName},
},
};

use crate::controller::ValidatedCluster;

stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none");
stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none");

/// Builds the [`ServiceAccount`] that the role-group Pods run under.
pub fn build_service_account(cluster: &ValidatedCluster) -> ServiceAccount {
rbac::build_service_account(
cluster,
&cluster.cluster_resource_names(),
rbac_labels(cluster),
)
}

/// Builds the [`RoleBinding`] that binds the [`ServiceAccount`] from [`build_service_account`] to
/// the operator-deployed ClusterRole.
pub fn build_role_binding(cluster: &ValidatedCluster) -> RoleBinding {
rbac::build_role_binding(
cluster,
&cluster.cluster_resource_names(),
rbac_labels(cluster),
)
}

/// Both resources are shared by the whole cluster rather than tied to a role or role group, so
/// the recommended labels carry `none` for both values.
fn rbac_labels(cluster: &ValidatedCluster) -> Labels {
cluster.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME)
}
4 changes: 2 additions & 2 deletions rust/operator-binary/src/controller/build/resource/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub(crate) fn build_rolegroup_headless_service(
let metadata = cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.headless_service_name()
.to_string(),
role_group_name,
Expand Down Expand Up @@ -117,7 +117,7 @@ pub(crate) fn build_rolegroup_metrics_service(
let metadata = cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.metrics_service_name()
.to_string(),
role_group_name,
Expand Down
Loading
Loading