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 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 diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 52ba3a04..23034272 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,8 @@ use crate::{ }, }; +mod validate; + pub const OPA_CONTROLLER_NAME: &str = "opacluster"; pub const OPA_FULL_CONTROLLER_NAME: &str = concatcp!(OPA_CONTROLLER_NAME, '.', OPERATOR_NAME); @@ -230,11 +231,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 +244,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 +323,12 @@ 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 validate cluster"))] + ValidateCluster { source: validate::Error }, + #[snafu(display("failed to build TLS volume"))] TlsVolumeBuild { source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError, @@ -448,15 +437,12 @@ 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)?; + + // NOTE(@maltesander): There currently is no dereference (client required) step for OPA. + // validate (no client required) + let validated = validate::validate(opa, &ctx.operator_environment, &ctx.product_config) + .context(ValidateClusterSnafu)?; + let opa_role = OpaRole::Server; let mut cluster_resources = ClusterResources::new( @@ -469,36 +455,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 +498,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 +569,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/validate.rs b/rust/operator-binary/src/controller/validate.rs new file mode 100644 index 00000000..ed3deec8 --- /dev/null +++ b/rust/operator-binary/src/controller/validate.rs @@ -0,0 +1,89 @@ +//! 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::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, + 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, + }) +} 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..9fcaa65d --- /dev/null +++ b/tests/templates/kuttl/smoke/12-assert.yaml.j2 @@ -0,0 +1,281 @@ +{%- 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 +--- +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