Uh oh!
There was an error while loading. Please reload this page.
[SPARK-57900][K8S][TESTS] Add OIDC credential propagation E2E tests on Minikube with moto - #58426
[SPARK-57900][K8S][TESTS] Add OIDC credential propagation E2E tests on Minikube with moto#58426sarutak wants to merge 3 commits into
Conversation
Introduce a new optional integration-test module, connector/credential-aws-integration-tests, that validates the end-to-end OIDC credential propagation pipeline on a real Kubernetes cluster (Minikube): projected ServiceAccount token -> AwsStsCredentialProvider -> STS -> S3A read/write, mid-job token rotation, and late-registering executors. The tests use moto (Apache 2.0) as a lightweight S3 + STS backend instead of LocalStack or MinIO, both of which have moved away from freely usable OSS distributions and are incompatible with the ASF license policy. moto runs as a plain process (no extra container) and does not verify the OIDC JWT, keeping the test focused on Spark's credential propagation logic. Three scenarios are implemented: 1. Basic flow: a Spark job on Minikube exchanges the identity token for STS credentials and reads/writes S3 via S3A (OidcS3ReadWriteJob). 2. Mid-job token rotation: a long-running job (OidcTokenRotationJob) writes to S3 repeatedly while the test rewrites the identity token file in the driver pod. The initial token is supplied by an init container into an emptyDir (an externally-provided, rotatable token file, as the SPIP assumes). The rotated token carries a DIFFERENT principal, and with a short renewal interval UserCredentialManager re-reads it, re-exchanges it via STS, and propagates fresh credentials. The test asserts the driver logged the rotated principal (proving the new token was actually read, not a no-op) and that S3 output for all iterations spanning the rotation is present. 3. Late-registering executor: with dynamic allocation and a short idle timeout, a job (OidcLateExecutorJob) warms up, idles until executors scale down, then runs a wider stage that forces new executors to register after credentials were acquired. Each wide-stage task writes to S3, so an executor that did not receive credentials (via the SparkAppConfig registration response) would fail the job. The test asserts more than one distinct executor registered over the run (evidence of a genuinely late-registering executor) and that the wide stage produced all outputs. The module is gated behind the -Poidc-e2e Maven profile (and requires -Pkubernetes), so it is skipped by default. Image building is handled by an explicit step (docker-image-tool.sh) in CI and by dev-run-integration-tests.sh locally, rather than being bound to the sbt test task. Jobs run on the cluster live in src/main so they are packaged into the module jar and baked into the Spark image (test classes are not packaged). S3A support (hadoop-aws + AWS SDK) is provided by building the image with -Phadoop-cloud. The suite drives jobs with spark.security.oidc.* configuration and selects SparkOidcAwsCredentialsProvider for S3A explicitly. The spark-submit helpers (SparkAppLauncher, SparkAppConf, SparkAppArguments, ProcessUtils) are implemented locally instead of depending on the spark-kubernetes-integration-tests test-jar, which sbt could not resolve as an inter-project reference. The Spark home used to locate bin/spark-submit is resolved by probing spark.kubernetes.test.unpackSparkDir, spark.test.home and user.dir for the first directory that contains bin/spark-submit. moto is reached from two vantage points: pods use the host gateway IP (spark.oidc.test.s3Endpoint / stsEndpoint), while the test process uses loopback (spark.oidc.test.s3ClientEndpoint). In CI, moto is installed into an isolated virtualenv (to avoid the OS-provided urllib3/pyOpenSSL that crashes moto on startup) and started inside the same workflow step that runs the tests. Correctness of the rotation and Maven paths (verified by running the suite on Minikube under both sbt and Maven): - The init container writes the token as the driver's user (uid 185, gid 0) and makes it group-writable, and the rotation waits on ExecWatch.exitCode() rather than the WebSocket onClose callback. Otherwise the driver (uid 185) cannot overwrite a root-owned token file, and the exec can return before the write lands -- both of which let a stale token survive a "successful" rotation. - System properties are normalized so that unset/empty/"null" values fall back to defaults. Maven forwards empty pom properties (e.g. spark.kubernetes.test.master) as the string "null", which previously produced "--master null"; sbt omits them entirely. Normalizing keeps both build paths working. - The baked job jar is referenced by the runtime Scala binary version instead of a hard-coded 2.13, and sparkImage fails fast with an actionable message when no concrete image tag is configured (instead of pulling an unpullable spark:N/A). Changes: - New module with test suite, Spark jobs, spark-submit helpers, pom.xml, log4j2 config, local runner script, and README. - Root pom.xml: add oidc-e2e profile. - project/SparkBuild.scala: register credentialAwsIntegrationTests. - .github/workflows/build_and_test.yml: add oidc-e2e job (moto + Minikube).
dongjoon-hyun
commented
Sep 1, 2026
Thanks for working on this! The test design is genuinely good — I especially like that the assertions can't pass vacuously: the rotation test checks for the rotated principal in the driver log rather than just "all iterations wrote", and the late-executor test requires more than one distinct executor ID from the My main concern is the CI wiring rather than the test code: as written, the new job will rarely (and on 1. |
…anup, and cleanups This follow-up addresses the review feedback on apache#58426. CI wiring (so the oidc-e2e job actually runs against the code it guards): - Register connector/credential-aws-integration-tests/ under the credential-aws module in dev/sparktestsupport/modules.py, so changes to it map to credential-aws instead of falling through to root (which would run the entire CI matrix). - Trigger the oidc-e2e job when either kubernetes or credential-aws changes: compute oidc_e2e from `is-changed.py -m kubernetes,credential-aws` and map the oidc-e2e precondition to it (previously keyed off $kubernetes only). - Add "oidc-e2e": "true" to the scheduled builds (build_java17/21/25.yml), so it runs against master on apache/spark (previously never ran there). Robustness: - Delete the driver pod in each test's finally (executors follow via owner references), so a failed test does not leave pods contending for the next test's resources. - Wrap each afterAll teardown step (namespace delete, client closes) in Utils.tryLogNonFatalError so one failure does not skip the others. - Reuse the Spark image for the token init container instead of pulling busybox from Docker Hub at test time (avoids rate limits / network flakiness). Cleanups: - Remove the unused spark.oidc.test.outputPath conf and baseSparkConf's outputPath parameter (the path is passed as argv(0)). - Remove the never-set spark.kubernetes.test.unpackSparkDir candidate from resolveSparkHomeDir and fix its error message. - Remove the no-op --spark-tgz and unused --java-version options from dev-run-integration-tests.sh and the README. - Drop the redundant `with BeforeAndAfterAll`/`with Logging` (already provided by SparkFunSuite) and the now-unused imports. - Move spark.executor.instances=1 out of baseSparkConf into the two non-dynamic -allocation tests so it does not muddy the dynamic-allocation test. - Pin moto to >=5.0.0,<6.0.0 in CI, the README, and the dev script. - Include connector/credential-aws in the oidc-e2e profile so -Poidc-e2e resolves without also passing -Pcredential-aws. Was this patch authored or co-authored using generative AI tooling? Kiro CLI / Claude
sarutak
commented
Sep 2, 2026
Thank you for the comments, @dongjoon-hyun . I've addressed all of them except the following one.
I'd like to keep the current, verified-working layout for this PR and unify the build tool as a follow-up once I've confirmed the unified flow end-to-end on Minikube. The double build is a local-dev-only inefficiency and doesn't affect CI (which uses sbt throughout). |
dongjoon-hyun
commented
Sep 2, 2026
Thanks for the update, @sarutak. I confirmed all the items from the previous round are in the head commit (daily-build wiring, A second pass surfaced a few more things. The first two affect what the suite actually proves, so I'd like to see them addressed before merge; the rest are robustness/docs. Assertions1. The basic test passes even when the driver ends in 2. The Local run path3. RBAC is only granted in CI. The suite comment says RBAC is granted "by the CI workflow / dev-run script", but the dev script and README contain no rolebinding; on a fresh RBAC-enabled Minikube the 4. 5. 6. Cleanup / coverage7. Namespace and pod deletes aren't awaited. 8. Rotation test launches outside 9. The new module is never scalastyle-checked. 10. The SPARK-43540 justification is inverted.SPARK-43540added the working directory to the driver classpath, and Minor, no action needed: the |
…AC, and CI/local run robustness Addresses the second round of review feedback on apache#58426. Assertions (so the suite cannot pass vacuously): - Basic test: assert the driver reached Succeeded and logged OidcS3ReadWriteJob.SUCCESS_MARKER. spark-submit exits 0 for a Failed driver too (LoggingPodStatusWatcherImpl.hasCompleted is true for Succeeded and Failed), so the exit code plus a non-empty S3 listing did not actually prove success. The misleading "a non-zero exit means the driver failed" comment is corrected. - Replace the `eventually { assert(phase != "Failed"); ... }` blocks with a poll helper (awaitDriverLogContains) that fails fast on the terminal Failed phase. ScalaTest's `eventually` retries on any exception, including that assert, so the guards never failed fast -- a dead pod just retried until the timeout. Local run path: - Grant the driver ServiceAccount a namespaced Role + RoleBinding (pods etc.) in ensureNamespace(), so the tests work on an RBAC-enabled cluster without relying on a cluster-wide grant (previously only the CI workflow created a binding). - Derive spark.master from the fabric8 client (kubeconfig) instead of a separate, un-normalized spark.kubernetes.test.master property, so spark-submit and the fabric8 client target the same cluster and SparkSubmit never sees a raw "https://" master. The property is removed. - In dev-run-integration-tests.sh --skip-build, derive SPARK_IMAGE as "<repo>/spark:<tag>-job" so the job-jar image is used instead of falling back to the plain "<repo>/spark:<tag>" (which lacks the job classes -> ClassNotFoundException). Cleanup robustness: - Await namespace and driver-pod deletion (poll until get() == null) so an immediate re-run with a fixed namespace does not find it still Terminating. - Move the rotation test's SparkAppLauncher.launch inside the try so a launch failure still runs the finally cleanup. Lint / docs: - Add -Poidc-e2e to dev/scalastyle's SPARK_PROFILES so this module is actually scalastyle-checked. - Correct the SPARK-43540 justification in the workflow and the dev script: a local:// primary resource is already on the driver classpath; the jar is baked in because docker-image-tool.sh only copies examples/jars, not because of a classpath gap. - Clarify the SparkAppLauncher comment: the helpers are duplicated (rather than reused from the kubernetes-integration-tests test-jar) to avoid forcing every build to also activate -Pkubernetes-integration-tests. Was this patch authored or co-authored using generative AI tooling? Kiro CLI / Claude
… Minikube with moto ### What changes were proposed in this pull request? Add a new optional integration-test module, `connector/credential-aws-integration-tests`, that validates the end-to-end OIDC credential propagation pipeline on a real Kubernetes cluster (Minikube). This is Sub-task 11 of the OIDC Credential Propagation SPIP ([SPARK-57703](https://issues.apache.org/jira/browse/SPARK-57703)), and it exercises the whole feature together: projected ServiceAccount token -> `FileTokenIngestor` -> `AwsStsCredentialProvider` -> STS -> S3A read/write, plus mid-job token rotation and late-registering executors. The tests use [moto](https://github.com/getmoto/moto) (Apache 2.0-licensed) as a lightweight S3 + STS backend. The original SPIP mentioned LocalStack, but both LocalStack and MinIO have moved away from freely usable OSS distributions and are incompatible with the ASF license policy. moto runs as a plain HTTP server (no extra container) and does not verify the OIDC JWT, keeping the test focused on Spark's credential propagation logic. **Three scenarios are implemented:** 1. **Basic flow** (`OidcS3ReadWriteJob`): a Spark job on Minikube exchanges the identity token for STS credentials and reads/writes S3 via S3A. 2. **Mid-job token rotation** (`OidcTokenRotationJob`): a long-running job writes to S3 repeatedly while the test rewrites the identity token file in the driver pod. The initial token is supplied by an init container into an emptyDir (an externally-provided, rotatable token file, as the SPIP assumes). The rotated token carries a *different* principal; with a short renewal interval, `UserCredentialManager` re-reads it, re-exchanges it via STS, and propagates fresh credentials. The test asserts the driver logged the rotated principal (proving the new token was actually read, not a no-op) and that S3 output for all iterations spanning the rotation is present. 3. **Late-registering executor** (`OidcLateExecutorJob`): with dynamic allocation and a short idle timeout, a job warms up, idles until executors scale down, then runs a wider stage that forces new executors to register *after* credentials were acquired. The test asserts more than one distinct executor registered over the run (evidence of a genuinely late-registering executor) and that the wide stage produced all outputs — an executor that did not receive credentials via the `SparkAppConfig` registration response would have failed its task. **Structure and design:** - The module is gated behind the `-Poidc-e2e` Maven profile (and requires `-Pkubernetes`), so it is skipped by default. - Jobs that run on the cluster live in `src/main` so they are packaged into the module jar and baked into the Spark image; test classes are not packaged. - S3A support (hadoop-aws + AWS SDK) is provided by building the image with `-Phadoop-cloud`. - Image building is handled by an explicit step (`docker-image-tool.sh`) in CI and by `dev-run-integration-tests.sh` locally, rather than being bound to the sbt test task. - The spark-submit helpers (`SparkAppLauncher`, `SparkAppConf`, `SparkAppArguments`, `ProcessUtils`) are implemented locally instead of depending on the `spark-kubernetes-integration-tests` test-jar, which sbt could not resolve as an inter-project reference. They mirror the equivalents there. - moto is reached from two vantage points: pods use the host gateway IP (`spark.oidc.test.s3Endpoint` / `stsEndpoint`), while the test process uses loopback (`spark.oidc.test.s3ClientEndpoint`). In CI, moto is installed into an isolated virtualenv (to avoid the OS-provided urllib3/pyOpenSSL that crashes moto on startup) and started inside the same workflow step that runs the tests. **New files:** - `connector/credential-aws-integration-tests/` — module with the test suite (`OidcCredentialE2ESuite`), the three Spark jobs, spark-submit helpers, `pom.xml`, `log4j2.properties`, a local runner script (`dev-run-integration-tests.sh`), and `README.md`. **Modified files:** - `pom.xml` (root) — add the `oidc-e2e` profile / module. - `project/SparkBuild.scala` — register `credentialAwsIntegrationTests`. - `.github/workflows/build_and_test.yml` — add the `oidc-e2e` job (moto + Minikube). ### Why are the changes needed? The SPIP calls for an end-to-end test that validates the full credential propagation pipeline in a realistic Kubernetes environment. The prior sub-tasks each cover a slice with unit/integration tests, but nothing exercised the entire flow — token ingestion, STS exchange, RPC + SparkAppConfig propagation, S3A read/write, and mid-job refresh — against a real cluster. This module provides that coverage and guards against regressions in how the pieces fit together. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? This *is* the test. The suite was run on a local Minikube (with moto) under both build tools and all three scenarios passed: - sbt: `build/sbt -Phadoop-3 -Pkubernetes -Pcredential-aws -Poidc-e2e ... credential-aws-integration-tests/test` - Maven: `build/mvn integration-test -pl connector/credential-aws-integration-tests -Phadoop-3 -Pkubernetes -Pcredential-aws -Poidc-e2e ...` The new `oidc-e2e` GitHub Actions job (Minikube + moto) is green. `dev-run-integration-tests.sh` was also verified to build the image, start/stop moto, and run the suite end-to-end. ### Was this patch authored or co-authored using generative AI tooling? Kiro CLI / Claude Closes#58426 from sarutak/oidc-propagation/e2e-tests. Authored-by: Kousuke Saruta <sarutak@apache.org> Signed-off-by: Dongjoon Hyun <dongjoon@apache.org> (cherry picked from commit bcea2b2) Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
dongjoon-hyun
commented
Sep 3, 2026
dongjoon-hyun
commented
Sep 3, 2026
There was a conflict on branch-4.3. If you need this in order to complete the SPIP, please make a backporting PR to branch-4.3, @sarutak . |
sarutak
commented
Sep 3, 2026
Thank you @dongjoon-hyun and @uros-b !
Since |
dongjoon-hyun
commented
Sep 3, 2026
Got it. Sounds good to me too because we can have more time to validate before the official announcement, @sarutak . |
What changes were proposed in this pull request?
Add a new optional integration-test module,
connector/credential-aws-integration-tests, that validates the end-to-end OIDC credential propagation pipeline on a real Kubernetes cluster (Minikube). This is Sub-task 11 of the OIDC Credential Propagation SPIP (SPARK-57703), and it exercises thewhole feature together: projected ServiceAccount token ->
FileTokenIngestor->AwsStsCredentialProvider-> STS -> S3A read/write, plus mid-job token rotation and late-registering executors.The tests use moto (Apache 2.0-licensed) as a lightweight S3 + STS backend. The original SPIP mentioned LocalStack, but both LocalStack and MinIO have moved away from freely usable OSS distributions and are incompatible with the ASF license policy. moto runs as a plain HTTP server (no extra container) and does not
verify the OIDC JWT, keeping the test focused on Spark's credential propagation logic.
Three scenarios are implemented:
OidcS3ReadWriteJob): a Spark job on Minikube exchanges the identity token for STS credentials and reads/writes S3 via S3A.OidcTokenRotationJob): a long-running job writes to S3 repeatedly while the test rewrites the identity token file in the driver pod. The initial token is supplied by an init container into an emptyDir (an externally-provided, rotatable token file, as the SPIP assumes). The rotated token carries a different principal; with a short renewal interval,UserCredentialManagerre-reads it, re-exchanges it via STS, and propagates fresh credentials. The test asserts the driver logged the rotated principal (proving the new token was actually read, not a no-op) and that S3 output for all iterations spanning the rotation is present.OidcLateExecutorJob): with dynamic allocation and a short idle timeout, a job warms up, idles until executors scale down, then runs a wider stage that forces new executors to register after credentials were acquired. The test asserts more than one distinct executor registered over the run (evidence of a genuinely late-registering executor) and that the wide stage produced all outputs — an executor that did not receive credentials via theSparkAppConfigregistration response would have failed its task.Structure and design:
-Poidc-e2eMaven profile (and requires-Pkubernetes), so it is skipped by default.src/mainso they are packaged into the module jar and baked into the Spark image; test classes are not packaged.-Phadoop-cloud.docker-image-tool.sh) in CI and bydev-run-integration-tests.shlocally, rather than being bound to the sbt test task.SparkAppLauncher,SparkAppConf,SparkAppArguments,ProcessUtils) are implemented locally instead of depending on thespark-kubernetes-integration-teststest-jar, which sbt could not resolve as an inter-project reference. They mirror the equivalents there.spark.oidc.test.s3Endpoint/stsEndpoint), while the test process uses loopback (spark.oidc.test.s3ClientEndpoint). In CI, moto is installed into an isolated virtualenv(to avoid the OS-provided urllib3/pyOpenSSL that crashes moto on startup) and started inside the same workflow step that runs the tests.
New files:
connector/credential-aws-integration-tests/— module with the test suite (OidcCredentialE2ESuite), the three Spark jobs, spark-submit helpers,pom.xml,log4j2.properties, a local runner script (dev-run-integration-tests.sh), andREADME.md.Modified files:
pom.xml(root) — add theoidc-e2eprofile / module.project/SparkBuild.scala— registercredentialAwsIntegrationTests..github/workflows/build_and_test.yml— add theoidc-e2ejob (moto + Minikube).Why are the changes needed?
The SPIP calls for an end-to-end test that validates the full credential propagation pipeline in a realistic Kubernetes environment. The prior sub-tasks each cover a slice with unit/integration tests, but nothing exercised the entire flow — token ingestion, STS exchange, RPC + SparkAppConfig propagation, S3A read/write, and mid-job refresh — against a real cluster. This module provides that coverage and guards against regressions in how the pieces fit together.
Does this PR introduce any user-facing change?
No.
How was this patch tested?
This is the test. The suite was run on a local Minikube (with moto) under both build tools and all three scenarios passed:
build/sbt -Phadoop-3 -Pkubernetes -Pcredential-aws -Poidc-e2e ... credential-aws-integration-tests/testbuild/mvn integration-test -pl connector/credential-aws-integration-tests -Phadoop-3 -Pkubernetes -Pcredential-aws -Poidc-e2e ...The new
oidc-e2eGitHub Actions job (Minikube + moto) is green.dev-run-integration-tests.shwas also verified to build the image, start/stop moto, and run the suite end-to-end.Was this patch authored or co-authored using generative AI tooling?
Kiro CLI / Claude