You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/12 05:15:43 UTC

(texera) 01/01: fix(pve): serve PVE requests under /api/pve and drop per-gateway rewrites (#6130)

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

github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6130-e462f228e44e84668a7b2582dc2ba777717ff83a
in repository https://gitbox.apache.org/repos/asf/texera.git

commit df8f133b24fb932d207bb8b853fdaa14dd984b9f
Author: Xuan Gu <16...@users.noreply.github.com>
AuthorDate: Sat Jul 11 22:15:22 2026 -0700

    fix(pve): serve PVE requests under /api/pve and drop per-gateway rewrites (#6130)
    
    ### What changes were proposed in this PR?
    
    The single-node NGINX gateway (`bin/single-node/nginx.conf`) had no
    route for the Python Virtual Environment (PVE) REST calls, so they fell
    through to the catch-all `location /` and returned 404 from
    `dashboard-service`. In the UI this surfaces as a *"Could not load
    Python virtual environments"* popup on every Python UDF operator
    selection.
    
    The root cause is that the frontend requested `/pve/...` while the
    backend (`PveResource`, `@Path("/pve")`, Jersey mounted under `/api/*`
    on the coordinator) serves `/api/pve/...`, so every gateway had to carry
    its own `/pve` to `/api/pve` rewrite rule, and the single-node config
    was missing one.
    
    Per review feedback, instead of adding the missing rewrite, this PR
    removes the mismatch at the source: the frontend now builds PVE URLs
    with `AppSettings.getApiEndpoint()` and requests `/api/pve/...`
    directly. Every routing layer then becomes a plain routing entry with no
    path rewriting:
    
    - `bin/single-node/nginx.conf`: `location /api/pve/` passes through to
    the coordinator
    - `frontend/proxy.config.json`: `/api/pve` entry (ordered before
    `/api`), `pathRewrite` removed
    - `bin/k8s/templates/base/gateway/gateway-routes.yaml`: `/api/pve` joins
    the existing dynamic-backend match list, the `URLRewrite` filter is
    removed
    
    The backend is unchanged. Net effect: the 404 is fixed, and the
    per-gateway rewrite special-casing that allowed the three configs to
    drift apart is gone.
    
    ### Any related issues, documentation, discussions?
    
    Related to #5963 (PVE single-node configuration).
    
    ### How was this PR tested?
    
    - `virtual-environment.service.spec.ts` passes with the new URLs (5
    specs).
    - On the single-node Docker stack: `GET /api/pve/pves?cuid=1` returns
    200 JSON from the coordinator, and the old `/pve` path falls through to
    the dashboard as expected.
    - `helm template` renders the updated k8s route with the `URLRewrite`
    filter removed.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8, Claude Fable 5)
    
    ---------
    
    Co-authored-by: Claude Fable 5 <no...@anthropic.com>
---
 bin/k8s/templates/base/gateway/gateway-routes.yaml      | 13 +------------
 bin/single-node/nginx.conf                              |  7 +++++++
 frontend/proxy.config.json                              | 13 +++++--------
 .../power-button/computing-unit-selection.component.ts  |  2 +-
 .../virtual-environment.service.spec.ts                 |  9 +++++----
 .../virtual-environment/virtual-environment.service.ts  | 17 +++++++++--------
 6 files changed, 28 insertions(+), 33 deletions(-)

diff --git a/bin/k8s/templates/base/gateway/gateway-routes.yaml b/bin/k8s/templates/base/gateway/gateway-routes.yaml
index ac8096aa21..935d38bb07 100644
--- a/bin/k8s/templates/base/gateway/gateway-routes.yaml
+++ b/bin/k8s/templates/base/gateway/gateway-routes.yaml
@@ -124,20 +124,9 @@ spec:
         - path:
             type: PathPrefix
             value: /api/executions/result/export
-      backendRefs:
-        - group: gateway.envoyproxy.io
-          kind: Backend
-          name: texera-dynamic-backend
-    - matches:
         - path:
             type: PathPrefix
-            value: /pve
-      filters:
-        - type: URLRewrite
-          urlRewrite:
-            path:
-              type: ReplacePrefixMatch
-              replacePrefixMatch: /api/pve
+            value: /api/pve
       backendRefs:
         - group: gateway.envoyproxy.io
           kind: Backend
diff --git a/bin/single-node/nginx.conf b/bin/single-node/nginx.conf
index 515a7016da..c5e500e667 100644
--- a/bin/single-node/nginx.conf
+++ b/bin/single-node/nginx.conf
@@ -81,6 +81,13 @@ http {
             proxy_send_timeout 1d;
         }
 
+        location /api/pve/ {
+            proxy_pass http://workflow-runtime-coordinator-service:8085;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+        }
+
+        # nginx prefix locations match by longest prefix
         location /api/ {
             proxy_pass http://dashboard-service:8080;
             proxy_set_header Host $host;
diff --git a/frontend/proxy.config.json b/frontend/proxy.config.json
index 7d68961d9b..c26a239b17 100755
--- a/frontend/proxy.config.json
+++ b/frontend/proxy.config.json
@@ -55,18 +55,15 @@
     "secure": false,
     "changeOrigin": true
   },
-  "/api": {
-    "target": "http://localhost:8080",
+  "/api/pve": {
+    "target": "http://localhost:8085",
     "secure": false,
     "changeOrigin": false
   },
-  "/pve": {
-    "target": "http://localhost:8085",
+  "/api": {
+    "target": "http://localhost:8080",
     "secure": false,
-    "changeOrigin": false,
-    "pathRewrite": {
-      "^/pve": "/api/pve"
-    }
+    "changeOrigin": false
   },
   "/wsapi": {
     "target": "http://localhost:8085",
diff --git a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts
index fed31fddb4..b633e6eca3 100644
--- a/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts
+++ b/frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts
@@ -134,7 +134,7 @@ export class ComputingUnitSelectionComponent implements OnInit {
   // variables for creating a virtual environment
   pves: PveDraft[] = [];
   systemPackages: { name: string; version: string }[] = [];
-  // True while a /pve/system response is in flight. The server resolves
+  // True while an /api/pve/system response is in flight. The server resolves
   // the full pinned set with a `pip freeze` against a throwaway venv,
   // which can take 30–60s on the first request after a server restart.
   systemPackagesLoading = false;
diff --git a/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts b/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts
index c194c115cc..c124972baf 100644
--- a/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts
+++ b/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.spec.ts
@@ -18,6 +18,7 @@
  */
 
 import { TestBed } from "@angular/core/testing";
+import { AppSettings } from "../../../common/app-setting";
 import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
 import { UserPveRecord, WorkflowPveService } from "./virtual-environment.service";
 import { commonTestProviders } from "../../../common/testing/test-utils";
@@ -45,7 +46,7 @@ describe("WorkflowPveService", () => {
       expect(resp.veid).toBe(42);
     });
 
-    const req = httpTestingController.expectOne("/pve/db");
+    const req = httpTestingController.expectOne(`${AppSettings.getApiEndpoint()}/pve/db`);
     expect(req.request.method).toBe("POST");
     expect(req.request.body).toEqual({ name: "env-a", packages });
     req.flush({ veid: 42 });
@@ -57,7 +58,7 @@ describe("WorkflowPveService", () => {
       expect(resp.veid).toBe(7);
     });
 
-    const req = httpTestingController.expectOne("/pve/db/7");
+    const req = httpTestingController.expectOne(`${AppSettings.getApiEndpoint()}/pve/db/7`);
     expect(req.request.method).toBe("PUT");
     expect(req.request.body).toEqual({ name: "env-b", packages });
     req.flush({ veid: 7 });
@@ -69,7 +70,7 @@ describe("WorkflowPveService", () => {
       expect(resp).toEqual(records);
     });
 
-    const req = httpTestingController.expectOne("/pve/db");
+    const req = httpTestingController.expectOne(`${AppSettings.getApiEndpoint()}/pve/db`);
     expect(req.request.method).toBe("GET");
     req.flush(records);
   });
@@ -77,7 +78,7 @@ describe("WorkflowPveService", () => {
   it("deleteUserPve() DELETEs /pve/db/{veid}", () => {
     service.deleteUserPve(9).subscribe();
 
-    const req = httpTestingController.expectOne("/pve/db/9");
+    const req = httpTestingController.expectOne(`${AppSettings.getApiEndpoint()}/pve/db/9`);
     expect(req.request.method).toBe("DELETE");
     req.flush(null);
   });
diff --git a/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.ts b/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.ts
index d399f643a0..cc4d7df942 100644
--- a/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.ts
+++ b/frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.ts
@@ -21,6 +21,7 @@ import { Observable } from "rxjs";
 import { map } from "rxjs/operators";
 import { HttpClient, HttpParams } from "@angular/common/http";
 import { AuthService } from "../../../common/service/user/auth.service";
+import { AppSettings } from "../../../common/app-setting";
 
 export interface PackageResponse {
   system: string[];
@@ -42,19 +43,19 @@ export class WorkflowPveService {
   constructor(private http: HttpClient) {}
 
   savePve(name: string, packages: Record<string, string>): Observable<{ veid: number }> {
-    return this.http.post<{ veid: number }>("/pve/db", { name, packages });
+    return this.http.post<{ veid: number }>(`${AppSettings.getApiEndpoint()}/pve/db`, { name, packages });
   }
 
   updateUserPve(veid: number, name: string, packages: Record<string, string>): Observable<{ veid: number }> {
-    return this.http.put<{ veid: number }>(`/pve/db/${veid}`, { name, packages });
+    return this.http.put<{ veid: number }>(`${AppSettings.getApiEndpoint()}/pve/db/${veid}`, { name, packages });
   }
 
   listUserPves(): Observable<UserPveRecord[]> {
-    return this.http.get<UserPveRecord[]>("/pve/db");
+    return this.http.get<UserPveRecord[]>(`${AppSettings.getApiEndpoint()}/pve/db`);
   }
 
   deleteUserPve(veid: number): Observable<void> {
-    return this.http.delete<void>(`/pve/db/${veid}`);
+    return this.http.delete<void>(`${AppSettings.getApiEndpoint()}/pve/db/${veid}`);
   }
 
   getAccessToken(): string | null {
@@ -73,12 +74,12 @@ export class WorkflowPveService {
 
   getSystemPackages(cuid: number): Observable<PackageResponse> {
     const params = this.buildBaseParams().set("cuid", cuid.toString());
-    return this.http.get<PackageResponse>("/pve/system", { params });
+    return this.http.get<PackageResponse>(`${AppSettings.getApiEndpoint()}/pve/system`, { params });
   }
 
   fetchPVEs(cuid: number): Observable<PvePackageResponse[]> {
     const params = this.buildBaseParams().set("cuid", cuid.toString());
-    return this.http.get<PvePackageResponse[]>("/pve/pves", { params });
+    return this.http.get<PvePackageResponse[]>(`${AppSettings.getApiEndpoint()}/pve/pves`, { params });
   }
 
   getUserPackages(cuid: number, pveName: string): Observable<string[]> {
@@ -86,14 +87,14 @@ export class WorkflowPveService {
   }
 
   deleteEnvironments(cuid: number) {
-    return this.http.delete(`/pve/pves/${cuid}`);
+    return this.http.delete(`${AppSettings.getApiEndpoint()}/pve/pves/${cuid}`);
   }
 
   deletePackage(cuid: number, pveName: string, packageName: string) {
     const params = this.buildBaseParams();
 
     return this.http.delete<string[]>(
-      `/pve/${cuid}/${encodeURIComponent(pveName)}/packages/${encodeURIComponent(packageName)}`,
+      `${AppSettings.getApiEndpoint()}/pve/${cuid}/${encodeURIComponent(pveName)}/packages/${encodeURIComponent(packageName)}`,
       { params }
     );
   }