This is an automated email from the ASF dual-hosted git repository.

xuang7 pushed a commit to branch release/v1.2
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/release/v1.2 by this push:
     new 261d4c5231 fix(access-control-service, v1.2): include port in 
computing unit pod URI and use Envoy Gateway for distributed CUs  (#7009)
261d4c5231 is described below

commit 261d4c52318fec1d9d2d917979e85b37008799b2
Author: Yicong Huang <[email protected]>
AuthorDate: Wed Jul 29 14:18:58 2026 -0400

    fix(access-control-service, v1.2): include port in computing unit pod URI 
and use Envoy Gateway for distributed CUs  (#7009)
    
    ### What changes were proposed in this PR?
    
    Backport of #5629 to `release/v1.2`, cherry-picked from
    07724d54fda8b1425489fa31143c5a15738578ba.
    
    Resolved a package-path divergence in the imports: `release/v1.2` uses
    `org.apache.texera.config` (main uses
    `org.apache.texera.common.config`). Kept v1.2's package path and added
    the `org.apache.texera.dao.SqlServer` import the fix needs; dropped the
    now-unused `KubernetesConfig` import. Source + tests backported.
    
    ### Any related issues, documentation, discussions?
    
    Backport of #5629. Originally linked #5630.
    
    ### How was this PR tested?
    
    Release-branch CI runs on this PR.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Yes — backport prepared with Claude Code (cherry-pick + manual conflict
    resolution; the change itself is #5629 by its original author).
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: ali risheh <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../service/resource/AccessControlResource.scala   | 29 +++++++---
 .../apache/texera/AccessControlResourceSpec.scala  | 65 ++++++++++++++++++++--
 .../texera/service/util/KubernetesClient.scala     |  2 +-
 3 files changed, 82 insertions(+), 14 deletions(-)

diff --git 
a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala
 
b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala
index 0c90a6ce31..119206fe67 100644
--- 
a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala
+++ 
b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala
@@ -27,8 +27,10 @@ import jakarta.ws.rs.{Consumes, DELETE, GET, POST, Path, 
Produces}
 import org.apache.texera.auth.JwtParser.parseToken
 import org.apache.texera.auth.SessionUser
 import org.apache.texera.auth.util.{ComputingUnitAccess, HeaderField}
-import org.apache.texera.config.{GuiConfig, KubernetesConfig, LLMConfig}
+import org.apache.texera.config.{GuiConfig, LLMConfig}
+import org.apache.texera.dao.SqlServer
 import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
+import 
org.apache.texera.dao.jooq.generated.tables.daos.WorkflowComputingUnitDao
 
 import java.net.URLDecoder
 import java.nio.charset.StandardCharsets
@@ -136,12 +138,25 @@ object AccessControlResource extends LazyLogging {
     }
 
     // Dynamic Routing Logic
-    val workflowComputingUnitPoolName = KubernetesConfig.computeUnitPoolName
-    val workflowComputingUnitPoolNamespace = 
KubernetesConfig.computeUnitPoolNamespace
-    val workflowComputingUnitPoolPort = KubernetesConfig.computeUnitPortNumber
-
-    val targetHost =
-      
s"computing-unit-$cuidInt.$workflowComputingUnitPoolName-svc.$workflowComputingUnitPoolNamespace.svc.cluster.local:$workflowComputingUnitPoolPort"
+    // Route to the URI recorded for the computing unit (written by the 
managing
+    // service when the pod is created). This recorded URI is the single source
+    // of truth for where the unit is reachable, allowing units to live 
anywhere
+    // the gateway can route to. If no URI has been recorded, the unit is not
+    // routable and the connection is refused.
+    val cuDao = new WorkflowComputingUnitDao(
+      SqlServer.getInstance().createDSLContext().configuration()
+    )
+    val unit = cuDao.fetchOneByCuid(cuidInt)
+    val recordedUri = Option(unit).flatMap(u => 
Option(u.getUri)).map(_.trim).filter(_.nonEmpty)
+
+    val targetHost = recordedUri match {
+      case Some(uri) =>
+        logger.info(s"Routing CU $cuidInt to recorded host: $uri")
+        uri
+      case None =>
+        logger.warn(s"Refusing CU $cuidInt: no URI recorded for the computing 
unit")
+        return Response.status(Response.Status.FORBIDDEN).build()
+    }
 
     Response
       .ok()
diff --git 
a/access-control-service/src/test/scala/org/apache/texera/AccessControlResourceSpec.scala
 
b/access-control-service/src/test/scala/org/apache/texera/AccessControlResourceSpec.scala
index 75f3bacb10..e1dbf89724 100644
--- 
a/access-control-service/src/test/scala/org/apache/texera/AccessControlResourceSpec.scala
+++ 
b/access-control-service/src/test/scala/org/apache/texera/AccessControlResourceSpec.scala
@@ -55,6 +55,11 @@ class AccessControlResourceSpec
   private val testURI: String = "http://localhost:8080/";
   private val testPath: String = "/api/executions/1/stats/1"
 
+  // The host:port the managing service records for a computing unit when it
+  // creates the pod. The access-control-service routes to this recorded URI.
+  private val testRecordedUri: String =
+    "computing-unit-2.compute-unit-svc.default.svc.cluster.local:8888"
+
   private val testUser1: User = {
     val user = new User()
     user.setUid(1)
@@ -81,6 +86,31 @@ class AccessControlResourceSpec
     cu.setType(WorkflowComputingUnitTypeEnum.kubernetes)
     cu.setCuid(2)
     cu.setName("test-cu")
+    cu.setUri(testRecordedUri)
+    cu
+  }
+
+  // A computing unit the user can access but for which no URI was ever 
recorded
+  // (e.g. the pod was never created). Such a unit is not routable and must be
+  // refused.
+  private val testCUNoUri: WorkflowComputingUnit = {
+    val cu = new WorkflowComputingUnit()
+    cu.setUid(2)
+    cu.setType(WorkflowComputingUnitTypeEnum.kubernetes)
+    cu.setCuid(3)
+    cu.setName("test-cu-no-uri")
+    cu
+  }
+
+  // A computing unit whose recorded URI is blank/whitespace-only — also 
treated
+  // as "no URI recorded" and refused.
+  private val testCUBlankUri: WorkflowComputingUnit = {
+    val cu = new WorkflowComputingUnit()
+    cu.setUid(2)
+    cu.setType(WorkflowComputingUnitTypeEnum.kubernetes)
+    cu.setCuid(4)
+    cu.setName("test-cu-blank-uri")
+    cu.setUri("   ")
     cu
   }
 
@@ -96,12 +126,18 @@ class AccessControlResourceSpec
     userDao.insert(testUser1)
     userDao.insert(testUser2)
     computingUnitDao.insert(testCU)
-
-    val cuAccess = new ComputingUnitUserAccess()
-    cuAccess.setUid(testUser1.getUid)
-    cuAccess.setCuid(testCU.getCuid)
-    cuAccess.setPrivilege(PrivilegeEnum.WRITE)
-    computingUnitOfUserDao.insert(cuAccess)
+    computingUnitDao.insert(testCUNoUri)
+    computingUnitDao.insert(testCUBlankUri)
+
+    // Grant testUser1 WRITE access to every test computing unit so the routing
+    // logic (not the access check) is what each routing test exercises.
+    Seq(testCU, testCUNoUri, testCUBlankUri).foreach { cu =>
+      val cuAccess = new ComputingUnitUserAccess()
+      cuAccess.setUid(testUser1.getUid)
+      cuAccess.setCuid(cu.getCuid)
+      cuAccess.setPrivilege(PrivilegeEnum.WRITE)
+      computingUnitOfUserDao.insert(cuAccess)
+    }
 
     val claims = JwtAuth.jwtClaims(testUser1, 1)
     token = JwtAuth.jwtToken(claims)
@@ -232,6 +268,23 @@ class AccessControlResourceSpec
     response.getHeaderString(HeaderField.UserId) shouldBe 
testUser1.getUid.toString
     response.getHeaderString(HeaderField.UserName) shouldBe testUser1.getName
     response.getHeaderString(HeaderField.UserEmail) shouldBe testUser1.getEmail
+    // Envoy routes by the rewritten Host header, which must be the URI 
recorded
+    // for the computing unit.
+    response.getHeaderString("Host") shouldBe testRecordedUri
+  }
+
+  it should "refuse the connection when no URI is recorded for the computing 
unit" in {
+    val (uri, headers) = mockRequest(testPath, 
Some(testCUNoUri.getCuid.toString))
+    val response = new AccessControlResource().authorizeGet(uri, headers)
+
+    response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode
+  }
+
+  it should "refuse the connection when the recorded URI is blank" in {
+    val (uri, headers) = mockRequest(testPath, 
Some(testCUBlankUri.getCuid.toString))
+    val response = new AccessControlResource().authorizeGet(uri, headers)
+
+    response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode
   }
 
   private def mockRequest(
diff --git 
a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/KubernetesClient.scala
 
b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/KubernetesClient.scala
index cfc01b83b6..5078e73f01 100644
--- 
a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/KubernetesClient.scala
+++ 
b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/KubernetesClient.scala
@@ -35,7 +35,7 @@ object KubernetesClient {
   private val podNamePrefix = "computing-unit"
 
   def generatePodURI(cuid: Int): String = {
-    
s"${generatePodName(cuid)}.${KubernetesConfig.computeUnitServiceName}.$namespace.svc.cluster.local"
+    
s"${generatePodName(cuid)}.${KubernetesConfig.computeUnitServiceName}.$namespace.svc.cluster.local:${KubernetesConfig.computeUnitPortNumber}"
   }
 
   def generatePodName(cuid: Int): String = s"$podNamePrefix-$cuid"

Reply via email to