Skip to content

Commit 48753f1

Browse files
Merge pull request #702 from microsoft/dev
chore: Enhance error handling and user guidance in scripts and documentation
2 parents a250c27 + fb2e4ea commit 48753f1

File tree

12 files changed

+9250
-3727
lines changed

12 files changed

+9250
-3727
lines changed

azure.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ hooks:
1818
run: |
1919
Write-Host "Web app URL: "
2020
Write-Host "$env:WEB_APP_URL" -ForegroundColor Cyan
21-
Write-Host "`nRun the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application."
21+
Write-Host "`nCreate and activate a virtual environment if not already done, then run the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application."
2222
Write-Host "bash ./infra/scripts/process_sample_data.sh" -ForegroundColor Cyan
2323
shell: pwsh
2424
continueOnError: false
@@ -28,7 +28,7 @@ hooks:
2828
echo "Web app URL: "
2929
echo $WEB_APP_URL
3030
echo ""
31-
echo "Run the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application."
31+
echo "Create and activate a virtual environment if not already done, then run the following command in your Bash terminal. It will grant the necessary permissions between resources and your user account, and also process and load the sample data into the application."
3232
echo "bash ./infra/scripts/process_sample_data.sh"
3333
shell: sh
3434
continueOnError: false

documents/DeploymentGuide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain
253253
- This deployment generally takes **7-10 minutes** to provision the resources in your account and set up the solution.
254254
- If you encounter an error or timeout during deployment, changing the location may help, as there could be availability constraints for the resources.
255255
256-
5. Once the deployment has completed successfully, copy the bash command from terminal: (ex: `bash ./infra/scripts/process_sample_data.sh`) for later use.
256+
5. Once the deployment has completed successfully, continue with the following steps to process and load the sample data.
257257
258258
6. Create and activate a virtual environment in bash terminal:
259259

infra/main.bicep

Lines changed: 146 additions & 164 deletions
Large diffs are not rendered by default.

infra/main.json

Lines changed: 8755 additions & 3507 deletions
Large diffs are not rendered by default.

infra/modules/ai-services.bicep

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ resource cognitiveServiceNew 'Microsoft.CognitiveServices/accounts@2025-06-01' =
190190
? {
191191
defaultAction: networkAcls.?defaultAction
192192
virtualNetworkRules: networkAcls.?virtualNetworkRules ?? []
193-
ipRules: networkAcls.?ipRules ?? []
193+
ipRules: networkAcls.?ipRules ?? []
194194
bypass: networkAcls.?bypass ?? 'None'
195195
}
196196
: null
@@ -236,7 +236,7 @@ resource cognitiveServiceNew 'Microsoft.CognitiveServices/accounts@2025-06-01' =
236236

237237
var existingCognitiveServiceDetails = split(existingFoundryProjectResourceId, '/')
238238

