From ad9c683057bf9db22f568be81b2b0a2ed5c35e8b Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 8 Apr 2026 17:00:43 -0700 Subject: [PATCH 1/2] chore: display selected agents in UI --- .../_components/DeploymentAgentCard.tsx | 52 ++++--------------- .../settings/page.$deploymentId.job-agent.tsx | 40 +++++++++++++- apps/workspace-engine/oapi/openapi.json | 50 ++++++++++++++++++ apps/workspace-engine/oapi/spec/main.jsonnet | 3 +- .../oapi/spec/paths/deployment.jsonnet | 24 +++++++++ apps/workspace-engine/pkg/oapi/oapi.gen.go | 28 ++++++++++ .../http/server/openapi/deployments/server.go | 49 +++++++++++++++++ .../svc/http/server/openapi/server.go | 2 + packages/trpc/src/routes/deployments.ts | 24 +++++++++ packages/workspace-engine-sdk/src/schema.ts | 51 ++++++++++++++++++ 10 files changed, 277 insertions(+), 46 deletions(-) create mode 100644 apps/workspace-engine/oapi/spec/paths/deployment.jsonnet create mode 100644 apps/workspace-engine/svc/http/server/openapi/deployments/server.go diff --git a/apps/web/app/routes/ws/deployments/settings/_components/DeploymentAgentCard.tsx b/apps/web/app/routes/ws/deployments/settings/_components/DeploymentAgentCard.tsx index 48df61dd2..bd21e097f 100644 --- a/apps/web/app/routes/ws/deployments/settings/_components/DeploymentAgentCard.tsx +++ b/apps/web/app/routes/ws/deployments/settings/_components/DeploymentAgentCard.tsx @@ -23,18 +23,14 @@ import { DialogTrigger, } from "~/components/ui/dialog"; import { ConfigEntry } from "~/components/config-entry"; -import { Skeleton } from "~/components/ui/skeleton"; -import { useJobAgent } from "../_hooks/job-agents"; -type DeploymentJobAgent = { - deploymentId: string; - jobAgentId: string; +type JobAgent = { + id: string; + name: string; + type: string; config: Record; }; -type DeploymentAgentCardProps = { - deploymentAgent: DeploymentJobAgent; -}; function TypeIcon({ type }: { type: string }) { if (type === "github-app") return ; if (type === "argo-cd") @@ -44,26 +40,6 @@ function TypeIcon({ type }: { type: string }) { return ; } -function SkeletonCard() { - return ( - - - - - - - - - - - - - - - - ); -} - function ConfigExpanded({ config, label, @@ -120,29 +96,19 @@ function Config({ ); } -export function DeploymentAgentCard({ - deploymentAgent, -}: DeploymentAgentCardProps) { - const { jobAgent, isLoading } = useJobAgent(deploymentAgent.jobAgentId); - if (isLoading) return ; - if (jobAgent == null) return null; - +export function DeploymentAgentCard({ agent }: { agent: JobAgent }) { return ( - - {jobAgent.name} + + {agent.name} - {jobAgent.type} + {agent.type} - - + ); diff --git a/apps/web/app/routes/ws/deployments/settings/page.$deploymentId.job-agent.tsx b/apps/web/app/routes/ws/deployments/settings/page.$deploymentId.job-agent.tsx index 3c8f6c95a..2b919339f 100644 --- a/apps/web/app/routes/ws/deployments/settings/page.$deploymentId.job-agent.tsx +++ b/apps/web/app/routes/ws/deployments/settings/page.$deploymentId.job-agent.tsx @@ -1,17 +1,53 @@ +import { trpc } from "~/api/trpc"; +import { Skeleton } from "~/components/ui/skeleton"; import { useDeployment } from "../_components/DeploymentProvider"; +import { DeploymentAgentCard } from "./_components/DeploymentAgentCard"; export default function DeploymentJobAgentPage() { const { deployment } = useDeployment(); + const { data: agents, isLoading } = trpc.deployment.jobAgents.useQuery({ + deploymentId: deployment.id, + }); return (

Job Agents

- Job agents are used to dispatch jobs to the correct service. Without - an agent new deployment versions will not take any action. + Job agents matched by this deployment's selector. Without a matching + agent, new deployment versions will not take any action.

+ {deployment.jobAgentSelector != null && + deployment.jobAgentSelector !== "false" && ( +
+

Selector

+
+                {deployment.jobAgentSelector}
+              
+
+ )}
+ + {isLoading && ( +
+ + +
+ )} + + {!isLoading && agents != null && agents.length > 0 && ( +
+ {agents.map((agent) => ( + + ))} +
+ )} + + {!isLoading && (agents == null || agents.length === 0) && ( +

+ No job agents match the current selector. +

+ )}
); } diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 784f81d9c..77933a911 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -3180,6 +3180,56 @@ }, "openapi": "3.0.0", "paths": { + "/v1/deployments/{deploymentId}/job-agents": { + "get": { + "operationId": "getJobAgentsForDeployment", + "parameters": [ + { + "description": "ID of the deployment", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/JobAgent" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + } + } + }, + "description": "Job agents matching the deployment selector" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + } + }, + "summary": "Get job agents matching a deployment selector" + } + }, "/v1/deployments/{deploymentId}/release-targets": { "get": { "operationId": "listReleaseTargets", diff --git a/apps/workspace-engine/oapi/spec/main.jsonnet b/apps/workspace-engine/oapi/spec/main.jsonnet index 038a0027a..8ceeb705e 100644 --- a/apps/workspace-engine/oapi/spec/main.jsonnet +++ b/apps/workspace-engine/oapi/spec/main.jsonnet @@ -12,7 +12,8 @@ (import 'paths/release_targets.jsonnet') + (import 'paths/jobs.jsonnet') + (import 'paths/validate.jsonnet') + - (import 'paths/workflows.jsonnet'), + (import 'paths/workflows.jsonnet') + + (import 'paths/deployment.jsonnet'), components: { parameters: (import 'parameters/core.jsonnet'), diff --git a/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet b/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet new file mode 100644 index 000000000..aa067477e --- /dev/null +++ b/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet @@ -0,0 +1,24 @@ +local openapi = import '../lib/openapi.libsonnet'; + +{ + '/v1/deployments/{deploymentId}/job-agents': { + get: { + summary: 'Get job agents matching a deployment selector', + operationId: 'getJobAgentsForDeployment', + parameters: [ + openapi.deploymentIdParam(), + ], + responses: openapi.okResponse({ + type: 'object', + properties: { + items: { + type: 'array', + items: openapi.schemaRef('JobAgent'), + }, + }, + required: ['items'], + }, 'Job agents matching the deployment selector') + + openapi.badRequestResponse(), + }, + }, +} diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 885f84d79..c5e07c599 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -2452,6 +2452,9 @@ func (t *WorkflowInput) UnmarshalJSON(b []byte) error { // ServerInterface represents all server handlers. type ServerInterface interface { + // Get job agents matching a deployment selector + // (GET /v1/deployments/{deploymentId}/job-agents) + GetJobAgentsForDeployment(c *gin.Context, deploymentId string) // List release targets for a deployment // (GET /v1/deployments/{deploymentId}/release-targets) ListReleaseTargets(c *gin.Context, deploymentId string) @@ -2484,6 +2487,30 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) +// GetJobAgentsForDeployment operation middleware +func (siw *ServerInterfaceWrapper) GetJobAgentsForDeployment(c *gin.Context) { + + var err error + + // ------------- Path parameter "deploymentId" ------------- + var deploymentId string + + err = runtime.BindStyledParameterWithOptions("simple", "deploymentId", c.Param("deploymentId"), &deploymentId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter deploymentId: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetJobAgentsForDeployment(c, deploymentId) +} + // ListReleaseTargets operation middleware func (siw *ServerInterfaceWrapper) ListReleaseTargets(c *gin.Context) { @@ -2705,6 +2732,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } + router.GET(options.BaseURL+"/v1/deployments/:deploymentId/job-agents", wrapper.GetJobAgentsForDeployment) router.GET(options.BaseURL+"/v1/deployments/:deploymentId/release-targets", wrapper.ListReleaseTargets) router.GET(options.BaseURL+"/v1/jobs/:jobId/verification-status", wrapper.GetJobVerificationStatus) router.POST(options.BaseURL+"/v1/validate/resource-selector", wrapper.ValidateResourceSelector) diff --git a/apps/workspace-engine/svc/http/server/openapi/deployments/server.go b/apps/workspace-engine/svc/http/server/openapi/deployments/server.go new file mode 100644 index 000000000..801535211 --- /dev/null +++ b/apps/workspace-engine/svc/http/server/openapi/deployments/server.go @@ -0,0 +1,49 @@ +package deployments + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/selector" +) + +type Deployments struct{} + +func (d *Deployments) GetJobAgentsForDeployment(c *gin.Context, deploymentId string) { + ctx := c.Request.Context() + queries := db.GetQueries(ctx) + + deploymentUUID, err := uuid.Parse(deploymentId) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid deployment ID"}) + return + } + + deployment, err := queries.GetDeploymentByID(ctx, deploymentUUID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) + return + } + + allAgents, err := queries.ListJobAgentsByWorkspaceID(ctx, deployment.WorkspaceID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list job agents"}) + return + } + + oapiAgents := make([]oapi.JobAgent, len(allAgents)) + for i, row := range allAgents { + oapiAgents[i] = *db.ToOapiJobAgent(row) + } + + matched, err := selector.MatchJobAgents(ctx, deployment.JobAgentSelector, oapiAgents) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to evaluate selector: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"items": matched}) +} diff --git a/apps/workspace-engine/svc/http/server/openapi/server.go b/apps/workspace-engine/svc/http/server/openapi/server.go index 111cf5258..fdf4b5ade 100644 --- a/apps/workspace-engine/svc/http/server/openapi/server.go +++ b/apps/workspace-engine/svc/http/server/openapi/server.go @@ -3,6 +3,7 @@ package openapi import ( "github.com/jackc/pgx/v5/pgxpool" "workspace-engine/pkg/oapi" + "workspace-engine/svc/http/server/openapi/deployments" release_targets "workspace-engine/svc/http/server/openapi/release_targets" "workspace-engine/svc/http/server/openapi/resources" "workspace-engine/svc/http/server/openapi/validators" @@ -21,6 +22,7 @@ func New(pool *pgxpool.Pool) *Server { var _ oapi.ServerInterface = &Server{} type Server struct { + deployments.Deployments resources.Resources validators.Validator workflows.Workflows diff --git a/packages/trpc/src/routes/deployments.ts b/packages/trpc/src/routes/deployments.ts index 95c92e440..023349ecd 100644 --- a/packages/trpc/src/routes/deployments.ts +++ b/packages/trpc/src/routes/deployments.ts @@ -571,4 +571,28 @@ export const deploymentsRouter = router({ }; }); }), + + jobAgents: protectedProcedure + .input(z.object({ deploymentId: z.string() })) + .meta({ + authorizationCheck: ({ canUser, input }) => + canUser + .perform(Permission.DeploymentGet) + .on({ type: "deployment", id: input.deploymentId }), + }) + .query(async ({ input }) => { + const result = await getClientFor().GET( + "/v1/deployments/{deploymentId}/job-agents", + { params: { path: { deploymentId: input.deploymentId } } }, + ); + + if (result.error != null) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Failed to list job agents for deployment: ${JSON.stringify(result.error)}`, + }); + } + + return result.data.items; + }), }); diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index a42323424..4f392c7d1 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -4,6 +4,23 @@ */ export interface paths { + "/v1/deployments/{deploymentId}/job-agents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get job agents matching a deployment selector */ + get: operations["getJobAgentsForDeployment"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/deployments/{deploymentId}/release-targets": { parameters: { query?: never; @@ -1342,6 +1359,40 @@ export interface components { } export type $defs = Record; export interface operations { + getJobAgentsForDeployment: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the deployment */ + deploymentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Job agents matching the deployment selector */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + items: components["schemas"]["JobAgent"][]; + }; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; listReleaseTargets: { parameters: { query?: never; From c588859b3ceb2138f89a352b4411362e9d9c9dc9 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 8 Apr 2026 19:33:02 -0700 Subject: [PATCH 2/2] cleanup --- apps/workspace-engine/oapi/openapi.json | 10 ++++++++++ .../oapi/spec/paths/deployment.jsonnet | 3 ++- .../svc/http/server/openapi/deployments/server.go | 12 +++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 77933a911..44b9f11ff 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -3225,6 +3225,16 @@ } }, "description": "Invalid request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found" } }, "summary": "Get job agents matching a deployment selector" diff --git a/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet b/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet index aa067477e..4ade31f43 100644 --- a/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet +++ b/apps/workspace-engine/oapi/spec/paths/deployment.jsonnet @@ -18,7 +18,8 @@ local openapi = import '../lib/openapi.libsonnet'; }, required: ['items'], }, 'Job agents matching the deployment selector') - + openapi.badRequestResponse(), + + openapi.badRequestResponse() + + openapi.notFoundResponse(), }, }, } diff --git a/apps/workspace-engine/svc/http/server/openapi/deployments/server.go b/apps/workspace-engine/svc/http/server/openapi/deployments/server.go index 801535211..cea936154 100644 --- a/apps/workspace-engine/svc/http/server/openapi/deployments/server.go +++ b/apps/workspace-engine/svc/http/server/openapi/deployments/server.go @@ -1,10 +1,12 @@ package deployments import ( + "errors" "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/jackc/pgx/v5" "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" @@ -24,7 +26,11 @@ func (d *Deployments) GetJobAgentsForDeployment(c *gin.Context, deploymentId str deployment, err := queries.GetDeploymentByID(ctx, deploymentUUID) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) + if errors.Is(err, pgx.ErrNoRows) { + c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get deployment"}) return } @@ -45,5 +51,9 @@ func (d *Deployments) GetJobAgentsForDeployment(c *gin.Context, deploymentId str return } + if matched == nil { + matched = []oapi.JobAgent{} + } + c.JSON(http.StatusOK, gin.H{"items": matched}) }