Repository navigation
feat(sparkconnect): add a Dependencies field to SparkConnectSpec - #3120
google-oss-prow[bot] merged 9 commits into
Conversation
|
🎉 Welcome to the Kubeflow Spark Operator! 🎉 Thanks for opening your first PR! We're happy to have you as part of our community 🚀 Here's what happens next:
Join the community:
Feel free to ask questions in the comments if you need any help or clarification! |
|
You need to run Let's add more tests too. |
|
Addressed the review: aligned Spark Connect with the SparkApplication deps field, shell-quoted all dependency arguments for the bash -c entrypoint, added shell-sensitive dependency coverage, and regenerated Go, CRD, OpenAPI, and Python API artifacts. d024dc8 |
|
/retitle feat(sparkconnect): add a |
Dependencies field to SparkConnectSpec for structuDependencies field to SparkConnectSpec
|
/ok-to-test |
tariq-hasan
left a comment
There was a problem hiding this comment.
Hi @Mr-Neutr0n! I have added some more comments.
Please also update docs/website/user-guide/spark-connect.md, add a new examples/sparkconnect/spark-connect-dependencies.yaml and add/update e2e tests.
|
Addressed the review in
Verification: |
244c6dd to
c9a8561
Compare
|
Two CI checks needed generated updates rather than code changes: the Python model was missing the generator's trailing blank lines, and |
c9a8561 to
2e5413c
Compare
|
The e2e run was right: the dependencies test timed out waiting for the server pod to become ready, which needs the kind cluster to resolve the Maven artifacts (the rest of the suite only uses artifacts bundled in the Spark image). I narrowed the test to what this change owns: the SparkConnect is reconciled into a server pod whose |
Add spec.deps to Spark Connect with the Maven-related fields from the review: jars, packages, excludePackages, and repositories. The controller passes them to spark-submit as --jars, --packages, --exclude-packages, and --repositories. - Regenerate the deepcopy, OpenAPI, Swagger, CRD, API docs, and Python API artifacts. - Document .spec.deps in the Spark Connect user guide. - Add examples/sparkconnect/spark-connect-dependencies.yaml. - Add an e2e test that starts the server from the new example and checks that the dependency flags reach spark-submit. - Drop two unnecessary string conversions flagged by golangci-lint. Fixes kubeflow#2959 Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
2e5413c to
ae912f8
Compare
|
Rebased onto current Conflicts were with the Spark Connect GPU work and #3188 (per-arg container args). Review asks still hold: only |
tariq-hasan
left a comment
There was a problem hiding this comment.
Hi @Mr-Neutr0n! Thanks for addressing the earlier comments. I have some final comments on the implementation - mostly around the example YAML and the e2e test.
| # | ||
| # Copyright 2026 The Kubeflow authors. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # https://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. |
There was a problem hiding this comment.
Nit: Let's match the copyright with the template from the testing repo: https://github.com/kubeflow/testing/blob/8483606e0f8a34bf647f3b62a516efa17096c58c/hack/boilerplate/boilerplate.sh.txt.
| # | |
| # Copyright 2026 The Kubeflow authors. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # https://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # Copyright The Kubeflow Authors. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. |
| server: | ||
| template: | ||
| spec: | ||
| containers: | ||
| - name: spark-kubernetes-driver | ||
| image: docker.io/apache/spark:4.0.4 | ||
| imagePullPolicy: IfNotPresent | ||
| serviceAccount: spark-operator-spark | ||
| securityContext: | ||
| capabilities: | ||
| drop: | ||
| - ALL | ||
| runAsGroup: 185 | ||
| runAsUser: 185 | ||
| runAsNonRoot: true | ||
| allowPrivilegeEscalation: false | ||
| seccompProfile: | ||
| type: RuntimeDefault | ||
| executor: | ||
| instances: 1 | ||
| cores: 1 | ||
| memory: 512m | ||
| template: | ||
| spec: | ||
| containers: | ||
| - name: spark-kubernetes-executor | ||
| image: docker.io/apache/spark:4.0.4 | ||
| imagePullPolicy: IfNotPresent | ||
| securityContext: | ||
| capabilities: | ||
| drop: | ||
| - ALL | ||
| runAsGroup: 185 | ||
| runAsUser: 185 | ||
| runAsNonRoot: true | ||
| allowPrivilegeEscalation: false | ||
| seccompProfile: | ||
| type: RuntimeDefault |
There was a problem hiding this comment.
Let's vary only on spec.deps and match the rest of the spec with examples/sparkconnect/spark-connect.yaml to ensure we isolate the spec.deps behavior in the e2e.
| server: | |
| template: | |
| spec: | |
| containers: | |
| - name: spark-kubernetes-driver | |
| image: docker.io/apache/spark:4.0.4 | |
| imagePullPolicy: IfNotPresent | |
| serviceAccount: spark-operator-spark | |
| securityContext: | |
| capabilities: | |
| drop: | |
| - ALL | |
| runAsGroup: 185 | |
| runAsUser: 185 | |
| runAsNonRoot: true | |
| allowPrivilegeEscalation: false | |
| seccompProfile: | |
| type: RuntimeDefault | |
| executor: | |
| instances: 1 | |
| cores: 1 | |
| memory: 512m | |
| template: | |
| spec: | |
| containers: | |
| - name: spark-kubernetes-executor | |
| image: docker.io/apache/spark:4.0.4 | |
| imagePullPolicy: IfNotPresent | |
| securityContext: | |
| capabilities: | |
| drop: | |
| - ALL | |
| runAsGroup: 185 | |
| runAsUser: 185 | |
| runAsNonRoot: true | |
| allowPrivilegeEscalation: false | |
| seccompProfile: | |
| type: RuntimeDefault | |
| server: | |
| template: | |
| metadata: | |
| labels: | |
| key1: value1 | |
| key2: value2 | |
| annotations: | |
| key3: value3 | |
| key4: value4 | |
| spec: | |
| containers: | |
| - name: spark-kubernetes-driver | |
| image: docker.io/apache/spark:4.0.4 | |
| imagePullPolicy: Always | |
| resources: | |
| requests: | |
| cpu: 1 | |
| memory: 1Gi | |
| limits: | |
| cpu: 1 | |
| memory: 1Gi | |
| serviceAccount: spark-operator-spark | |
| securityContext: | |
| capabilities: | |
| drop: | |
| - ALL | |
| runAsGroup: 185 | |
| runAsUser: 185 | |
| runAsNonRoot: true | |
| allowPrivilegeEscalation: false | |
| seccompProfile: | |
| type: RuntimeDefault | |
| executor: | |
| instances: 2 | |
| cores: 1 | |
| memory: 512m | |
| template: | |
| metadata: | |
| labels: | |
| key1: value1 | |
| key2: value2 | |
| annotations: | |
| key3: value3 | |
| key4: value4 | |
| spec: | |
| containers: | |
| - name: spark-kubernetes-executor | |
| image: docker.io/apache/spark:4.0.4 | |
| imagePullPolicy: Always | |
| securityContext: | |
| capabilities: | |
| drop: | |
| - ALL | |
| runAsGroup: 185 | |
| runAsUser: 185 | |
| runAsNonRoot: true | |
| allowPrivilegeEscalation: false | |
| seccompProfile: | |
| type: RuntimeDefault |
| /* | ||
| Copyright 2026 The Kubeflow authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| https://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ |
There was a problem hiding this comment.
Nit comment.
| /* | |
| Copyright 2026 The Kubeflow authors. | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| https://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| /* | |
| Copyright The Kubeflow Authors. | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ |
| It("passes dependency values through unchanged", func() { | ||
| conn := &v1alpha1.SparkConnect{ | ||
| Spec: v1alpha1.SparkConnectSpec{ | ||
| Deps: v1alpha1.Dependencies{ | ||
| Jars: []string{"local:///opt/spark/jars/with space.jar", "local:///opt/spark/jars/quote'file.jar"}, | ||
| Packages: []string{"org.example:package:1.0; printf injected"}, | ||
| ExcludePackages: []string{"org.example:excluded"}, | ||
| Repositories: []string{"https://repo.example/a path"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| args, err := dependenciesOption(conn) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(args).To(Equal([]string{ | ||
| "--jars", "local:///opt/spark/jars/with space.jar,local:///opt/spark/jars/quote'file.jar", | ||
| "--packages", "org.example:package:1.0; printf injected", | ||
| "--exclude-packages", "org.example:excluded", | ||
| "--repositories", "https://repo.example/a path", | ||
| })) | ||
| }) |
There was a problem hiding this comment.
We should validate group:artifact:version for packages, group:artifact for excludePackages, and no commas anywhere in the admission webhook. That way we would avoid failures in the runtime pod and enable a better user experience.
Please create a follow-up issue for this. We can defer this to a subsequent PR.
There was a problem hiding this comment.
For the e2e test please validate server pod readiness. We need to ensure that the user is able to exercise spec.deps within the runtime server pod.
The e2e cluster has Maven egress so this should be feasible. If there are errors you may want to debug on a local kind cluster to determine the root cause of the error.
| Expect(serverArgs).To(ContainElement("--jars")) | ||
| Expect(serverArgs).To(ContainElement(ContainSubstring("commons-lang3-3.17.0.jar"))) | ||
| Expect(serverArgs).To(ContainElement("--packages")) | ||
| Expect(serverArgs).To(ContainElement(ContainSubstring("org.apache.commons:commons-text:1.12.0"))) | ||
| Expect(serverArgs).To(ContainElement("--exclude-packages")) | ||
| Expect(serverArgs).To(ContainElement(ContainSubstring("org.apache.commons:commons-lang3"))) | ||
| Expect(serverArgs).To(ContainElement("--repositories")) | ||
| Expect(serverArgs).To(ContainElement(ContainSubstring("https://repo1.maven.org/maven2"))) |
There was a problem hiding this comment.
I think these checks are already covered by the unit tests so these are redundant on the e2e.
We would want to validate server pod readiness and run actual queries on the server pod to ensure that spec.deps can be exercised by the user.
| deps: | ||
| jars: | ||
| - https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar | ||
| packages: | ||
| - org.apache.commons:commons-text:1.12.0 | ||
| excludePackages: | ||
| - org.apache.commons:commons-lang3 | ||
| repositories: | ||
| - https://repo1.maven.org/maven2 |
There was a problem hiding this comment.
These jars and packages already exist in the base Spark image. Let's use the following as they truly reflect external dependencies. Note here that the excluded packages come as transitive dependencies of spark-avro but already present in the base Spark image so this is a good test for package exclusion.
I've deliberately left out spec.repositories because Maven Central is checked in the Spark runtime and there is little point in exercising third-party archives on the CI.
| deps: | |
| jars: | |
| - https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar | |
| packages: | |
| - org.apache.commons:commons-text:1.12.0 | |
| excludePackages: | |
| - org.apache.commons:commons-lang3 | |
| repositories: | |
| - https://repo1.maven.org/maven2 | |
| deps: | |
| jars: | |
| - https://repo1.maven.org/maven2/com/google/re2j/re2j/1.8/re2j-1.8.jar | |
| packages: | |
| # spark-avro must match the image's Spark version | |
| - org.apache.spark:spark-avro_2.13:4.0.4 | |
| excludePackages: | |
| - org.tukaani:xz | |
| - org.apache.spark:spark-tags_2.13 | |
| # repositories: | |
| # - https://packages.confluent.io/maven/ # only for artifacts not on Maven Central |
Please run SELECT hex(to_avro(42)) and SELECT reflect('com.google.re2j.Pattern', 'matches', 'a.c', 'abc') in the e2e and validate ContainSubstring("AVRO=54") and ContainSubstring("RE2J=true") respectively to ensure that the jars and packages have resolved in the Spark runtime.
…add-a-dependencies-fie
|
All points are in at
I could not run the e2e locally (no kind cluster with Maven egress here), so this needs the CI run. |
…ark Connect server subm Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
|
Pushed a fix for the failing CI. Set |
- Match examples/sparkconnect/spark-connect.yaml apart from spec.deps so the e2e isolates the dependency behaviour, and use external artifacts that are not already in the base Spark image (re2j jar, spark-avro package, with transitive excludePackages). Drop spec.repositories since Maven Central is already in the runtime and third-party archives add no signal. - Use the Kubeflow Authors boilerplate in both files. - e2e now waits for server and executor pod readiness and runs an actual Spark Connect query that resolves the declared spark-avro package (to_avro), instead of asserting the container args that the unit tests already cover. Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
…ark Connect server subm Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
The dependency e2e drove spark-shell with --master 'local[1]', which selects the Classic session; the .remote() URL is ignored in that mode, so to_avro was evaluated on the client JVM and never reached the server. That produced a failure that looked like a dependency problem but was a client-mode problem, and the same query 'passed' against no server at all. Drive the Connect REPL through a pty instead. That keeps the client a real Spark Connect session, so the query runs against the operator's server and sees the packages declared in spec.deps. The helper also asserts the session type, so a classic fallback fails loudly rather than silently. Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
8141b10 to
4504dfc
Compare
|
Fixed the real cause of the e2e failure at The e2e was not actually exercising the server. The client ran So the query was evaluated on the client JVM, where the declared
So the Ivy fix you pushed is correct; it was masked by the client-mode bug. The new test drives the Connect REPL through a pty to keep a real server session, asserts the session type is I verified the helper in the image with the exact |
tariq-hasan
left a comment
There was a problem hiding this comment.
Thanks for addressing the comments @Mr-Neutr0n! I have left some final comments on the e2e.
| # Matches examples/sparkconnect/spark-connect.yaml except for spec.deps, so the | ||
| # e2e isolates the dependency behavior from the rest of the SparkConnect spec. |
There was a problem hiding this comment.
Let's remove this comment.
| # Matches examples/sparkconnect/spark-connect.yaml except for spec.deps, so the | |
| # e2e isolates the dependency behavior from the rest of the SparkConnect spec. |
There was a problem hiding this comment.
Please address this review comment.
There was a problem hiding this comment.
Please add the copyright header at the start of this file (above the module docstring).
# Copyright The Kubeflow Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.| if rows is None: | ||
| print("CONNECT_ROWS_MISSING") | ||
| sys.exit(1) | ||
| print("CONNECT_ROWS=" + rows) |
There was a problem hiding this comment.
The script currently only exercises --packages, and only through a row count. Let's assert on values so that both --packages and --jars are proven to resolve on the server session:
println("AVRO=" + spark.sql("SELECT hex(to_avro(42))").collect()(0).getString(0))
println("RE2J=" + spark.sql("SELECT reflect('com.google.re2j.Pattern', 'matches', 'a.c', 'abc')").collect()(0).getString(0))| Expect(output).To(ContainSubstring("CONNECT_ROWS=10"), "expected the spark-avro query to return 10 rows, got: %s", output) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Please also replace the CONNECT_ROWS=10 assertion with ContainSubstring("AVRO=54") and ContainSubstring("RE2J=true").
| # | ||
| # Copyright The Kubeflow Authors. |
There was a problem hiding this comment.
| # | |
| # Copyright The Kubeflow Authors. | |
| # Copyright The Kubeflow Authors. |
There was a problem hiding this comment.
Please remove the single hash in the top line. The copyright needs to match the template in the testing repo.
…add-a-dependencies-fie # Conflicts: # internal/controller/sparkconnect/options_test.go
|
Rebased onto current One conflict, in Verified on the merged head: |
|
Thanks @Mr-Neutr0n! Please address the remaining open review comments in my last round so we can move this PR forward. |
…add-a-dependencies-fie
Addresses the remaining review points on kubeflow#3120: - Add the Kubeflow license header above the helper's docstring. - Prove both halves of spec.deps resolve on the server session by asserting on values: hex(to_avro(42)) from the declared spark-avro package, and reflect('com.google.re2j.Pattern', ...) from the declared jar. A row count could pass with only one of them present. - Assert AVRO=54 and RE2J=true in the Go test instead of CONNECT_ROWS=10. - Drop the explanatory comment from the example manifest as requested. Verified against a local Spark Connect server started with this example's spec.deps: AVRO=54, RE2J=true. Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
|
All five points are in at
Verified against a local Spark Connect server started with this example's exact |
| # AVRO comes from the declared spark-avro package, RE2J from the declared jar. | ||
| # Asserting on the values (not a row count) proves each one resolved on the | ||
| # server session; either being absent makes the SQL itself fail. |
There was a problem hiding this comment.
Let's remove this comment.
| # AVRO comes from the declared spark-avro package, RE2J from the declared jar. | |
| # Asserting on the values (not a row count) proves each one resolved on the | |
| # server session; either being absent makes the SQL itself fail. |
| // AVRO proves the declared spark-avro package resolved on the server session; | ||
| // RE2J proves the declared jar did. Both are asserted on values, not row counts. |
There was a problem hiding this comment.
Let's remove this comment.
| // AVRO proves the declared spark-avro package resolved on the server session; | |
| // RE2J proves the declared jar did. Both are asserted on values, not row counts. |
The example manifest began with a bare '#' line before the copyright, which the testing repo's boilerplate does not have. The e2e helper and its Go assertion also carried explanatory comments the reviewer asked to remove. Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
|
All three are in at
|
tariq-hasan
left a comment
There was a problem hiding this comment.
Thanks!
/lgtm
As a follow-up I think we should refactor the e2e to remove test/e2e/scripts/connect_query.py and replace it with the same approach as sparkconnect_query_test.go. I have raised a separate issue for this: #3260.
|
/approve |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: nabuskey The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Add a Deps field to SparkConnectSpec (mirroring the pending kubeflow#3120 pass-through) and reject malformed dependency entries in the SparkConnect admission webhook instead of letting them fail inside the runtime pod with a Maven resolution error. Rules enforced: - packages entries must be "groupId:artifactId:version". - excludePackages entries must be "groupId:artifactId" (no version). - jars, packages, excludePackages and repositories entries must not contain a comma, since the values are joined with "," when building the spark-submit flags. Does not wire Deps into the spark-submit argument builder; that is left to kubeflow#3120. Closes kubeflow#3233 Signed-off-by: sunilkumarpradhan <sunilkumarweb47@gmail.com>
Add a Deps field to SparkConnectSpec (mirroring the pending kubeflow#3120 pass-through) and reject malformed dependency entries in the SparkConnect admission webhook instead of letting them fail inside the runtime pod with a Maven resolution error. Rules enforced: - packages entries must be "groupId:artifactId:version". - excludePackages entries must be "groupId:artifactId" (no version). - jars, packages, excludePackages and repositories entries must not contain a comma, since the values are joined with "," when building the spark-submit flags. Does not wire Deps into the spark-submit argument builder; that is left to kubeflow#3120. Closes kubeflow#3233 Signed-off-by: sunilkumarpradhan <sunilkumarweb47@gmail.com>
Add a Deps field to SparkConnectSpec (mirroring the pending kubeflow#3120 pass-through) and reject malformed dependency entries in the SparkConnect admission webhook instead of letting them fail inside the runtime pod with a Maven resolution error. Rules enforced: - packages entries must be "groupId:artifactId:version". - excludePackages entries must be "groupId:artifactId" (no version). - jars, packages, excludePackages and repositories entries must not contain a comma, since the values are joined with "," when building the spark-submit flags. Does not wire Deps into the spark-submit argument builder; that is left to kubeflow#3120. Closes kubeflow#3233 Signed-off-by: sunilkumarpradhan <sunilkumarweb47@gmail.com>
Fixes #2959
Added a
Dependenciesstruct andspec.dependenciesfield toSparkConnectSpec, and wired the Spark Connect controller to pass dependency flags through tostart-connect-server.shthe same waySparkApplicationdoes forspark-submit.Local tests pass.
This change was prepared with AI assistance under human direction and review.