239-
resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if(useExistingService) {
239+
resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-09-01' existing = if(useExistingService) {
240240
name: existingCognitiveServiceDetails[8]
241241
scope: resourceGroup(existingCognitiveServiceDetails[2], existingCognitiveServiceDetails[4])
242242
}
@@ -245,7 +245,7 @@ module cognitive_service_dependencies './dependencies.bicep' = if(!useExistingSe
245245
params: {
246246
projectName: projectName
247247
projectDescription: projectDescription
248-
name: cognitiveServiceNew.name
248+
name: cognitiveServiceNew.name
249249
location: location
250250
deployments: deployments
251251
diagnosticSettings: diagnosticSettings
@@ -259,7 +259,7 @@ module cognitive_service_dependencies './dependencies.bicep' = if(!useExistingSe
259259

260260
module existing_cognitive_service_dependencies './dependencies.bicep' = if(useExistingService) {
261261
params: {
262-
name: cognitiveServiceExisting.name
262+
name: cognitiveServiceExisting.name
263263
projectName: projectName
264264
projectDescription: projectDescription
265265
existingFoundryProjectResourceId: existingFoundryProjectResourceId

infra/modules/dependencies.bicep

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ resource cognitiveService_diagnosticSettings 'Microsoft.Insights/diagnosticSetti
248248
}
249249
]
250250

251-
module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [
251+
module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.1' = [
252252
for (privateEndpoint, index) in (privateEndpoints ?? []): {
253253
name: '${uniqueString(deployment().name, location)}-cognitiveService-PrivateEndpoint-${index}'
254254
scope: resourceGroup(
@@ -322,7 +322,6 @@ resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignmen
322322
}
323323
]
324324

325-
326325
module aiProject 'project.bicep' = if(!empty(projectName) || !empty(existingFoundryProjectResourceId)) {
327326
name: take('${name}-ai-project-${projectName}-deployment', 64)
328327
params: {

infra/modules/virtualNetwork.bicep

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Networking - NSGs, VNET and Subnets. Each subnet has its own NSG
33
/****************************************************************************************************************************/
44
@description('Name of the virtual network.')
5-
param name string
5+
param name string
66

77
@description('Azure region to deploy resources.')
88
param location string = resourceGroup().location
@@ -155,14 +155,14 @@ param subnets subnetType[] = [
155155
}
156156
}
157157
{
158-
name: 'deployment-scripts'
159-
addressPrefixes: ['10.0.4.0/24']
160-
networkSecurityGroup: {
161-
name: 'nsg-deployment-scripts'
162-
securityRules: []
163-
}
164-
delegation: 'Microsoft.ContainerInstance/containerGroups'
165-
serviceEndpoints: ['Microsoft.Storage']
158+
name: 'deployment-scripts'
159+
addressPrefixes: ['10.0.4.0/24']
160+
networkSecurityGroup: {
161+
name: 'nsg-deployment-scripts'
162+
securityRules: []
163+
}
164+
delegation: 'Microsoft.ContainerInstance/containerGroups'
165+
serviceEndpoints: ['Microsoft.Storage']
166166
}
167167
]
168168

@@ -185,7 +185,6 @@ param resourceSuffix string
185185
// Standard_D2s_v3 (2 vCPU, 8 GiB RAM, Premium SSD) // next most common
186186
// Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Newest, so fewer regions availabl
187187

188-
189188
// Subnet Classless Inter-Doman Routing (CIDR) Sizing Reference Table (Best Practices)
190189
// | CIDR | # of Addresses | # of /24s | Notes |
191190
// |-----------|---------------|-----------|----------------------------------------|
@@ -215,12 +214,12 @@ param resourceSuffix string
215214
// - Document subnet usage and purpose in code comments.
216215
// - For AVM modules, ensure only one delegation per subnet and leave delegations empty if not required.
217216

218-
// 1. Create NSGs for subnets
217+
// 1. Create NSGs for subnets
219218
// using AVM Network Security Group module
220219
// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/network-security-group
221220

222221
@batchSize(1)
223-
module nsgs 'br/public:avm/res/network/network-security-group:0.5.1' = [
222+
module nsgs 'br/public:avm/res/network/network-security-group:0.5.2' = [
224223
for (subnet, i) in subnets: if (!empty(subnet.?networkSecurityGroup)) {
225224
name: take('avm.res.network.network-security-group.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64)
226225
params: {
@@ -237,7 +236,7 @@ module nsgs 'br/public:avm/res/network/network-security-group:0.5.1' = [
237236
// using AVM Virtual Network module
238237
// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/virtual-network
239238

240-
module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.0' = {
239+
module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.1' = {
241240
name: take('avm.res.network.virtual-network.${name}', 64)
242241
params: {
243242
name: name
@@ -290,11 +289,21 @@ output subnets subnetOutputType[] = [
290289
]
291290

292291
// Dynamic outputs for individual subnets for backward compatibility
293-
output webSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'web') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'web')] : ''
294-
output pepsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'peps') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'peps')] : ''
295-
output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] : ''
296-
output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] : ''
297-
output deploymentScriptsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'deployment-scripts') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'deployment-scripts')] : ''
292+
output webSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'web')
293+
? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'web')]
294+
: ''
295+
output pepsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'peps')
296+
? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'peps')]
297+
: ''
298+
output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')
299+
? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')]
300+
: ''
301+
output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox')
302+
? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')]
303+
: ''
304+
output deploymentScriptsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'deployment-scripts')
305+
? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'deployment-scripts')]
306+
: ''
298307

