|
13 | 13 | - AZURE_AI_SEARCH_ENDPOINT |
14 | 14 | - AZURE_AI_PROJECT_ENDPOINT (the full project endpoint URL) |
15 | 15 |
|
| 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 | +
|
16 | 25 | Authentication: DefaultAzureCredential — caller needs Contributor on the AI project. |
17 | 26 | Idempotent: PUTs connections (creates or updates). |
18 | 27 | """ |
19 | 28 |
|
20 | 29 | import os |
21 | 30 | import sys |
22 | 31 | from pathlib import Path |
| 32 | +from urllib.parse import urlparse |
23 | 33 |
|
24 | 34 | import httpx |
25 | 35 | from azure.identity import DefaultAzureCredential |
@@ -115,6 +125,63 @@ def _discover_project_resource_id(credential: DefaultAzureCredential) -> str: |
115 | 125 | return "" |
116 | 126 |
|
117 | 127 |
|
| 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 | + |
118 | 185 | def _create_connection_via_arm( |
119 | 186 | resource_id: str, connection_name: str, target_url: str, credential: DefaultAzureCredential |
120 | 187 | ) -> bool: |
@@ -171,27 +238,47 @@ def main() -> None: |
171 | 238 |
|
172 | 239 | credential = DefaultAzureCredential() |
173 | 240 |
|
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}") |
191 | 273 |
|
192 | 274 | if not resource_id: |
193 | 275 | 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 | + ) |
195 | 282 | sys.exit(1) |
196 | 283 |
|
197 | 284 | print(f" Project resource ID: {resource_id}") |
|
0 commit comments