From 8206b3d5b1617bec79075f5aba78d34b632dca4d Mon Sep 17 00:00:00 2001 From: Nick <10092581+NickLarsenNZ@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:48:29 +0200 Subject: [PATCH 1/5] fix: Use operator name in docs templating script (#723) Co-authored-by: Stacky McStackface <95074132+stackable-bot@users.noreply.github.com> Co-authored-by: Techassi --- scripts/docs_templating.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/docs_templating.sh b/scripts/docs_templating.sh index 8468a971..51dd9bd1 100755 --- a/scripts/docs_templating.sh +++ b/scripts/docs_templating.sh @@ -39,6 +39,8 @@ do done # Ensure this script is executable -chmod +x docs/modules/opensearch/examples/getting_started/getting_started.sh +chmod +x "docs/modules/spark-k8s/examples/getting_started/getting_started.sh" \ + || chmod +x "docs/modules/spark-k8s/examples/getting_started/code/getting_started.sh" \ + || true echo "done" From fdc7567d92fca6ff32221c63b290bd97eafd3c25 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 16 Jul 2026 19:48:03 +0200 Subject: [PATCH 2/5] fix: Bump spin to 0.9.9 (#725) --- Cargo.lock | 4 ++-- Cargo.nix | 22 +++++++++++----------- crate-hashes.json | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fce88c0..d255d1bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2992,9 +2992,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" [[package]] name = "spki" diff --git a/Cargo.nix b/Cargo.nix index fd10413f..c6b84297 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -5109,7 +5109,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "k8s_version"; authors = [ @@ -9817,9 +9817,9 @@ rec { }; "spin" = rec { crateName = "spin"; - version = "0.9.8"; + version = "0.9.9"; edition = "2015"; - sha256 = "0rvam5r0p3a6qhc18scqpvpgb3ckzyqxpgdfyjnghh8ja7byi039"; + sha256 = "03psal0vh1xdxp7agphw09p7kf50v3bj1zshijq1s5bkdd7jcqrp"; authors = [ "Mathijs van de Nes " "John Ericson " @@ -9893,7 +9893,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "stackable_certs"; authors = [ @@ -9996,7 +9996,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "stackable_operator"; authors = [ @@ -10195,7 +10195,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -10230,7 +10230,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "stackable_shared"; authors = [ @@ -10417,7 +10417,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "stackable_telemetry"; authors = [ @@ -10527,7 +10527,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "stackable_versioned"; authors = [ @@ -10577,7 +10577,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10645,7 +10645,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; rev = "013bbf43f7006a4ddfc08a147f68441ed88b462b"; - sha256 = "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps"; + sha256 = "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index cd3917c3..77adf52a 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#k8s-version@0.1.3": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-certs@0.4.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator-derive@0.3.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator@0.113.3": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-shared@0.1.2": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-telemetry@0.6.5": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned-macros@0.11.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned@0.11.1": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-webhook@0.9.2": "054p2mcinq3x4iykmc72nhga7kapc8ihkx9mnzqq3qdm2bk2qcps", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#k8s-version@0.1.3": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-certs@0.4.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator-derive@0.3.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-operator@0.113.3": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-shared@0.1.2": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-telemetry@0.6.5": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned-macros@0.11.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-versioned@0.11.1": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.113.3#stackable-webhook@0.9.2": "1ab5qmawljwbll6b44gw3md57s9kjiy5p16akz6rv52j6fdxjpgr", "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 601fe9b786bfd617635f29b661574c4d2b42849d Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 17 Jul 2026 14:27:53 +0200 Subject: [PATCH 3/5] chore: Release 26.7.0-rc1 (#724) Signed-off-by: Techassi Co-authored-by: Nick <10092581+NickLarsenNZ@users.noreply.github.com> --- CHANGELOG.md | 2 + Cargo.lock | 2 +- Cargo.nix | 2 +- Cargo.toml | 2 +- deploy/helm/spark-k8s-operator/Chart.yaml | 4 +- docs/antora.yml | 3 +- .../getting_started/getting_started.sh | 16 +- .../getting_started/install_output.txt | 8 +- docs/templating_vars.yaml | 8 +- extra/crds.yaml | 250 +++++++++--------- tests/release.yaml | 16 +- 11 files changed, 158 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efaf4921..2e9e3870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [26.7.0-rc1] - 2026-07-16 + ### Added - BREAKING: Add required CLI argument and env var to set the image repository used to construct final product image names: `IMAGE_REPOSITORY` (`--image-repository`), eg. `oci.example.org/my/namespace` ([#684]). diff --git a/Cargo.lock b/Cargo.lock index d255d1bd..3430ed4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3111,7 +3111,7 @@ dependencies = [ [[package]] name = "stackable-spark-k8s-operator" -version = "0.0.0-dev" +version = "26.7.0-rc1" dependencies = [ "anyhow", "built", diff --git a/Cargo.nix b/Cargo.nix index c6b84297..0e723ea0 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10305,7 +10305,7 @@ rec { }; "stackable-spark-k8s-operator" = rec { crateName = "stackable-spark-k8s-operator"; - version = "0.0.0-dev"; + version = "26.7.0-rc1"; edition = "2024"; crateBin = [ { diff --git a/Cargo.toml b/Cargo.toml index b7cb1efb..dd74de20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["rust/operator-binary"] resolver = "2" [workspace.package] -version = "0.0.0-dev" +version = "26.7.0-rc1" authors = ["Stackable GmbH "] license = "OSL-3.0" edition = "2024" diff --git a/deploy/helm/spark-k8s-operator/Chart.yaml b/deploy/helm/spark-k8s-operator/Chart.yaml index 5d137d3a..3293099b 100644 --- a/deploy/helm/spark-k8s-operator/Chart.yaml +++ b/deploy/helm/spark-k8s-operator/Chart.yaml @@ -1,8 +1,8 @@ --- apiVersion: v2 name: spark-k8s-operator -version: "0.0.0-dev" -appVersion: "0.0.0-dev" +version: "26.7.0-rc1" +appVersion: "26.7.0-rc1" description: The Stackable Operator for Apache Spark-on-Kubernetes home: https://github.com/stackabletech/spark-k8s-operator maintainers: diff --git a/docs/antora.yml b/docs/antora.yml index d522680b..931469da 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,4 @@ --- name: home -version: "nightly" +version: "26.7" +prerelease: false diff --git a/docs/modules/spark-k8s/examples/getting_started/getting_started.sh b/docs/modules/spark-k8s/examples/getting_started/getting_started.sh index 36e5f126..d25768d7 100755 --- a/docs/modules/spark-k8s/examples/getting_started/getting_started.sh +++ b/docs/modules/spark-k8s/examples/getting_started/getting_started.sh @@ -23,20 +23,20 @@ case "$1" in "helm") echo "Installing Operators with Helm" # tag::helm-install-operators[] -helm install --wait commons-operator oci://oci.stackable.tech/sdp-charts/commons-operator --version 0.0.0-dev -helm install --wait secret-operator oci://oci.stackable.tech/sdp-charts/secret-operator --version 0.0.0-dev -helm install --wait listener-operator oci://oci.stackable.tech/sdp-charts/listener-operator --version 0.0.0-dev -helm install --wait spark-k8s-operator oci://oci.stackable.tech/sdp-charts/spark-k8s-operator --version 0.0.0-dev +helm install --wait commons-operator oci://oci.stackable.tech/sdp-charts/commons-operator --version 26.7.0-rc1 +helm install --wait secret-operator oci://oci.stackable.tech/sdp-charts/secret-operator --version 26.7.0-rc1 +helm install --wait listener-operator oci://oci.stackable.tech/sdp-charts/listener-operator --version 26.7.0-rc1 +helm install --wait spark-k8s-operator oci://oci.stackable.tech/sdp-charts/spark-k8s-operator --version 26.7.0-rc1 # end::helm-install-operators[] ;; "stackablectl") echo "installing Operators with stackablectl" # tag::stackablectl-install-operators[] stackablectl operator install \ - commons=0.0.0-dev \ - secret=0.0.0-dev \ - listener=0.0.0-dev \ - spark-k8s=0.0.0-dev + commons=26.7.0-rc1 \ + secret=26.7.0-rc1 \ + listener=26.7.0-rc1 \ + spark-k8s=26.7.0-rc1 # end::stackablectl-install-operators[] ;; *) diff --git a/docs/modules/spark-k8s/examples/getting_started/install_output.txt b/docs/modules/spark-k8s/examples/getting_started/install_output.txt index 1664d30f..43bf4a4e 100644 --- a/docs/modules/spark-k8s/examples/getting_started/install_output.txt +++ b/docs/modules/spark-k8s/examples/getting_started/install_output.txt @@ -1,4 +1,4 @@ -Installed commons=0.0.0-dev operator -Installed secret=0.0.0-dev operator -Installed listener=0.0.0-dev operator -Installed spark-k8s=0.0.0-dev operator +Installed commons=26.7.0-rc1 operator +Installed secret=26.7.0-rc1 operator +Installed listener=26.7.0-rc1 operator +Installed spark-k8s=26.7.0-rc1 operator diff --git a/docs/templating_vars.yaml b/docs/templating_vars.yaml index c7d50fdf..c783eb96 100644 --- a/docs/templating_vars.yaml +++ b/docs/templating_vars.yaml @@ -3,7 +3,7 @@ helm: repo_name: sdp-charts repo_url: oci.stackable.tech versions: - commons: 0.0.0-dev - secret: 0.0.0-dev - listener: 0.0.0-dev - spark: 0.0.0-dev + commons: 26.7.0-rc1 + secret: 26.7.0-rc1 + listener: 26.7.0-rc1 + spark: 26.7.0-rc1 diff --git a/extra/crds.yaml b/extra/crds.yaml index ab28308f..57cff175 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -24,7 +24,7 @@ spec: description: |- A Spark application run on Kubernetes by the Stackable operator for Apache Spark. Find more information on how to use it and the resources that the operator generates in the - [operator documentation](https://docs.stackable.tech/home/nightly/spark-k8s/). + [operator documentation](https://docs.stackable.tech/home/26.7/spark-k8s/). The SparkApplication CRD looks a little different than the CRDs of the other products on the Stackable Data Platform. @@ -95,7 +95,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -123,7 +123,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -685,7 +685,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -710,7 +710,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -721,7 +721,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -750,7 +750,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -884,7 +884,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -912,7 +912,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -1474,7 +1474,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -1499,7 +1499,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -1510,7 +1510,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -1539,7 +1539,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -1553,7 +1553,7 @@ spec: image: description: |- User-supplied image containing spark-job dependencies that will be copied to the specified volume mount. - See the [examples](https://docs.stackable.tech/home/nightly/spark-k8s/usage-guide/examples). + See the [examples](https://docs.stackable.tech/home/26.7/spark-k8s/usage-guide/examples). nullable: true type: string job: @@ -1580,7 +1580,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -1682,7 +1682,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -1707,7 +1707,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -1718,7 +1718,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -1747,7 +1747,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -1777,7 +1777,7 @@ spec: inline: description: |- S3 bucket specification containing the bucket name and an inlined or referenced connection specification. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: bucketName: description: The name of the S3 bucket. @@ -1793,7 +1793,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -1808,14 +1808,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -1848,7 +1848,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -1907,7 +1907,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -1960,7 +1960,7 @@ spec: s3connection: description: |- Configure an S3 connection that the SparkApplication has access to. - Read more in the [Spark S3 usage guide](https://docs.stackable.tech/home/nightly/spark-k8s/usage-guide/s3). + Read more in the [Spark S3 usage guide](https://docs.stackable.tech/home/26.7/spark-k8s/usage-guide/s3). nullable: true oneOf: - required: @@ -1971,7 +1971,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -1986,14 +1986,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -2026,7 +2026,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -2085,7 +2085,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -2126,7 +2126,7 @@ spec: You can also configure a custom image registry to pull from, as well as completely custom images. - Consult the [Product image selection documentation](https://docs.stackable.tech/home/nightly/concepts/product_image_selection) + Consult the [Product image selection documentation](https://docs.stackable.tech/home/26.7/concepts/product_image_selection) for details. properties: custom: @@ -2179,9 +2179,9 @@ spec: type: object vectorAggregatorConfigMapName: description: |- - Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery). + Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/26.7/concepts/service_discovery). It must contain the key `ADDRESS` with the address of the Vector aggregator. - Follow the [logging tutorial](https://docs.stackable.tech/home/nightly/tutorials/logging-vector-aggregator) + Follow the [logging tutorial](https://docs.stackable.tech/home/26.7/tutorials/logging-vector-aggregator) to learn how to configure log aggregation with Vector. maxLength: 253 minLength: 1 @@ -2256,7 +2256,7 @@ spec: description: |- A Spark cluster history server component. This resource is managed by the Stackable operator for Apache Spark. Find more information on how to use it in the - [operator documentation](https://docs.stackable.tech/home/nightly/spark-k8s/usage-guide/history-server). + [operator documentation](https://docs.stackable.tech/home/26.7/spark-k8s/usage-guide/history-server). properties: image: anyOf: @@ -2270,7 +2270,7 @@ spec: You can also configure a custom image registry to pull from, as well as completely custom images. - Consult the [Product image selection documentation](https://docs.stackable.tech/home/nightly/concepts/product_image_selection) + Consult the [Product image selection documentation](https://docs.stackable.tech/home/26.7/concepts/product_image_selection) for details. properties: custom: @@ -2345,7 +2345,7 @@ spec: inline: description: |- S3 bucket specification containing the bucket name and an inlined or referenced connection specification. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: bucketName: description: The name of the S3 bucket. @@ -2361,7 +2361,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -2376,14 +2376,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -2416,7 +2416,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -2475,7 +2475,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -2531,7 +2531,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -2562,7 +2562,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -2798,7 +2798,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -2829,7 +2829,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -2840,7 +2840,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -2869,7 +2869,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -2901,7 +2901,7 @@ spec: 2. The allowed number of Pods to be unavailable (`maxUnavailable`) Learn more in the - [allowed Pod disruptions documentation](https://docs.stackable.tech/home/nightly/concepts/operations/pod_disruptions). + [allowed Pod disruptions documentation](https://docs.stackable.tech/home/26.7/concepts/operations/pod_disruptions). properties: enabled: default: true @@ -2941,7 +2941,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -2972,7 +2972,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -3208,7 +3208,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -3239,7 +3239,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -3250,7 +3250,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -3279,7 +3279,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -3303,7 +3303,7 @@ spec: names it `default`. Read the - [roles and role groups concept documentation](https://docs.stackable.tech/home/nightly/concepts/roles-and-role-groups) + [roles and role groups concept documentation](https://docs.stackable.tech/home/26.7/concepts/roles-and-role-groups) for more details. type: object required: @@ -3317,7 +3317,7 @@ spec: List entries are arbitrary YAML objects, which need to be valid Kubernetes objects. - Read the [Object overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#object-overrides) + Read the [Object overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#object-overrides) for more information. items: type: object @@ -3376,7 +3376,7 @@ spec: description: |- An Apache Spark Connect server component. This resource is managed by the Stackable operator for Apache Spark. Find more information on how to use it in the - [operator documentation](https://docs.stackable.tech/home/nightly/spark-k8s/usage-guide/connect-server). + [operator documentation](https://docs.stackable.tech/home/26.7/spark-k8s/usage-guide/connect-server). properties: args: default: [] @@ -3389,7 +3389,7 @@ spec: reconciliationPaused: false stopped: false description: |- - [Cluster operations](https://docs.stackable.tech/home/nightly/concepts/operations/cluster_operations) + [Cluster operations](https://docs.stackable.tech/home/26.7/concepts/operations/cluster_operations) properties, allow stopping the product instance as well as pausing reconciliation. properties: reconciliationPaused: @@ -3429,7 +3429,7 @@ spec: inline: description: |- S3 bucket specification containing the bucket name and an inlined or referenced connection specification. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: bucketName: description: The name of the S3 bucket. @@ -3445,7 +3445,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -3460,14 +3460,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -3500,7 +3500,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -3559,7 +3559,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -3601,7 +3601,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -3616,14 +3616,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -3656,7 +3656,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -3715,7 +3715,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -3759,7 +3759,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -3787,7 +3787,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -4023,7 +4023,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -4066,7 +4066,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -4077,7 +4077,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -4106,7 +4106,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -4123,7 +4123,7 @@ spec: You can also configure a custom image registry to pull from, as well as completely custom images. - Consult the [Product image selection documentation](https://docs.stackable.tech/home/nightly/concepts/product_image_selection) + Consult the [Product image selection documentation](https://docs.stackable.tech/home/26.7/concepts/product_image_selection) for details. properties: custom: @@ -4182,7 +4182,7 @@ spec: List entries are arbitrary YAML objects, which need to be valid Kubernetes objects. - Read the [Object overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#object-overrides) + Read the [Object overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#object-overrides) for more information. items: type: object @@ -4206,7 +4206,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -4442,7 +4442,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -4485,7 +4485,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -4496,7 +4496,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -4525,7 +4525,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -4537,7 +4537,7 @@ spec: listenerClass: default: cluster-internal description: |- - This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) + This field controls which [ListenerClass](https://docs.stackable.tech/home/26.7/listener-operator/listenerclass.html) is used to expose the Spark Connect services. maxLength: 253 minLength: 1 @@ -4633,7 +4633,7 @@ spec: description: |- A Spark application template. This resource is managed by the Stackable operator for Apache Spark. Find more information on how to use it and the resources that the operator generates in the - [operator documentation](https://docs.stackable.tech/home/nightly/spark-k8s/). + [operator documentation](https://docs.stackable.tech/home/26.7/spark-k8s/). properties: args: default: [] @@ -4701,7 +4701,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -4729,7 +4729,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -5291,7 +5291,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -5316,7 +5316,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -5327,7 +5327,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -5356,7 +5356,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -5490,7 +5490,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -5518,7 +5518,7 @@ spec: default: containers: {} enableVectorAgent: null - description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/nightly/concepts/logging). + description: Logging configuration, learn more in the [logging concept documentation](https://docs.stackable.tech/home/26.7/concepts/logging). properties: containers: description: Log configuration per container. @@ -6080,7 +6080,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -6105,7 +6105,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -6116,7 +6116,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -6145,7 +6145,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -6159,7 +6159,7 @@ spec: image: description: |- User-supplied image containing spark-job dependencies that will be copied to the specified volume mount. - See the [examples](https://docs.stackable.tech/home/nightly/spark-k8s/usage-guide/examples). + See the [examples](https://docs.stackable.tech/home/26.7/spark-k8s/usage-guide/examples). nullable: true type: string job: @@ -6186,7 +6186,7 @@ spec: podAntiAffinity: null description: |- These configuration settings control - [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + [Pod placement](https://docs.stackable.tech/home/26.7/concepts/operations/pod_placement). properties: nodeAffinity: description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) @@ -6288,7 +6288,7 @@ spec: description: |- The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the - [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) + [config overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. properties: @@ -6313,7 +6313,7 @@ spec: `envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the - [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) + [environment variable overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available. type: object @@ -6324,7 +6324,7 @@ spec: removeRegex: [] description: |- Allows overriding JVM arguments. - Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#jvm-argument-overrides) + Please read on the [JVM argument overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#jvm-argument-overrides) for details on the usage. properties: add: @@ -6353,7 +6353,7 @@ spec: [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the - [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) + [Pod overrides documentation](https://docs.stackable.tech/home/26.7/concepts/overrides#pod-overrides) for more information. type: object x-kubernetes-preserve-unknown-fields: true @@ -6383,7 +6383,7 @@ spec: inline: description: |- S3 bucket specification containing the bucket name and an inlined or referenced connection specification. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: bucketName: description: The name of the S3 bucket. @@ -6399,7 +6399,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -6414,14 +6414,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -6454,7 +6454,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -6513,7 +6513,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -6566,7 +6566,7 @@ spec: s3connection: description: |- Configure an S3 connection that the SparkApplication has access to. - Read more in the [Spark S3 usage guide](https://docs.stackable.tech/home/nightly/spark-k8s/usage-guide/s3). + Read more in the [Spark S3 usage guide](https://docs.stackable.tech/home/26.7/spark-k8s/usage-guide/s3). nullable: true oneOf: - required: @@ -6577,7 +6577,7 @@ spec: inline: description: |- S3 connection definition as a resource. - Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/nightly/concepts/s3). + Learn more on the [S3 concept documentation](https://docs.stackable.tech/home/26.7/concepts/s3). properties: accessStyle: default: VirtualHosted @@ -6592,14 +6592,14 @@ spec: credentials: description: |- If the S3 uses authentication you have to specify you S3 credentials. - In the most cases a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) + In the most cases a [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing `accessKey` and `secretKey` is sufficient. nullable: true properties: scope: description: |- - [Scope](https://docs.stackable.tech/home/nightly/secret-operator/scope) of the - [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + [Scope](https://docs.stackable.tech/home/26.7/secret-operator/scope) of the + [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass). nullable: true properties: listenerVolumes: @@ -6632,7 +6632,7 @@ spec: type: array type: object secretClass: - description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.' + description: '[SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) providing the requested secrets.' type: string required: - secretClass @@ -6691,7 +6691,7 @@ spec: properties: secretClass: description: |- - Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate. + Name of the [SecretClass](https://docs.stackable.tech/home/26.7/secret-operator/secretclass) which will provide the CA certificate. Note that a SecretClass does not need to have a key but can also work with just a CA certificate, so if you got provided with a CA cert but don't have access to the key you can still use this method. type: string @@ -6732,7 +6732,7 @@ spec: You can also configure a custom image registry to pull from, as well as completely custom images. - Consult the [Product image selection documentation](https://docs.stackable.tech/home/nightly/concepts/product_image_selection) + Consult the [Product image selection documentation](https://docs.stackable.tech/home/26.7/concepts/product_image_selection) for details. properties: custom: @@ -6785,9 +6785,9 @@ spec: type: object vectorAggregatorConfigMapName: description: |- - Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery). + Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/26.7/concepts/service_discovery). It must contain the key `ADDRESS` with the address of the Vector aggregator. - Follow the [logging tutorial](https://docs.stackable.tech/home/nightly/tutorials/logging-vector-aggregator) + Follow the [logging tutorial](https://docs.stackable.tech/home/26.7/tutorials/logging-vector-aggregator) to learn how to configure log aggregation with Vector. maxLength: 253 minLength: 1 diff --git a/tests/release.yaml b/tests/release.yaml index 7f215241..373b41a9 100644 --- a/tests/release.yaml +++ b/tests/release.yaml @@ -7,18 +7,18 @@ releases: description: Integration test products: commons: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 secret: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 listener: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 zookeeper: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 hive: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 hdfs: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 hbase: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 spark-k8s: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0-rc1 From 81bdd9293307c195f132e52c84c807383a46ad50 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 21 Jul 2026 13:26:43 +0200 Subject: [PATCH 4/5] chore: Release 26.7.0 (#728) Signed-off-by: Techassi --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.nix | 2 +- Cargo.toml | 2 +- deploy/helm/spark-k8s-operator/Chart.yaml | 4 ++-- .../examples/getting_started/getting_started.sh | 16 ++++++++-------- .../examples/getting_started/install_output.txt | 8 ++++---- docs/templating_vars.yaml | 8 ++++---- tests/release.yaml | 16 ++++++++-------- 9 files changed, 31 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9e3870..57c2da92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [26.7.0] - 2026-07-21 + ## [26.7.0-rc1] - 2026-07-16 ### Added diff --git a/Cargo.lock b/Cargo.lock index 3430ed4a..0fd07fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3111,7 +3111,7 @@ dependencies = [ [[package]] name = "stackable-spark-k8s-operator" -version = "26.7.0-rc1" +version = "26.7.0" dependencies = [ "anyhow", "built", diff --git a/Cargo.nix b/Cargo.nix index 0e723ea0..6be74974 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10305,7 +10305,7 @@ rec { }; "stackable-spark-k8s-operator" = rec { crateName = "stackable-spark-k8s-operator"; - version = "26.7.0-rc1"; + version = "26.7.0"; edition = "2024"; crateBin = [ { diff --git a/Cargo.toml b/Cargo.toml index dd74de20..46d9bd17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["rust/operator-binary"] resolver = "2" [workspace.package] -version = "26.7.0-rc1" +version = "26.7.0" authors = ["Stackable GmbH "] license = "OSL-3.0" edition = "2024" diff --git a/deploy/helm/spark-k8s-operator/Chart.yaml b/deploy/helm/spark-k8s-operator/Chart.yaml index 3293099b..26108222 100644 --- a/deploy/helm/spark-k8s-operator/Chart.yaml +++ b/deploy/helm/spark-k8s-operator/Chart.yaml @@ -1,8 +1,8 @@ --- apiVersion: v2 name: spark-k8s-operator -version: "26.7.0-rc1" -appVersion: "26.7.0-rc1" +version: "26.7.0" +appVersion: "26.7.0" description: The Stackable Operator for Apache Spark-on-Kubernetes home: https://github.com/stackabletech/spark-k8s-operator maintainers: diff --git a/docs/modules/spark-k8s/examples/getting_started/getting_started.sh b/docs/modules/spark-k8s/examples/getting_started/getting_started.sh index d25768d7..550e734e 100755 --- a/docs/modules/spark-k8s/examples/getting_started/getting_started.sh +++ b/docs/modules/spark-k8s/examples/getting_started/getting_started.sh @@ -23,20 +23,20 @@ case "$1" in "helm") echo "Installing Operators with Helm" # tag::helm-install-operators[] -helm install --wait commons-operator oci://oci.stackable.tech/sdp-charts/commons-operator --version 26.7.0-rc1 -helm install --wait secret-operator oci://oci.stackable.tech/sdp-charts/secret-operator --version 26.7.0-rc1 -helm install --wait listener-operator oci://oci.stackable.tech/sdp-charts/listener-operator --version 26.7.0-rc1 -helm install --wait spark-k8s-operator oci://oci.stackable.tech/sdp-charts/spark-k8s-operator --version 26.7.0-rc1 +helm install --wait commons-operator oci://oci.stackable.tech/sdp-charts/commons-operator --version 26.7.0 +helm install --wait secret-operator oci://oci.stackable.tech/sdp-charts/secret-operator --version 26.7.0 +helm install --wait listener-operator oci://oci.stackable.tech/sdp-charts/listener-operator --version 26.7.0 +helm install --wait spark-k8s-operator oci://oci.stackable.tech/sdp-charts/spark-k8s-operator --version 26.7.0 # end::helm-install-operators[] ;; "stackablectl") echo "installing Operators with stackablectl" # tag::stackablectl-install-operators[] stackablectl operator install \ - commons=26.7.0-rc1 \ - secret=26.7.0-rc1 \ - listener=26.7.0-rc1 \ - spark-k8s=26.7.0-rc1 + commons=26.7.0 \ + secret=26.7.0 \ + listener=26.7.0 \ + spark-k8s=26.7.0 # end::stackablectl-install-operators[] ;; *) diff --git a/docs/modules/spark-k8s/examples/getting_started/install_output.txt b/docs/modules/spark-k8s/examples/getting_started/install_output.txt index 43bf4a4e..1cafc6a5 100644 --- a/docs/modules/spark-k8s/examples/getting_started/install_output.txt +++ b/docs/modules/spark-k8s/examples/getting_started/install_output.txt @@ -1,4 +1,4 @@ -Installed commons=26.7.0-rc1 operator -Installed secret=26.7.0-rc1 operator -Installed listener=26.7.0-rc1 operator -Installed spark-k8s=26.7.0-rc1 operator +Installed commons=26.7.0 operator +Installed secret=26.7.0 operator +Installed listener=26.7.0 operator +Installed spark-k8s=26.7.0 operator diff --git a/docs/templating_vars.yaml b/docs/templating_vars.yaml index c783eb96..64abf4c1 100644 --- a/docs/templating_vars.yaml +++ b/docs/templating_vars.yaml @@ -3,7 +3,7 @@ helm: repo_name: sdp-charts repo_url: oci.stackable.tech versions: - commons: 26.7.0-rc1 - secret: 26.7.0-rc1 - listener: 26.7.0-rc1 - spark: 26.7.0-rc1 + commons: 26.7.0 + secret: 26.7.0 + listener: 26.7.0 + spark: 26.7.0 diff --git a/tests/release.yaml b/tests/release.yaml index 373b41a9..dc63000a 100644 --- a/tests/release.yaml +++ b/tests/release.yaml @@ -7,18 +7,18 @@ releases: description: Integration test products: commons: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 secret: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 listener: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 zookeeper: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 hive: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 hdfs: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 hbase: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 spark-k8s: - operatorVersion: 26.7.0-rc1 + operatorVersion: 26.7.0 From d1d4c486da8a5fd717328196b9612523e36ce089 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:37:42 +0200 Subject: [PATCH 5/5] refactor: snapshot tests to be Spark image agnostic (#733) --- tests/templates/kuttl/smoke/43-assert.yaml.j2 | 468 +++-- tests/templates/kuttl/smoke/53-assert.yaml.j2 | 1513 ++++++----------- .../kuttl/spark-connect/13-assert.yaml.j2 | 1193 ++++++------- .../kuttl/spark-connect/14-assert.yaml.j2 | 228 +++ 4 files changed, 1469 insertions(+), 1933 deletions(-) create mode 100644 tests/templates/kuttl/spark-connect/14-assert.yaml.j2 diff --git a/tests/templates/kuttl/smoke/43-assert.yaml.j2 b/tests/templates/kuttl/smoke/43-assert.yaml.j2 index 1a07ff2b..d879ecaf 100644 --- a/tests/templates/kuttl/smoke/43-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/43-assert.yaml.j2 @@ -1,278 +1,266 @@ --- -# Snapshot the full `.data` of each operator-managed ConfigMap. -# Any code change that alters rendered config values will fail these diffs. +# Snapshot the `.data` of the operator-managed ConfigMap as a plain Kubernetes +# object and let kuttl compare it. Any code change that alters a rendered +# config value fails this assert. # -# Runs as its own step (after 40/41/42) so kuttl does not re-evaluate the heavy -# heredoc on every 1-second readiness retry of the install step. By this point -# the cluster is in steady state, so each script runs once. +# Notes: # -# The heredoc is quoted (`<<'YAMLEOF'`) so shell substitution is disabled and -# any property-style escapes survive verbatim. Only `__NAMESPACE__` is -# substituted afterwards via `sed`, because kuttl tests run in a randomized -# namespace per invocation. Both sides are normalized to canonical JSON via -# `yq -o=json` before comparison. +# * kuttl subset-matches maps, so a key the operator newly adds to `.data` does +# not fail this assert. Add it below to cover it. apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 60 -commands: - - script: | - expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json - security.properties: "networkaddress.cache.negative.ttl=0\nnetworkaddress.cache.ttl=30\n" - spark-defaults.conf: | +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spark-history-node-default +data: + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=30 + spark-defaults.conf: | {% if test_scenario['values']['s3-use-tls'] == 'true' %} - spark.hadoop.fs.s3a.endpoint=https\://eventlog-minio\:9000/ + spark.hadoop.fs.s3a.endpoint=https\://eventlog-minio\:9000/ {% else %} - spark.hadoop.fs.s3a.endpoint=http\://eventlog-minio\:9000/ + spark.hadoop.fs.s3a.endpoint=http\://eventlog-minio\:9000/ {% endif %} - spark.hadoop.fs.s3a.endpoint.region=us-east-1 - spark.hadoop.fs.s3a.path.style.access=true - spark.history.fs.cleaner.enabled=true - spark.history.fs.logDirectory=s3a\://spark-logs/eventlogs/ - spark-env.sh: "" + spark.hadoop.fs.s3a.endpoint.region=us-east-1 + spark.hadoop.fs.s3a.path.style.access=true + spark.history.fs.cleaner.enabled=true + spark.history.fs.logDirectory=s3a\://spark-logs/eventlogs/ + spark-env.sh: "" {% if lookup('env', 'VECTOR_AGGREGATOR') %} - vector.yaml: | - --- - data_dir: ${DATA_DIR} + vector.yaml: | + --- + data_dir: ${DATA_DIR} - log_schema: - host_key: pod + log_schema: + host_key: pod - sources: - # Reads the internal Vector logs - vector: - type: internal_logs + sources: + # Reads the internal Vector logs + vector: + type: internal_logs - files_stdout: - type: file - include: - - ${LOG_DIR}/*/*.stdout.log + files_stdout: + type: file + include: + - ${LOG_DIR}/*/*.stdout.log - files_stderr: - type: file - include: - - ${LOG_DIR}/*/*.stderr.log + files_stderr: + type: file + include: + - ${LOG_DIR}/*/*.stderr.log - files_log4j2: - type: file - include: - - ${LOG_DIR}/*/*.log4j2.xml - line_delimiter: "\r\n" + files_log4j2: + type: file + include: + - ${LOG_DIR}/*/*.log4j2.xml + line_delimiter: "\r\n" - transforms: - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" + transforms: + processed_files_stdout: + inputs: + - files_stdout + type: remap + source: | + .logger = "ROOT" + .level = "INFO" - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" + processed_files_stderr: + inputs: + - files_stderr + type: remap + source: | + .logger = "ROOT" + .level = "ERROR" - processed_files_log4j2: - inputs: - - files_log4j2 - type: remap - source: | - raw_message = string!(.message) + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] - event = {} - parsed_event, err = parse_xml(raw_message) - if err != null { - error = "XML not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - if !is_object(parsed_event.Event) { - error = "Parsed event contains no \"Event\" tag." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event.Event) + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) - tag_instant_valid = false - instant, err = object(event.Instant) + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") if err == null { - epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) - if err == null && epoch_nanoseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") - if err == null { - .timestamp = converted_timestamp - tag_instant_valid = true - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) } - if !tag_instant_valid { - epoch_milliseconds, err = to_int(event.@timeMillis) - if err == null && epoch_milliseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") - if err == null { - .timestamp = converted_timestamp - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } - .logger, err = string(event.@loggerName) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } - level, err = string(event.@level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } - exception = null - thrown = event.Thrown - if is_object(thrown) { - exception = "Exception" - thread, err = string(event.@thread) - if err == null && !is_empty(thread) { - exception = exception + " in thread \"" + thread + "\"" - } - thrown_name, err = string(thrown.@name) - if err == null && !is_empty(exception) { - exception = exception + " " + thrown_name - } - message = string(thrown.@localizedMessage) ?? - string(thrown.@message) ?? - "" - if !is_empty(message) { - exception = exception + ": " + message - } - stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] - stacktrace = "" - for_each(stacktrace_items) -> |_index, value| { - stacktrace = stacktrace + " " - class = string(value.@class) ?? "" - method = string(value.@method) ?? "" - if !is_empty(class) && !is_empty(method) { - stacktrace = stacktrace + "at " + class + "." + method - } - file = string(value.@file) ?? "" - line = string(value.@line) ?? "" - if !is_empty(file) && !is_empty(line) { - stacktrace = stacktrace + "(" + file + ":" + line + ")" - } - exact = to_bool(value.@exact) ?? false - location = string(value.@location) ?? "" - version = string(value.@version) ?? "" - if !is_empty(location) && !is_empty(version) { - stacktrace = stacktrace + " " - if !exact { - stacktrace = stacktrace + "~" - } - stacktrace = stacktrace + "[" + location + ":" + version + "]" - } - stacktrace = stacktrace + "\n" - } - if stacktrace != "" { - exception = exception + "\n" + stacktrace - } + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method } - - message, err = string(event.Message) - if err != null || is_empty(message) { - message = null - .errors = push(.errors, "Message not found.") + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" } - .message = join!(compact([message, exception]), "\n") + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" + } + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace } } - # Extends the processed files with the fields "container" and "file" - extended_logs_files: - inputs: - - processed_files_* - type: remap - source: | - del(.source_type) - if .errors == [] { - del(.errors) + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") } - . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + .message = join!(compact([message, exception]), "\n") + } + } + + # Extends the processed files with the fields "container" and "file" + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') - # Filters the logs of the Vector agent according to the defined log level - filtered_logs_vector: - inputs: - - vector - type: filter - condition: > - (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || - (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + # Filters the logs of the Vector agent according to the defined log level + filtered_logs_vector: + inputs: + - vector + type: filter + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) - # Aligns the logs of the Vector agent with the common format - extended_logs_vector: - inputs: - - filtered_logs_vector - type: remap - source: | - .container = "vector" - .level = .metadata.level - .logger = .metadata.module_path - if exists(.file) { .processed_file = del(.file) } - del(.metadata) - del(.pid) - del(.source_type) + # Aligns the logs of the Vector agent with the common format + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) - # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs - extended_logs: - inputs: - - extended_logs_* - type: remap - source: | - .namespace = "${NAMESPACE}" - .cluster = "${CLUSTER_NAME}" - .role = "${ROLE_NAME}" - .roleGroup = "${ROLE_GROUP_NAME}" + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" - sinks: - # Forward the logs to the Vector aggregator - aggregator: - inputs: - - extended_logs - type: vector - address: ${VECTOR_AGGREGATOR_ADDRESS} + sinks: + # Forward the logs to the Vector aggregator + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} - YAMLEOF - ) - actual=$(kubectl -n $NAMESPACE get cm spark-history-node-default -o yaml | yq -o=json '.data') - expected_file=$(mktemp) && actual_file=$(mktemp) - printf '%s\n' "$expected" > "$expected_file" - printf '%s\n' "$actual" > "$actual_file" - if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then - echo "ERROR: ConfigMap spark-history-node-default data drifted from snapshot." - printf '%s\n' "$diff_out" - rm -f "$expected_file" "$actual_file" - exit 1 - fi - rm -f "$expected_file" "$actual_file" diff --git a/tests/templates/kuttl/smoke/53-assert.yaml.j2 b/tests/templates/kuttl/smoke/53-assert.yaml.j2 index 4991a928..831300b0 100644 --- a/tests/templates/kuttl/smoke/53-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/53-assert.yaml.j2 @@ -1,1013 +1,568 @@ --- -# Snapshot the full `.data` of each SparkApplication-owned ConfigMap. -# Any code change that alters rendered config values will fail these diffs. +# Snapshot the `.data` of the SparkApplication-owned ConfigMaps as plain +# Kubernetes objects and let kuttl compare them. Any code change that alters a +# rendered config value fails this assert. # -# Runs as its own step (after 50/51/52) so kuttl does not re-evaluate the heavy -# heredoc on every 1-second readiness retry of the install step. By this point -# the cluster is in steady state, so each script runs once. +# Notes: # -# The heredoc is quoted (`<<'YAMLEOF'`) so shell substitution is disabled and -# any property-style escapes survive verbatim. Only `__NAMESPACE__` is -# substituted afterwards via `sed`, because kuttl tests run in a randomized -# namespace per invocation. Both sides are normalized to canonical JSON via -# `yq -o=json` before comparison. +# * kuttl subset-matches maps, so a key the operator newly adds to `.data` does +# not fail this assert. Add it below to cover it. apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 60 -commands: - - script: | - expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json - log4j2.properties: |- - appenders = FILE, CONSOLE - - appender.CONSOLE.type = Console - appender.CONSOLE.name = CONSOLE - appender.CONSOLE.target = SYSTEM_ERR - appender.CONSOLE.layout.type = PatternLayout - appender.CONSOLE.layout.pattern = %d{ISO8601} %p [%t] %c - %m%n - appender.CONSOLE.filter.threshold.type = ThresholdFilter - appender.CONSOLE.filter.threshold.level = INFO - - appender.FILE.type = RollingFile - appender.FILE.name = FILE - appender.FILE.fileName = /stackable/log/spark/spark.log4j2.xml - appender.FILE.filePattern = /stackable/log/spark/spark.log4j2.xml.%i - appender.FILE.layout.type = XMLLayout - appender.FILE.policies.type = Policies - appender.FILE.policies.size.type = SizeBasedTriggeringPolicy - appender.FILE.policies.size.size = 5MB - appender.FILE.strategy.type = DefaultRolloverStrategy - appender.FILE.strategy.max = 1 - appender.FILE.filter.threshold.type = ThresholdFilter - appender.FILE.filter.threshold.level = INFO - - - rootLogger.level=INFO - rootLogger.appenderRefs = CONSOLE, FILE - rootLogger.appenderRef.CONSOLE.ref = CONSOLE - rootLogger.appenderRef.FILE.ref = FILE - security.properties: | - networkaddress.cache.negative.ttl=0 - networkaddress.cache.ttl=30 - spark-env.sh: "" - template.yaml: | - metadata: - labels: - app.kubernetes.io/component: spark - app.kubernetes.io/instance: spark-pi-s3-1 - app.kubernetes.io/managed-by: spark.stackable.tech_sparkapplication - app.kubernetes.io/name: spark-k8s - app.kubernetes.io/role-group: sparkapplication - app.kubernetes.io/version: {{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - prometheus.io/scrape: 'true' - stackable.tech/vendor: Stackable - name: spark - spec: - affinity: {} - containers: - - env: - - name: CONTAINERDEBUG_LOG_DIRECTORY - value: /stackable/log/containerdebug -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - name: STACKABLE_TLS_STORE_PASSWORD - value: changeit -{% endif %} - - name: _STACKABLE_PRE_HOOK - value: containerdebug --output=/stackable/log/containerdebug-state.json --loop & -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - name: _STACKABLE_POST_HOOK - value: sleep 10; mkdir -p /stackable/log/_vector && touch /stackable/log/_vector/shutdown -{% endif %} - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - imagePullPolicy: IfNotPresent - name: spark - resources: - limits: - cpu: '1' - memory: 1Gi - requests: - cpu: 250m - memory: 1Gi - volumeMounts: - - mountPath: /stackable/secrets/s3-credentials-class - name: s3-credentials-class - - mountPath: /stackable/secrets/history-credentials-class - name: history-credentials-class - - mountPath: /stackable/log_config - name: log-config - - mountPath: /stackable/log - name: log -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - mountPath: /stackable/truststore - name: stackable-truststore - - mountPath: /stackable/mount_server_tls/minio-tls-eventlog - name: minio-tls-eventlog -{% endif %} -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - args: - - |- - mkdir --parents /stackable/log/_vector-state - # Vector will ignore SIGTERM (as PID != 1) and must be shut down by writing a shutdown trigger file - vector & vector_pid=$! - if [ ! -f "/stackable/log/_vector/shutdown" ]; then - mkdir -p /stackable/log/_vector - inotifywait -qq --event create /stackable/log/_vector; - fi - sleep 1 - kill $vector_pid - command: - - /bin/bash - - -x - - -euo - - pipefail - - -c - env: - - name: CLUSTER_NAME - value: spark-pi-s3-1 - - name: DATA_DIR - value: /stackable/log/_vector-state - - name: LOG_DIR - value: /stackable/log - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: ROLE_GROUP_NAME - value: default - - name: ROLE_NAME - value: driver - - name: VECTOR_AGGREGATOR_ADDRESS - valueFrom: - configMapKeyRef: - key: ADDRESS - name: vector-aggregator-discovery - - name: VECTOR_CONFIG_YAML - value: /stackable/config/vector.yaml - - name: VECTOR_FILE_LOG_LEVEL - value: info - - name: VECTOR_LOG - value: info - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - imagePullPolicy: IfNotPresent - name: vector - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 250m - memory: 128Mi - volumeMounts: - - mountPath: /stackable/config/vector.yaml - name: config - readOnly: true - subPath: vector.yaml - - mountPath: /stackable/log - name: log -{% endif %} - enableServiceLinks: false -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - initContainers: - - args: - - |- - cert-tools generate-pkcs12-truststore --pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem --out /stackable/truststore/truststore.p12 --out-password changeit - cert-tools generate-pkcs12-truststore --pkcs12 /stackable/truststore/truststore.p12:changeit --pkcs12 /stackable/mount_server_tls/minio-tls-eventlog/truststore.p12 --out /stackable/truststore/truststore.p12 --out-password changeit - command: - - /bin/bash - - -x - - -euo - - pipefail - - -c - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - name: tls - resources: - limits: - cpu: 1000m - memory: 1024Mi - requests: - cpu: 250m - memory: 1024Mi - volumeMounts: - - mountPath: /stackable/mount_server_tls/minio-tls-eventlog - name: minio-tls-eventlog - - mountPath: /stackable/truststore - name: stackable-truststore -{% endif %} - securityContext: - fsGroup: 1000 - serviceAccountName: spark-pi-s3-1 - volumes: - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: history-credentials-class - secrets.stackable.tech/provision-parts: public-private - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: history-credentials-class - - emptyDir: - sizeLimit: 39Mi - name: log - - configMap: - name: spark-pi-s3-1-driver-pod-template - name: log-config -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/backend.autotls.cert.lifetime: 1d - secrets.stackable.tech/class: minio-tls-eventlog - secrets.stackable.tech/format: tls-pkcs12 - secrets.stackable.tech/provision-parts: public - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: minio-tls-eventlog -{% endif %} - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: s3-credentials-class - secrets.stackable.tech/provision-parts: public-private - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: s3-credentials-class -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - emptyDir: - sizeLimit: 5Mi - name: stackable-truststore -{% endif %} - - configMap: - name: spark-pi-s3-1-driver-pod-template - name: config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spark-pi-s3-1-driver-pod-template +data: + log4j2.properties: |- + appenders = FILE, CONSOLE + + appender.CONSOLE.type = Console + appender.CONSOLE.name = CONSOLE + appender.CONSOLE.target = SYSTEM_ERR + appender.CONSOLE.layout.type = PatternLayout + appender.CONSOLE.layout.pattern = %d{ISO8601} %p [%t] %c - %m%n + appender.CONSOLE.filter.threshold.type = ThresholdFilter + appender.CONSOLE.filter.threshold.level = INFO + + appender.FILE.type = RollingFile + appender.FILE.name = FILE + appender.FILE.fileName = /stackable/log/spark/spark.log4j2.xml + appender.FILE.filePattern = /stackable/log/spark/spark.log4j2.xml.%i + appender.FILE.layout.type = XMLLayout + appender.FILE.policies.type = Policies + appender.FILE.policies.size.type = SizeBasedTriggeringPolicy + appender.FILE.policies.size.size = 5MB + appender.FILE.strategy.type = DefaultRolloverStrategy + appender.FILE.strategy.max = 1 + appender.FILE.filter.threshold.type = ThresholdFilter + appender.FILE.filter.threshold.level = INFO + + + rootLogger.level=INFO + rootLogger.appenderRefs = CONSOLE, FILE + rootLogger.appenderRef.CONSOLE.ref = CONSOLE + rootLogger.appenderRef.FILE.ref = FILE + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=30 + spark-env.sh: "" {% if lookup('env', 'VECTOR_AGGREGATOR') %} - vector.yaml: | - --- - data_dir: ${DATA_DIR} - - log_schema: - host_key: pod - - sources: - # Reads the internal Vector logs - vector: - type: internal_logs - - files_stdout: - type: file - include: - - ${LOG_DIR}/*/*.stdout.log - - files_stderr: - type: file - include: - - ${LOG_DIR}/*/*.stderr.log - - files_log4j2: - type: file - include: - - ${LOG_DIR}/*/*.log4j2.xml - line_delimiter: "\r\n" - - transforms: - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" - - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" - - processed_files_log4j2: - inputs: - - files_log4j2 - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - event = {} - parsed_event, err = parse_xml(raw_message) - if err != null { - error = "XML not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - if !is_object(parsed_event.Event) { - error = "Parsed event contains no \"Event\" tag." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event.Event) - - tag_instant_valid = false - instant, err = object(event.Instant) + vector.yaml: | + --- + data_dir: ${DATA_DIR} + + log_schema: + host_key: pod + + sources: + # Reads the internal Vector logs + vector: + type: internal_logs + + files_stdout: + type: file + include: + - ${LOG_DIR}/*/*.stdout.log + + files_stderr: + type: file + include: + - ${LOG_DIR}/*/*.stderr.log + + files_log4j2: + type: file + include: + - ${LOG_DIR}/*/*.log4j2.xml + line_delimiter: "\r\n" + + transforms: + processed_files_stdout: + inputs: + - files_stdout + type: remap + source: | + .logger = "ROOT" + .level = "INFO" + + processed_files_stderr: + inputs: + - files_stderr + type: remap + source: | + .logger = "ROOT" + .level = "ERROR" + + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) + + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] + + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) + + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") if err == null { - epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) - if err == null && epoch_nanoseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") - if err == null { - .timestamp = converted_timestamp - tag_instant_valid = true - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) } - if !tag_instant_valid { - epoch_milliseconds, err = to_int(event.@timeMillis) - if err == null && epoch_milliseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") - if err == null { - .timestamp = converted_timestamp - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } - .logger, err = string(event.@loggerName) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } - level, err = string(event.@level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } - exception = null - thrown = event.Thrown - if is_object(thrown) { - exception = "Exception" - thread, err = string(event.@thread) - if err == null && !is_empty(thread) { - exception = exception + " in thread \"" + thread + "\"" - } - thrown_name, err = string(thrown.@name) - if err == null && !is_empty(exception) { - exception = exception + " " + thrown_name - } - message = string(thrown.@localizedMessage) ?? - string(thrown.@message) ?? - "" - if !is_empty(message) { - exception = exception + ": " + message - } - stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] - stacktrace = "" - for_each(stacktrace_items) -> |_index, value| { - stacktrace = stacktrace + " " - class = string(value.@class) ?? "" - method = string(value.@method) ?? "" - if !is_empty(class) && !is_empty(method) { - stacktrace = stacktrace + "at " + class + "." + method - } - file = string(value.@file) ?? "" - line = string(value.@line) ?? "" - if !is_empty(file) && !is_empty(line) { - stacktrace = stacktrace + "(" + file + ":" + line + ")" - } - exact = to_bool(value.@exact) ?? false - location = string(value.@location) ?? "" - version = string(value.@version) ?? "" - if !is_empty(location) && !is_empty(version) { - stacktrace = stacktrace + " " - if !exact { - stacktrace = stacktrace + "~" - } - stacktrace = stacktrace + "[" + location + ":" + version + "]" - } - stacktrace = stacktrace + "\n" - } - if stacktrace != "" { - exception = exception + "\n" + stacktrace - } + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method } - - message, err = string(event.Message) - if err != null || is_empty(message) { - message = null - .errors = push(.errors, "Message not found.") + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" + } + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" } - .message = join!(compact([message, exception]), "\n") + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace } } - # Extends the processed files with the fields "container" and "file" - extended_logs_files: - inputs: - - processed_files_* - type: remap - source: | - del(.source_type) - if .errors == [] { - del(.errors) + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") } - . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') - - # Filters the logs of the Vector agent according to the defined log level - filtered_logs_vector: - inputs: - - vector - type: filter - condition: > - (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || - (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) - - # Aligns the logs of the Vector agent with the common format - extended_logs_vector: - inputs: - - filtered_logs_vector - type: remap - source: | - .container = "vector" - .level = .metadata.level - .logger = .metadata.module_path - if exists(.file) { .processed_file = del(.file) } - del(.metadata) - del(.pid) - del(.source_type) - - # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs - extended_logs: - inputs: - - extended_logs_* - type: remap - source: | - .namespace = "${NAMESPACE}" - .cluster = "${CLUSTER_NAME}" - .role = "${ROLE_NAME}" - .roleGroup = "${ROLE_GROUP_NAME}" - - sinks: - # Forward the logs to the Vector aggregator - aggregator: - inputs: - - extended_logs - type: vector - address: ${VECTOR_AGGREGATOR_ADDRESS} -{% endif %} - YAMLEOF - ) - actual=$(kubectl -n $NAMESPACE get cm spark-pi-s3-1-driver-pod-template -o yaml | yq -o=json '.data') - expected_file=$(mktemp) && actual_file=$(mktemp) - printf '%s\n' "$expected" > "$expected_file" - printf '%s\n' "$actual" > "$actual_file" - if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then - echo "ERROR: ConfigMap spark-pi-s3-1-driver-pod-template data drifted from snapshot." - printf '%s\n' "$diff_out" - rm -f "$expected_file" "$actual_file" - exit 1 - fi - rm -f "$expected_file" "$actual_file" - - script: | - expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json - log4j2.properties: |- - appenders = FILE, CONSOLE - - appender.CONSOLE.type = Console - appender.CONSOLE.name = CONSOLE - appender.CONSOLE.target = SYSTEM_ERR - appender.CONSOLE.layout.type = PatternLayout - appender.CONSOLE.layout.pattern = %d{ISO8601} %p [%t] %c - %m%n - appender.CONSOLE.filter.threshold.type = ThresholdFilter - appender.CONSOLE.filter.threshold.level = INFO - - appender.FILE.type = RollingFile - appender.FILE.name = FILE - appender.FILE.fileName = /stackable/log/spark/spark.log4j2.xml - appender.FILE.filePattern = /stackable/log/spark/spark.log4j2.xml.%i - appender.FILE.layout.type = XMLLayout - appender.FILE.policies.type = Policies - appender.FILE.policies.size.type = SizeBasedTriggeringPolicy - appender.FILE.policies.size.size = 5MB - appender.FILE.strategy.type = DefaultRolloverStrategy - appender.FILE.strategy.max = 1 - appender.FILE.filter.threshold.type = ThresholdFilter - appender.FILE.filter.threshold.level = INFO - - - rootLogger.level=INFO - rootLogger.appenderRefs = CONSOLE, FILE - rootLogger.appenderRef.CONSOLE.ref = CONSOLE - rootLogger.appenderRef.FILE.ref = FILE - security.properties: | - networkaddress.cache.negative.ttl=0 - networkaddress.cache.ttl=30 - spark-env.sh: "" - template.yaml: | - metadata: - labels: - app.kubernetes.io/component: spark - app.kubernetes.io/instance: spark-pi-s3-1 - app.kubernetes.io/managed-by: spark.stackable.tech_sparkapplication - app.kubernetes.io/name: spark-k8s - app.kubernetes.io/role-group: sparkapplication - app.kubernetes.io/version: {{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - stackable.tech/vendor: Stackable - name: spark - spec: - affinity: {} - containers: - - env: - - name: CONTAINERDEBUG_LOG_DIRECTORY - value: /stackable/log/containerdebug -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - name: STACKABLE_TLS_STORE_PASSWORD - value: changeit + .message = join!(compact([message, exception]), "\n") + } + } + + # Extends the processed files with the fields "container" and "file" + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + # Filters the logs of the Vector agent according to the defined log level + filtered_logs_vector: + inputs: + - vector + type: filter + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) + + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + + sinks: + # Forward the logs to the Vector aggregator + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} - - name: _STACKABLE_PRE_HOOK - value: containerdebug --output=/stackable/log/containerdebug-state.json --loop & -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - name: _STACKABLE_POST_HOOK - value: sleep 10; mkdir -p /stackable/log/_vector && touch /stackable/log/_vector/shutdown -{% endif %} - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - imagePullPolicy: IfNotPresent - name: spark - resources: - limits: - cpu: '1' - memory: 1Gi - requests: - cpu: 250m - memory: 1Gi - volumeMounts: - - mountPath: /stackable/secrets/s3-credentials-class - name: s3-credentials-class - - mountPath: /stackable/secrets/history-credentials-class - name: history-credentials-class - - mountPath: /stackable/log_config - name: log-config - - mountPath: /stackable/log - name: log -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - mountPath: /stackable/truststore - name: stackable-truststore - - mountPath: /stackable/mount_server_tls/minio-tls-eventlog - name: minio-tls-eventlog -{% endif %} -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - args: - - |- - mkdir --parents /stackable/log/_vector-state - # Vector will ignore SIGTERM (as PID != 1) and must be shut down by writing a shutdown trigger file - vector & vector_pid=$! - if [ ! -f "/stackable/log/_vector/shutdown" ]; then - mkdir -p /stackable/log/_vector - inotifywait -qq --event create /stackable/log/_vector; - fi - sleep 1 - kill $vector_pid - command: - - /bin/bash - - -x - - -euo - - pipefail - - -c - env: - - name: CLUSTER_NAME - value: spark-pi-s3-1 - - name: DATA_DIR - value: /stackable/log/_vector-state - - name: LOG_DIR - value: /stackable/log - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: ROLE_GROUP_NAME - value: default - - name: ROLE_NAME - value: executor - - name: VECTOR_AGGREGATOR_ADDRESS - valueFrom: - configMapKeyRef: - key: ADDRESS - name: vector-aggregator-discovery - - name: VECTOR_CONFIG_YAML - value: /stackable/config/vector.yaml - - name: VECTOR_FILE_LOG_LEVEL - value: info - - name: VECTOR_LOG - value: info - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - imagePullPolicy: IfNotPresent - name: vector - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 250m - memory: 128Mi - volumeMounts: - - mountPath: /stackable/config/vector.yaml - name: config - readOnly: true - subPath: vector.yaml - - mountPath: /stackable/log - name: log -{% endif %} - enableServiceLinks: false -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - initContainers: - - args: - - |- - cert-tools generate-pkcs12-truststore --pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem --out /stackable/truststore/truststore.p12 --out-password changeit - cert-tools generate-pkcs12-truststore --pkcs12 /stackable/truststore/truststore.p12:changeit --pkcs12 /stackable/mount_server_tls/minio-tls-eventlog/truststore.p12 --out /stackable/truststore/truststore.p12 --out-password changeit - command: - - /bin/bash - - -x - - -euo - - pipefail - - -c - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark'].split(',')[0] }}-stackable0.0.0-dev - name: tls - resources: - limits: - cpu: 1000m - memory: 1024Mi - requests: - cpu: 250m - memory: 1024Mi - volumeMounts: - - mountPath: /stackable/mount_server_tls/minio-tls-eventlog - name: minio-tls-eventlog - - mountPath: /stackable/truststore - name: stackable-truststore -{% endif %} - securityContext: - fsGroup: 1000 - serviceAccountName: spark-pi-s3-1 - volumes: - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: history-credentials-class - secrets.stackable.tech/provision-parts: public-private - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: history-credentials-class - - emptyDir: - sizeLimit: 39Mi - name: log - - configMap: - name: spark-pi-s3-1-executor-pod-template - name: log-config -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/backend.autotls.cert.lifetime: 1d - secrets.stackable.tech/class: minio-tls-eventlog - secrets.stackable.tech/format: tls-pkcs12 - secrets.stackable.tech/provision-parts: public - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: minio-tls-eventlog -{% endif %} - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: s3-credentials-class - secrets.stackable.tech/provision-parts: public-private - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: s3-credentials-class -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - emptyDir: - sizeLimit: 5Mi - name: stackable-truststore -{% endif %} - - configMap: - name: spark-pi-s3-1-executor-pod-template - name: config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spark-pi-s3-1-executor-pod-template +data: + log4j2.properties: |- + appenders = FILE, CONSOLE + + appender.CONSOLE.type = Console + appender.CONSOLE.name = CONSOLE + appender.CONSOLE.target = SYSTEM_ERR + appender.CONSOLE.layout.type = PatternLayout + appender.CONSOLE.layout.pattern = %d{ISO8601} %p [%t] %c - %m%n + appender.CONSOLE.filter.threshold.type = ThresholdFilter + appender.CONSOLE.filter.threshold.level = INFO + + appender.FILE.type = RollingFile + appender.FILE.name = FILE + appender.FILE.fileName = /stackable/log/spark/spark.log4j2.xml + appender.FILE.filePattern = /stackable/log/spark/spark.log4j2.xml.%i + appender.FILE.layout.type = XMLLayout + appender.FILE.policies.type = Policies + appender.FILE.policies.size.type = SizeBasedTriggeringPolicy + appender.FILE.policies.size.size = 5MB + appender.FILE.strategy.type = DefaultRolloverStrategy + appender.FILE.strategy.max = 1 + appender.FILE.filter.threshold.type = ThresholdFilter + appender.FILE.filter.threshold.level = INFO + + + rootLogger.level=INFO + rootLogger.appenderRefs = CONSOLE, FILE + rootLogger.appenderRef.CONSOLE.ref = CONSOLE + rootLogger.appenderRef.FILE.ref = FILE + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=30 + spark-env.sh: "" {% if lookup('env', 'VECTOR_AGGREGATOR') %} - vector.yaml: | - --- - data_dir: ${DATA_DIR} - - log_schema: - host_key: pod - - sources: - # Reads the internal Vector logs - vector: - type: internal_logs - - files_stdout: - type: file - include: - - ${LOG_DIR}/*/*.stdout.log - - files_stderr: - type: file - include: - - ${LOG_DIR}/*/*.stderr.log - - files_log4j2: - type: file - include: - - ${LOG_DIR}/*/*.log4j2.xml - line_delimiter: "\r\n" - - transforms: - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" - - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" - - processed_files_log4j2: - inputs: - - files_log4j2 - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - event = {} - parsed_event, err = parse_xml(raw_message) - if err != null { - error = "XML not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - if !is_object(parsed_event.Event) { - error = "Parsed event contains no \"Event\" tag." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event.Event) - - tag_instant_valid = false - instant, err = object(event.Instant) + vector.yaml: | + --- + data_dir: ${DATA_DIR} + + log_schema: + host_key: pod + + sources: + # Reads the internal Vector logs + vector: + type: internal_logs + + files_stdout: + type: file + include: + - ${LOG_DIR}/*/*.stdout.log + + files_stderr: + type: file + include: + - ${LOG_DIR}/*/*.stderr.log + + files_log4j2: + type: file + include: + - ${LOG_DIR}/*/*.log4j2.xml + line_delimiter: "\r\n" + + transforms: + processed_files_stdout: + inputs: + - files_stdout + type: remap + source: | + .logger = "ROOT" + .level = "INFO" + + processed_files_stderr: + inputs: + - files_stderr + type: remap + source: | + .logger = "ROOT" + .level = "ERROR" + + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) + + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] + + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) + + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") if err == null { - epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) - if err == null && epoch_nanoseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") - if err == null { - .timestamp = converted_timestamp - tag_instant_valid = true - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) } - if !tag_instant_valid { - epoch_milliseconds, err = to_int(event.@timeMillis) - if err == null && epoch_milliseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") - if err == null { - .timestamp = converted_timestamp - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } - .logger, err = string(event.@loggerName) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } - level, err = string(event.@level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } - exception = null - thrown = event.Thrown - if is_object(thrown) { - exception = "Exception" - thread, err = string(event.@thread) - if err == null && !is_empty(thread) { - exception = exception + " in thread \"" + thread + "\"" - } - thrown_name, err = string(thrown.@name) - if err == null && !is_empty(exception) { - exception = exception + " " + thrown_name - } - message = string(thrown.@localizedMessage) ?? - string(thrown.@message) ?? - "" - if !is_empty(message) { - exception = exception + ": " + message - } - stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] - stacktrace = "" - for_each(stacktrace_items) -> |_index, value| { - stacktrace = stacktrace + " " - class = string(value.@class) ?? "" - method = string(value.@method) ?? "" - if !is_empty(class) && !is_empty(method) { - stacktrace = stacktrace + "at " + class + "." + method - } - file = string(value.@file) ?? "" - line = string(value.@line) ?? "" - if !is_empty(file) && !is_empty(line) { - stacktrace = stacktrace + "(" + file + ":" + line + ")" - } - exact = to_bool(value.@exact) ?? false - location = string(value.@location) ?? "" - version = string(value.@version) ?? "" - if !is_empty(location) && !is_empty(version) { - stacktrace = stacktrace + " " - if !exact { - stacktrace = stacktrace + "~" - } - stacktrace = stacktrace + "[" + location + ":" + version + "]" - } - stacktrace = stacktrace + "\n" - } - if stacktrace != "" { - exception = exception + "\n" + stacktrace - } + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method } - - message, err = string(event.Message) - if err != null || is_empty(message) { - message = null - .errors = push(.errors, "Message not found.") + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" + } + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" } - .message = join!(compact([message, exception]), "\n") + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace } } - # Extends the processed files with the fields "container" and "file" - extended_logs_files: - inputs: - - processed_files_* - type: remap - source: | - del(.source_type) - if .errors == [] { - del(.errors) + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") } - . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') - - # Filters the logs of the Vector agent according to the defined log level - filtered_logs_vector: - inputs: - - vector - type: filter - condition: > - (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || - (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) - - # Aligns the logs of the Vector agent with the common format - extended_logs_vector: - inputs: - - filtered_logs_vector - type: remap - source: | - .container = "vector" - .level = .metadata.level - .logger = .metadata.module_path - if exists(.file) { .processed_file = del(.file) } - del(.metadata) - del(.pid) - del(.source_type) - - # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs - extended_logs: - inputs: - - extended_logs_* - type: remap - source: | - .namespace = "${NAMESPACE}" - .cluster = "${CLUSTER_NAME}" - .role = "${ROLE_NAME}" - .roleGroup = "${ROLE_GROUP_NAME}" - - sinks: - # Forward the logs to the Vector aggregator - aggregator: - inputs: - - extended_logs - type: vector - address: ${VECTOR_AGGREGATOR_ADDRESS} + .message = join!(compact([message, exception]), "\n") + } + } + + # Extends the processed files with the fields "container" and "file" + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + # Filters the logs of the Vector agent according to the defined log level + filtered_logs_vector: + inputs: + - vector + type: filter + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) + + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + + sinks: + # Forward the logs to the Vector aggregator + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} - YAMLEOF - ) - actual=$(kubectl -n $NAMESPACE get cm spark-pi-s3-1-executor-pod-template -o yaml | yq -o=json '.data') - expected_file=$(mktemp) && actual_file=$(mktemp) - printf '%s\n' "$expected" > "$expected_file" - printf '%s\n' "$actual" > "$actual_file" - if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then - echo "ERROR: ConfigMap spark-pi-s3-1-executor-pod-template data drifted from snapshot." - printf '%s\n' "$diff_out" - rm -f "$expected_file" "$actual_file" - exit 1 - fi - rm -f "$expected_file" "$actual_file" - - script: | - expected=$(cat <<'YAMLEOF' | yq -o=json - security.properties: | - networkaddress.cache.negative.ttl=0 - networkaddress.cache.ttl=30 - spark-env.sh: "" - YAMLEOF - ) - actual=$(kubectl -n $NAMESPACE get cm spark-pi-s3-1-submit-job -o yaml | yq -o=json '.data') - expected_file=$(mktemp) && actual_file=$(mktemp) - printf '%s\n' "$expected" > "$expected_file" - printf '%s\n' "$actual" > "$actual_file" - if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then - echo "ERROR: ConfigMap spark-pi-s3-1-submit-job data drifted from snapshot." - printf '%s\n' "$diff_out" - rm -f "$expected_file" "$actual_file" - exit 1 - fi - rm -f "$expected_file" "$actual_file" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spark-pi-s3-1-submit-job +data: + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=30 + spark-env.sh: "" diff --git a/tests/templates/kuttl/spark-connect/13-assert.yaml.j2 b/tests/templates/kuttl/spark-connect/13-assert.yaml.j2 index 747390f7..893197b8 100644 --- a/tests/templates/kuttl/spark-connect/13-assert.yaml.j2 +++ b/tests/templates/kuttl/spark-connect/13-assert.yaml.j2 @@ -1,748 +1,513 @@ --- -# Snapshot the full `.data` of each operator-managed ConfigMap. -# Any code change that alters rendered config values will fail these diffs. +# Snapshot the `.data` of the operator-managed ConfigMaps as plain Kubernetes +# objects and let kuttl compare them. Any code change that alters a rendered +# config value fails this assert. # -# Runs as its own step (after 10/11/12) so kuttl does not re-evaluate the heavy -# heredoc on every 1-second readiness retry of the install step. +# Two things to know before adding keys here: # -# The heredoc is quoted (`<<'YAMLEOF'`) so shell substitution is disabled and -# property-style escapes (`\:`, `\=`) survive verbatim. Only `__NAMESPACE__` is -# substituted afterwards via `sed`, because kuttl tests run in a randomized -# namespace per invocation. Both sides are normalized to canonical JSON via -# `yq -o=json` before comparison. +# * kuttl compares each `.data` value as one exact string, so a single volatile +# line inside a value cannot be masked. +# * cannot test for "spark-defaults.conf" :( +# This entry contains full image strings for driver and executor pods as +# spark properties. These images include the SDP release version as part +# of the tag. This version is not available to beku/kuttl and therefore +# cannot be templated. +# Since it cannot be templated, the test will fail when run from a release +# branch or with a custom image name. +# * kuttl subset-matches maps, so a key the operator newly adds to `.data` does +# not fail this assert. Add it below to cover it. apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 60 -commands: - - script: | - expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json - metrics.properties: | - *.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet - *.sink.prometheusServlet.path=/metrics/prometheus - security.properties: | - networkaddress.cache.negative.ttl=0 - networkaddress.cache.ttl=30 - spark-defaults.conf: | - spark.driver.defaultJavaOptions=-Djava.security.properties\=/stackable/spark/conf/security.properties\ -Dlog4j.configurationFile\=/stackable/log_config/log4j2.properties\ -Dmy.custom.jvm.arg\=customValue - spark.driver.extraClassPath=/stackable/spark/extra-jars/*\:/stackable/spark/connect/spark-connect-{{ test_scenario['values']['spark-connect'].split(',')[0] }}.jar -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - spark.driver.extraJavaOptions=-Djavax.net.ssl.trustStore\=/stackable/truststore/truststore.p12\ -Djavax.net.ssl.trustStorePassword\=changeit\ -Djavax.net.ssl.trustStoreType\=pkcs12 -{% endif %} - spark.driver.host=spark-connect-server-headless - spark.executor.defaultJavaOptions=-Djava.security.properties\=/stackable/spark/conf/security.properties\ -Dlog4j.configurationFile\=/stackable/log_config/log4j2.properties -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - spark.executor.extraJavaOptions=-Djavax.net.ssl.trustStore\=/stackable/truststore/truststore.p12\ -Djavax.net.ssl.trustStorePassword\=changeit\ -Djavax.net.ssl.trustStoreType\=pkcs12 -{% endif %} - spark.executor.instances=1 - spark.executor.memory=1024M - spark.executor.memoryOverhead=1m - spark.hadoop.fs.s3a.access.key=${file\:UTF-8\:/stackable/secrets/minio-credentials-class/accessKey} - spark.hadoop.fs.s3a.aws.credentials.provider=org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider - spark.hadoop.fs.s3a.bucket.ingest-bucket.access.key=${file\:UTF-8\:/stackable/secrets/minio-credentials-class/accessKey} - spark.hadoop.fs.s3a.bucket.ingest-bucket.aws.credentials.provider=org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - spark.hadoop.fs.s3a.bucket.ingest-bucket.endpoint=https\://minio\:9000/ -{% else %} - spark.hadoop.fs.s3a.bucket.ingest-bucket.endpoint=http\://minio\:9000/ -{% endif %} - spark.hadoop.fs.s3a.bucket.ingest-bucket.endpoint.region=us-east-1 - spark.hadoop.fs.s3a.bucket.ingest-bucket.path.style.access=true - spark.hadoop.fs.s3a.bucket.ingest-bucket.secret.key=${file\:UTF-8\:/stackable/secrets/minio-credentials-class/secretKey} -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - spark.hadoop.fs.s3a.endpoint=https\://minio\:9000/ -{% else %} - spark.hadoop.fs.s3a.endpoint=http\://minio\:9000/ -{% endif %} - spark.hadoop.fs.s3a.endpoint.region=us-east-1 - spark.hadoop.fs.s3a.path.style.access=true - spark.hadoop.fs.s3a.secret.key=${file\:UTF-8\:/stackable/secrets/minio-credentials-class/secretKey} - spark.jars.ivy=/tmp/ivy2 - spark.kubernetes.authenticate.driver.serviceAccountName=spark-connect-serviceaccount - spark.kubernetes.driver.container.image=oci.stackable.tech/sdp/spark-k8s\:{{ test_scenario['values']['spark-connect'].split(',')[0] }}-stackable0.0.0-dev - spark.kubernetes.driver.pod.name=${env\:HOSTNAME} - spark.kubernetes.executor.container.image=oci.stackable.tech/sdp/spark-k8s\:{{ test_scenario['values']['spark-connect'].split(',')[0] }}-stackable0.0.0-dev - spark.kubernetes.executor.limit.cores=1 - spark.kubernetes.executor.podTemplateContainerName=spark - spark.kubernetes.executor.podTemplateFile=/stackable/spark/conf/template.yaml - spark.kubernetes.executor.request.cores=1 - spark.kubernetes.namespace=__NAMESPACE__ - spark.metrics.conf=/stackable/spark/conf/metrics.properties - spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions - spark.ui.prometheus.enabled=true - template.yaml: | - metadata: - labels: - app.kubernetes.io/component: executor - app.kubernetes.io/instance: spark-connect - app.kubernetes.io/managed-by: spark.stackable.tech_connect - app.kubernetes.io/name: spark-connect - app.kubernetes.io/role-group: default - app.kubernetes.io/version: {{ test_scenario['values']['spark-connect'].split(',')[0] }}-stackable0.0.0-dev - stackable.tech/vendor: Stackable - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/component: executor - app.kubernetes.io/instance: spark-connect - app.kubernetes.io/name: spark-connect - topologyKey: kubernetes.io/hostname - weight: 70 - containers: - - env: - - name: CONTAINERDEBUG_LOG_DIRECTORY - value: /stackable/log/containerdebug - name: spark - volumeMounts: - - mountPath: /stackable/spark/conf - name: config - - mountPath: /stackable/log - name: log - - mountPath: /stackable/secrets/minio-credentials-class - name: minio-credentials-class-s3-credentials -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - mountPath: /stackable/secrets/minio-tls-ca - name: minio-tls-ca-ca-cert -{% endif %} - - mountPath: /stackable/truststore - name: stackable-truststore - - mountPath: /stackable/log_config - name: log-config -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - - args: - - |- - mkdir --parents /stackable/log/_vector-state - # Vector will ignore SIGTERM (as PID != 1) and must be shut down by writing a shutdown trigger file - vector & vector_pid=$! - if [ ! -f "/stackable/log/_vector/shutdown" ]; then - mkdir -p /stackable/log/_vector - inotifywait -qq --event create /stackable/log/_vector; - fi - sleep 1 - kill $vector_pid - command: - - /bin/bash - - -x - - -euo - - pipefail - - -c - env: - - name: CLUSTER_NAME - value: spark-connect - - name: DATA_DIR - value: /stackable/log/_vector-state - - name: LOG_DIR - value: /stackable/log - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: ROLE_GROUP_NAME - value: default - - name: ROLE_NAME - value: executor - - name: VECTOR_AGGREGATOR_ADDRESS - valueFrom: - configMapKeyRef: - key: ADDRESS - name: vector-aggregator-discovery - - name: VECTOR_CONFIG_YAML - value: /stackable/config/vector.yaml - - name: VECTOR_FILE_LOG_LEVEL - value: info - - name: VECTOR_LOG - value: info - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark-connect'].split(',')[0] }}-stackable0.0.0-dev - imagePullPolicy: IfNotPresent - name: vector - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 250m - memory: 128Mi - volumeMounts: - - mountPath: /stackable/config/vector.yaml - name: config - readOnly: true - subPath: vector.yaml - - mountPath: /stackable/log - name: log -{% endif %} - enableServiceLinks: false -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - initContainers: - - command: - - /bin/bash - - -x - - -euo - - pipefail - - -c - - cert-tools generate-pkcs12-truststore --pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem --out /stackable/truststore/truststore.p12 --out-password changeit && cert-tools generate-pkcs12-truststore --out /stackable/truststore/truststore.p12 --out-password changeit --pkcs12 /stackable/truststore/truststore.p12:changeit --pem /stackable/secrets/minio-tls-ca/ca.crt - image: oci.stackable.tech/sdp/spark-k8s:{{ test_scenario['values']['spark-connect'].split(',')[0] }}-stackable0.0.0-dev - name: tls-truststore-init - resources: - limits: - cpu: 10m - memory: 128Mi - requests: - cpu: 10m - memory: 128Mi - volumeMounts: - - mountPath: /stackable/secrets/minio-credentials-class - name: minio-credentials-class-s3-credentials - - mountPath: /stackable/secrets/minio-tls-ca - name: minio-tls-ca-ca-cert - - mountPath: /stackable/truststore - name: stackable-truststore -{% endif %} - securityContext: - fsGroup: 1000 - volumes: - - emptyDir: - sizeLimit: 30Mi - name: log - - configMap: - name: spark-connect-executor - name: config - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: minio-credentials-class - secrets.stackable.tech/provision-parts: public-private - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: minio-credentials-class-s3-credentials -{% if test_scenario['values']['s3-use-tls'] == 'true' %} - - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: minio-tls-ca - secrets.stackable.tech/provision-parts: public - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: '1' - storageClassName: secrets.stackable.tech - name: minio-tls-ca-ca-cert -{% endif %} - - emptyDir: {} - name: stackable-truststore - - configMap: - name: spark-connect-log-config - name: log-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spark-connect-server +data: + metrics.properties: | + *.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet + *.sink.prometheusServlet.path=/metrics/prometheus + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=30 {% if lookup('env', 'VECTOR_AGGREGATOR') %} - vector.yaml: | - --- - data_dir: ${DATA_DIR} - - log_schema: - host_key: pod - - sources: - # Reads the internal Vector logs - vector: - type: internal_logs - - files_stdout: - type: file - include: - - ${LOG_DIR}/*/*.stdout.log - - files_stderr: - type: file - include: - - ${LOG_DIR}/*/*.stderr.log - - files_log4j2: - type: file - include: - - ${LOG_DIR}/*/*.log4j2.xml - line_delimiter: "\r\n" - - transforms: - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" - - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" - - processed_files_log4j2: - inputs: - - files_log4j2 - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - event = {} - parsed_event, err = parse_xml(raw_message) - if err != null { - error = "XML not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - if !is_object(parsed_event.Event) { - error = "Parsed event contains no \"Event\" tag." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event.Event) - - tag_instant_valid = false - instant, err = object(event.Instant) + vector.yaml: | + --- + data_dir: ${DATA_DIR} + + log_schema: + host_key: pod + + sources: + # Reads the internal Vector logs + vector: + type: internal_logs + + files_stdout: + type: file + include: + - ${LOG_DIR}/*/*.stdout.log + + files_stderr: + type: file + include: + - ${LOG_DIR}/*/*.stderr.log + + files_log4j2: + type: file + include: + - ${LOG_DIR}/*/*.log4j2.xml + line_delimiter: "\r\n" + + transforms: + processed_files_stdout: + inputs: + - files_stdout + type: remap + source: | + .logger = "ROOT" + .level = "INFO" + + processed_files_stderr: + inputs: + - files_stderr + type: remap + source: | + .logger = "ROOT" + .level = "ERROR" + + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) + + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] + + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) + + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") if err == null { - epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) - if err == null && epoch_nanoseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") - if err == null { - .timestamp = converted_timestamp - tag_instant_valid = true - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) } - if !tag_instant_valid { - epoch_milliseconds, err = to_int(event.@timeMillis) - if err == null && epoch_milliseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") - if err == null { - .timestamp = converted_timestamp - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } - .logger, err = string(event.@loggerName) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } - level, err = string(event.@level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } - exception = null - thrown = event.Thrown - if is_object(thrown) { - exception = "Exception" - thread, err = string(event.@thread) - if err == null && !is_empty(thread) { - exception = exception + " in thread \"" + thread + "\"" - } - thrown_name, err = string(thrown.@name) - if err == null && !is_empty(exception) { - exception = exception + " " + thrown_name - } - message = string(thrown.@localizedMessage) ?? - string(thrown.@message) ?? - "" - if !is_empty(message) { - exception = exception + ": " + message - } - stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] - stacktrace = "" - for_each(stacktrace_items) -> |_index, value| { - stacktrace = stacktrace + " " - class = string(value.@class) ?? "" - method = string(value.@method) ?? "" - if !is_empty(class) && !is_empty(method) { - stacktrace = stacktrace + "at " + class + "." + method - } - file = string(value.@file) ?? "" - line = string(value.@line) ?? "" - if !is_empty(file) && !is_empty(line) { - stacktrace = stacktrace + "(" + file + ":" + line + ")" - } - exact = to_bool(value.@exact) ?? false - location = string(value.@location) ?? "" - version = string(value.@version) ?? "" - if !is_empty(location) && !is_empty(version) { - stacktrace = stacktrace + " " - if !exact { - stacktrace = stacktrace + "~" - } - stacktrace = stacktrace + "[" + location + ":" + version + "]" - } - stacktrace = stacktrace + "\n" - } - if stacktrace != "" { - exception = exception + "\n" + stacktrace - } + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method } - - message, err = string(event.Message) - if err != null || is_empty(message) { - message = null - .errors = push(.errors, "Message not found.") + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" + } + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" } - .message = join!(compact([message, exception]), "\n") + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace } } - # Extends the processed files with the fields "container" and "file" - extended_logs_files: - inputs: - - processed_files_* - type: remap - source: | - del(.source_type) - if .errors == [] { - del(.errors) + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") } - . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') - - # Filters the logs of the Vector agent according to the defined log level - filtered_logs_vector: - inputs: - - vector - type: filter - condition: > - (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || - (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) - - # Aligns the logs of the Vector agent with the common format - extended_logs_vector: - inputs: - - filtered_logs_vector - type: remap - source: | - .container = "vector" - .level = .metadata.level - .logger = .metadata.module_path - if exists(.file) { .processed_file = del(.file) } - del(.metadata) - del(.pid) - del(.source_type) - - # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs - extended_logs: - inputs: - - extended_logs_* - type: remap - source: | - .namespace = "${NAMESPACE}" - .cluster = "${CLUSTER_NAME}" - .role = "${ROLE_NAME}" - .roleGroup = "${ROLE_GROUP_NAME}" - - sinks: - # Forward the logs to the Vector aggregator - aggregator: - inputs: - - extended_logs - type: vector - address: ${VECTOR_AGGREGATOR_ADDRESS} + .message = join!(compact([message, exception]), "\n") + } + } + + # Extends the processed files with the fields "container" and "file" + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + # Filters the logs of the Vector agent according to the defined log level + filtered_logs_vector: + inputs: + - vector + type: filter + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) + + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + + sinks: + # Forward the logs to the Vector aggregator + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} - YAMLEOF - ) - actual=$(kubectl -n $NAMESPACE get cm spark-connect-server -o yaml | yq -o=json '.data') - expected_file=$(mktemp) && actual_file=$(mktemp) - printf '%s\n' "$expected" > "$expected_file" - printf '%s\n' "$actual" > "$actual_file" - if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then - echo "ERROR: ConfigMap spark-connect-server data drifted from snapshot." - printf '%s\n' "$diff_out" - rm -f "$expected_file" "$actual_file" - exit 1 - fi - rm -f "$expected_file" "$actual_file" - - script: | - expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json - metrics.properties: | - *.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet - *.sink.prometheusServlet.path=/metrics/prometheus - security.properties: | - networkaddress.cache.negative.ttl=0 - networkaddress.cache.ttl=30 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: spark-connect-executor +data: + metrics.properties: | + *.sink.prometheusServlet.class=org.apache.spark.metrics.sink.PrometheusServlet + *.sink.prometheusServlet.path=/metrics/prometheus + security.properties: | + networkaddress.cache.negative.ttl=0 + networkaddress.cache.ttl=30 {% if lookup('env', 'VECTOR_AGGREGATOR') %} - vector.yaml: | - --- - data_dir: ${DATA_DIR} - - log_schema: - host_key: pod - - sources: - # Reads the internal Vector logs - vector: - type: internal_logs - - files_stdout: - type: file - include: - - ${LOG_DIR}/*/*.stdout.log - - files_stderr: - type: file - include: - - ${LOG_DIR}/*/*.stderr.log - - files_log4j2: - type: file - include: - - ${LOG_DIR}/*/*.log4j2.xml - line_delimiter: "\r\n" - - transforms: - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" - - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" - - processed_files_log4j2: - inputs: - - files_log4j2 - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - event = {} - parsed_event, err = parse_xml(raw_message) - if err != null { - error = "XML not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - if !is_object(parsed_event.Event) { - error = "Parsed event contains no \"Event\" tag." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event.Event) - - tag_instant_valid = false - instant, err = object(event.Instant) + vector.yaml: | + --- + data_dir: ${DATA_DIR} + + log_schema: + host_key: pod + + sources: + # Reads the internal Vector logs + vector: + type: internal_logs + + files_stdout: + type: file + include: + - ${LOG_DIR}/*/*.stdout.log + + files_stderr: + type: file + include: + - ${LOG_DIR}/*/*.stderr.log + + files_log4j2: + type: file + include: + - ${LOG_DIR}/*/*.log4j2.xml + line_delimiter: "\r\n" + + transforms: + processed_files_stdout: + inputs: + - files_stdout + type: remap + source: | + .logger = "ROOT" + .level = "INFO" + + processed_files_stderr: + inputs: + - files_stderr + type: remap + source: | + .logger = "ROOT" + .level = "ERROR" + + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) + + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] + + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) + + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") if err == null { - epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) - if err == null && epoch_nanoseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") - if err == null { - .timestamp = converted_timestamp - tag_instant_valid = true - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } - } else { - .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) - } + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) } - if !tag_instant_valid { - epoch_milliseconds, err = to_int(event.@timeMillis) - if err == null && epoch_milliseconds != 0 { - converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") - if err == null { - .timestamp = converted_timestamp - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) - } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } - .logger, err = string(event.@loggerName) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } - level, err = string(event.@level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } - exception = null - thrown = event.Thrown - if is_object(thrown) { - exception = "Exception" - thread, err = string(event.@thread) - if err == null && !is_empty(thread) { - exception = exception + " in thread \"" + thread + "\"" - } - thrown_name, err = string(thrown.@name) - if err == null && !is_empty(exception) { - exception = exception + " " + thrown_name - } - message = string(thrown.@localizedMessage) ?? - string(thrown.@message) ?? - "" - if !is_empty(message) { - exception = exception + ": " + message - } - stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] - stacktrace = "" - for_each(stacktrace_items) -> |_index, value| { - stacktrace = stacktrace + " " - class = string(value.@class) ?? "" - method = string(value.@method) ?? "" - if !is_empty(class) && !is_empty(method) { - stacktrace = stacktrace + "at " + class + "." + method - } - file = string(value.@file) ?? "" - line = string(value.@line) ?? "" - if !is_empty(file) && !is_empty(line) { - stacktrace = stacktrace + "(" + file + ":" + line + ")" - } - exact = to_bool(value.@exact) ?? false - location = string(value.@location) ?? "" - version = string(value.@version) ?? "" - if !is_empty(location) && !is_empty(version) { - stacktrace = stacktrace + " " - if !exact { - stacktrace = stacktrace + "~" - } - stacktrace = stacktrace + "[" + location + ":" + version + "]" - } - stacktrace = stacktrace + "\n" - } - if stacktrace != "" { - exception = exception + "\n" + stacktrace - } + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method } - - message, err = string(event.Message) - if err != null || is_empty(message) { - message = null - .errors = push(.errors, "Message not found.") + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" + } + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" } - .message = join!(compact([message, exception]), "\n") + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace } } - # Extends the processed files with the fields "container" and "file" - extended_logs_files: - inputs: - - processed_files_* - type: remap - source: | - del(.source_type) - if .errors == [] { - del(.errors) + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") } - . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') - - # Filters the logs of the Vector agent according to the defined log level - filtered_logs_vector: - inputs: - - vector - type: filter - condition: > - (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || - (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || - (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) - - # Aligns the logs of the Vector agent with the common format - extended_logs_vector: - inputs: - - filtered_logs_vector - type: remap - source: | - .container = "vector" - .level = .metadata.level - .logger = .metadata.module_path - if exists(.file) { .processed_file = del(.file) } - del(.metadata) - del(.pid) - del(.source_type) - - # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs - extended_logs: - inputs: - - extended_logs_* - type: remap - source: | - .namespace = "${NAMESPACE}" - .cluster = "${CLUSTER_NAME}" - .role = "${ROLE_NAME}" - .roleGroup = "${ROLE_GROUP_NAME}" - - sinks: - # Forward the logs to the Vector aggregator - aggregator: - inputs: - - extended_logs - type: vector - address: ${VECTOR_AGGREGATOR_ADDRESS} + .message = join!(compact([message, exception]), "\n") + } + } + + # Extends the processed files with the fields "container" and "file" + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + # Filters the logs of the Vector agent according to the defined log level + filtered_logs_vector: + inputs: + - vector + type: filter + condition: > + (.metadata.level == "TRACE" && "${VECTOR_FILE_LOG_LEVEL}" == "trace") || + (.metadata.level == "DEBUG" && includes(["trace", "debug"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "INFO" && includes(["trace", "debug", "info"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "WARN" && includes(["trace", "debug", "info", "warn"], "${VECTOR_FILE_LOG_LEVEL}")) || + (.metadata.level == "ERROR" && includes(["trace", "debug", "info", "warn", "error"], "${VECTOR_FILE_LOG_LEVEL}")) + + # Aligns the logs of the Vector agent with the common format + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) + + # Add the fields "namespace", "cluster", "role" and "roleGroup" to all logs + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + + sinks: + # Forward the logs to the Vector aggregator + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} - YAMLEOF - ) - actual=$(kubectl -n $NAMESPACE get cm spark-connect-executor -o yaml | yq -o=json '.data') - expected_file=$(mktemp) && actual_file=$(mktemp) - printf '%s\n' "$expected" > "$expected_file" - printf '%s\n' "$actual" > "$actual_file" - if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then - echo "ERROR: ConfigMap spark-connect-executor data drifted from snapshot." - printf '%s\n' "$diff_out" - rm -f "$expected_file" "$actual_file" - exit 1 - fi - rm -f "$expected_file" "$actual_file" diff --git a/tests/templates/kuttl/spark-connect/14-assert.yaml.j2 b/tests/templates/kuttl/spark-connect/14-assert.yaml.j2 new file mode 100644 index 00000000..6fcac551 --- /dev/null +++ b/tests/templates/kuttl/spark-connect/14-assert.yaml.j2 @@ -0,0 +1,228 @@ +--- +# Snapshot assert for the executor Pod that Spark Connect created. +# The `template.yaml` entry in the ConfigMap value cannot be asserted declaratively. +# +# kuttl mechanics that shape this file: +# +# * Spark names executor pods `--exec-N`, so there is no +# deterministic `metadata.name`. Omitting the name makes kuttl use +# `metadata.labels` as a label selector, and it then passes if ANY listed Pod +# is a subset match -- hence `status.phase: Running`, so a terminating +# executor from an earlier reconcile cannot produce a false pass. +# * kuttl requires every array to have the SAME LENGTH as the live object and +# compares arrays positionally (`IsSubset` -> "slice length mismatch"). Every +# container, volume and volumeMount below must therefore be listed, including +# the ones Spark and Kubernetes inject. Array *elements* are maps and are +# subset-matched, so volatile fields inside an element are simply omitted; +# each such omission is called out inline. +# * Live container order is [vector, spark] -- the reverse of the template. +# Spark extracts the container named by +# `spark.kubernetes.executor.podTemplateContainerName` and re-appends it last. +# +# Deliberately not asserted: +# * every container `image` -- the reason the ConfigMap snapshot was dropped. +# * `containers[spark].env` -- Spark injects ~40 variables and emits +# `SPARK_JAVA_OPT_*` in a different order on every run, so a positional +# comparison can never pass reliably. +# * `containers[spark].args` / `.ports` -- Spark-owned, would couple this assert +# to the Spark version for no operator coverage. +# * `app.kubernetes.io/version` (changes on a release build) and the `spark-*` +# labels carrying generated ids. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: v1 +kind: Pod +metadata: + # No name on purpose: these labels are the selector (see header). + labels: + app.kubernetes.io/component: executor + app.kubernetes.io/instance: spark-connect + app.kubernetes.io/managed-by: spark.stackable.tech_connect + app.kubernetes.io/name: spark-connect + app.kubernetes.io/role-group: default + spark-role: executor + stackable.tech/vendor: Stackable +spec: + # Comes from spark.kubernetes.authenticate.driver.serviceAccountName; the + # operator's pod template does not set it. + serviceAccountName: spark-connect-serviceaccount + enableServiceLinks: false + securityContext: + # Required so the spark container can read the truststore the init container + # writes into the shared emptyDir. + fsGroup: 1000 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: executor + app.kubernetes.io/instance: spark-connect + app.kubernetes.io/name: spark-connect + topologyKey: kubernetes.io/hostname + weight: 70 + containers: +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + # Spark passes non-primary template containers through untouched. + - name: vector + command: + - /bin/bash + - -x + - -euo + - pipefail + - -c + env: + - name: CLUSTER_NAME + value: spark-connect + - name: DATA_DIR + value: /stackable/log/_vector-state + - name: LOG_DIR + value: /stackable/log + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: ROLE_GROUP_NAME + value: default + - name: ROLE_NAME + value: executor + - name: VECTOR_AGGREGATOR_ADDRESS + valueFrom: + configMapKeyRef: + key: ADDRESS + name: vector-aggregator-discovery + - name: VECTOR_CONFIG_YAML + value: /stackable/config/vector.yaml + - name: VECTOR_FILE_LOG_LEVEL + value: info + - name: VECTOR_LOG + value: info + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 250m + memory: 128Mi + volumeMounts: + - name: config + mountPath: /stackable/config/vector.yaml + subPath: vector.yaml + readOnly: true + - name: log + mountPath: /stackable/log + # name is kube-api-access-, injected by Kubernetes + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount +{% endif %} + - name: spark + resources: + # Set by Spark from spark.executor.* -- 1024M plus the 1m + # memoryOverhead the test scenario overrides. + limits: + cpu: "1" + memory: 1025Mi + requests: + cpu: "1" + memory: 1025Mi + volumeMounts: + - name: config + mountPath: /stackable/spark/conf + - name: log + mountPath: /stackable/log + - name: minio-credentials-class-s3-credentials + mountPath: /stackable/secrets/minio-credentials-class +{% if test_scenario['values']['s3-use-tls'] == 'true' %} + - name: minio-tls-ca-ca-cert + mountPath: /stackable/secrets/minio-tls-ca +{% endif %} + - name: stackable-truststore + mountPath: /stackable/truststore + - name: log-config + mountPath: /stackable/log_config + - name: spark-conf-volume-exec + mountPath: /opt/spark/conf + # mountPath is /var/data/spark-, generated per run + - name: spark-local-dir-1 + # name is kube-api-access-, injected by Kubernetes + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount +{% if test_scenario['values']['s3-use-tls'] == 'true' %} + initContainers: + - name: tls-truststore-init + command: + - /bin/bash + - -x + - -euo + - pipefail + - -c + - cert-tools generate-pkcs12-truststore --pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem --out /stackable/truststore/truststore.p12 --out-password changeit && cert-tools generate-pkcs12-truststore --out /stackable/truststore/truststore.p12 --out-password changeit --pkcs12 /stackable/truststore/truststore.p12:changeit --pem /stackable/secrets/minio-tls-ca/ca.crt + resources: + limits: + cpu: 10m + memory: 128Mi + requests: + cpu: 10m + memory: 128Mi + volumeMounts: + - name: minio-credentials-class-s3-credentials + mountPath: /stackable/secrets/minio-credentials-class + - name: minio-tls-ca-ca-cert + mountPath: /stackable/secrets/minio-tls-ca + - name: stackable-truststore + mountPath: /stackable/truststore + # name is kube-api-access-, injected by Kubernetes + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount +{% endif %} + volumes: + - name: log + emptyDir: + sizeLimit: 30Mi + - name: config + configMap: + name: spark-connect-executor + - name: minio-credentials-class-s3-credentials + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: minio-credentials-class + secrets.stackable.tech/provision-parts: public-private + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech +{% if test_scenario['values']['s3-use-tls'] == 'true' %} + - name: minio-tls-ca-ca-cert + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: minio-tls-ca + secrets.stackable.tech/provision-parts: public + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech +{% endif %} + - name: stackable-truststore + emptyDir: {} + - name: log-config + configMap: + name: spark-connect-log-config + # configMap.name is spark-exec--conf-map, generated per run + - name: spark-conf-volume-exec + - name: spark-local-dir-1 + emptyDir: {} + # name is kube-api-access-, injected by Kubernetes + - projected: + defaultMode: 420 +status: + phase: Running