299308
@export()
300309
@description('Custom type definition for subnet resource information as output')
@@ -318,8 +327,8 @@ type subnetType = {
318327
@description('Required. The Name of the subnet resource.')
319328
name: string
320329

321-
@description('Required. Prefixes for the subnet.') // Required to ensure at least one prefix is provided
322-
addressPrefixes: string[]
330+
@description('Required. Prefixes for the subnet.') // Required to ensure at least one prefix is provided
331+
addressPrefixes: string[]
323332

324333
@description('Optional. The delegation to enable on the subnet.')
325334
delegation: string?

infra/modules/web-sites.bicep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-0
260260
}
261261
]
262262

263-
module app_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [
263+
module app_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.1' = [
264264
for (privateEndpoint, index) in (privateEndpoints ?? []): {
265265
name: '${uniqueString(deployment().name, location)}-app-PrivateEndpoint-${index}'
266266
scope: resourceGroup(

infra/scripts/copy_kb_files.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,22 @@ if ! az account show &> /dev/null; then
4646
fi
4747

4848
# Check and assign Storage Blob Data Contributor role to current user
49-
signed_user_id=$(az ad signed-in-user show --query id --output tsv)
49+
signed_user_id=$(az ad signed-in-user show --query id --output tsv 2>&1)
50+
if [ -z "$signed_user_id" ] || [[ "$signed_user_id" == *"ERROR"* ]] || [[ "$signed_user_id" == *"InteractionRequired"* ]]; then
51+
echo "✗ Failed to get signed-in user ID. Token may have expired. Re-authenticating..."
52+
az login --use-device-code
53+
signed_user_id=$(az ad signed-in-user show --query id --output tsv)
54+
if [ -z "$signed_user_id" ]; then
55+
echo "✗ Failed to get signed-in user ID after re-authentication"
56+
exit 1
57+
fi
58+
fi
59+
5060
storage_resource_id=$(az storage account show --name "$storageAccountName" --resource-group "$resourceGroupName" --query id --output tsv)
61+
if [ -z "$storage_resource_id" ]; then
62+
echo "✗ Failed to get storage account resource ID"
63+
exit 1
64+
fi
5165

5266
role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_resource_id --query "[].roleDefinitionId" -o tsv)
5367
if [ -z "$role_assignment" ]; then

infra/scripts/process_custom_data.sh

Lines changed: 126 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/bin/bash
22

3+
# Get the directory where this script is located
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
36
# Variables - Grouped by service for clarity
47
# General Azure
58
resourceGroupName="${1}"
@@ -345,6 +348,92 @@ get_values_from_azd_env() {
345348
return 0
346349
}
347350

351+
get_values_from_az_deployment() {
352+
echo "Getting values from Azure deployment outputs..."
353+
354+
deploymentName=$(az group show --name "$resourceGroupName" --query "tags.DeploymentName" -o tsv)
355+
echo "Deployment Name (from tag): $deploymentName"
356+
357+
echo "Fetching deployment outputs..."
358+
# Get all outputs
359+
deploymentOutputs=$(az deployment group show \
360+
--name "$deploymentName" \
361+
--resource-group "$resourceGroupName" \
362+
--query "properties.outputs" -o json)
363+
364+
# Helper function to extract value from deployment outputs
365+
# Usage: extract_value "primaryKey" "fallbackKey"
366+
extract_value() {
367+
local primary_key="$1"
368+
local fallback_key="$2"
369+
local value
370+
371+
value=$(echo "$deploymentOutputs" | grep -A 3 "\"$primary_key\"" | grep '"value"' | sed 's/.*"value": *"\([^"]*\)".*/\1/')
372+
if [ -z "$value" ] && [ -n "$fallback_key" ]; then
373+
value=$(echo "$deploymentOutputs" | grep -A 3 "\"$fallback_key\"" | grep '"value"' | sed 's/.*"value": *"\([^"]*\)".*/\1/')
374+
fi
375+
echo "$value"
376+
}
377+
378+
# Extract each value using the helper function
379+
storageAccountName=$(extract_value "storageAccountName" "storagE_ACCOUNT_NAME")
380+
fileSystem=$(extract_value "storageContainerName" "storagE_CONTAINER_NAME")
381+
sqlServerName=$(extract_value "sqlDBServer" "sqldB_SERVER")
382+
SqlDatabaseName=$(extract_value "sqlDBDatabase" "sqldB_DATABASE")
383+
backendUserMidClientId=$(extract_value "backendUserMid" "backenD_USER_MID")
384+
backendUserMidDisplayName=$(extract_value "backendUserMidName" "backenD_USER_MID_NAME")
385+
aiSearchName=$(extract_value "azureAISearchName" "azurE_AI_SEARCH_NAME")
386+
searchEndpoint=$(extract_value "azureAISearchEndpoint" "azurE_AI_SEARCH_ENDPOINT")
387+
aif_resource_id=$(extract_value "aiFoundryResourceId" "aI_FOUNDRY_RESOURCE_ID")
388+
cu_foundry_resource_id=$(extract_value "cuFoundryResourceId" "cU_FOUNDRY_RESOURCE_ID")
389+
openaiEndpoint=$(extract_value "azureOpenAIEndpoint" "azurE_OPENAI_ENDPOINT")
390+
embeddingModel=$(extract_value "azureOpenAIEmbeddingModel" "azurE_OPENAI_EMBEDDING_MODEL")
391+
cuEndpoint=$(extract_value "azureOpenAICuEndpoint" "azurE_OPENAI_CU_ENDPOINT")
392+
aiAgentEndpoint=$(extract_value "azureAiAgentEndpoint" "azurE_AI_AGENT_ENDPOINT")
393+
cuApiVersion=$(extract_value "azureContentUnderstandingApiVersion" "azurE_CONTENT_UNDERSTANDING_API_VERSION")
394+
deploymentModel=$(extract_value "azureOpenAIDeploymentModel" "azurE_OPENAI_DEPLOYMENT_MODEL")
395+
usecase=$(extract_value "useCase" "usE_CASE")
396+
397+
# Strip FQDN suffix from SQL server name if present (Azure CLI needs just the server name)
398+
sqlServerName="${sqlServerName%.database.windows.net}"
399+
400+
# Define required values with their display names for error reporting
401+
declare -A required_values=(
402+
["storageAccountName"]="STORAGE_ACCOUNT_NAME"
403+
["fileSystem"]="STORAGE_CONTAINER_NAME"
404+
["sqlServerName"]="SQLDB_SERVER"
405+
["SqlDatabaseName"]="SQLDB_DATABASE"
406+
["backendUserMidClientId"]="BACKEND_USER_MID"
407+
["backendUserMidDisplayName"]="BACKEND_USER_MID_NAME"
408+
["aiSearchName"]="AZURE_AI_SEARCH_NAME"
409+
["aif_resource_id"]="AI_FOUNDRY_RESOURCE_ID"
410+
["cu_foundry_resource_id"]="CU_FOUNDRY_RESOURCE_ID"
411+
["searchEndpoint"]="AZURE_AI_SEARCH_ENDPOINT"
412+
["openaiEndpoint"]="AZURE_OPENAI_ENDPOINT"
413+
["embeddingModel"]="AZURE_OPENAI_EMBEDDING_MODEL"
414+
["cuEndpoint"]="AZURE_OPENAI_CU_ENDPOINT"
415+
["aiAgentEndpoint"]="AZURE_AI_AGENT_ENDPOINT"
416+
["cuApiVersion"]="AZURE_CONTENT_UNDERSTANDING_API_VERSION"
417+
["deploymentModel"]="AZURE_OPENAI_DEPLOYMENT_MODEL"
418+
["usecase"]="USE_CASE"
419+
)
420+
421+
# Validate and collect missing values
422+
missing_values=()
423+
for var_name in "${!required_values[@]}"; do
424+
if [ -z "${!var_name}" ]; then
425+
missing_values+=("${required_values[$var_name]}")
426+
fi
427+
done
428+
429+
if [ ${#missing_values[@]} -gt 0 ]; then
430+
echo "Error: The following required values could not be retrieved from Azure deployment outputs:"
431+
printf ' - %s\n' "${missing_values[@]}" | sort
432+
return 1
433+
fi
434+
return 0
435+
}
436+
348437
# Check if user is logged in to Azure
349438
echo "Checking Azure authentication..."
350439
if az account show &> /dev/null; then
@@ -404,10 +493,39 @@ fi
404493
echo ""
405494

406495
echo ""
407-
if ! get_values_from_azd_env; then
408-
echo "Failed to get values from azd environment."
409-
echo ""
410-
exit 1
496+
497+
if [ -z "$resourceGroupName" ]; then
498+
# No resource group provided - use azd env
499+
if ! get_values_from_azd_env; then
500+
echo "Failed to get values from azd environment."
501+
echo ""
502+
echo "If you want to use deployment outputs instead, please provide the resource group name as an argument."
503+
echo "Usage: $0 [ResourceGroupName]"
504+
echo "Example: $0 my-resource-group"
505+
echo ""
506+
exit 1
507+
fi
508+
else
509+
# Resource group provided - use deployment outputs
510+
echo ""
511+
echo "Resource group provided: $resourceGroupName"
512+
513+
# Call deployment function
514+
if ! get_values_from_az_deployment; then
515+
echo "Failed to get values from deployment outputs."
516+
echo ""
517+
echo "Would you like to enter the values manually? (y/n): "
518+
read -r manual_input_choice
519+
if [[ "$manual_input_choice" == "y" || "$manual_input_choice" == "Y" ]]; then
520+
if ! get_values_from_user; then
521+
echo "Error: Manual input failed."
522+
exit 1
523+
fi
524+
else
525+
echo "Exiting script."
526+
exit 1
527+
fi
528+
fi
411529
fi
412530

413531
echo ""
@@ -441,7 +559,7 @@ if [ $? -ne 0 ]; then
441559
exit 1
442560
fi
443561

444-
pythonScriptPath="infra/scripts/index_scripts/"
562+
pythonScriptPath="$SCRIPT_DIR/index_scripts/"
445563

446564
# Install the requirements
447565
pip install --quiet -r ${pythonScriptPath}requirements.txt
@@ -452,13 +570,13 @@ fi
452570

453571
# Create Content Understanding analyzers
454572
echo "✓ Creating Content Understanding analyzer templates"
455-
python infra/scripts/index_scripts/02_create_cu_template_text.py --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion"
573+
python "${pythonScriptPath}02_create_cu_template_text.py" --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion"
456574
if [ $? -ne 0 ]; then
457575
echo "Error: 02_create_cu_template_text.py failed."
458576
exit 1
459577
fi
460578

461-
python infra/scripts/index_scripts/02_create_cu_template_audio.py --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion"
579+
python "${pythonScriptPath}02_create_cu_template_audio.py" --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion"
462580
if [ $? -ne 0 ]; then
463581
echo "Error: 02_create_cu_template_audio.py failed."
464582
exit 1
@@ -467,7 +585,7 @@ fi
467585
# Run 04_cu_process_custom_data.py
468586
echo "✓ Processing custom data"
469587
sql_server_fqdn="$sqlServerName.database.windows.net"
470-
python infra/scripts/index_scripts/04_cu_process_custom_data.py \
588+
python "${pythonScriptPath}04_cu_process_custom_data.py" \
471589
--search_endpoint "$searchEndpoint" \
472590
--openai_endpoint "$openaiEndpoint" \
473591
--ai_project_endpoint "$aiAgentEndpoint" \

0 commit comments

Comments
 (0)