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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ All notable changes to this project will be documented in this file.
deletion is required ([#880]).
- The operator now watches all resources that it creates and early-exits the reconcile action when the
cluster is marked for deletion ([#882]).
- Make operations infallible where appropriate ([#886]).

### Fixed

Expand All @@ -81,6 +82,7 @@ All notable changes to this project will be documented in this file.
[#872]: https://github.com/stackabletech/opa-operator/pull/872
[#880]: https://github.com/stackabletech/opa-operator/pull/880
[#882]: https://github.com/stackabletech/opa-operator/pull/882
[#886]: https://github.com/stackabletech/opa-operator/pull/886

## [26.7.0] - 2026-07-21

Expand Down
2 changes: 1 addition & 1 deletion rust/info-fetcher-commons/src/utils/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const REDACTED: &str = "[redacted]";
/// `?token` or `#[instrument]` away from writing that token to the log file the Vector agent ships
/// off the node. Wrapping the value means the leak has to be an explicit decision ([`Secret::expose`])
/// rather than an accident: the type has no [`Display`](fmt::Display), and its
/// [`Debug`](fmt::Debug) renders [`REDACTED`], so every struct that holds one can keep deriving
/// [`Debug`](fmt::Debug) renders a default value, so every struct that holds one can keep deriving
/// `Debug` safely.
#[derive(Clone, PartialEq, Eq, Deserialize)]
#[serde(transparent)]
Expand Down
5 changes: 1 addition & 4 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ pub enum Error {
source: resource::daemonset::Error,
role_group: RoleGroupName,
},

#[snafu(display("failed to build the discovery ConfigMap"))]
Discovery { source: resource::discovery::Error },
}

/// Builds every Kubernetes resource for the given validated cluster.
Expand Down Expand Up @@ -105,7 +102,7 @@ pub fn build(
}

// The cluster-level discovery ConfigMap.
config_maps.push(build_discovery_config_map(cluster, cluster_info).context(DiscoverySnafu)?);
config_maps.push(build_discovery_config_map(cluster, cluster_info));

Ok(KubernetesResources {
daemon_sets,
Expand Down
24 changes: 9 additions & 15 deletions rust/operator-binary/src/controller/build/resource/config_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,15 @@ use crate::controller::{
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("failed to build config.json"))]
BuildConfigJson { source: config_json::Error },
ConfigJson { source: config_json::Error },

#[snafu(display("failed to build user-info-fetcher.json"))]
BuildUserInfoFetcher { source: user_info_fetcher::Error },
UserInfoFetcher { source: user_info_fetcher::Error },

#[snafu(display("failed to build resource-info-fetcher.json"))]
BuildResourceInfoFetcher {
ResourceInfoFetcher {
source: resource_info_fetcher::Error,
},

#[snafu(display("failed to assemble ConfigMap for role group {role_group}"))]
Assemble {
source: stackable_operator::builder::configmap::Error,
role_group: RoleGroupName,
},
}

type Result<T, E = Error> = std::result::Result<T, E>;
Expand Down Expand Up @@ -66,19 +60,19 @@ pub fn build_rolegroup_config_map(
cm_builder.metadata(metadata).add_data(
ConfigFileName::ConfigJson.to_string(),
config_json::build(&rolegroup_config.config, &rolegroup_config.config_overrides)
.context(BuildConfigJsonSnafu)?,
.context(ConfigJsonSnafu)?,
);

if let Some(user_info) = &cluster.cluster_config.user_info {
cm_builder.add_data(
ConfigFileName::UserInfoFetcher.to_string(),
user_info_fetcher::build(user_info).context(BuildUserInfoFetcherSnafu)?,
user_info_fetcher::build(user_info).context(UserInfoFetcherSnafu)?,
);
}
if let Some(resource_info) = &cluster.cluster_config.resource_info {
cm_builder.add_data(
ConfigFileName::ResourceInfoFetcher.to_string(),
resource_info_fetcher::build(resource_info).context(BuildResourceInfoFetcherSnafu)?,
resource_info_fetcher::build(resource_info).context(ResourceInfoFetcherSnafu)?,
);
}

Expand All @@ -89,9 +83,9 @@ pub fn build_rolegroup_config_map(
);
}

cm_builder.build().with_context(|_| AssembleSnafu {
role_group: role_group_name.clone(),
})
Ok(cm_builder
.build()
.expect("The ConfigMap metadata is set in this function."))
}

#[cfg(test)]
Expand Down
101 changes: 75 additions & 26 deletions rust/operator-binary/src/controller/build/resource/daemonset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use snafu::{ResultExt, Snafu};
use stackable_opa_operator::crd::{Container, DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT, OpaRole};
use stackable_operator::{
builder::{
self,
meta::ObjectMetaBuilder,
pod::{
PodBuilder,
Expand Down Expand Up @@ -174,19 +173,6 @@ pub enum Error {
source: crate::operations::graceful_shutdown::Error,
},

#[snafu(display("failed to add needed volume"))]
AddVolume { source: builder::pod::Error },

#[snafu(display("failed to add needed volumeMount"))]
AddVolumeMount {
source: builder::pod::container::Error,
},

#[snafu(display("failed to build TLS volume"))]
TlsVolumeBuild {
source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError,
},

#[snafu(display("failed to build User Info Fetcher sidecar"))]
BuildUserInfoFetcherSidecar { source: user_info_fetcher::Error },

Expand Down Expand Up @@ -312,9 +298,9 @@ pub fn build_server_rolegroup_daemonset(
.join(" && "),
])
.add_volume_mount(BUNDLES_VOLUME_NAME.as_ref(), BUNDLES_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(LOG_VOLUME_NAME.as_ref(), STACKABLE_LOG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.resources(merged_config.resources.to_owned().into());

// All operator-set environment variables of the bundle-builder container, collected into an
Expand All @@ -337,9 +323,9 @@ pub fn build_server_rolegroup_daemonset(
)])
.add_env_vars(bundle_builder_env_vars)
.add_volume_mount(BUNDLES_VOLUME_NAME.as_ref(), BUNDLES_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(LOG_VOLUME_NAME.as_ref(), STACKABLE_LOG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.resources(sidecar_resource_requirements())
.readiness_probe(http_readiness_probe(
BUNDLE_BUILDER_PROBE_PATH,
Expand Down Expand Up @@ -383,16 +369,16 @@ pub fn build_server_rolegroup_daemonset(
cb_opa.add_container_port(service::APP_TLS_PORT_NAME, service::APP_TLS_PORT.into());
cb_opa
.add_volume_mount(TLS_VOLUME_NAME.as_ref(), TLS_STORE_DIR)
.context(AddVolumeMountSnafu)?;
.expect("The mount paths are statically defined and there should be no duplicates.");
} else {
cb_opa.add_container_port(APP_PORT_NAME, APP_PORT.into());
}

cb_opa
.add_volume_mount(CONFIG_VOLUME_NAME.as_ref(), CONFIG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(LOG_VOLUME_NAME.as_ref(), STACKABLE_LOG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.resources(merged_config.resources.to_owned().into());

let (probe_port_name, probe_scheme) = if cluster.is_tls_enabled() {
Expand Down Expand Up @@ -437,13 +423,13 @@ pub fn build_server_rolegroup_daemonset(
)
.build(),
)
.context(AddVolumeSnafu)?
.expect("The volume names are statically defined and there should be no duplicates.")
.add_volume(
VolumeBuilder::new(BUNDLES_VOLUME_NAME.as_ref())
.with_empty_dir(None::<String>, None)
.build(),
)
.context(AddVolumeSnafu)?
.expect("The volume names are statically defined and there should be no duplicates.")
.add_volume(
VolumeBuilder::new(LOG_VOLUME_NAME.as_ref())
.empty_dir(EmptyDirVolumeSource {
Expand All @@ -452,7 +438,7 @@ pub fn build_server_rolegroup_daemonset(
})
.build(),
)
.context(AddVolumeSnafu)?
.expect("The volume names are statically defined and there should be no duplicates.")
.service_account_name(
cluster
.cluster_resource_names()
Expand Down Expand Up @@ -488,13 +474,21 @@ pub fn build_server_rolegroup_daemonset(
.to_string(),
)
.build()
.context(TlsVolumeBuildSnafu)?,
.expect(
"The annotation keys are static and annotation values cannot be invalid.",
),
)
.build(),
)
.context(AddVolumeSnafu)?;
.expect("The volume names are statically defined and there should be no duplicates.");
}

// Both sidecars add their statically named volumes with `expect`, and the TLS/LDAP helpers
// from operator-rs add volumes named after user-supplied SecretClasses fallibly. The
// user-info-fetcher's SecretClass-derived volumes precede the resource-info-fetcher's
// static one, which is fine: the derived names always end in `-ca-cert` or
// `-bind-credentials` and so can never equal a static volume name (the alternative would be
// to split both calls into two parts, static and derived).
add_user_info_fetcher_sidecar(
&mut pb,
cluster,
Expand Down Expand Up @@ -853,6 +847,7 @@ mod tests {
let _ = *BUNDLES_VOLUME_NAME;
let _ = *USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME;
let _ = *USER_INFO_FETCHER_KERBEROS_VOLUME_NAME;
let _ = *RESOURCE_INFO_FETCHER_CREDENTIALS_VOLUME_NAME;
let _ = *TLS_VOLUME_NAME;
let _ = *CONTAINERDEBUG_LOG_DIRECTORY;
let _ = *WATCH_NAMESPACE;
Expand Down Expand Up @@ -1264,6 +1259,60 @@ mod tests {
);
}

/// The Entra backend projects its client credentials Secret like the Keycloak backend does. Its
/// TLS CA volume is named after the user's SecretClass and is added to the pod *before* the
/// resource-info-fetcher's statically named credentials volume, so this also pins that the two
/// cannot collide (see the comment above the sidecar calls in `build_server_rolegroup_daemonset`).
#[test]
fn user_info_fetcher_entra_backend_mounts_client_credentials_next_to_resource_info_fetcher() {
let ds = build(&validated_cluster_from_spec(json!({
"image": { "productVersion": "1.2.3" },
"clusterConfig": {
"userInfo": {
"backend": {
"entra": {
"tenantId": "my-tenant",
"clientCredentialsSecret": "entra-credentials",
"tls": {
"verification": {
"server": { "caCert": { "secretClass": "my-ca" } }
}
},
}
}
},
"resourceInfo": {
"backend": {
"dataHub": {
"hostname": "datahub-gms.default.svc.cluster.local",
"credentialsSecretName": "datahub-credentials",
}
}
},
},
"servers": { "roleGroups": { "default": {} } },
})));

let volumes = volume_names(&ds);
for expected in [
"user-info-fetcher-credentials",
"my-ca-ca-cert",
"resource-info-fetcher-credentials",
] {
assert!(
volumes.contains(&expected.to_owned()),
"missing volume {expected}"
);
}

let uif = uif_container(&ds);
assert_eq!(
mount_path(&uif, "user-info-fetcher-credentials"),
"/stackable/credentials"
);
assert_eq!(read_only(&uif, "user-info-fetcher-credentials"), Some(true));
}

/// A cluster running both info-fetcher sidecars, so their shared wiring can be asserted in one go.
fn cluster_with_both_info_fetchers() -> ValidatedCluster {
validated_cluster_from_spec(json!({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ use std::str::FromStr;
use snafu::{ResultExt, Snafu};
use stackable_opa_operator::crd::{Container, resource_info_fetcher};
use stackable_operator::{
builder::{
self,
pod::{PodBuilder, volume::VolumeBuilder},
},
builder::pod::{PodBuilder, volume::VolumeBuilder},
commons::tls_verification::TlsClientDetailsError,
constant,
k8s_openapi::api::core::v1::SecretVolumeSource,
Expand Down Expand Up @@ -36,18 +33,17 @@ pub enum Error {
"failed to build volume or volume mount spec for the Resource Info Fetcher TLS config"
))]
TlsVolumeAndMounts { source: TlsClientDetailsError },

#[snafu(display("failed to add needed volume"))]
AddVolume { source: builder::pod::Error },

#[snafu(display("failed to add needed volumeMount"))]
AddVolumeMount {
source: builder::pod::container::Error,
},
}

type Result<T, E = Error> = std::result::Result<T, E>;

/// Adds the Resource Info Fetcher sidecar container to the given [`PodBuilder`].
///
/// # Panics
///
/// Panics if the volumes or volume mounts cannot be added to the builders. Only call this
/// on builders whose volume names and mount paths are still distinct from the ones added
/// here.
pub fn add_resource_info_fetcher_sidecar(
pb: &mut PodBuilder,
cluster: &ValidatedCluster,
Expand Down Expand Up @@ -82,12 +78,12 @@ pub fn add_resource_info_fetcher_sidecar(
.image(resource_info_fetcher_image) // ...override the image
.command(vec!["stackable-opa-resource-info-fetcher".to_string()])
.add_volume_mounts([read_only_mount(CONFIG_VOLUME_NAME.as_ref(), CONFIG_DIR)])
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
// The sidecar writes its file logs below this directory (see
// `stackable_rust_cli_env_vars`). They have to land on the shared log volume,
// because that is the only place the Vector agent collects them from.
.add_volume_mount(LOG_VOLUME_NAME.as_ref(), STACKABLE_LOG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.resources(sidecar_resource_requirements());

match &resource_info.backend {
Expand All @@ -100,13 +96,17 @@ pub fn add_resource_info_fetcher_sidecar(
})
.build(),
)
.context(AddVolumeSnafu)?;
.expect(
"The volume names are statically defined and there should be no duplicates.",
);
cb_rif
.add_volume_mounts([read_only_mount(
RESOURCE_INFO_FETCHER_CREDENTIALS_VOLUME_NAME.as_ref(),
RESOURCE_INFO_FETCHER_CREDENTIALS_DIR,
)])
.context(AddVolumeMountSnafu)?;
.expect(
"The mount paths are statically defined and there should be no duplicates.",
);
data_hub
.tls
.add_volumes_and_mounts(pb, vec![&mut cb_rif])
Expand Down
Loading
Loading