Repository navigation
fix: resolve Foundry endpoints for custom subdomains - #10887
Hui Miao (huimiu) wants to merge 5 commits into
Conversation
|
Azure Pipelines: Successfully started running 4 pipeline(s). 23 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
e05bec3 to
a9d243b
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A critical finding remains in project endpoint metadata handling: inconsistent hosts can produce mismatched project and OpenAI URLs and allow incorrect adoption or provisioning.
1 open issue
What changed in this PR
This PR fixes Microsoft Foundry custom-subdomain handling by resolving project and Azure OpenAI endpoints from ARM metadata and propagating them across the Agents and Projects extensions, provisioning lifecycle, connection discovery, doctor checks, and generated infrastructure.
Changes:
- Adds shared ARM endpoint resolution and validation for project adoption, agent initialization, deployment authoring, provisioning, teardown, and infrastructure export.
- Updates Bicep and Terraform templates to honor custom subdomains and verify project identity.
- Adds regression coverage, recording updates, and documentation for custom endpoints, metadata failures, identity mismatches, and cleanup.
| File | Description |
|---|---|
| cli/azd/test/functional/ai_agents/testdata/recordings/Test_AIAgent_Init_NoPrompt_WithProject.yaml | Updates recorded ARM interactions for endpoint validation. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/terraform-existing-project/outputs.tf.tmpl | Emits the resolved OpenAI endpoint and validates project identity. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/terraform-existing-project/main.tf | Reads ARM account and project metadata to derive custom-subdomain endpoints. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/terraform-existing-project/container-registry-reuse.tf | Removes the duplicate project data source. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/terraform-existing-project/container-registry-create.tf | Reuses the centralized project data source. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/existing-project.bicep | Uses the account custom subdomain for the OpenAI output. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/existing-project.arm.json | Regenerates ARM output logic for custom subdomains. |
| cli/azd/extensions/azure.ai.projects/synthesis/templates/existing-project-eject.bicep.tmpl | Applies custom-subdomain handling to ejected Bicep. |
| cli/azd/extensions/azure.ai.projects/synthesis/project_endpoints.go | Adds shared ARM metadata lookup, endpoint resolution, and validation. |
| cli/azd/extensions/azure.ai.projects/synthesis/project_endpoints_test.go | Tests endpoint resolution, fallbacks, invalid metadata, and URL validation. |
| cli/azd/extensions/azure.ai.projects/synthesis/project_endpoints_plan_test.go | Tests generated Bicep and Terraform endpoint behavior. |
| cli/azd/extensions/azure.ai.projects/README.md | Documents custom-subdomain adoption, validation, and cleanup behavior. |
| cli/azd/extensions/azure.ai.projects/internal/provisioning/project_endpoints_test.go | Covers provisioning identity checks and teardown behavior. |
| cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider.go | Verifies existing-project identity during state, provisioning, preview, and teardown. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_transaction_test.go | Updates transaction tests for resolved project targets. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_ownership_test.go | Extends project identity and infrastructure ownership tests. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_existing_infra.go | Carries ARM-resolved project identity through infrastructure ejection. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_environment.go | Preserves state for same-project endpoint repairs and clears replaced state. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_endpoints_test.go | Tests project adoption, endpoint repair, deployment authoring, replacement, and ejection. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_deployment_lifecycle_test.go | Updates lifecycle tests for resolved project targets. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_deployment_add.go | Validates configured endpoints against ARM-resolved project metadata. |
| cli/azd/extensions/azure.ai.projects/internal/cmd/project_add.go | Hydrates selected projects with ARM-resolved endpoints. |
| cli/azd/extensions/azure.ai.agents/README.md | Documents ARM endpoint resolution and infrastructure-export validation. |
| cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go | Accepts validated project endpoints and hardens pagination URL checks. |
| cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client_test.go | Tests custom-subdomain requests and pagination-origin validation. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/init_managed_foundry.go | Uses resolved endpoints for managed Foundry initialization and environment values. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/init_infra.go | Verifies existing-project identity before infrastructure export. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go | Hydrates project metadata and routes connection discovery through resolved endpoints. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers_test.go | Updates connection-discovery tests for endpoint-based loading. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_endpoints_test.go | Adds custom-subdomain initialization and infrastructure-ejection coverage. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/doctor/shared_test.go | Updates doctor test identity documentation. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/doctor/checks_local.go | Changes connection probing to use validated endpoints. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/doctor/checks_connections.go | Validates and probes the resolved Foundry project endpoint. |
| cli/azd/extensions/azure.ai.agents/internal/cmd/doctor/checks_connections_test.go | Tests endpoint-driven connection checks and custom-subdomain behavior. |
Review effort: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Critical unresolved identity-validation gaps allow deployments to proceed with missing or mismatched Foundry project endpoints, risking mutations to the wrong project and inconsistent persisted state.
3 open issues
1 resolved since last review
Review effort: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Maintainer validation is still needed for the unresolved endpoint-identity gap and cross-extension ARM, Bicep, Terraform, provisioning, and teardown interactions that static review cannot fully verify.
2 resolved since last review
Review effort: Lite
| response_export_values = ["properties.customSubDomainName"] | ||
| } | ||
|
|
||
| data "azapi_resource" "project" { |
There was a problem hiding this comment.
container-registry-connect.tf (lines 1–5) still declares data "azapi_resource" "project". When exporting Terraform in reuse-connect mode with AZD_FOUNDRY_ACR_PULL_ASSIGNED=true, main.tf and container-registry-connect.tf are emitted together and fail validation with a duplicate data source error. Please remove it there too and cover the AcrMode variants in TestTerraformExistingProjectEndpointPlans.
| if err != nil { | ||
| return err | ||
| } | ||
| return p.validateProjectIdentity(ctx, projectID, endpoint) |
There was a problem hiding this comment.
validateExistingProjectTemplateIdentity checks that source's projectResourceId and projectEndpoint match each other, but not p.existingProjectID / p.brownfieldEndpoint. Under microsoft.foundry, if main.parameters.json overrides both to a different project, Deploy writes the overridden FOUNDRY_PROJECT_ENDPOINT to .env (wedging future Initialize calls against azure.yaml), while State and Destroy still target p.existingProjectID. Should this also require projectID and endpoint to match p.existingProjectID and p.brownfieldEndpoint?
| if err != nil { | ||
| return nil, fmt.Errorf("read Foundry project: %w", err) | ||
| } | ||
| account, err := factory.NewAccountsClient().Get(ctx, id.ResourceGroupName, id.Parent.Name, nil) |
There was a problem hiding this comment.
Previously, project_add.go only called ProjectsClient.Get and foundryAccountNetworkInjected treated AccountsClient.Get as best-effort. For users with project-scoped RBAC only (Azure AI User / Azure AI Project Manager on .../projects/<proj>), ProjectsClient.Get succeeds and returns endpoints["AI Foundry API"], while AccountsClient.Get returns 403. When AI Foundry API is already present and valid, can we treat a 403 on AccountsClient.Get as non-fatal so project-scoped users aren't blocked?
| } | ||
| metadata, err := synthesis.LookupFoundryProject(ctx, id, p.credential, p.armClientOptions) | ||
| if err != nil { | ||
| return fmt.Errorf("verify existing Foundry project identity: %w", err) |
There was a problem hiding this comment.
Returning a plain fmt.Errorf here (and in azure.ai.agents/internal/cmd/init_infra.go:500) skips exterrors.ServiceFromAzure for ARM *azcore.ResponseError failures across gRPC. Conversely, hydrateResolvedProject (project_add.go:1102) passes non-HTTP metadata validation errors from LookupFoundryProject into ServiceFromAzure, classifying them as Internal instead of Validation. Could we classify HTTP errors via ServiceFromAzure and invalid ARM metadata as Validation across all three callers?
| strings.ContainsAny(project, `/\?#`) { | ||
| return "", fmt.Errorf("Foundry project endpoint must have the path /api/projects/<project>") | ||
| } | ||
| return "https://" + strings.ToLower(parsed.Hostname()) + strings.TrimRight(parsed.EscapedPath(), "/"), nil |
There was a problem hiding this comment.
ValidateFoundryProjectEndpoint validates unescaped parsed.Path but returns strings.TrimRight(parsed.EscapedPath(), "/") verbatim. Inputs like /api/projects/%70roject or trailing %2f pass validation here, but foundryEndpointIdentity (init_infra.go:2430) slices the raw string ("%70roject" != "project"), and resolveEndpoints / project_deployment_add.go:655 see a trailing / on %2f. Reconstructing the path from the prefix + (&url.URL{Path: project}).EscapedPath() would normalize both cleanly.
There was a problem hiding this comment.
🟡 Changes recommended
The new shared helper uses the outdated generic errors.As form instead of the repository-required errors.AsType pattern, making it inconsistent with the surrounding endpoint code.
1 open issue
1 resolved since last review
Review effort: Lite
| func IsFoundryProjectMetadataValidationError(err error) bool { | ||
| var target *FoundryProjectMetadataValidationError | ||
| return errors.As(err, &target) | ||
| } |
azure.ai.projects PR buildNote This is an unsigned development build. Install it only if you trust this PR. Install the extension: azd ext install "https://azuresdkartifacts.z5.web.core.windows.net/azd/extensions/pr/10887/azure-ai-projects.zip"
|
azure.ai.agents PR buildNote This is an unsigned development build. Install it only if you trust this PR. Install the extension: azd ext install "https://azuresdkartifacts.z5.web.core.windows.net/azd/extensions/pr/10887/azure-ai-agents.zip"
|
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|


Summary
Foundry accounts can have a custom subdomain that differs from their ARM resource name. Resolve project and Azure OpenAI endpoints from ARM metadata so agent initialization writes the correct URLs to
azure.yamland the azd environment.Fixes #10866.
Changes
User-facing changes
azd ai agent initandazd ai project addsave the custom-domain endpoints returned by ARM instead of assuming the ARM account name is the DNS prefix. Provisioning, teardown, and infrastructure export reject an endpoint that does not match the selected project's ARM metadata. No commands or flags were added or removed.E2E testing
Tested against real Azure resources whose ARM account name differed from their DNS prefix. The initial matrix recorded 25 PASS, 2 FAIL, 4 BLOCKED. The follow-up matrix recorded 14 PASS, 1 BLOCKED. The matrices overlap and include setup and negative checks.
Commands below show the tested operations. Project IDs and sample-specific arguments are omitted where noted.
azd ai project add --project-id <project-id> --output json --no-prompt, also with--forceazd ai project deployment add --model OpenAI/gpt-4.1 --name e2e-chat --sku GlobalStandard --capacity 1 --output json --no-promptazd ai agent initwith hosted-source or--kind promptargumentsazd provision --preview --no-prompt,azd provision --no-promptazd env refresh --no-prompt,azd env get-values --output json --no-promptazd ai agent init --infra=bicep --no-prompt,azd ai agent init --infra=terraform --no-prompt, andazd ai project addwith the same--infrachoicesazd provision --no-promptin generated Bicep and Terraform projectsazd deploy --service endpoint-prompt-r2 --no-prompt,azd ai agent doctor --debug --no-promptazd provision --preview --no-prompt,azd provision --no-prompt,azd down --force --purge --no-promptfor an owned native adjunctCleanup used
azd down --force --purge --no-promptand targeted agent/Connection deletion. Both runs removed their deployed test resources and purged the accounts. The retry verified HTTP 404 for all 11 ARM targets and the exact prompt agent/version. Deployment-history metadata remains. Independent Microsoft Graph object absence was not verified.Live coverage does not include missing-metadata fallbacks, guest/multitenant behavior, or interactive choice flows.