Skip to content

Commit db11d70

Browse files
Merge pull request #98 from microsoft/psl-ui-integration
feat: UI integration for processor apis
2 parents 298822e + 1bf68c5 commit db11d70

11 files changed

Lines changed: 410 additions & 88 deletions

File tree

infra/main.bicep

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.
11701170

11711171
var backendContainerPort = 80
11721172
var backendContainerAppName = take('ca-backend-api-${solutionSuffix}', 32)
1173+
var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32)
11731174
module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = {
11741175
name: take('avm.res.app.container-app.${backendContainerAppName}', 64)
11751176
#disable-next-line no-unnecessary-dependson
@@ -1197,6 +1198,11 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = {
11971198
name: 'AZURE_CLIENT_ID'
11981199
value: appIdentity.outputs.clientId
11991200
}
1201+
{
1202+
name: 'PROCESSOR_CONTROL_URL'
1203+
// Internal ingress FQDN format: https://<app-name>.internal.<environment-default-domain>
1204+
value: 'https://${processorContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}'
1205+
}
12001206
],
12011207
enableMonitoring
12021208
? [
@@ -1320,7 +1326,6 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = {
13201326
}
13211327
}
13221328

1323-
var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32)
13241329
module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
13251330
name: take('avm.res.app.container-app.${processorContainerAppName}', 64)
13261331
#disable-next-line no-unnecessary-dependson
@@ -1356,6 +1361,14 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
13561361
name: 'STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed
13571362
value: storageAccount.outputs.name
13581363
}
1364+
{
1365+
name: 'CONTROL_API_ENABLED'
1366+
value: '1'
1367+
}
1368+
{
1369+
name: 'CONTROL_API_PORT'
1370+
value: '8080'
1371+
}
13591372
],
13601373
enableMonitoring
13611374
? [
@@ -1373,9 +1386,10 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
13731386
}
13741387
}
13751388
]
1376-
ingressTransport: null
1377-
disableIngress: true
1389+
// Internal ingress required for container-to-container communication
1390+
ingressTargetPort: 8080
13781391
ingressExternal: false
1392+
ingressAllowInsecure: true // Allow HTTP without SSL redirect for internal calls
13791393
scaleSettings: {
13801394
maxReplicas: enableScalability ? 3 : 1
13811395
minReplicas: 1

infra/main_custom.bicep

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.
11081108

11091109
var backendContainerPort = 80
11101110
var backendContainerAppName = take('ca-backend-api-${solutionSuffix}', 32)
1111+
var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32)
11111112
module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = {
11121113
name: take('avm.res.app.container-app.${backendContainerAppName}', 64)
11131114
#disable-next-line no-unnecessary-dependson
@@ -1142,6 +1143,11 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = {
11421143
name: 'AZURE_CLIENT_ID'
11431144
value: appIdentity.outputs.clientId
11441145
}
1146+
{
1147+
name: 'PROCESSOR_CONTROL_URL'
1148+
// Internal ingress FQDN format: https://<app-name>.internal.<environment-default-domain>
1149+
value: 'https://${processorContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}'
1150+
}
11451151
],
11461152
enableMonitoring
11471153
? [
@@ -1262,7 +1268,6 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = {
12621268
}
12631269
}
12641270

1265-
var processorContainerAppName = take('ca-processor-${solutionSuffix}', 32)
12661271
module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
12671272
name: take('avm.res.app.container-app.${processorContainerAppName}', 64)
12681273
#disable-next-line no-unnecessary-dependson
@@ -1305,6 +1310,14 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
13051310
name: 'STORAGE_ACCOUNT_NAME' // TODO - verify name and if needed
13061311
value: storageAccount.outputs.name
13071312
}
1313+
{
1314+
name: 'CONTROL_API_ENABLED'
1315+
value: '1'
1316+
}
1317+
{
1318+
name: 'CONTROL_API_PORT'
1319+
value: '8080'
1320+
}
13081321
],
13091322
enableMonitoring
13101323
? [
@@ -1322,9 +1335,10 @@ module containerAppProcessor 'br/public:avm/res/app/container-app:0.18.1' = {
13221335
}
13231336
}
13241337
]
1325-
ingressTransport: null
1326-
disableIngress: true
1338+
// Internal ingress required for container-to-container communication
1339+
ingressTargetPort: 8080
13271340
ingressExternal: false
1341+
ingressAllowInsecure: true // Allow HTTP without SSL redirect for internal calls
13281342
scaleSettings: {
13291343
maxReplicas: enableScalability ? 3 : 1
13301344
minReplicas: 1

src/backend-api/src/app/libs/application/application_configuration.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ class Configuration(_configuration_base, KernelBaseSettings):
7373
default=None, env="APPLICATIONINSIGHTS_CONNECTION_STRING"
7474
)
7575

