Skip to content

Commit 219c75f

Browse files
feat: Add support for AI Foundry resource ID and project name resolution in deployment scripts
1 parent 6493698 commit 219c75f

3 files changed

Lines changed: 195 additions & 19 deletions

File tree

infra/scripts/post_deploy.ps1

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ function Get-ValuesFromAzdEnv {
250250
$script:aiSearchEndpoint = $(azd env get-value AZURE_SEARCH_ENDPOINT 2>$null)
251251
$script:openaiEndpoint = $(azd env get-value AZURE_OPENAI_ENDPOINT 2>$null)
252252
$script:projectEndpoint = $(azd env get-value AZURE_AI_PROJECT_ENDPOINT 2>$null)
253+
$script:aiFoundryResourceId = $(azd env get-value AI_FOUNDRY_RESOURCE_ID 2>$null)
254+
$script:aiProjectName = $(azd env get-value AZURE_AI_PROJECT_NAME 2>$null)
253255
$script:ResourceGroup = $(azd env get-value AZURE_RESOURCE_GROUP 2>$null)
254256

255257
if (-not $script:backendUrl -or -not $script:storageAccount -or -not $script:aiSearch -or -not $script:ResourceGroup) {
@@ -283,6 +285,8 @@ function Get-ValuesFromAzDeployment {
283285
$script:aiSearchEndpoint = Get-DeploymentValue -DeploymentOutputs $deploymentOutputs -PrimaryKey "azurE_SEARCH_ENDPOINT" -FallbackKey "azureSearchEndpoint"
284286
$script:openaiEndpoint = Get-DeploymentValue -DeploymentOutputs $deploymentOutputs -PrimaryKey "azurE_OPENAI_ENDPOINT" -FallbackKey "azureOpenaiEndpoint"
285287
$script:projectEndpoint = Get-DeploymentValue -DeploymentOutputs $deploymentOutputs -PrimaryKey "azurE_AI_PROJECT_ENDPOINT" -FallbackKey "azureAiProjectEndpoint"
288+
$script:aiFoundryResourceId = Get-DeploymentValue -DeploymentOutputs $deploymentOutputs -PrimaryKey "aI_FOUNDRY_RESOURCE_ID" -FallbackKey "aiFoundryResourceId"
289+
$script:aiProjectName = Get-DeploymentValue -DeploymentOutputs $deploymentOutputs -PrimaryKey "azurE_AI_PROJECT_NAME" -FallbackKey "azureAiProjectName"
286290

287291
if (-not $script:storageAccount -or -not $script:aiSearch -or -not $script:backendUrl) {
288292
Write-Host "Error: Could not extract all required values from deployment outputs."
@@ -636,6 +640,40 @@ try {
636640
if ($script:aiSearchEndpoint) { $env:AZURE_AI_SEARCH_ENDPOINT = $script:aiSearchEndpoint }
637641
if ($script:openaiEndpoint) { $env:AZURE_OPENAI_ENDPOINT = $script:openaiEndpoint }
638642

643+
# Resolve AI Foundry account resource ID and project name. Prefer values
644+
# from azd env / deployment outputs; otherwise fall back to az CLI lookup.
645+
# These are exported so seed_kb_connections.py can construct the project
646+
# ARM resource ID directly, avoiding fragile data-plane discovery that
647+
# fails for service principals on fresh deployments.
648+
if (-not $script:aiFoundryResourceId -and $script:openaiEndpoint -and $script:ResourceGroup) {
649+
$foundryAccountName = $null
650+
if ($script:openaiEndpoint -match '^https?://([^.]+)\.') {
651+
$foundryAccountName = $Matches[1]
652+
}
653+
if ($foundryAccountName) {
654+
$script:aiFoundryResourceId = az cognitiveservices account show --name $foundryAccountName --resource-group $script:ResourceGroup --query id -o tsv 2>$null
655+
}
656+
}
657+
if (-not $script:aiProjectName -and $script:projectEndpoint) {
658+
# Project endpoint format: https://{account}.services.ai.azure.com/api/projects/{project}
659+
$script:aiProjectName = ($script:projectEndpoint.TrimEnd('/') -split '/')[-1]
660+
}
661+
662+
if ($script:aiFoundryResourceId) { $env:AI_FOUNDRY_RESOURCE_ID = $script:aiFoundryResourceId }
663+
if ($script:aiProjectName) { $env:AZURE_AI_PROJECT_NAME = $script:aiProjectName }
664+
if ($script:azSubscriptionId) {
665+
$env:AZURE_AI_SUBSCRIPTION_ID = $script:azSubscriptionId
666+
$env:AZURE_SUBSCRIPTION_ID = $script:azSubscriptionId
667+
}
668+
if ($script:ResourceGroup) {
669+
$env:AZURE_AI_RESOURCE_GROUP = $script:ResourceGroup
670+
$env:AZURE_RESOURCE_GROUP = $script:ResourceGroup
671+
}
672+
673+
if (-not $script:aiFoundryResourceId -or -not $script:aiProjectName) {
674+
Write-Host "Warning: AI_FOUNDRY_RESOURCE_ID or AZURE_AI_PROJECT_NAME not resolved. KB MCP connection provisioning may fall back to data-plane discovery." -ForegroundColor Yellow
675+
}
676+
639677
# ── WAF: temporarily enable public access for use cases that need data ──
640678
$usesData = $useCaseSelection -in @("1","2","5","6","7")
641679
if ($usesData) {

infra/scripts/post_deploy.sh

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ ai_search=""
1111
ai_search_endpoint=""
1212
openai_endpoint=""
1313
project_endpoint=""
14+
ai_foundry_resource_id=""
15+
ai_project_name=""
1416
az_subscription_id=""
1517
resource_group=""
1618
user_principal_id=""
@@ -297,6 +299,8 @@ get_values_from_azd_env() {
297299
if [ -z "$project_endpoint" ]; then
298300
project_endpoint="$(azd env get-value AZURE_AI_AGENT_ENDPOINT 2>/dev/null || true)"
299301
fi
302+
ai_foundry_resource_id="$(azd env get-value AI_FOUNDRY_RESOURCE_ID 2>/dev/null || true)"
303+
ai_project_name="$(azd env get-value AZURE_AI_PROJECT_NAME 2>/dev/null || true)"
300304
resource_group="$(azd env get-value AZURE_RESOURCE_GROUP 2>/dev/null || true)"
301305

302306
if [ -z "$backend_url" ] || [ -z "$storage_account" ] || [ -z "$ai_search" ] || [ -z "$resource_group" ]; then
@@ -325,7 +329,7 @@ get_values_from_az_deployment() {
325329
return 1
326330
fi
327331

328-
local dep_storage_account dep_ai_search dep_backend_url dep_ai_search_endpoint dep_openai_endpoint dep_project_endpoint
332+
local dep_storage_account dep_ai_search dep_backend_url dep_ai_search_endpoint dep_openai_endpoint dep_project_endpoint dep_ai_foundry_resource_id dep_ai_project_name
329333
dep_storage_account="$(printf '%s' "$deployment_outputs" | get_value_from_deployment "azurE_STORAGE_ACCOUNT_NAME" "azureStorageAccountName" 2>/dev/null || true)"
330334
dep_ai_search="$(printf '%s' "$deployment_outputs" | get_value_from_deployment "azurE_AI_SEARCH_NAME" "azureAiSearchName" 2>/dev/null || true)"
331335
dep_backend_url="$(printf '%s' "$deployment_outputs" | get_value_from_deployment "backenD_URL" "backendUrl" 2>/dev/null || true)"
@@ -338,6 +342,8 @@ get_values_from_az_deployment() {
338342
if [ -z "$dep_project_endpoint" ]; then
339343
dep_project_endpoint="$(printf '%s' "$deployment_outputs" | get_value_from_deployment "azurE_AI_AGENT_ENDPOINT" "azureAiAgentEndpoint" 2>/dev/null || true)"
340344
fi
345+
dep_ai_foundry_resource_id="$(printf '%s' "$deployment_outputs" | get_value_from_deployment "aI_FOUNDRY_RESOURCE_ID" "aiFoundryResourceId" 2>/dev/null || true)"
346+
dep_ai_project_name="$(printf '%s' "$deployment_outputs" | get_value_from_deployment "azurE_AI_PROJECT_NAME" "azureAiProjectName" 2>/dev/null || true)"
341347

342348
if [ -n "$dep_storage_account" ]; then
343349
storage_account="$dep_storage_account"
@@ -357,6 +363,12 @@ get_values_from_az_deployment() {
357363
if [ -n "$dep_project_endpoint" ]; then
358364
project_endpoint="$dep_project_endpoint"
359365
fi
366+
if [ -n "$dep_ai_foundry_resource_id" ]; then
367+
ai_foundry_resource_id="$dep_ai_foundry_resource_id"
368+
fi
369+
if [ -n "$dep_ai_project_name" ]; then
370+
ai_project_name="$dep_ai_project_name"
371+
fi
360372

361373
if [ -z "$storage_account" ] || [ -z "$ai_search" ] || [ -z "$backend_url" ]; then
362374
error "Could not extract all required values from deployment outputs."
@@ -723,6 +735,45 @@ main() {
723735
warn "AZURE_OPENAI_ENDPOINT is not set. Knowledge base reasoning may fall back to default or fail."
724736
fi
725737

738+
# Resolve AI Foundry account resource ID and project name. Prefer the values
739+
# already retrieved from azd env / deployment outputs; otherwise fall back to
740+
# querying the resource group with az CLI. These are exported so that
741+
# seed_kb_connections.py can construct the project ARM resource ID directly,
742+
# avoiding fragile data-plane discovery that fails for service principals on
743+
# fresh deployments.
744+
if [ -z "$ai_foundry_resource_id" ] && [ -n "$resource_group" ] && [ -n "$openai_endpoint" ]; then
745+
local foundry_account_name=""
746+
if [[ "$openai_endpoint" =~ ^https?://([^.]+)\. ]]; then
747+
foundry_account_name="${BASH_REMATCH[1]}"
748+
fi
749+
if [ -n "$foundry_account_name" ]; then
750+
ai_foundry_resource_id="$(az cognitiveservices account show --name "$foundry_account_name" --resource-group "$resource_group" --query id -o tsv 2>/dev/null || true)"
751+
fi
752+
fi
753+
if [ -z "$ai_project_name" ] && [ -n "$project_endpoint" ]; then
754+
# Project endpoint format: https://{account}.services.ai.azure.com/api/projects/{project}
755+
ai_project_name="${project_endpoint##*/}"
756+
fi
757+
758+
if [ -n "$ai_foundry_resource_id" ]; then
759+
export AI_FOUNDRY_RESOURCE_ID="$ai_foundry_resource_id"
760+
fi
761+
if [ -n "$ai_project_name" ]; then
762+
export AZURE_AI_PROJECT_NAME="$ai_project_name"
763+
fi
764+
if [ -n "$az_subscription_id" ]; then
765+
export AZURE_AI_SUBSCRIPTION_ID="$az_subscription_id"
766+
export AZURE_SUBSCRIPTION_ID="$az_subscription_id"
767+
fi
768+
if [ -n "$resource_group" ]; then
769+
export AZURE_AI_RESOURCE_GROUP="$resource_group"
770+
export AZURE_RESOURCE_GROUP="$resource_group"
771+
fi
772+
773+
if [ -z "$ai_foundry_resource_id" ] || [ -z "$ai_project_name" ]; then
774+
warn "AI_FOUNDRY_RESOURCE_ID or AZURE_AI_PROJECT_NAME not resolved. KB MCP connection provisioning may fall back to data-plane discovery."
775+
fi
776+
726777
local uses_data=false
727778
case "$selected_use_case" in
728779
1|2|5|6|7) uses_data=true ;;

infra/scripts/seed_kb_connections.py

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,23 @@
1313
- AZURE_AI_SEARCH_ENDPOINT
1414
- AZURE_AI_PROJECT_ENDPOINT (the full project endpoint URL)
1515
16+
Optional (preferred — used to construct the project ARM resource ID directly,
17+
avoiding fragile data-plane discovery that fails for service principals on
18+
fresh deployments):
19+
- AI_FOUNDRY_RESOURCE_ID (the AI Foundry / Cognitive Services account ARM id)
20+
- AZURE_AI_PROJECT_NAME (the project name under the account)
21+
- AZURE_AI_PROJECT_RESOURCE_ID (explicit full project ARM id; overrides the above)
22+
- AZURE_AI_SUBSCRIPTION_ID (or AZURE_SUBSCRIPTION_ID)
23+
- AZURE_AI_RESOURCE_GROUP (or AZURE_RESOURCE_GROUP)
24+
1625
Authentication: DefaultAzureCredential — caller needs Contributor on the AI project.
1726
Idempotent: PUTs connections (creates or updates).
1827
"""
1928

2029
import os
2130
import sys
2231
from pathlib import Path
32+
from urllib.parse import urlparse
2333

2434
import httpx
2535
from azure.identity import DefaultAzureCredential
@@ -115,6 +125,63 @@ def _discover_project_resource_id(credential: DefaultAzureCredential) -> str:
115125
return ""
116126

117127

128+
def _build_project_resource_id_from_env() -> str:
129+
"""Construct the project ARM resource ID from environment variables.
130+
131+
Preferred resolution order:
132+
1. AZURE_AI_PROJECT_RESOURCE_ID (explicit override)
133+
2. AI_FOUNDRY_RESOURCE_ID + AZURE_AI_PROJECT_NAME
134+
3. (subscription, resource group) + (account, project) parsed from PROJECT_ENDPOINT
135+
136+
Returns empty string if it cannot be constructed.
137+
"""
138+
explicit = os.environ.get("AZURE_AI_PROJECT_RESOURCE_ID", "").strip().rstrip("/")
139+
if explicit:
140+
return explicit
141+
142+
foundry_id = os.environ.get("AI_FOUNDRY_RESOURCE_ID", "").strip().rstrip("/")
143+
project_name_env = os.environ.get("AZURE_AI_PROJECT_NAME", "").strip()
144+
145+
# Parse account name and project name from the project endpoint as a fallback.
146+
parsed = urlparse(PROJECT_ENDPOINT) if PROJECT_ENDPOINT else None
147+
parsed_account = ""
148+
parsed_project = ""
149+
if parsed and parsed.hostname:
150+
parsed_account = parsed.hostname.split(".", 1)[0]
151+
path_parts = [p for p in (parsed.path or "").split("/") if p]
152+
if path_parts:
153+
parsed_project = path_parts[-1]
154+
155+
project_name = project_name_env or parsed_project
156+
157+
# Path 2: foundry resource id + project name
158+
if foundry_id and project_name:
159+
# Ensure the id points to the account (not the project itself) before appending.
160+
if "/projects/" in foundry_id.lower():
161+
return foundry_id
162+
return f"{foundry_id}/projects/{project_name}"
163+
164+
# Path 3: build from sub + rg + parsed account/project
165+
sub = (
166+
os.environ.get("AZURE_AI_SUBSCRIPTION_ID")
167+
or os.environ.get("AZURE_SUBSCRIPTION_ID")
168+
or ""
169+
).strip()
170+
rg = (
171+
os.environ.get("AZURE_AI_RESOURCE_GROUP")
172+
or os.environ.get("AZURE_RESOURCE_GROUP")
173+
or ""
174+
).strip()
175+
if sub and rg and parsed_account and project_name:
176+
return (
177+
f"/subscriptions/{sub}/resourceGroups/{rg}"
178+
f"/providers/Microsoft.CognitiveServices/accounts/{parsed_account}"
179+
f"/projects/{project_name}"
180+
)
181+
182+
return ""
183+
184+
118185
def _create_connection_via_arm(
119186
resource_id: str, connection_name: str, target_url: str, credential: DefaultAzureCredential
120187
) -> bool:
@@ -171,27 +238,47 @@ def main() -> None:
171238

172239
credential = DefaultAzureCredential()
173240

174-
# Discover the ARM resource ID of the project
175-
print("Discovering project resource ID...")
176-
resource_id = _discover_project_resource_id(credential)
177-
if not resource_id:
178-
# Try to get it from an existing connection
179-
print(" Could not discover via data-plane. Trying existing connection...")
180-
from azure.ai.projects import AIProjectClient
181-
182-
client = AIProjectClient(endpoint=PROJECT_ENDPOINT, credential=credential)
183-
connections = list(client.connections.list())
184-
if connections:
185-
# Parse resource ID from any connection's ID
186-
# Format: /subscriptions/.../connections/name
187-
conn_id = connections[0].id
188-
# Remove the /connections/... suffix to get the project resource ID
189-
resource_id = conn_id.rsplit("/connections/", 1)[0]
190-
client.close()
241+
# Resolve the project ARM resource ID.
242+
# Preferred path: build it directly from environment variables exported by
243+
# post_deploy.{sh,ps1} (these come from azd env / deployment outputs). This
244+
# avoids fragile data-plane discovery that fails for service principals on
245+
# fresh deployments where no connections exist yet.
246+
print("Resolving project resource ID...")
247+
resource_id = _build_project_resource_id_from_env()
248+
if resource_id:
249+
print(f" Using project resource ID from environment: {resource_id}")
250+
else:
251+
print(" Env-based resolution unavailable. Trying data-plane discovery...")
252+
resource_id = _discover_project_resource_id(credential)
253+
if not resource_id:
254+
# Last-resort: parse from any existing connection.
255+
print(" Could not discover via data-plane. Trying existing connection...")
256+
try:
257+
from azure.ai.projects import AIProjectClient
258+
259+
client = AIProjectClient(endpoint=PROJECT_ENDPOINT, credential=credential)
260+
try:
261+
connections = list(client.connections.list())
262+
except Exception as exc: # noqa: BLE001
263+
print(f" Connection list failed: {exc}")
264+
connections = []
265+
finally:
266+
client.close()
267+
if connections:
268+
# Format: /subscriptions/.../connections/name
269+
conn_id = connections[0].id
270+
resource_id = conn_id.rsplit("/connections/", 1)[0]
271+
except Exception as exc: # noqa: BLE001
272+
print(f" AIProjectClient fallback failed: {exc}")
191273

192274
if not resource_id:
193275
print("ERROR: Could not determine project ARM resource ID.")
194-
print(" Ensure at least one connection exists or AZURE_AI_PROJECT_ENDPOINT is correct.")
276+
print(
277+
" Set one of the following so the script can build the resource ID:\n"
278+
" - AZURE_AI_PROJECT_RESOURCE_ID (full ARM id), OR\n"
279+
" - AI_FOUNDRY_RESOURCE_ID + AZURE_AI_PROJECT_NAME, OR\n"
280+
" - AZURE_AI_SUBSCRIPTION_ID + AZURE_AI_RESOURCE_GROUP + AZURE_AI_PROJECT_ENDPOINT."
281+
)
195282
sys.exit(1)
196283

197284
print(f" Project resource ID: {resource_id}")

0 commit comments

Comments
 (0)