This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 7dbdb72880 feat(computing-unit): name the configuration a unit cannot
start without (#8472)
7dbdb72880 is described below
commit 7dbdb728807ce53c6266b5b49b278d04ad2df390
Author: Tanishq Gandhi <[email protected]>
AuthorDate: Thu Sep 10 18:59:42 2026 +0000
feat(computing-unit): name the configuration a unit cannot start without
(#8472)
### What changes were proposed in this PR?
Six environment variables were read with `Option.get`, so a deployment
missing any of them failed with `None.get`. Dropwizard shows that as
"There was an error processing your request" — no variable named, no
cause, and six candidates to check.
Every missing variable is now listed at once, as a 503. All at once
because when these are missing they are usually all missing: the helm
chart supplies them, so they are present together or absent together.
Reviewing what each one is for shrank the list. A variable belongs in it
only if the unit has no usable default without it:
| Variable | Without it, the unit | Verdict |
| --- | --- | --- |
| `FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT` | falls back to
`localhost:9092` | required |
| `FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT` | falls back to
`localhost:9092` | required |
| `AUTH_JWT_SECRET` | falls back to the published literal in `auth.conf`
| required |
| `MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB` | uses
`application.conf`'s 1024 | optional, forwarded when set |
| `USER_SYS_ENABLED` | ignores it — conf key removed in #3831 | dropped
|
| `SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR` | ignores it
— conf key removed in #3542 | dropped |
Required values are forwarded raw: `LakeFSFileDocument` and
`ResultExportService` trim the endpoints themselves, and the secret has
to stay byte-identical to what `AuthConfig` read or the unit and this
service verify the token against different keys. The optional override
is trimmed, because HOCON reads `" 1024"` as a string and refuses it as
an int — the unit then dies at startup naming nothing.
**No change for any working deployment.** A chart deployment sets all of
these, unpadded, so every forwarded value is byte-identical to today's;
the only difference in the pod's environment is the two dropped
variables, which nothing there read. A deployment that omits the payload
size is no longer refused.
### Any related issues, documentation, discussions?
Closes #8467
Part of #8466
### How was this PR tested?
Ten new tests for `requiredComputingUnitEnv`, and the existing
default-image assertion updated.
| Case | What it pins |
| --- | --- |
| all set | exactly the three keys come back, values untouched |
| not needed | none of the three defaulted/dead variables is required,
nor forwarded |
| one missing | that one is named — **and the ones that are set are
not** |
| blank | a whitespace-only value counts as missing |
| padded | forwarded raw, so unit and manager verify against the same
key |
| wording | "Unset or blank environment variable(s)", not "missing" |
| all missing | every name in one message |
| override set | the payload size is forwarded, trimmed |
| override unset/blank | nothing forwarded, so `application.conf`'s 1024
stands |
The negative half of the one-missing case was verified by mutation:
computing `missing` as every name whenever one is absent leaves the
suite green without it, and fails only that test with it.
```
sbt "ComputingUnitManagingService/testOnly
org.apache.texera.service.resource.ComputingUnitManagingResourceSpec" \
"Config/testOnly org.apache.texera.common.config.KubernetesConfigSpec"
ComputingUnitManagingResourceSpec 41 passed, 0 failed
KubernetesConfigSpec 6 passed, 0 failed
```
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)
---
common/config/src/main/resources/kubernetes.conf | 4 +-
.../common/config/KubernetesConfigSpec.scala | 3 +-
.../resource/ComputingUnitManagingResource.scala | 80 +++++++++++-----
.../ComputingUnitManagingResourceSpec.scala | 105 ++++++++++++++++++++-
4 files changed, 167 insertions(+), 25 deletions(-)
diff --git a/common/config/src/main/resources/kubernetes.conf
b/common/config/src/main/resources/kubernetes.conf
index da72b930d3..e3edf31aff 100644
--- a/common/config/src/main/resources/kubernetes.conf
+++ b/common/config/src/main/resources/kubernetes.conf
@@ -34,7 +34,9 @@ kubernetes {
compute-unit-pod-name-prefix = "computing-unit"
compute-unit-pod-name-prefix = ${?KUBERNETES_COMPUTE_UNIT_POD_NAME_PREFIX}
- image-name = "bobbai/texera-workflow-computing-unit:dev"
+ # Nightly image, tracking main. The chart always sets KUBERNETES_IMAGE_NAME
to its own
+ # registry and tag, so only a service run outside the chart reaches this
default.
+ image-name = "ghcr.io/apache/texera-workflow-execution-coordinator:latest"
image-name = ${?KUBERNETES_IMAGE_NAME}
image-pull-policy = "Always"
diff --git
a/common/config/src/test/scala/org/apache/texera/common/config/KubernetesConfigSpec.scala
b/common/config/src/test/scala/org/apache/texera/common/config/KubernetesConfigSpec.scala
index 3b3c194b63..7dbeedd07c 100644
---
a/common/config/src/test/scala/org/apache/texera/common/config/KubernetesConfigSpec.scala
+++
b/common/config/src/test/scala/org/apache/texera/common/config/KubernetesConfigSpec.scala
@@ -49,7 +49,8 @@ class KubernetesConfigSpec extends AnyFlatSpec with Matchers {
KubernetesConfig.computeUnitPoolNamespace shouldBe
"texera-workflow-computing-unit-pool"
)
ifUnset("KUBERNETES_IMAGE_NAME")(
- KubernetesConfig.computeUnitImageName shouldBe
"bobbai/texera-workflow-computing-unit:dev"
+ KubernetesConfig.computeUnitImageName shouldBe
+ "ghcr.io/apache/texera-workflow-execution-coordinator:latest"
)
ifUnset("KUBERNETES_IMAGE_PULL_POLICY")(
KubernetesConfig.computingUnitImagePullPolicy shouldBe "Always"
diff --git
a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitManagingResource.scala
b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitManagingResource.scala
index 3a249d296e..c5df9078b0 100644
---
a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitManagingResource.scala
+++
b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitManagingResource.scala
@@ -93,6 +93,61 @@ object ComputingUnitManagingResource {
}
}
+ // Required: the endpoints default to localhost:9092 (LakeFSFileDocument,
+ // ResultExportService) and the secret to a published literal (auth.conf),
none of which
+ // suits a real deployment. Forwarded raw -- the endpoints are trimmed by
their own readers,
+ // and trimming the secret would leave the unit and this service verifying
the token against
+ // different keys, since AuthConfig does not trim.
+ private val requiredComputingUnitEnvNames: Seq[String] = Seq(
+ EnvironmentalVariable.ENV_FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT,
+ EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT,
+ EnvironmentalVariable.ENV_AUTH_JWT_SECRET
+ )
+
+ // Overrides, forwarded only when set: application.conf defaults the payload
size to 1024,
+ // so its absence is not an error. USER_SYS_ENABLED and
+ // SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR are absent from
both lists --
+ // their conf keys went away with #3831 and #3542, so nothing reads them.
+ // TODO: use AmberConfig here; it is only accessible in
workflow-executing-service
+ private val optionalComputingUnitEnvNames: Seq[String] = Seq(
+ EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB
+ )
+
+ /**
+ * Returns the variables, or fails with a 503 listing every one that is
unset or blank.
+ *
+ * A WebApplicationException so the message survives: dropwizard replaces a
plain 500's
+ * with generic text. 503 because the deployment is not ready, not the
request wrong.
+ */
+ private[resource] def requiredComputingUnitEnv(
+ lookup: String => Option[String]
+ ): Map[String, String] = {
+ // Blank counts as missing: the chart renders every value as "{{ .value
}}", so an unset
+ // one arrives as "" rather than absent.
+ val looked =
+ requiredComputingUnitEnvNames.map(name => name ->
lookup(name).filter(_.trim.nonEmpty))
+ val missing = looked.collect { case (name, None) => name }
+ if (missing.nonEmpty) {
+ throw new ServiceUnavailableException(
+ "This deployment cannot create a computing unit. Unset or blank
environment " +
+ s"variable(s): ${missing.mkString(", ")}."
+ )
+ }
+ looked.collect { case (name, Some(value)) => name -> value }.toMap
+ }
+
+ /**
+ * The overrides that are set, trimmed. A blank one is dropped rather than
forwarded, and
+ * a padded one is trimmed, because HOCON reads " 1024" as a string and
refuses it as an
+ * int -- the unit then dies at startup naming nothing.
+ */
+ private[resource] def optionalComputingUnitEnv(
+ lookup: String => Option[String]
+ ): Map[String, String] =
+ optionalComputingUnitEnvNames.flatMap { name =>
+ lookup(name).map(_.trim).filter(_.nonEmpty).map(name -> _)
+ }.toMap
+
// Environment variables passed to the created computing unit(pod)
private lazy val computingUnitEnvironmentVariables: Map[String, Any] =
icebergEnvironmentVariables ++ Map(
@@ -108,28 +163,9 @@ object ComputingUnitManagingResource {
EnvironmentalVariable.ENV_S3_ENDPOINT -> StorageConfig.s3Endpoint,
EnvironmentalVariable.ENV_S3_REGION -> StorageConfig.s3Region,
EnvironmentalVariable.ENV_S3_AUTH_USERNAME -> StorageConfig.s3Username,
- EnvironmentalVariable.ENV_S3_AUTH_PASSWORD -> StorageConfig.s3Password,
-
EnvironmentalVariable.ENV_FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT ->
EnvironmentalVariable
-
.get(EnvironmentalVariable.ENV_FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT)
- .get,
-
EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT ->
EnvironmentalVariable
-
.get(EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT)
- .get,
- // Variables for amber setting
- // TODO: use AmberConfig for the following items. Currently AmberConfig
is only accessible in workflow-executing-service
-
EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR
-> EnvironmentalVariable
-
.get(EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR)
- .get,
- EnvironmentalVariable.ENV_USER_SYS_ENABLED -> EnvironmentalVariable
- .get(EnvironmentalVariable.ENV_USER_SYS_ENABLED)
- .get,
- EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB
-> EnvironmentalVariable
-
.get(EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB)
- .get,
- EnvironmentalVariable.ENV_AUTH_JWT_SECRET -> EnvironmentalVariable
- .get(EnvironmentalVariable.ENV_AUTH_JWT_SECRET)
- .get
- )
+ EnvironmentalVariable.ENV_S3_AUTH_PASSWORD -> StorageConfig.s3Password
+ ) ++ requiredComputingUnitEnv(EnvironmentalVariable.get) ++
+ optionalComputingUnitEnv(EnvironmentalVariable.get)
case class WorkflowComputingUnitCreationParams(
name: String,
diff --git
a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/ComputingUnitManagingResourceSpec.scala
b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/ComputingUnitManagingResourceSpec.scala
index 4cbb0e0781..34b3c996cb 100644
---
a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/ComputingUnitManagingResourceSpec.scala
+++
b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/ComputingUnitManagingResourceSpec.scala
@@ -19,7 +19,13 @@
package org.apache.texera.service.resource
-import jakarta.ws.rs.{BadRequestException, ForbiddenException,
NotFoundException}
+import jakarta.ws.rs.{
+ BadRequestException,
+ ForbiddenException,
+ NotFoundException,
+ ServiceUnavailableException
+}
+import org.apache.texera.common.config.EnvironmentalVariable
import org.apache.texera.auth.SessionUser
import
org.apache.texera.common.config.KubernetesConfig.maxNumOfRunningComputingUnitsPerUser
import org.apache.texera.dao.MockTexeraDB
@@ -460,4 +466,101 @@ class ComputingUnitManagingResourceSpec
a[NotFoundException] should be thrownBy
resource.getComputingUnitResourceLimit("99999", user)
}
+
+ // Mirrors the private production list, so a name added or dropped there
fails the all-set
+ // case.
+ private val requiredEnvNames = Seq(
+ EnvironmentalVariable.ENV_FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT,
+ EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT,
+ EnvironmentalVariable.ENV_AUTH_JWT_SECRET
+ )
+
+ private val payloadSize =
EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB
+
+ "requiredComputingUnitEnv" should "return every variable when all are set"
in {
+ val env = ComputingUnitManagingResource.requiredComputingUnitEnv(name =>
Some(s"value-$name"))
+ env.keySet shouldBe requiredEnvNames.toSet
+ env.values.foreach(_ should startWith("value-"))
+ }
+
+ // USER_SYS_ENABLED and
SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR lost their
+ // conf keys (#3831, #3542); the payload size defaults to 1024 in
application.conf. None of
+ // the three stops a unit from starting, so none may refuse to create one.
+ it should "not require a variable the unit does not need" in {
+ val notNeeded = Seq(
+ EnvironmentalVariable.ENV_USER_SYS_ENABLED,
+
EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR,
+ payloadSize
+ )
+ val env = ComputingUnitManagingResource.requiredComputingUnitEnv(name =>
+ if (notNeeded.contains(name)) None else Some("set")
+ )
+ notNeeded.foreach(name => env.keySet should not contain name)
+ }
+
+ it should "name the missing variable and leave the ones that are set out of
it" in {
+ val absent = EnvironmentalVariable.ENV_AUTH_JWT_SECRET
+ val thrown = intercept[ServiceUnavailableException] {
+ ComputingUnitManagingResource.requiredComputingUnitEnv(name =>
+ if (name == absent) None else Some("set")
+ )
+ }
+ thrown.getMessage should include(absent)
+ requiredEnvNames
+ .filterNot(_ == absent)
+ .foreach(name => thrown.getMessage should not include name)
+ }
+
+ // The chart renders every value as "{{ .value }}", so an unset one arrives
as "".
+ it should "treat a blank variable as missing" in {
+ val blank = EnvironmentalVariable.ENV_AUTH_JWT_SECRET
+ val thrown = intercept[ServiceUnavailableException] {
+ ComputingUnitManagingResource.requiredComputingUnitEnv(name =>
+ if (name == blank) Some(" ") else Some("set")
+ )
+ }
+ thrown.getMessage should include(blank)
+ }
+
+ // AuthConfig does not trim, so a trimmed copy would verify against a
different key. The
+ // endpoints are trimmed by their own readers, so they need nothing here
either.
+ it should "hand on the value untrimmed" in {
+ val env =
+ ComputingUnitManagingResource.requiredComputingUnitEnv(_ => Some("
s3cret "))
+ env(EnvironmentalVariable.ENV_AUTH_JWT_SECRET) shouldBe " s3cret "
+ }
+
+ // The variable is there in the pod's env, so calling it "missing" would
read as wrong.
+ it should "say unset or blank rather than missing" in {
+ val thrown = intercept[ServiceUnavailableException] {
+ ComputingUnitManagingResource.requiredComputingUnitEnv(_ => Some(" "))
+ }
+ thrown.getMessage should include("Unset or blank environment variable(s)")
+ }
+
+ it should "name every missing variable at once" in {
+ val thrown = intercept[ServiceUnavailableException] {
+ ComputingUnitManagingResource.requiredComputingUnitEnv(_ => None)
+ }
+ requiredEnvNames.foreach(name => thrown.getMessage should include(name))
+ }
+
+ "optionalComputingUnitEnv" should "forward an override that is set" in {
+ ComputingUnitManagingResource.optionalComputingUnitEnv(_ => Some("2048"))
shouldBe
+ Map(payloadSize -> "2048")
+ }
+
+ // HOCON refuses " 2048" as an int, and the unit then dies at startup naming
nothing.
+ it should "trim what it forwards" in {
+ ComputingUnitManagingResource.optionalComputingUnitEnv(_ => Some("
2048\n")) shouldBe
+ Map(payloadSize -> "2048")
+ }
+
+ // application.conf already defaults this to 1024; forwarding "" would
override the default
+ // with a value HOCON cannot read as an int.
+ it should "forward nothing when unset or blank" in {
+ ComputingUnitManagingResource.optionalComputingUnitEnv(_ => None) shouldBe
empty
+ ComputingUnitManagingResource.optionalComputingUnitEnv(_ => Some(" "))
shouldBe empty
+ }
+
}