76+
# Processor Control API configuration
77+
# In Azure Container Apps, apps call each other by name: http://<container-app-name>
78+
# The actual URL is set via PROCESSOR_CONTROL_URL env var from Bicep
79+
processor_control_url: str | None = Field(
80+
default="http://localhost:8080", env="PROCESSOR_CONTROL_URL"
81+
)
82+
processor_control_token: str | None = Field(
83+
default=None, env="PROCESSOR_CONTROL_TOKEN"
84+
)
85+
7686

7787
class _envConfiguration(_configuration_base):
7888
"""

src/backend-api/src/app/routers/router_process.py

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import io
22
import zipfile
33
from enum import Enum
4-
from typing import List
4+
from typing import List, Optional
55
from uuid import uuid4
66

7+
import httpx
78
from fastapi import APIRouter, File, Form, HTTPException, Request, Response, UploadFile
89
from fastapi.responses import JSONResponse, StreamingResponse
910
from libs.base.typed_fastapi import TypedFastAPI
@@ -37,6 +38,8 @@ class process_router_paths(str, Enum):
3738
START_PROCESSING = "/start-processing"
3839
DELETE_FILE = "/delete-file/{file_name}"
3940
DELETE_PROCESS = "/delete-process/{process_id}"
41+
CANCEL_PROCESS = "/cancel/{process_id}"
42+
CANCEL_STATUS = "/cancel/{process_id}/status"
4043
STATUS = "/status/{process_id}/"
4144
RENDER_STATUS = "/status/{process_id}/render/"
4245
PROCESS_AGENT_ACTIVITIES = "/status/{process_id}/activities"
@@ -578,3 +581,188 @@ async def get_file_content(
578581
raise HTTPException(
579582
status_code=500, detail=f"Error retrieving file content: {str(e)}"
580583
)
584+
585+
586+
@router.post(process_router_paths.CANCEL_PROCESS, status_code=202)
587+
async def cancel_process(
588+
process_id: str,
589+
request: Request,
590+
reason: Optional[str] = None,
591+
):
592+
"""
593+
Request cancellation of a running process.
594+
This endpoint forwards the kill request to the Processor's Control API.
595+
The processor will observe this request and terminate the running process.
596+
"""
597+
app: TypedFastAPI = request.app
598+
logger_service: ILoggerService = app.app_context.get_service(ILoggerService)
599+
600+
try:
601+
logger_service.log_info(f"Cancel process request for process_id: {process_id}")
602+
603+
# Get authenticated user
604+
authenticated_user = get_authenticated_user(request)
605+
user_id = authenticated_user.user_principal_id
606+
607+
if not user_id:
608+
raise HTTPException(status_code=401, detail="User not authenticated")
609+
610+
# Get processor control URL from configuration
611+
config = app.app_context.configuration
612+
processor_url = config.processor_control_url or "http://processor:8080"
613+
processor_token = config.processor_control_token or ""
614+
615+
# Prepare headers for processor control API
616+
headers = {}
617+
if processor_token:
618+
headers["Authorization"] = f"Bearer {processor_token}"
619+
620+
# Build the full URL for the kill endpoint
621+
kill_url = f"{processor_url}/processes/{process_id}/kill"
622+
logger_service.log_info(f"Calling processor kill API at: {kill_url}")
623+
624+
# Forward kill request to Processor Control API
625+
# Note: verify=False is needed for internal ACA communication (self-signed certs)
626+
async with httpx.AsyncClient(timeout=30.0, verify=False) as client:
627+
response = await client.post(
628+
kill_url,
629+
json={"reason": reason or f"User {user_id} cancelled from UI"},
630+
headers=headers,
631+
)
632+
logger_service.log_info(f"Processor kill API response: {response.status_code}")
633+
634+
if response.status_code == 401:
635+
logger_service.log_error("Unauthorized access to processor control API")
636+
raise HTTPException(
637+
status_code=502,
638+
detail="Failed to authenticate with processor control API",
639+
)
640+
641+
if response.status_code >= 400:
642+
logger_service.log_error(
643+
f"Processor control API error: {response.status_code} - {response.text}"
644+
)
645+
raise HTTPException(
646+
status_code=502,
647+
detail=f"Processor control API error: {response.text}",
648+
)
649+
650+
result = response.json()
651+
652+
logger_service.log_info(
653+
f"Cancel request sent for process {process_id}, state: {result.get('kill_state', 'unknown')}"
654+
)
655+
656+
return {
657+
"message": "Cancellation request submitted",
658+
"process_id": process_id,
659+
"kill_requested": result.get("kill_requested", True),
660+
"kill_state": result.get("kill_state", "pending"),
661+
"kill_requested_at": result.get("kill_requested_at", ""),
662+
}
663+
664+
except httpx.TimeoutException:
665+
logger_service.log_error(f"Timeout connecting to processor control API")
666+
raise HTTPException(
667+
status_code=504,
668+
detail="Timeout connecting to processor control API",
669+
)
670+
except httpx.ConnectError:
671+
logger_service.log_error(f"Failed to connect to processor control API")
672+
raise HTTPException(
673+
status_code=503,
674+
detail="Processor control API is unavailable",
675+
)
676+
except HTTPException:
677+
raise
678+
except Exception as e:
679+
logger_service.log_error(f"Error in cancel_process: {str(e)}")
680+
raise HTTPException(
681+
status_code=500, detail=f"Error cancelling process: {str(e)}"
682+
)
683+
684+
685+
@router.get(process_router_paths.CANCEL_STATUS, status_code=200)
686+
async def get_cancel_status(
687+
process_id: str,
688+
request: Request,
689+
):
690+
"""
691+
Get the cancellation status of a process.
692+
Returns the current kill state from the Processor's Control API.
693+
"""
694+
app: TypedFastAPI = request.app
695+
logger_service: ILoggerService = app.app_context.get_service(ILoggerService)
696+
697+
try:
698+
logger_service.log_info(f"Get cancel status for process_id: {process_id}")
699+
700+
# Get authenticated user
701+
authenticated_user = get_authenticated_user(request)
702+
user_id = authenticated_user.user_principal_id
703+
704+
if not user_id:
705+
raise HTTPException(status_code=401, detail="User not authenticated")
706+
707+
# Get processor control URL from configuration
708+
config = app.app_context.configuration
709+
processor_url = config.processor_control_url or "http://processor:8080"
710+
processor_token = config.processor_control_token or ""
711+
712+
# Prepare headers for processor control API
713+
headers = {}
714+
if processor_token:
715+
headers["Authorization"] = f"Bearer {processor_token}"
716+
717+
# Build the full URL for the control status endpoint
718+
control_url = f"{processor_url}/processes/{process_id}/control"
719+
logger_service.log_info(f"Calling processor control API at: {control_url}")
720+
721+
# Get control status from Processor Control API
722+
# Note: verify=False is needed for internal ACA communication (self-signed certs)
723+
async with httpx.AsyncClient(timeout=30.0, verify=False) as client:
724+
response = await client.get(
725+
control_url,
726+
headers=headers,
727+
)
728+
logger_service.log_info(f"Processor control API response: {response.status_code}")
729+
730+
if response.status_code == 401:
731+
logger_service.log_error("Unauthorized access to processor control API")
732+
raise HTTPException(
733+
status_code=502,
734+
detail="Failed to authenticate with processor control API",
735+
)
736+
737+
if response.status_code >= 400:
738+
logger_service.log_error(
739+
f"Processor control API error: {response.status_code} - {response.text}"
740+
)
741+
raise HTTPException(
742+
status_code=502,
743+
detail=f"Processor control API error: {response.text}",
744+
)
745+
746+
result = response.json()
747+
748+
return result
749+
750+
except httpx.TimeoutException:
751+
logger_service.log_error(f"Timeout connecting to processor control API")
752+
raise HTTPException(
753+
status_code=504,
754+
detail="Timeout connecting to processor control API",
755+
)
756+
except httpx.ConnectError:
757+
logger_service.log_error(f"Failed to connect to processor control API")
758+
raise HTTPException(
759+
status_code=503,
760+
detail="Processor control API is unavailable",
761+
)
762+
except HTTPException:
763+
raise
764+
except Exception as e:
765+
logger_service.log_error(f"Error in get_cancel_status: {str(e)}")
766+
raise HTTPException(
767+
status_code=500, detail=f"Error getting cancel status: {str(e)}"
768+
)

src/frontend/src/commonComponents/ProgressModal/progressModal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,15 @@ const ProgressModal: React.FC<ProgressModalProps> = ({
6868
setOpen(false);
6969
};
7070

71-
const handleCancel = () => {
72-
// Trigger onCancel (navigate to landing page) and close modal
71+
const handleCancel = async () => {
72+
console.log('=== ProgressModal handleCancel called ===');
73+
// Trigger onCancel (calls cancel API and navigates to landing page) and close modal
7374
if (onCancel) {
74-
onCancel();
75+
console.log('Calling onCancel callback...');
76+
await onCancel();
77+
console.log('onCancel callback completed');
78+
} else {
79+
console.warn('No onCancel callback provided');
7580
}
7681
setOpen(false);
7782
};

0 commit comments

Comments
 (0)