diff --git a/CHANGELOG.md b/CHANGELOG.md index efaf4921..57c2da92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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 - 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 0fce88c0..0fd07fd8 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" @@ -3111,7 +3111,7 @@ dependencies = [ [[package]] name = "stackable-spark-k8s-operator" -version = "0.0.0-dev" +version = "26.7.0" dependencies = [ "anyhow", "built", diff --git a/Cargo.nix b/Cargo.nix index fd10413f..6be74974 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 = [ @@ -10305,7 +10305,7 @@ rec { }; "stackable-spark-k8s-operator" = rec { crateName = "stackable-spark-k8s-operator"; - version = "0.0.0-dev"; + version = "26.7.0"; edition = "2024"; crateBin = [ { @@ -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/Cargo.toml b/Cargo.toml index b7cb1efb..46d9bd17 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" authors = ["Stackable GmbH "] license = "OSL-3.0" edition = "2024" 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 diff --git a/deploy/helm/spark-k8s-operator/Chart.yaml b/deploy/helm/spark-k8s-operator/Chart.yaml index 5d137d3a..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: "0.0.0-dev" -appVersion: "0.0.0-dev" +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/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..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 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 +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=0.0.0-dev \ - secret=0.0.0-dev \ - listener=0.0.0-dev \ - spark-k8s=0.0.0-dev + 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 1664d30f..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=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 operator +Installed secret=26.7.0 operator +Installed listener=26.7.0 operator +Installed spark-k8s=26.7.0 operator diff --git a/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc b/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc index be8ebb9f..1e3fc716 100644 --- a/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc +++ b/docs/modules/spark-k8s/pages/usage-guide/app_templates.adoc @@ -12,16 +12,10 @@ Application templates are available for the `v1alpha1` version of the SparkAppli 4. Application template references are immutable in the sense that once applied to an application they cannot be changed again. Currently templates are applied upon the creation of the application, and any changes to the template references after that will be ignored. 5. Application and template CRDs must have the exact same versions. Currently only `v1alpha1` is supported. -== Migrating from cluster-scoped templates +IMPORTANT: Application templates were cluster-scoped when they were first released in SDP 26.3, and are namespace-scoped from SDP 26.7 onwards. +Upgrading across that change requires manual steps, because a CustomResourceDefinition cannot change its scope in place. +See xref:usage-guide/upgrade.adoc[] before upgrading. -IMPORTANT: Application templates used to be cluster wide resources when they were first released. This was a mistake. Many users do not have the access rights to create cluster scoped resources and so the templates are now namespace scoped. - -If you are migrating from older installations where templates were treated as cluster-wide resources, account for the following: - -1. Recreate each template in every namespace where SparkApplications use it. -2. Keep template names consistent per namespace if you want the same application annotations to continue working. -3. Cross-namespace template references are no longer resolved; templates and applications must be in the same namespace. -4. Update GitOps/automation manifests to create templates as namespace-targeted resources before reconciling dependent SparkApplications. == Examples Applications use `metadata.annotations` to reference application templates as shown below: diff --git a/docs/modules/spark-k8s/pages/usage-guide/upgrade.adoc b/docs/modules/spark-k8s/pages/usage-guide/upgrade.adoc new file mode 100644 index 00000000..f2eccdde --- /dev/null +++ b/docs/modules/spark-k8s/pages/usage-guide/upgrade.adoc @@ -0,0 +1,93 @@ += SDP upgrade notes +:description: Instructions for upgrading the SDP versions. + +== Upgrade from SDP 26.3 to 26.7 + +=== Application templates changed from cluster-scoped to namespace-scoped + +`SparkApplicationTemplate` was a cluster-scoped resource when it was first released in SDP 26.3. +As of SDP 26.7 the resource is namespace-scoped because many users do not have the access rights to create cluster-scoped resources. + +WARNING: This change requires manual intervention *before* the upgrade. +A Kubernetes CustomResourceDefinition cannot change its scope in place, so the 26.7 operator cannot perform this migration for you, and it does not start until the old CustomResourceDefinition is gone. + +==== What you see if you upgrade without preparing + +The operator maintains its own CustomResourceDefinitions. +On startup it tries to reconcile `sparkapptemplates.spark.stackable.tech` to the namespace-scoped definition, the API server rejects the change because `spec.scope` is immutable, and the operator exits: + +[source] +---- +Error: failed to run webhook server + +Caused by: + 0: failed to update certificate + 1: conversion webhook error + 2: failed to patch CRD "sparkapptemplates.spark.stackable.tech" + 3: ApiError: CustomResourceDefinition.apiextensions.k8s.io + "sparkapptemplates.spark.stackable.tech" is invalid: + spec.scope: Invalid value: "Namespaced": field is immutable: Invalid +---- + +The operator Pod then enters `CrashLoopBackOff` and no Spark resources are reconciled — not just templates, but `SparkApplication`, `SparkConnectServer` and `SparkHistoryServer` as well, because the operator never finishes starting. + +When installed through OLM on OpenShift, the ClusterServiceVersion stays in the `Installing` phase with `waiting for deployment spark-k8s-operator-deployment to become ready`. + +=== Migration procedure + +Deleting the CustomResourceDefinition deletes every `SparkApplicationTemplate` in the cluster. +Export them first if you want to keep them. + +. Export the existing templates, while the 26.3 operator is still running: ++ +[source,shell] +---- +kubectl get sparkapptemplates.spark.stackable.tech -o yaml > spark-app-templates.yaml +---- ++ +NOTE: Because the resource is still cluster-scoped at this point, this single command captures every template in the cluster. +Note which namespaces the SparkApplications that reference them live in — you need that in step 4. + +. Delete the CustomResourceDefinition. +This also deletes all `SparkApplicationTemplate` objects: ++ +[source,shell] +---- +kubectl delete crd sparkapptemplates.spark.stackable.tech +---- + +. Upgrade the operator to 26.7. +It recreates `sparkapptemplates.spark.stackable.tech` as a namespace-scoped resource on startup. +Confirm it did: ++ +[source,shell] +---- +kubectl get crd sparkapptemplates.spark.stackable.tech -o jsonpath='{.spec.scope}' +---- ++ +This must print `Namespaced`. + +. Recreate the templates from your export, once per namespace that needs them. +Strip the `metadata.resourceVersion`, `metadata.uid` and `metadata.creationTimestamp` fields first, otherwise the apply is rejected. +The target namespace comes from `-n`, so the manifests themselves do not need a `metadata.namespace`: ++ +[source,shell] +---- +kubectl apply -n -f spark-app-templates.yaml +---- ++ +If only some templates belong in a given namespace, split the exported file accordingly rather than applying all of them everywhere. + +If the operator is already crashlooping because the upgrade happened first, the recovery is the same, except that the templates are no longer readable and cannot be exported. +Delete the CustomResourceDefinition, let the operator restart, and recreate the templates from your source manifests or GitOps repository. + +=== Consequences for existing manifests + +A template must now exist in the same namespace as the SparkApplications that reference it: + +. Recreate each template in every namespace where SparkApplications use it. +. Keep template names consistent per namespace if you want the same application annotations to keep working. +. Cross-namespace template references are no longer resolved; templates and applications must be in the same namespace. +. Update GitOps and automation manifests to create templates as namespace-targeted resources before reconciling dependent SparkApplications. + +See xref:usage-guide/app_templates.adoc[] for the current behaviour of application templates. diff --git a/docs/modules/spark-k8s/partials/nav.adoc b/docs/modules/spark-k8s/partials/nav.adoc index 71f2278e..fe02cc88 100644 --- a/docs/modules/spark-k8s/partials/nav.adoc +++ b/docs/modules/spark-k8s/partials/nav.adoc @@ -13,6 +13,7 @@ ** xref:spark-k8s:usage-guide/spark-connect.adoc[] ** xref:spark-k8s:usage-guide/examples.adoc[] ** xref:spark-k8s:usage-guide/overrides.adoc[] +** xref:spark-k8s:usage-guide/upgrade.adoc[] ** xref:spark-k8s:usage-guide/operations/index.adoc[] *** xref:spark-k8s:usage-guide/operations/applications.adoc[] *** xref:spark-k8s:usage-guide/operations/pod-placement.adoc[] diff --git a/docs/templating_vars.yaml b/docs/templating_vars.yaml index c7d50fdf..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: 0.0.0-dev - secret: 0.0.0-dev - listener: 0.0.0-dev - spark: 0.0.0-dev + commons: 26.7.0 + secret: 26.7.0 + listener: 26.7.0 + spark: 26.7.0 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/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" diff --git a/tests/release.yaml b/tests/release.yaml index 7f215241..dc63000a 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 secret: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 listener: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 zookeeper: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 hive: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 hdfs: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 hbase: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 spark-k8s: - operatorVersion: 0.0.0-dev + operatorVersion: 26.7.0 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