From cad0c7958ed72e4b7615b46fdcbac2317a8e46d2 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 13:40:46 +0200 Subject: [PATCH 1/6] feat: extract dereference and validate steps --- rust/operator-binary/src/controller.rs | 98 +++++++------------ .../src/controller/dereference.rs | 27 +++++ .../src/controller/validate.rs | 93 ++++++++++++++++++ 3 files changed, 155 insertions(+), 63 deletions(-) create mode 100644 rust/operator-binary/src/controller/dereference.rs create mode 100644 rust/operator-binary/src/controller/validate.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 52ba3a04..d6cfa562 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -26,7 +26,7 @@ use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, commons::{ - product_image_selection::{self, ResolvedProductImage}, + product_image_selection::ResolvedProductImage, rbac::build_rbac_resources, secret_class::{ SecretClassVolume, SecretClassVolumeProvisionParts, SecretClassVolumeScope, @@ -54,7 +54,6 @@ use stackable_operator::{ kvp::{LabelError, Labels, ObjectLabels}, logging::controller::ReconcilerError, memory::{BinaryMultiple, MemoryQuantity}, - product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, product_logging::{ self, framework::{ @@ -89,6 +88,9 @@ use crate::{ }, }; +mod dereference; +mod validate; + pub const OPA_CONTROLLER_NAME: &str = "opacluster"; pub const OPA_FULL_CONTROLLER_NAME: &str = concatcp!(OPA_CONTROLLER_NAME, '.', OPERATOR_NAME); @@ -230,11 +232,6 @@ pub enum Error { source: stackable_operator::client::Error, }, - #[snafu(display("invalid product config"))] - InvalidProductConfig { - source: stackable_operator::product_config_utils::Error, - }, - #[snafu(display("object is missing metadata to build owner reference"))] ObjectMissingMetadataForOwnerRef { source: stackable_operator::builder::meta::Error, @@ -248,11 +245,6 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to transform configs"))] - ProductConfigTransform { - source: stackable_operator::product_config_utils::Error, - }, - #[snafu(display("failed to resolve and merge config for role and role group"))] FailedToResolveConfig { source: crate::crd::Error }, @@ -332,14 +324,15 @@ pub enum Error { source: builder::pod::container::Error, }, - #[snafu(display("failed to resolve product image"))] - ResolveProductImage { - source: product_image_selection::Error, - }, - #[snafu(display("failed to build service"))] BuildService { source: service::Error }, + #[snafu(display("failed to dereference resources"))] + Dereference { source: dereference::Error }, + + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + #[snafu(display("failed to build TLS volume"))] TlsVolumeBuild { source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError, @@ -448,15 +441,21 @@ pub async fn reconcile_opa( let opa_ref = ObjectRef::from_obj(opa); let client = &ctx.client; - let resolved_product_image = opa - .spec - .image - .resolve( - CONTAINER_IMAGE_BASE_NAME, - &ctx.operator_environment.image_repository, - crate::built_info::PKG_VERSION, - ) - .context(ResolveProductImageSnafu)?; + + // dereference (client required) + let dereferenced_objects = dereference::dereference(client, opa) + .await + .context(DereferenceSnafu)?; + + // validate (no client required) + let validated = validate::validate( + opa, + &dereferenced_objects, + &ctx.operator_environment, + &ctx.product_config, + ) + .context(ValidateClusterSnafu)?; + let opa_role = OpaRole::Server; let mut cluster_resources = ClusterResources::new( @@ -469,36 +468,14 @@ pub async fn reconcile_opa( ) .context(FailedToCreateClusterResourcesSnafu)?; - let validated_config = validate_all_roles_and_groups_config( - &resolved_product_image.product_version, - &transform_all_roles_to_config( - opa, - &[( - opa_role.to_string(), - ( - vec![ - PropertyNameKind::File(CONFIG_FILE.to_string()), - PropertyNameKind::Env, - PropertyNameKind::Cli, - ], - opa.spec.servers.clone(), - ), - )] - .into(), - ) - .context(ProductConfigTransformSnafu)?, - &ctx.product_config, - false, - false, - ) - .context(InvalidProductConfigSnafu)?; - let role_server_config = validated_config + let role_server_config = validated + .validated_role_config .get(&opa_role.to_string()) .map(Cow::Borrowed) .unwrap_or_default(); let server_role_service = - build_server_role_service(opa, &resolved_product_image).context(BuildServiceSnafu)?; + build_server_role_service(opa, &validated.image).context(BuildServiceSnafu)?; // required for discovery config map later let server_role_service = cluster_resources .add(client, server_role_service) @@ -534,20 +511,15 @@ pub async fn reconcile_opa( .merged_config(&opa_role, &rolegroup) .context(FailedToResolveConfigSnafu)?; - let rg_configmap = build_server_rolegroup_config_map( - opa, - &resolved_product_image, - &rolegroup, - &merged_config, - )?; - let rg_service = build_rolegroup_headless_service(opa, &resolved_product_image, &rolegroup) + let rg_configmap = + build_server_rolegroup_config_map(opa, &validated.image, &rolegroup, &merged_config)?; + let rg_service = build_rolegroup_headless_service(opa, &validated.image, &rolegroup) + .context(BuildServiceSnafu)?; + let rg_metrics_service = build_rolegroup_metrics_service(opa, &validated.image, &rolegroup) .context(BuildServiceSnafu)?; - let rg_metrics_service = - build_rolegroup_metrics_service(opa, &resolved_product_image, &rolegroup) - .context(BuildServiceSnafu)?; let rg_daemonset = build_server_rolegroup_daemonset( opa, - &resolved_product_image, + &validated.image, &opa_role, &rolegroup, rolegroup_config, @@ -610,7 +582,7 @@ pub async fn reconcile_opa( for discovery_cm in build_discovery_configmaps( opa, opa, - &resolved_product_image, + &validated.image, &server_role_service, &client.kubernetes_cluster_info, ) diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs new file mode 100644 index 00000000..6c69daa1 --- /dev/null +++ b/rust/operator-binary/src/controller/dereference.rs @@ -0,0 +1,27 @@ +//! The dereference step in the OpaCluster controller +//! +//! Fetches all Kubernetes objects referenced by the OpaCluster spec and returns them in +//! [`DereferencedObjects`]. This is currently scaffolding — no objects are dereferenced yet. +//! Follow-up work might move existing inline lookups (e.g. the `user_info_fetcher` Secrets and +//! SecretClasses) through here. + +use snafu::Snafu; +use stackable_operator::client::Client; + +use crate::crd::v1alpha2; + +#[derive(Snafu, Debug)] +pub enum Error {} + +type Result = std::result::Result; + +/// Kubernetes objects referenced from the OpaCluster spec, already fetched. +pub struct DereferencedObjects {} + +/// Fetches all Kubernetes objects referenced from the [`v1alpha2::OpaCluster`] spec. +pub async fn dereference( + _client: &Client, + _opa: &v1alpha2::OpaCluster, +) -> Result { + Ok(DereferencedObjects {}) +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs new file mode 100644 index 00000000..cd78ba4e --- /dev/null +++ b/rust/operator-binary/src/controller/validate.rs @@ -0,0 +1,93 @@ +//! The validate step in the OpaCluster controller +//! +//! Synchronously validates inputs that don't require a Kubernetes client. Produces +//! [`ValidatedInputs`], consumed by the rest of `reconcile_opa`. + +use product_config::{ProductConfigManager, types::PropertyNameKind}; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + cli::OperatorEnvironmentOptions, + commons::product_image_selection::{self, ResolvedProductImage}, + product_config_utils::{ + ValidatedRoleConfigByPropertyKind, transform_all_roles_to_config, + validate_all_roles_and_groups_config, + }, +}; + +use crate::{ + controller::dereference::DereferencedObjects, + crd::{OpaRole, v1alpha2}, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to resolve product image"))] + ResolveProductImage { + source: product_image_selection::Error, + }, + + #[snafu(display("failed to transform configs"))] + ProductConfigTransform { + source: stackable_operator::product_config_utils::Error, + }, + + #[snafu(display("invalid product config"))] + InvalidProductConfig { + source: stackable_operator::product_config_utils::Error, + }, +} + +type Result = std::result::Result; + +/// Synchronous inputs the rest of `reconcile_opa` needs after dereferencing. +pub struct ValidatedInputs { + pub image: ResolvedProductImage, + pub validated_role_config: ValidatedRoleConfigByPropertyKind, +} + +/// Validates the cluster spec and the dereferenced inputs. +pub fn validate( + opa: &v1alpha2::OpaCluster, + _dereferenced_objects: &DereferencedObjects, + operator_environment: &OperatorEnvironmentOptions, + product_config: &ProductConfigManager, +) -> Result { + let image = opa + .spec + .image + .resolve( + super::CONTAINER_IMAGE_BASE_NAME, + &operator_environment.image_repository, + crate::built_info::PKG_VERSION, + ) + .context(ResolveProductImageSnafu)?; + + let validated_role_config = validate_all_roles_and_groups_config( + &image.product_version, + &transform_all_roles_to_config( + opa, + &[( + OpaRole::Server.to_string(), + ( + vec![ + PropertyNameKind::File(super::CONFIG_FILE.to_string()), + PropertyNameKind::Env, + PropertyNameKind::Cli, + ], + opa.spec.servers.clone(), + ), + )] + .into(), + ) + .context(ProductConfigTransformSnafu)?, + product_config, + false, + false, + ) + .context(InvalidProductConfigSnafu)?; + + Ok(ValidatedInputs { + image, + validated_role_config, + }) +} From a72f11011222aa87d8703c80ddd3d057db58f251 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 14:07:32 +0200 Subject: [PATCH 2/6] fix: update crate hashes --- Cargo.nix | 18 +++++++++--------- crate-hashes.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index 69527ec0..a8a32ce3 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -5686,7 +5686,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "k8s_version"; authors = [ @@ -11800,7 +11800,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "stackable_certs"; authors = [ @@ -12213,7 +12213,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "stackable_operator"; authors = [ @@ -12393,7 +12393,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -12428,7 +12428,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "stackable_shared"; authors = [ @@ -12509,7 +12509,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "stackable_telemetry"; authors = [ @@ -12619,7 +12619,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "stackable_versioned"; authors = [ @@ -12669,7 +12669,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -12737,7 +12737,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "57d8d1270016fdbe159fdf39b60cd02b465935af"; - sha256 = "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2"; + sha256 = "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index d202d9ab..afbf785b 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,14 +1,14 @@ { "git+https://github.com/stackabletech/krb5-rs.git?tag=v0.1.0#krb5-sys@0.1.0": "148zr0q04163hpirkrff5q7cbxqgwzzxh0091zr4g23x7l64jh39", "git+https://github.com/stackabletech/krb5-rs.git?tag=v0.1.0#krb5@0.1.0": "148zr0q04163hpirkrff5q7cbxqgwzzxh0091zr4g23x7l64jh39", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#k8s-version@0.1.3": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-certs@0.4.0": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-operator-derive@0.3.1": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-operator@0.111.1": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-shared@0.1.0": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-telemetry@0.6.3": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-versioned-macros@0.10.0": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-versioned@0.10.0": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", - "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-webhook@0.9.1": "1fgc7i8rhq1nl9m4s69sbfiywy2jx4narpynvm3g54vd5yd4c6m2", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#k8s-version@0.1.3": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-certs@0.4.0": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-operator-derive@0.3.1": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-operator@0.111.1": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-shared@0.1.0": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-telemetry@0.6.3": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-versioned-macros@0.10.0": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-versioned@0.10.0": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", + "git+https://github.com/stackabletech/operator-rs.git?rev=57d8d1270016fdbe159fdf39b60cd02b465935af#stackable-webhook@0.9.1": "1gnp9j4wqfbyycds640xcf31qdafp7fy1gy81iw78wrqx49p3fhf", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file From d59e3dc86c4b9a2406785ccf13a2ef06d418dcce Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 14:10:01 +0200 Subject: [PATCH 3/6] feat: add smoke snapshot test --- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 35 +-- tests/templates/kuttl/smoke/11-assert.yaml | 2 +- tests/templates/kuttl/smoke/12-assert.yaml.j2 | 288 ++++++++++++++++++ tests/templates/kuttl/smoke/13-assert.yaml.j2 | 75 +++++ 4 files changed, 365 insertions(+), 35 deletions(-) create mode 100644 tests/templates/kuttl/smoke/12-assert.yaml.j2 create mode 100644 tests/templates/kuttl/smoke/13-assert.yaml.j2 diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 76703fb2..79efbeb8 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -1,39 +1,6 @@ +--- apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 300 commands: - script: kubectl -n $NAMESPACE wait --for=condition=available opaclusters.opa.stackable.tech/test-opa --timeout 301s ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: test-opa-server-default -spec: - updateStrategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - template: - spec: - containers: - - name: opa - resources: - limits: - cpu: 500m - memory: 256Mi - requests: - cpu: 250m - memory: 256Mi - - name: bundle-builder - resources: - limits: - cpu: 200m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - name: vector -{% endif %} - terminationGracePeriodSeconds: 125 # 2 minutes + 5s safety buffer diff --git a/tests/templates/kuttl/smoke/11-assert.yaml b/tests/templates/kuttl/smoke/11-assert.yaml index 3136c288..b377064e 100644 --- a/tests/templates/kuttl/smoke/11-assert.yaml +++ b/tests/templates/kuttl/smoke/11-assert.yaml @@ -2,7 +2,7 @@ # This test checks if the containerdebug-state.json file is present and valid apiVersion: kuttl.dev/v1beta1 kind: TestAssert -timeout: 600 +timeout: 60 commands: - script: | FIRST_OPA_POD=$(kubectl get -n $NAMESPACE pods --field-selector=status.phase=Running --selector app.kubernetes.io/instance=test-opa -o jsonpath='{.items[0].metadata.name}') diff --git a/tests/templates/kuttl/smoke/12-assert.yaml.j2 b/tests/templates/kuttl/smoke/12-assert.yaml.j2 new file mode 100644 index 00000000..69db5f67 --- /dev/null +++ b/tests/templates/kuttl/smoke/12-assert.yaml.j2 @@ -0,0 +1,288 @@ +{%- set use_tls = test_scenario['values']['use-tls'] == "true" -%} +{%- set port_name = "https" if use_tls else "http" -%} +{%- set port_number = 8443 if use_tls else 8081 -%} +{%- set port_scheme = "HTTPS" if use_tls else "HTTP" -%} +--- +# Resource-level fingerprint of everything the OPA operator creates for the +# `test-opa` OpaCluster: DaemonSet, three Services, two ConfigMaps (existence +# only here — `.data` is asserted in 13-assert), ServiceAccount, RoleBinding. +# +# Catches drift in labels, owner references, selectors, ports, probe schemes, +# update strategy, container resources and TLS-dependent fields. The operator +# does not create a PodDisruptionBudget for OPA, so none is asserted here. +# +# `app.kubernetes.io/version` is intentionally omitted from label matchers so +# that product-version bumps in test-definition.yaml don't force snapshot +# updates everywhere. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 60 +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test-opa-server-default + labels: + app.kubernetes.io/component: server + 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: default + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +spec: + selector: + matchLabels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-opa + app.kubernetes.io/name: opa + app.kubernetes.io/role-group: default + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app.kubernetes.io/component: server + 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: default + stackable.tech/vendor: Stackable + spec: + serviceAccount: test-opa-serviceaccount + serviceAccountName: test-opa-serviceaccount + terminationGracePeriodSeconds: 125 + securityContext: + fsGroup: 1000 + containers: + - name: opa + ports: + - containerPort: {{ port_number }} + name: {{ port_name }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: {{ port_name }} + scheme: {{ port_scheme }} + readinessProbe: + httpGet: + path: / + port: {{ port_name }} + scheme: {{ port_scheme }} + resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 250m + memory: 256Mi + - name: bundle-builder + livenessProbe: + httpGet: + path: /status + port: 3030 + scheme: HTTP + readinessProbe: + httpGet: + path: /status + port: 3030 + scheme: HTTP + resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + - name: vector +{% endif %} + initContainers: + - name: prepare + resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 250m + memory: 256Mi +status: + currentNumberScheduled: 1 + desiredNumberScheduled: 1 + numberAvailable: 1 + numberMisscheduled: 0 + numberReady: 1 + updatedNumberScheduled: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: test-opa-server + labels: + app.kubernetes.io/component: server + 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: global + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +spec: + internalTrafficPolicy: Local + ports: + - name: {{ port_name }} + port: {{ port_number }} + protocol: TCP + targetPort: {{ port_number }} + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-opa + app.kubernetes.io/name: opa + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: test-opa-server-default-headless + labels: + app.kubernetes.io/component: server + 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: default + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +spec: + clusterIP: None + publishNotReadyAddresses: true + ports: + - name: {{ port_name }} + port: {{ port_number }} + protocol: TCP + targetPort: {{ port_number }} + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-opa + app.kubernetes.io/name: opa + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: test-opa-server-default-metrics + annotations: + prometheus.io/path: /metrics + prometheus.io/port: "{{ port_number }}" + prometheus.io/scheme: {{ port_name }} + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/component: server + 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: default + prometheus.io/scrape: "true" + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +spec: + clusterIP: None + ports: + - name: metrics + port: {{ port_number }} + protocol: TCP + targetPort: {{ port_number }} + selector: + app.kubernetes.io/component: server + app.kubernetes.io/instance: test-opa + app.kubernetes.io/name: opa + app.kubernetes.io/role-group: default + type: ClusterIP +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-opa-server-default + labels: + app.kubernetes.io/component: server + 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: default + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-opa + labels: + app.kubernetes.io/component: server + 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: discovery + stackable.tech/vendor: Stackable + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-opa-serviceaccount + labels: + app.kubernetes.io/instance: test-opa + app.kubernetes.io/managed-by: opa.stackable.tech_opacluster + app.kubernetes.io/name: opa + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: test-opa-rolebinding + labels: + app.kubernetes.io/instance: test-opa + app.kubernetes.io/managed-by: opa.stackable.tech_opacluster + app.kubernetes.io/name: opa + ownerReferences: + - apiVersion: opa.stackable.tech/v1alpha2 + controller: true + kind: OpaCluster + name: test-opa +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: opa-clusterrole +subjects: + - kind: ServiceAccount + name: test-opa-serviceaccount diff --git a/tests/templates/kuttl/smoke/13-assert.yaml.j2 b/tests/templates/kuttl/smoke/13-assert.yaml.j2 new file mode 100644 index 00000000..dcc0a21e --- /dev/null +++ b/tests/templates/kuttl/smoke/13-assert.yaml.j2 @@ -0,0 +1,75 @@ +--- +# Snapshot the full `.data` of each operator-managed ConfigMap. Any code change +# that alters rendered config values will fail these diffs. +# +# Both sides are normalized to canonical JSON via `jq -S` (sort-keys), and +# string-compared. Uses `jq` rather than `yq` because the local snap `yq` is +# broken and `yq` is not available in the test-opa pod either. +# +# The heredoc carrying the multi-line OPA config is quoted (`<<'CONFEOF'`) so +# shell substitution is disabled. Values that depend on the kuttl-randomized +# namespace are passed in via `--arg` rather than `sed`. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 60 +commands: + # ConfigMap test-opa-server-default — bundle-builder + server config.json. + # Content is identical between TLS and non-TLS (URL points at localhost:3030). + - script: | + expected_config=$(cat <<'CONFEOF' + { + "bundles": { + "stackable": { + "persist": true, + "polling": { + "max_delay_seconds": 20, + "min_delay_seconds": 10 + }, + "resource": "opa/bundle.tar.gz", + "service": "stackable" + } + }, + "services": [ + { + "name": "stackable", + "url": "http://localhost:3030/opa/v1" + } + ], + "status": { + "prometheus": true + } + } + CONFEOF + ) + expected=$(jq -Sn --arg cfg "$expected_config" '{"config.json": $cfg}') + actual=$(kubectl -n $NAMESPACE get cm test-opa-server-default -o json | jq -S '.data') + if [ "$expected" != "$actual" ]; then + echo "ERROR: ConfigMap test-opa-server-default data drifted from snapshot." + echo "=== expected ===" + printf '%s\n' "$expected" + echo "=== actual ===" + printf '%s\n' "$actual" + exit 1 + fi + # ConfigMap test-opa — discovery CM. URL scheme/port flip by TLS, and + # OPA_SECRET_CLASS only appears when TLS is on. + - script: | +{% if test_scenario['values']['use-tls'] == "true" %} + expected=$(jq -Sn --arg ns "$NAMESPACE" '{ + OPA: "https://test-opa-server.\($ns).svc.cluster.local:8443/", + OPA_SECRET_CLASS: "opa-tls-\($ns)" + }') +{% else %} + expected=$(jq -Sn --arg ns "$NAMESPACE" '{ + OPA: "http://test-opa-server.\($ns).svc.cluster.local:8081/" + }') +{% endif %} + actual=$(kubectl -n $NAMESPACE get cm test-opa -o json | jq -S '.data') + if [ "$expected" != "$actual" ]; then + echo "ERROR: ConfigMap test-opa data drifted from snapshot." + echo "=== expected ===" + printf '%s\n' "$expected" + echo "=== actual ===" + printf '%s\n' "$actual" + exit 1 + fi From f741165e9726ae3345221ec826bbe99055547fd1 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 19 May 2026 14:16:36 +0200 Subject: [PATCH 4/6] doc: adapted changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ebb929..cc5f66f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,14 @@ All notable changes to this project will be documented in this file. - Set `maxSurge=1` and `maxUnavailable=0` on the OPA DaemonSet rolling update strategy to eliminate availability gaps during rolling updates ([#819]). - Document Helm deployed RBAC permissions and remove unnecessary permissions ([#820]). +- Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#836]). [#818]: https://github.com/stackabletech/opa-operator/pull/818 [#819]: https://github.com/stackabletech/opa-operator/pull/819 [#820]: https://github.com/stackabletech/opa-operator/pull/820 [#830]: https://github.com/stackabletech/opa-operator/pull/830 [#831]: https://github.com/stackabletech/opa-operator/pull/831 +[#836]: https://github.com/stackabletech/opa-operator/pull/836 ## [26.3.0] - 2026-03-16 From 04c73279cea0bb4a846aba4f76fb2a2b6649de1e Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 20 May 2026 16:29:24 +0200 Subject: [PATCH 5/6] fix(test): remove hardcoded 1 replica from 1 node test cluster --- tests/templates/kuttl/smoke/12-assert.yaml.j2 | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/templates/kuttl/smoke/12-assert.yaml.j2 b/tests/templates/kuttl/smoke/12-assert.yaml.j2 index 69db5f67..9fcaa65d 100644 --- a/tests/templates/kuttl/smoke/12-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/12-assert.yaml.j2 @@ -114,13 +114,6 @@ spec: requests: cpu: 250m memory: 256Mi -status: - currentNumberScheduled: 1 - desiredNumberScheduled: 1 - numberAvailable: 1 - numberMisscheduled: 0 - numberReady: 1 - updatedNumberScheduled: 1 --- apiVersion: v1 kind: Service From abd6e554579de5dcd8e25883b8d18f19e626efb7 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Thu, 21 May 2026 09:17:52 +0200 Subject: [PATCH 6/6] fix: remove empty dereference step --- rust/operator-binary/src/controller.rs | 19 +++---------- .../src/controller/dereference.rs | 27 ------------------- .../src/controller/validate.rs | 6 +---- 3 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 rust/operator-binary/src/controller/dereference.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index d6cfa562..23034272 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -88,7 +88,6 @@ use crate::{ }, }; -mod dereference; mod validate; pub const OPA_CONTROLLER_NAME: &str = "opacluster"; @@ -327,9 +326,6 @@ pub enum Error { #[snafu(display("failed to build service"))] BuildService { source: service::Error }, - #[snafu(display("failed to dereference resources"))] - Dereference { source: dereference::Error }, - #[snafu(display("failed to validate cluster"))] ValidateCluster { source: validate::Error }, @@ -442,19 +438,10 @@ pub async fn reconcile_opa( let client = &ctx.client; - // dereference (client required) - let dereferenced_objects = dereference::dereference(client, opa) - .await - .context(DereferenceSnafu)?; - + // NOTE(@maltesander): There currently is no dereference (client required) step for OPA. // validate (no client required) - let validated = validate::validate( - opa, - &dereferenced_objects, - &ctx.operator_environment, - &ctx.product_config, - ) - .context(ValidateClusterSnafu)?; + let validated = validate::validate(opa, &ctx.operator_environment, &ctx.product_config) + .context(ValidateClusterSnafu)?; let opa_role = OpaRole::Server; diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs deleted file mode 100644 index 6c69daa1..00000000 --- a/rust/operator-binary/src/controller/dereference.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! The dereference step in the OpaCluster controller -//! -//! Fetches all Kubernetes objects referenced by the OpaCluster spec and returns them in -//! [`DereferencedObjects`]. This is currently scaffolding — no objects are dereferenced yet. -//! Follow-up work might move existing inline lookups (e.g. the `user_info_fetcher` Secrets and -//! SecretClasses) through here. - -use snafu::Snafu; -use stackable_operator::client::Client; - -use crate::crd::v1alpha2; - -#[derive(Snafu, Debug)] -pub enum Error {} - -type Result = std::result::Result; - -/// Kubernetes objects referenced from the OpaCluster spec, already fetched. -pub struct DereferencedObjects {} - -/// Fetches all Kubernetes objects referenced from the [`v1alpha2::OpaCluster`] spec. -pub async fn dereference( - _client: &Client, - _opa: &v1alpha2::OpaCluster, -) -> Result { - Ok(DereferencedObjects {}) -} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index cd78ba4e..ed3deec8 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -14,10 +14,7 @@ use stackable_operator::{ }, }; -use crate::{ - controller::dereference::DereferencedObjects, - crd::{OpaRole, v1alpha2}, -}; +use crate::crd::{OpaRole, v1alpha2}; #[derive(Snafu, Debug)] pub enum Error { @@ -48,7 +45,6 @@ pub struct ValidatedInputs { /// Validates the cluster spec and the dereferenced inputs. pub fn validate( opa: &v1alpha2::OpaCluster, - _dereferenced_objects: &DereferencedObjects, operator_environment: &OperatorEnvironmentOptions, product_config: &ProductConfigManager, ) -> Result {