diff --git a/azure.yaml b/azure.yaml index b5b9121e..d695029d 100644 --- a/azure.yaml +++ b/azure.yaml @@ -8,13 +8,6 @@ metadata: name: content-processinge@1.0 hooks: - preprovision: - posix: - shell: sh - run: timestamp=$(date +"%Y%m%d-%H%M%S"); logFile="azd_preprovision_$timestamp.log"; sed -i 's/\r$//' ./infra/scripts/docker-build.sh; ./infra/scripts/docker-build.sh "$AZURE_SUBSCRIPTION_ID" "$AZURE_ENV_NAME" "$AZURE_LOCATION" "$AZURE_RESOURCE_GROUP" "$USE_LOCAL_BUILD" "$AZURE_ENV_IMAGETAG" 2>&1 | tee "$logFile" - windows: - shell: pwsh - run: $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"; $logFile = "azd_preprovision_$timestamp.log"; ./infra/scripts/docker-build.ps1 $env:AZURE_SUBSCRIPTION_ID $env:AZURE_ENV_NAME $env:AZURE_LOCATION $env:AZURE_RESOURCE_GROUP $env:USE_LOCAL_BUILD $env:AZURE_ENV_IMAGETAG *>&1 | Tee-Object -FilePath $logFile postprovision: posix: shell: sh diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index d16f28ab..2ed4c383 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -16,7 +16,6 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_MODEL_NAME` | string | `gpt-4o` | Specifies the GPT model name (allowed values: `gpt-4o`). | `AZURE_ENV_MODEL_VERSION` | string | `2024-08-06` | Specifies the GPT model version (allowed values: `2024-08-06`). | | `AZURE_ENV_MODEL_CAPACITY` | integer | `30` | Sets the model capacity (choose based on your subscription's available GPT capacity). | -| `USE_LOCAL_BUILD` | boolean | `false` | Indicates whether to use a local container build for deployment. | | `AZURE_ENV_IMAGETAG` | boolean | `latest` | Set the Image tag Like (allowed values: latest, dev, hotfix) | | `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `` | Reuses an existing Log Analytics Workspace instead of provisioning a new one. | diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 0735575f..f91f28c1 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -173,40 +173,37 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain - This deployment will take *4-6 minutes* to provision the resources in your account and set up the solution with sample data. - If you encounter an error or timeout during deployment, changing the location may help, as there could be availability constraints for the resources. -5. Once the deployment has completed successfully, open the [Azure Portal](https://portal.azure.com/), go to the deployed resource group, find the App Service, and get the app URL from `Default domain`. +5. Once the deployment has completed successfully: + > Please check the terminal or console output for details of the successful deployment. It will display the Name, Endpoint (Application URL), and Azure Portal URL for both the Web and API Azure Container Apps. -6. If you are done trying out the application, you can delete the resources by running `azd down`. + ![](./images/cp-post-deployment.png) -### Publishing Local Build Container to Azure Container Registry + - You can find the Azure portal link in the screenshot above. Click on it to navigate to the corresponding resource group in the Azure portal. -If you need to rebuild the source code and push the updated container to the deployed Azure Container Registry, follow these steps: + > #### Important Note : Before accessing the application, ensure that all **[Post Deployment Steps](#post-deployment-steps)** are fully completed, as they are critical for the proper configuration of **Data Ingestion** and **Authentication** functionalities. -1. Set the environment variable `USE_LOCAL_BUILD` to `True`: +7. If you are done trying out the application, you can delete the resources by running `azd down`. + +## Post Deployment Steps +1. Optional: Publishing Local Build Container to Azure Container Registry + + If you need to rebuild the source code and push the updated container to the deployed Azure Container Registry, follow these steps: - **Linux/macOS**: ```bash - export USE_LOCAL_BUILD=True + cd ./infra/scripts/ + ./docker-build.sh ``` - **Windows (PowerShell)**: ```powershell - $env:USE_LOCAL_BUILD = $true + cd .\infra\scripts\ + .\docker-build.ps1 ``` -2. Run the `az login` command - ```bash - az login - ``` -3. Run the `azd up` command again to rebuild and push the updated container: - ```bash - azd up - ``` - -This will rebuild the source code, package it into a container, and push it to the Azure Container Registry associated with your deployment. - -## Post Deployment Steps + This will create a new Azure Container Registry, rebuild the source code, package it into a container, and push it to the Container Registry created. -1. **Register Schema Files** +2. **Register Schema Files** > Want to customize the schemas for your own documents? [Learn more about adding your own schemas here.](./CustomizeSchemaData.md) @@ -236,7 +233,7 @@ This will rebuild the source code, package it into a container, and push it to t - **Verify Results** ![schema file registration](./images/SchemaFileRegistration.png) -2. **Import Sample Data** +3. **Import Sample Data** - Grab the Schema IDs for Invoice and Property Damage Claim Form's Schema from first step - Move to the folder location to samples in ContentProcessorApi - [/src/ContentProcessorApi/samples/](/src/ContentProcessorApi/samples/) - Execute the script with Schema IDs diff --git a/docs/images/cp-post-deployment.png b/docs/images/cp-post-deployment.png new file mode 100644 index 00000000..3ee49835 Binary files /dev/null and b/docs/images/cp-post-deployment.png differ diff --git a/infra/container_app/deploy_container_app.bicep b/infra/container_app/deploy_container_app.bicep index a20a6b0c..5275a843 100644 --- a/infra/container_app/deploy_container_app.bicep +++ b/infra/container_app/deploy_container_app.bicep @@ -16,7 +16,6 @@ param probes array = [] param allowedOrigins array = [] param minReplicas int = 1 param maxReplicas int = 1 -param useLocalBuild string = 'false' //Todo: Add Appconfig endpoint as Env variable @@ -34,12 +33,7 @@ resource processorContainerApp 'Microsoft.App/containerApps@2024-03-01' = { environmentId: containerEnvId workloadProfileName: 'Consumption' configuration:{ - registries: useLocalBuild == 'true' ? [ - { - server: azureContainerRegistry - identity: managedIdentityId - } - ] : null + registries: null ingress: enableIngress ? { external: true transport: 'auto' diff --git a/infra/container_app/deploy_container_app_api_web.bicep b/infra/container_app/deploy_container_app_api_web.bicep index 41272dd2..9245898f 100644 --- a/infra/container_app/deploy_container_app_api_web.bicep +++ b/infra/container_app/deploy_container_app_api_web.bicep @@ -25,7 +25,6 @@ param maxReplicaContainerWeb int = 1 // Container related params param azureContainerRegistry string param containerRegistryReaderId string -param useLocalBuild string = 'false' param imageTag string var abbrs = loadJsonContent('../abbreviations.json') @@ -88,7 +87,6 @@ module containerApp 'deploy_container_app.bicep' = { enableIngress: false minReplicas: minReplicaContainerApp maxReplicas: maxReplicaContainerApp - useLocalBuild: useLocalBuild } } @@ -112,7 +110,6 @@ module containerAppApi 'deploy_container_app.bicep' = { probes: probes minReplicas: minReplicaContainerApi maxReplicas: maxReplicaContainerApi - useLocalBuild: useLocalBuild } } @@ -154,7 +151,6 @@ module containerAppWeb 'deploy_container_app.bicep' = { ] minReplicas: minReplicaContainerWeb maxReplicas: maxReplicaContainerWeb - useLocalBuild: useLocalBuild } } diff --git a/infra/deploy_container_registry.bicep b/infra/deploy_container_registry.bicep index 021fb7b4..571e10af 100644 --- a/infra/deploy_container_registry.bicep +++ b/infra/deploy_container_registry.bicep @@ -14,6 +14,9 @@ param location string = resourceGroup().location @description('Provide a tier of your Azure Container Registry.') param acrSku string = 'Basic' + +@description('List of Principal Ids to which ACR pull role assignment is required') +param acrPullPrincipalIds array = [] resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = { name: containerNameCleaned @@ -26,6 +29,19 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = zoneRedundancy: 'Disabled' } } + +// Add Role assignments for required principal id's +resource acrPullRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for principalId in acrPullPrincipalIds: { + name: guid(principalId, 'acrpull') + scope: containerRegistry + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7f951dda-4ed3-4680-a7ca-43fe172d538d' + ) + principalId: principalId + } +}] output createdAcrName string = containerNameCleaned output createdAcrId string = containerRegistry.id diff --git a/infra/deploy_role_assignments.bicep b/infra/deploy_role_assignments.bicep index a8c8e8f7..4c915418 100644 --- a/infra/deploy_role_assignments.bicep +++ b/infra/deploy_role_assignments.bicep @@ -9,8 +9,6 @@ param storagePrincipalId string // Resource ID of the Storage account param aiServiceCUId string // Resource ID of the Azure AI Content Understanding Service param aiServiceId string // Resource ID of the Azure Open AI service -param containerRegistryReaderPrincipalId string - resource appConfigDataReader 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { scope: resourceGroup() name: '516239f1-63e1-4d78-a4de-a74fb236a071' @@ -130,15 +128,3 @@ resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssign principalType: 'ServicePrincipal' } } - -resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerRegistryReaderPrincipalId, 'acrpull') - scope: resourceGroup() - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '7f951dda-4ed3-4680-a7ca-43fe172d538d' - ) // AcrPull role - principalId: containerRegistryReaderPrincipalId - } -} diff --git a/infra/main.bicep b/infra/main.bicep index b2cb538b..83501cf8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -20,7 +20,16 @@ param secondaryLocation string = 'EastUs2' type: 'location' } }) -param contentUnderstandingLocation string +param contentUnderstandingLocation string = 'WestUS' + +@metadata({azd: { + type: 'location' + usageName: [ + 'OpenAI.GlobalStandard.gpt-4o,100' + ] + } +}) +param aiDeploymentsLocation string @minLength(1) @description('GPT model deployment type:') @@ -36,8 +45,6 @@ param gptModelName string = 'gpt-4o' @description('Version of the GPT model to deploy:') param gptModelVersion string = '2024-08-06' -//var gptModelVersion = '2024-02-15-preview' - @minValue(10) @description('Capacity of the GPT deployment:') // You can increase this, but capacity is limited per model/region, so you will get errors if you go over @@ -62,9 +69,6 @@ param minReplicaContainerWeb int = 1 @description('Maximum number of replicas to be added for Container Web App') param maxReplicaContainerWeb int = 1 -@description('Set this flag to true only if you are deplpoying from Local') -param useLocalBuild string = 'false' - @description('Optional: Existing Log Analytics Workspace Resource ID') param existingLogAnalyticsWorkspaceId string = '' @@ -76,9 +80,6 @@ var resourceGroupLocation = resourceGroup().location // Load the abbrevations file required to name the azure resources. var abbrs = loadJsonContent('./abbreviations.json') -// Convert input to lowercase -var useLocalBuildLower = toLower(useLocalBuild) - // ========== Managed Identity ========== // module managedIdentityModule 'deploy_managed_identity.bicep' = { name: 'deploy_managed_identity' @@ -111,13 +112,13 @@ module applicationInsights 'deploy_app_insights.bicep' = { } } -// ========== Container Registry ========== // -module containerRegistry 'deploy_container_registry.bicep' = { - name: 'deploy_container_registry' - params: { - environmentName: environmentName - } -} +// // ========== Container Registry ========== // +// module containerRegistry 'deploy_container_registry.bicep' = { +// name: 'deploy_container_registry' +// params: { +// environmentName: environmentName +// } +// } // ========== Storage Account ========== // module storage 'deploy_storage_account.bicep' = { @@ -134,7 +135,7 @@ module aifoundry 'deploy_ai_foundry.bicep' = { name: 'deploy_ai_foundry' params: { solutionName: solutionPrefix - solutionLocation: resourceGroupLocation + solutionLocation: aiDeploymentsLocation cuLocation: contentUnderstandingLocation deploymentType: deploymentType gptModelName: gptModelName @@ -173,7 +174,6 @@ module containerApps './container_app/deploy_container_app_api_web.bicep' = { maxReplicaContainerApi: maxReplicaContainerApi minReplicaContainerWeb: minReplicaContainerWeb maxReplicaContainerWeb: maxReplicaContainerWeb - useLocalBuild: 'false' imageTag: 'latest' } } @@ -221,7 +221,6 @@ module roleAssignments 'deploy_role_assignments.bicep' = { containerAppPrincipalId: containerApps.outputs.containerAppPrincipalId aiServiceCUId: aifoundry.outputs.aiServicesCuId aiServiceId: aifoundry.outputs.aiServicesId - containerRegistryReaderPrincipalId: containerAppEnv.outputs.containerRegistryReaderPrincipalId } } @@ -230,7 +229,7 @@ module updateContainerApp './container_app/deploy_container_app_api_web.bicep' = params: { solutionName: solutionPrefix location: secondaryLocation - azureContainerRegistry: useLocalBuildLower == 'true' ? containerRegistry.outputs.acrEndpoint : containerImageEndPoint + azureContainerRegistry: containerImageEndPoint appConfigEndPoint: appconfig.outputs.appConfigEndpoint containerAppEnvId: containerAppEnv.outputs.containerEnvId containerRegistryReaderId: containerAppEnv.outputs.containerRegistryReaderId @@ -242,7 +241,6 @@ module updateContainerApp './container_app/deploy_container_app_api_web.bicep' = maxReplicaContainerApi: maxReplicaContainerApi minReplicaContainerWeb: minReplicaContainerWeb maxReplicaContainerWeb: maxReplicaContainerWeb - useLocalBuild: useLocalBuildLower imageTag: imageTag } dependsOn: [roleAssignments] @@ -251,4 +249,8 @@ module updateContainerApp './container_app/deploy_container_app_api_web.bicep' = output CONTAINER_WEB_APP_NAME string = containerApps.outputs.containerAppWebName output CONTAINER_API_APP_NAME string = containerApps.outputs.containerAppApiName output CONTAINER_WEB_APP_FQDN string = containerApps.outputs.containweAppWebEndPoint +output CONTAINER_APP_NAME string = containerApps.outputs.containerAppName output CONTAINER_API_APP_FQDN string = containerApps.outputs.containweAppApiEndPoint +output CONTAINER_APP_USER_IDENTITY_ID string = containerAppEnv.outputs.containerRegistryReaderId +output CONTAINER_APP_USER_PRINCIPAL_ID string = containerAppEnv.outputs.containerRegistryReaderPrincipalId +output AZURE_ENV_IMAGETAG string = imageTag diff --git a/infra/main.bicepparam b/infra/main.bicepparam deleted file mode 100644 index b6a15d71..00000000 --- a/infra/main.bicepparam +++ /dev/null @@ -1,12 +0,0 @@ -using './main.bicep' - -param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'cps') -param secondaryLocation = readEnvironmentVariable('AZURE_ENV_SECONDARY_LOCATION', 'EastUs2') -param contentUnderstandingLocation = readEnvironmentVariable('AZURE_ENV_CU_LOCATION', 'WestUS') -param deploymentType = readEnvironmentVariable('AZURE_ENV_MODEL_DEPLOYMENT_TYPE', 'GlobalStandard') -param gptModelName = readEnvironmentVariable('AZURE_ENV_MODEL_NAME', 'gpt-4o') -param gptModelVersion = readEnvironmentVariable('AZURE_ENV_MODEL_VERSION', '2024-08-06') -param gptDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_MODEL_CAPACITY', '30')) -param useLocalBuild = readEnvironmentVariable('USE_LOCAL_BUILD', 'false') -param imageTag = readEnvironmentVariable('AZURE_ENV_IMAGETAG', 'latest') -param existingLogAnalyticsWorkspaceId = readEnvironmentVariable('AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID', '') diff --git a/infra/main.json b/infra/main.json index 4221a0bb..cda89093 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "7133413659987113184" + "templateHash": "4455095207947106270" } }, "parameters": { @@ -26,6 +26,7 @@ }, "contentUnderstandingLocation": { "type": "string", + "defaultValue": "WestUS", "allowedValues": [ "WestUS", "SwedenCentral", @@ -39,6 +40,17 @@ }, "minLength": 1 }, + "aiDeploymentsLocation": { + "type": "string", + "metadata": { + "azd": { + "type": "location", + "usageName": [ + "OpenAI.GlobalStandard.gpt-4o,100" + ] + } + } + }, "deploymentType": { "type": "string", "defaultValue": "GlobalStandard", @@ -115,13 +127,6 @@ "description": "Maximum number of replicas to be added for Container Web App" } }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false", - "metadata": { - "description": "Set this flag to true only if you are deplpoying from Local" - } - }, "existingLogAnalyticsWorkspaceId": { "type": "string", "defaultValue": "", @@ -368,8 +373,7 @@ "solutionPrefix": "[format('cps-{0}', padLeft(take(variables('uniqueId'), 12), 12, '0'))]", "containerImageEndPoint": "cpscontainerreg.azurecr.io", "resourceGroupLocation": "[resourceGroup().location]", - "abbrs": "[variables('$fxv#0')]", - "useLocalBuildLower": "[toLower(parameters('useLocalBuild'))]" + "abbrs": "[variables('$fxv#0')]" }, "resources": [ { @@ -678,86 +682,6 @@ } } }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_container_registry", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "environmentName": { - "value": "[parameters('environmentName')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.36.1.42791", - "templateHash": "6955709595359352278" - } - }, - "parameters": { - "environmentName": { - "type": "string" - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Provide a location for the registry." - } - }, - "acrSku": { - "type": "string", - "defaultValue": "Basic", - "metadata": { - "description": "Provide a tier of your Azure Container Registry." - } - } - }, - "variables": { - "uniqueId": "[toLower(uniqueString(subscription().id, parameters('environmentName'), resourceGroup().location))]", - "solutionName": "[format('cps-{0}', padLeft(take(variables('uniqueId'), 12), 12, '0'))]", - "containerNameCleaned": "[replace(format('cr{0}', variables('solutionName')), '-', '')]" - }, - "resources": [ - { - "type": "Microsoft.ContainerRegistry/registries", - "apiVersion": "2021-09-01", - "name": "[variables('containerNameCleaned')]", - "location": "[parameters('location')]", - "sku": { - "name": "[parameters('acrSku')]" - }, - "properties": { - "publicNetworkAccess": "Enabled", - "zoneRedundancy": "Disabled" - } - } - ], - "outputs": { - "createdAcrName": { - "type": "string", - "value": "[variables('containerNameCleaned')]" - }, - "createdAcrId": { - "type": "string", - "value": "[resourceId('Microsoft.ContainerRegistry/registries', variables('containerNameCleaned'))]" - }, - "acrEndpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', variables('containerNameCleaned')), '2021-09-01').loginServer]" - } - } - } - } - }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", @@ -906,7 +830,7 @@ "value": "[variables('solutionPrefix')]" }, "solutionLocation": { - "value": "[variables('resourceGroupLocation')]" + "value": "[parameters('aiDeploymentsLocation')]" }, "cuLocation": { "value": "[parameters('contentUnderstandingLocation')]" @@ -1503,9 +1427,6 @@ "maxReplicaContainerWeb": { "value": "[parameters('maxReplicaContainerWeb')]" }, - "useLocalBuild": { - "value": "false" - }, "imageTag": { "value": "latest" } @@ -1517,7 +1438,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "9208787367229886695" + "templateHash": "10338800926086953929" } }, "parameters": { @@ -1590,10 +1511,6 @@ "containerRegistryReaderId": { "type": "string" }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" - }, "imageTag": { "type": "string" } @@ -1913,9 +1830,6 @@ }, "maxReplicas": { "value": "[parameters('maxReplicaContainerApp')]" - }, - "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" } }, "template": { @@ -1925,7 +1839,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "13562163699289922411" + "templateHash": "8984863566776850276" } }, "parameters": { @@ -1973,10 +1887,6 @@ "maxReplicas": { "type": "int", "defaultValue": 1 - }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" } }, "resources": [ @@ -1996,7 +1906,7 @@ "environmentId": "[parameters('containerEnvId')]", "workloadProfileName": "Consumption", "configuration": { - "registries": "[if(equals(parameters('useLocalBuild'), 'true'), createArray(createObject('server', parameters('azureContainerRegistry'), 'identity', parameters('managedIdentityId'))), null())]", + "registries": null, "ingress": "[if(parameters('enableIngress'), createObject('external', true(), 'transport', 'auto', 'allowInsecure', true(), 'corsPolicy', if(greater(length(parameters('allowedOrigins')), 0), createObject('allowedOrigins', parameters('allowedOrigins'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'), 'allowedHeaders', createArray('Authorization', 'Content-Type', '*')), null())), null())]" }, "template": { @@ -2093,9 +2003,6 @@ }, "maxReplicas": { "value": "[parameters('maxReplicaContainerApi')]" - }, - "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" } }, "template": { @@ -2105,7 +2012,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "13562163699289922411" + "templateHash": "8984863566776850276" } }, "parameters": { @@ -2153,10 +2060,6 @@ "maxReplicas": { "type": "int", "defaultValue": 1 - }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" } }, "resources": [ @@ -2176,7 +2079,7 @@ "environmentId": "[parameters('containerEnvId')]", "workloadProfileName": "Consumption", "configuration": { - "registries": "[if(equals(parameters('useLocalBuild'), 'true'), createArray(createObject('server', parameters('azureContainerRegistry'), 'identity', parameters('managedIdentityId'))), null())]", + "registries": null, "ingress": "[if(parameters('enableIngress'), createObject('external', true(), 'transport', 'auto', 'allowInsecure', true(), 'corsPolicy', if(greater(length(parameters('allowedOrigins')), 0), createObject('allowedOrigins', parameters('allowedOrigins'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'), 'allowedHeaders', createArray('Authorization', 'Content-Type', '*')), null())), null())]" }, "template": { @@ -2285,9 +2188,6 @@ }, "maxReplicas": { "value": "[parameters('maxReplicaContainerWeb')]" - }, - "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" } }, "template": { @@ -2297,7 +2197,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "13562163699289922411" + "templateHash": "8984863566776850276" } }, "parameters": { @@ -2345,10 +2245,6 @@ "maxReplicas": { "type": "int", "defaultValue": 1 - }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" } }, "resources": [ @@ -2368,7 +2264,7 @@ "environmentId": "[parameters('containerEnvId')]", "workloadProfileName": "Consumption", "configuration": { - "registries": "[if(equals(parameters('useLocalBuild'), 'true'), createArray(createObject('server', parameters('azureContainerRegistry'), 'identity', parameters('managedIdentityId'))), null())]", + "registries": null, "ingress": "[if(parameters('enableIngress'), createObject('external', true(), 'transport', 'auto', 'allowInsecure', true(), 'corsPolicy', if(greater(length(parameters('allowedOrigins')), 0), createObject('allowedOrigins', parameters('allowedOrigins'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'), 'allowedHeaders', createArray('Authorization', 'Content-Type', '*')), null())), null())]" }, "template": { @@ -2822,9 +2718,6 @@ }, "aiServiceId": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiServicesId.value]" - }, - "containerRegistryReaderPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_app_env'), '2022-09-01').outputs.containerRegistryReaderPrincipalId.value]" } }, "template": { @@ -2834,7 +2727,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "4418232655579357592" + "templateHash": "1992580664978730799" } }, "parameters": { @@ -2861,9 +2754,6 @@ }, "aiServiceId": { "type": "string" - }, - "containerRegistryReaderPrincipalId": { - "type": "string" } }, "resources": [ @@ -2963,15 +2853,6 @@ "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", "principalType": "ServicePrincipal" } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "name": "[guid(parameters('containerRegistryReaderPrincipalId'), 'acrpull')]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", - "principalId": "[parameters('containerRegistryReaderPrincipalId')]" - } } ] } @@ -2979,7 +2860,6 @@ "dependsOn": [ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_app_config_service')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_container_app_env')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_container_app_api_web')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_storage_account')]" ] @@ -3000,7 +2880,9 @@ "location": { "value": "[parameters('secondaryLocation')]" }, - "azureContainerRegistry": "[if(equals(variables('useLocalBuildLower'), 'true'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_registry'), '2022-09-01').outputs.acrEndpoint.value), createObject('value', variables('containerImageEndPoint')))]", + "azureContainerRegistry": { + "value": "[variables('containerImageEndPoint')]" + }, "appConfigEndPoint": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_app_config_service'), '2022-09-01').outputs.appConfigEndpoint.value]" }, @@ -3034,9 +2916,6 @@ "maxReplicaContainerWeb": { "value": "[parameters('maxReplicaContainerWeb')]" }, - "useLocalBuild": { - "value": "[variables('useLocalBuildLower')]" - }, "imageTag": { "value": "[parameters('imageTag')]" } @@ -3048,7 +2927,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "9208787367229886695" + "templateHash": "10338800926086953929" } }, "parameters": { @@ -3121,10 +3000,6 @@ "containerRegistryReaderId": { "type": "string" }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" - }, "imageTag": { "type": "string" } @@ -3444,9 +3319,6 @@ }, "maxReplicas": { "value": "[parameters('maxReplicaContainerApp')]" - }, - "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" } }, "template": { @@ -3456,7 +3328,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "13562163699289922411" + "templateHash": "8984863566776850276" } }, "parameters": { @@ -3504,10 +3376,6 @@ "maxReplicas": { "type": "int", "defaultValue": 1 - }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" } }, "resources": [ @@ -3527,7 +3395,7 @@ "environmentId": "[parameters('containerEnvId')]", "workloadProfileName": "Consumption", "configuration": { - "registries": "[if(equals(parameters('useLocalBuild'), 'true'), createArray(createObject('server', parameters('azureContainerRegistry'), 'identity', parameters('managedIdentityId'))), null())]", + "registries": null, "ingress": "[if(parameters('enableIngress'), createObject('external', true(), 'transport', 'auto', 'allowInsecure', true(), 'corsPolicy', if(greater(length(parameters('allowedOrigins')), 0), createObject('allowedOrigins', parameters('allowedOrigins'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'), 'allowedHeaders', createArray('Authorization', 'Content-Type', '*')), null())), null())]" }, "template": { @@ -3624,9 +3492,6 @@ }, "maxReplicas": { "value": "[parameters('maxReplicaContainerApi')]" - }, - "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" } }, "template": { @@ -3636,7 +3501,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "13562163699289922411" + "templateHash": "8984863566776850276" } }, "parameters": { @@ -3684,10 +3549,6 @@ "maxReplicas": { "type": "int", "defaultValue": 1 - }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" } }, "resources": [ @@ -3707,7 +3568,7 @@ "environmentId": "[parameters('containerEnvId')]", "workloadProfileName": "Consumption", "configuration": { - "registries": "[if(equals(parameters('useLocalBuild'), 'true'), createArray(createObject('server', parameters('azureContainerRegistry'), 'identity', parameters('managedIdentityId'))), null())]", + "registries": null, "ingress": "[if(parameters('enableIngress'), createObject('external', true(), 'transport', 'auto', 'allowInsecure', true(), 'corsPolicy', if(greater(length(parameters('allowedOrigins')), 0), createObject('allowedOrigins', parameters('allowedOrigins'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'), 'allowedHeaders', createArray('Authorization', 'Content-Type', '*')), null())), null())]" }, "template": { @@ -3816,9 +3677,6 @@ }, "maxReplicas": { "value": "[parameters('maxReplicaContainerWeb')]" - }, - "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" } }, "template": { @@ -3828,7 +3686,7 @@ "_generator": { "name": "bicep", "version": "0.36.1.42791", - "templateHash": "13562163699289922411" + "templateHash": "8984863566776850276" } }, "parameters": { @@ -3876,10 +3734,6 @@ "maxReplicas": { "type": "int", "defaultValue": 1 - }, - "useLocalBuild": { - "type": "string", - "defaultValue": "false" } }, "resources": [ @@ -3899,7 +3753,7 @@ "environmentId": "[parameters('containerEnvId')]", "workloadProfileName": "Consumption", "configuration": { - "registries": "[if(equals(parameters('useLocalBuild'), 'true'), createArray(createObject('server', parameters('azureContainerRegistry'), 'identity', parameters('managedIdentityId'))), null())]", + "registries": null, "ingress": "[if(parameters('enableIngress'), createObject('external', true(), 'transport', 'auto', 'allowInsecure', true(), 'corsPolicy', if(greater(length(parameters('allowedOrigins')), 0), createObject('allowedOrigins', parameters('allowedOrigins'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'), 'allowedHeaders', createArray('Authorization', 'Content-Type', '*')), null())), null())]" }, "template": { @@ -3985,7 +3839,6 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_app_config_service')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_container_app_env')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_container_app_api_web')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_container_registry')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_role_assignments')]" ] } @@ -4003,9 +3856,25 @@ "type": "string", "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_app_api_web'), '2022-09-01').outputs.containweAppWebEndPoint.value]" }, + "CONTAINER_APP_NAME": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_app_api_web'), '2022-09-01').outputs.containerAppName.value]" + }, "CONTAINER_API_APP_FQDN": { "type": "string", "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_app_api_web'), '2022-09-01').outputs.containweAppApiEndPoint.value]" + }, + "CONTAINER_APP_USER_IDENTITY_ID": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_app_env'), '2022-09-01').outputs.containerRegistryReaderId.value]" + }, + "CONTAINER_APP_USER_PRINCIPAL_ID": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_app_env'), '2022-09-01').outputs.containerRegistryReaderPrincipalId.value]" + }, + "AZURE_ENV_IMAGETAG": { + "type": "string", + "value": "[parameters('imageTag')]" } } } \ No newline at end of file diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 00000000..c38f5aa2 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "secondaryLocation": { + "value": "${AZURE_ENV_SECONDARY_LOCATION}" + }, + "contentUnderstandingLocation": { + "value": "${AZURE_ENV_CU_LOCATION}" + }, + "deploymentType": { + "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" + }, + "gptModelName": { + "value": "${AZURE_ENV_MODEL_NAME}" + }, + "gptModelVersion": { + "value": "${AZURE_ENV_MODEL_VERSION}" + }, + "gptDeploymentCapacity": { + "value": "${AZURE_ENV_MODEL_CAPACITY}" + }, + "imageTag": { + "value": "latest" + }, + "existingLogAnalyticsWorkspaceId": { + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + } + } +} diff --git a/infra/scripts/docker-build.ps1 b/infra/scripts/docker-build.ps1 index 8a0816d3..4af1ab6c 100644 --- a/infra/scripts/docker-build.ps1 +++ b/infra/scripts/docker-build.ps1 @@ -1,65 +1,22 @@ -# Define script parameters -param ( - [string]$AZURE_SUBSCRIPTION_ID, - [string]$ENV_NAME, - [string]$AZURE_LOCATION, - [string]$AZURE_RESOURCE_GROUP, - [string]$USE_LOCAL_BUILD, - [string]$AZURE_ENV_IMAGETAG -) - -# Convert USE_LOCAL_BUILD to Boolean -$USE_LOCAL_BUILD = if ($USE_LOCAL_BUILD -match "^(?i:true)$") { $true } else { $false } - -if ([string]::IsNullOrEmpty($AZURE_ENV_IMAGETAG)) { - $AZURE_ENV_IMAGETAG = "latest" -} - -# Validate required parameters -if (-not $AZURE_SUBSCRIPTION_ID -or -not $ENV_NAME -or -not $AZURE_LOCATION -or -not $AZURE_RESOURCE_GROUP) { - Write-Error "Missing required arguments. Usage: docker-build.ps1 " - exit 1 -} - -if ($USE_LOCAL_BUILD -eq $false) { - Write-Output "Local Build not enabled. Using prebuilt image." - exit 0 -} - -Write-Output "Local Build enabled. Starting build process." - -# Set Azure subscription -az account set --subscription "$AZURE_SUBSCRIPTION_ID" -if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to set Azure subscription." - exit 1 -} - -# Deploy container registry -Write-Host "Deploying container registry in location: $AZURE_LOCATION" -$OUTPUTS = az deployment group create --resource-group $AZURE_RESOURCE_GROUP --template-file "./infra/deploy_container_registry.bicep" --parameters environmentName=$ENV_NAME --query "properties.outputs" --output json | ConvertFrom-Json - -# Extract ACR name and endpoint -$ACR_NAME = $OUTPUTS.createdAcrName.value -$ACR_ENDPOINT = $OUTPUTS.acrEndpoint.value -Write-Host "Extracted ACR Name: $ACR_NAME" -Write-Host "Extracted ACR Endpoint: $ACR_ENDPOINT" +# Get all environment values +$envValues = azd env get-values --output json | ConvertFrom-Json -# Store outputs in a .env file -Set-Content -Path .env -Value "ACR_NAME=$ACR_NAME`nACR_ENDPOINT=$ACR_ENDPOINT" +# Full path to this script's folder +$ScriptDir = $PSScriptRoot -# Set ACR details as environment variables in AZD -azd env set ACR_NAME $ACR_NAME -azd env set ACR_ENDPOINT $ACR_ENDPOINT - -Write-Host "Saved ACR details to AZD environment variables." +# Resolve relative paths based on the script location +$TemplateFile = Join-Path $ScriptDir "..\deploy_container_registry.bicep" +$ContentProcessorPath = Join-Path $ScriptDir "..\..\src\ContentProcessor" +$ContentApiPath = Join-Path $ScriptDir "..\..\src\ContentProcessorAPI" +$ContentWebPath = Join-Path $ScriptDir "..\..\src\ContentProcessorWeb" # Define function to build and push Docker images function Build-And-Push-Image { param ( [string]$IMAGE_NAME, - [string]$BUILD_PATH + [string]$BUILD_PATH, + [string]$CONTAINER_APP_NAME ) $IMAGE_URI = "$ACR_NAME.azurecr.io/$($IMAGE_NAME):$AZURE_ENV_IMAGETAG" @@ -79,8 +36,104 @@ function Build-And-Push-Image { } Write-Host "Docker image pushed successfully: $IMAGE_URI" + + if($CONTAINER_APP_NAME) + { + Write-Host "Updating the Container app registry server & image" + az containerapp registry set --name $CONTAINER_APP_NAME --resource-group $AZURE_RESOURCE_GROUP --server "$ACR_NAME.azurecr.io" --identity $CONTAINER_APP_USER_IDENTITY_ID --only-show-errors + az containerapp update --name $CONTAINER_APP_NAME --resource-group $AZURE_RESOURCE_GROUP --image $IMAGE_URI --only-show-errors + Write-Host "Updated the registry for Container: $CONTAINER_APP_NAME" + } } +function Ensure-AzLogin { + try { + $accountInfo = az account show --only-show-errors | ConvertFrom-Json + Write-Host "Already logged in as: $($accountInfo.user.name)" + } catch { + Write-Host "No active Azure session found. Logging in..." + az login --only-show-errors | Out-Null + + if ($LASTEXITCODE -ne 0) { + Write-Error "Azure login failed." + exit 1 + } + + # Set Azure subscription + az account set --subscription "$AZURE_SUBSCRIPTION_ID" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to set Azure subscription." + exit 1 + } + } +} + +# Validate and fetch required parameters from azd env if missing +function Get-AzdEnvValueOrDefault { + param ( + [Parameter(Mandatory = $true)] + [string]$KeyName, + + [Parameter(Mandatory = $false)] + [string]$DefaultValue = "", + + [Parameter(Mandatory = $false)] + [bool]$Required = $false + ) + + # Check if key exists + if ($envValues.PSObject.Properties.Name -contains $KeyName) { + return $envValues.$KeyName + } + + # Key doesn't exist + if ($Required) { + Write-Error "Required environment key '$KeyName' not found in azd environment." + exit 1 + } else { + return $DefaultValue + } +} + +# Read the required details from Bicep deployment output +$AZURE_SUBSCRIPTION_ID = Get-AzdEnvValueOrDefault -KeyName "AZURE_SUBSCRIPTION_ID" -Required $true +$ENV_NAME = Get-AzdEnvValueOrDefault -KeyName "AZURE_ENV_NAME" -Required $true +$CONTAINER_APP_USER_IDENTITY_ID = Get-AzdEnvValueOrDefault -KeyName "CONTAINER_APP_USER_IDENTITY_ID" -Required $true +$AZURE_RESOURCE_GROUP = Get-AzdEnvValueOrDefault -KeyName "AZURE_RESOURCE_GROUP" -Required $true +$CONTAINER_APP_USER_PRINCIPAL_ID = Get-AzdEnvValueOrDefault -KeyName "CONTAINER_APP_USER_PRINCIPAL_ID" -Required $true +$AZURE_ENV_IMAGETAG = Get-AzdEnvValueOrDefault -KeyName "AZURE_ENV_IMAGETAG" -DefaultValue "latest" +$CONTAINER_WEB_APP_NAME=Get-AzdEnvValueOrDefault -KeyName "CONTAINER_WEB_APP_NAME" -Required $true +$CONTAINER_API_APP_NAME=Get-AzdEnvValueOrDefault -KeyName "CONTAINER_API_APP_NAME" -Required $true +$CONTAINER_APP_NAME=Get-AzdEnvValueOrDefault -KeyName "CONTAINER_APP_NAME" -Required $true + +# Export the variables for later use +Write-Host "Using the following parameters:" +Write-Host "AZURE_SUBSCRIPTION_ID = $AZURE_SUBSCRIPTION_ID" +Write-Host "ENV_NAME = $ENV_NAME" +Write-Host "AZURE_RESOURCE_GROUP = $AZURE_RESOURCE_GROUP" +Write-Host "AZURE_ENV_IMAGETAG = $AZURE_ENV_IMAGETAG" + +Ensure-AzLogin + +Write-Output "Starting build process." + +# Deploy container registry +Write-Host "Deploying container registry" +$OUTPUTS = az deployment group create --resource-group $AZURE_RESOURCE_GROUP --template-file "$TemplateFile" --parameters environmentName=$ENV_NAME acrPullPrincipalIds="['$($CONTAINER_APP_USER_PRINCIPAL_ID)']" --query "properties.outputs" --output json | ConvertFrom-Json + +# Extract ACR name and endpoint +$ACR_NAME = $OUTPUTS.createdAcrName.value +$ACR_ENDPOINT = $OUTPUTS.acrEndpoint.value + +Write-Host "Extracted ACR Name: $ACR_NAME" +Write-Host "Extracted ACR Endpoint: $ACR_ENDPOINT" + +# Set ACR details as environment variables in AZD +azd env set ACR_NAME $ACR_NAME +azd env set ACR_ENDPOINT $ACR_ENDPOINT + +Write-Host "Saved ACR details to AZD environment variables." + # Log in to Azure Container Registry Write-Host "Logging into Azure Container Registry: $ACR_NAME" az acr login -n $ACR_NAME @@ -90,8 +143,8 @@ if ($LASTEXITCODE -ne 0) { } # Build and push images -Build-And-Push-Image "contentprocessor" ".\src\ContentProcessor\" -Build-And-Push-Image "contentprocessorapi" ".\src\ContentProcessorAPI\" -Build-And-Push-Image "contentprocessorweb" ".\src\ContentProcessorWeb\" +Build-And-Push-Image "contentprocessor" "$ContentProcessorPath" $CONTAINER_APP_NAME +Build-And-Push-Image "contentprocessorapi" "$ContentApiPath" $CONTAINER_API_APP_NAME +Build-And-Push-Image "contentprocessorweb" "$ContentWebPath" $CONTAINER_WEB_APP_NAME Write-Host "All Docker images built and pushed successfully." diff --git a/infra/scripts/docker-build.sh b/infra/scripts/docker-build.sh old mode 100644 new mode 100755 index 3c080a9d..9e9e8cbc --- a/infra/scripts/docker-build.sh +++ b/infra/scripts/docker-build.sh @@ -1,88 +1,116 @@ #!/bin/bash -echo "In shell script" - -echo $1 -echo $2 -echo $3 -echo $4 -echo $5 -echo $6 - -# Check if the required arguments are provided -if [ "$#" -ne 6 ]; then - echo "Usage: docker-build.sh " - exit 1 -fi - -AZURE_SUBSCRIPTION_ID=$1 -ENV_NAME=$2 -AZURE_LOCATION=$3 -AZURE_RESOURCE_GROUP=$4 -USE_LOCAL_BUILD=$5 -AZURE_ENV_IMAGETAG=$6 - -USE_LOCAL_BUILD=$(echo "$USE_LOCAL_BUILD" | grep -iq "^true$" && echo "true" || echo "false") +set -e -AZURE_ENV_IMAGETAG=${AZURE_ENV_IMAGETAG:-latest} - -if [ "$USE_LOCAL_BUILD" = "true" ]; then - echo "Local Build enabled. Starting build process." - az account set --subscription "$AZURE_SUBSCRIPTION_ID" +# Get all environment values +echo "Fetching environment values from azd..." +ENV_VALUES_JSON=$(azd env get-values --output json) - # Deploy container registry - echo "Deploying container registry in location: $AZURE_LOCATION" - OUTPUTS=$(az deployment group create --resource-group "$AZURE_RESOURCE_GROUP" \ - --template-file "./infra/deploy_container_registry.bicep" \ - --parameters environmentName="$ENV_NAME" --query "properties.outputs" --output json) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATE_FILE="$SCRIPT_DIR/../deploy_container_registry.bicep" - ACR_NAME=$(echo "$OUTPUTS" | jq -r '.createdAcrName.value') - ACR_ENDPOINT=$(echo "$OUTPUTS" | jq -r '.acrEndpoint.value') +get_azd_env_value_or_default() { + local key="$1" + local default="$2" + local required="${3:-false}" - echo "Extracted ACR Name: $ACR_NAME" - echo "Extracted ACR Endpoint: $ACR_ENDPOINT" + value=$(azd env get-value "$key" 2>/dev/null || echo "") - # Store outputs in a .env file - echo -e "ACR_NAME=$ACR_NAME\nACR_ENDPOINT=$ACR_ENDPOINT" > .env - - # Set AZD environment variables - azd env set ACR_NAME "$ACR_NAME" - azd env set ACR_ENDPOINT "$ACR_ENDPOINT" - - echo "Saved ACR details to AZD environment variables." - echo "Deployed container registry in location." + if [ -z "$value" ]; then + if [ "$required" = true ]; then + echo "❌ Required environment key '$key' not found." >&2 + exit 1 + else + value="$default" + fi + fi - # Construct full image names - CONTENTPROCESSOR_IMAGE_URI="$ACR_NAME.azurecr.io/contentprocessor:$AZURE_ENV_IMAGETAG" - CONTENTPROCESSORAPI_IMAGE_URI="$ACR_NAME.azurecr.io/contentprocessorapi:$AZURE_ENV_IMAGETAG" - CONTENTPROCESSORWEB_IMAGE_URI="$ACR_NAME.azurecr.io/contentprocessorweb:$AZURE_ENV_IMAGETAG" + echo "$value" +} +# Required env variables +AZURE_SUBSCRIPTION_ID=$(get_azd_env_value_or_default "AZURE_SUBSCRIPTION_ID" "" true) +ENV_NAME=$(get_azd_env_value_or_default "AZURE_ENV_NAME" "" true) +CONTAINER_APP_USER_IDENTITY_ID=$(get_azd_env_value_or_default "CONTAINER_APP_USER_IDENTITY_ID" "" true) +AZURE_RESOURCE_GROUP=$(get_azd_env_value_or_default "AZURE_RESOURCE_GROUP" "" true) +CONTAINER_APP_USER_PRINCIPAL_ID=$(get_azd_env_value_or_default "CONTAINER_APP_USER_PRINCIPAL_ID" "" true) +AZURE_ENV_IMAGETAG=$(get_azd_env_value_or_default "AZURE_ENV_IMAGETAG" "latest" false) +CONTAINER_WEB_APP_NAME=$(get_azd_env_value_or_default "CONTAINER_WEB_APP_NAME" "" true) +CONTAINER_API_APP_NAME=$(get_azd_env_value_or_default "CONTAINER_API_APP_NAME" "" true) +CONTAINER_APP_NAME=$(get_azd_env_value_or_default "CONTAINER_APP_NAME" "" true) + + +echo "Using the following parameters:" +echo "AZURE_SUBSCRIPTION_ID = $AZURE_SUBSCRIPTION_ID" +echo "ENV_NAME = $ENV_NAME" +echo "AZURE_RESOURCE_GROUP = $AZURE_RESOURCE_GROUP" +echo "AZURE_ENV_IMAGETAG = $AZURE_ENV_IMAGETAG" + +# Ensure Azure login +echo "Checking Azure login status..." +if ! az account show --only-show-errors &>/dev/null; then + echo "No active Azure session found. Logging in..." + az login --only-show-errors + az account set --subscription "$AZURE_SUBSCRIPTION_ID" +fi - # Azure login - echo "Logging into Azure Container Registry: $ACR_NAME" - if ! az acr login -n "$ACR_NAME"; then - echo "Failed to log in to ACR" - exit 1 +# Deploy container registry +echo "Deploying container registry..." +DEPLOY_OUTPUT=$(az deployment group create \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --template-file "$TEMPLATE_FILE" \ + --parameters environmentName="$ENV_NAME" acrPullPrincipalIds="['$CONTAINER_APP_USER_PRINCIPAL_ID']" \ + --query "properties.outputs" \ + --output json) + +ACR_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.createdAcrName.value') +ACR_ENDPOINT=$(echo "$DEPLOY_OUTPUT" | jq -r '.acrEndpoint.value') + +echo "Extracted ACR Name: $ACR_NAME" +echo "Extracted ACR Endpoint: $ACR_ENDPOINT" + +azd env set ACR_NAME "$ACR_NAME" +azd env set ACR_ENDPOINT "$ACR_ENDPOINT" + +echo "Logging into ACR..." +az acr login -n "$ACR_NAME" + +# Build and push function +build_and_push_image() { + IMAGE_NAME="$1" + BUILD_PATH="$2" + CONTAINER_APP="$3" + + IMAGE_URI="$ACR_NAME.azurecr.io/$IMAGE_NAME:$AZURE_ENV_IMAGETAG" + echo "Building image: $IMAGE_URI" + docker build "$BUILD_PATH" --no-cache -t "$IMAGE_URI" + + echo "Pushing image: $IMAGE_URI" + docker push "$IMAGE_URI" + + if [ -n "$CONTAINER_APP" ]; then + echo "Updating container app: $CONTAINER_APP" + az containerapp registry set \ + --name "$CONTAINER_APP" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --server "$ACR_NAME.azurecr.io" \ + --identity "$CONTAINER_APP_USER_IDENTITY_ID" \ + --only-show-errors + + az containerapp update \ + --name "$CONTAINER_APP" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --image "$IMAGE_URI" \ + --only-show-errors + + echo "Updated registry for container app: $CONTAINER_APP" fi +} - # Build and push Docker images - for service in "ContentProcessor" "ContentProcessorAPI" "ContentProcessorWeb"; do - IMAGE_VAR_NAME="${service^^}_IMAGE_URI" - IMAGE_URI=${!IMAGE_VAR_NAME} +# Build and push all images +build_and_push_image "contentprocessor" "$SCRIPT_DIR/../../src/ContentProcessor/" "$CONTAINER_APP_NAME" - echo "Building Docker image: $IMAGE_URI" - if ! docker build "./src/$service/." --no-cache -t "$IMAGE_URI"; then - echo "Failed to build Docker image" - exit 1 - fi +build_and_push_image "contentprocessorapi" "$SCRIPT_DIR/../../src/ContentProcessorAPI/" "$CONTAINER_API_APP_NAME" - echo "Pushing Docker image to ACR: $IMAGE_URI" - if ! docker push "$IMAGE_URI"; then - echo "Failed to push Docker image" - exit 1 - fi +build_and_push_image "contentprocessorweb" "$SCRIPT_DIR/../../src/ContentProcessorWeb/" "$CONTAINER_WEB_APP_NAME" - echo "Docker image pushed successfully: $IMAGE_URI" - done -else - echo "Local Build not enabled. Using prebuilt image." -fi +echo "All Docker images built and pushed successfully." diff --git a/tests/e2e-test/pytest.ini b/tests/e2e-test/pytest.ini index 76eb64fc..05b7f91c 100644 --- a/tests/e2e-test/pytest.ini +++ b/tests/e2e-test/pytest.ini @@ -3,4 +3,5 @@ log_cli = true log_cli_level = INFO log_file = logs/tests.log log_file_level = INFO -addopts = -p no:warnings +addopts = -p no:warnings --tb=short + diff --git a/tests/e2e-test/readme.MD b/tests/e2e-test/readme.MD index 941d3653..13d4aa47 100644 --- a/tests/e2e-test/readme.MD +++ b/tests/e2e-test/readme.MD @@ -24,7 +24,7 @@ Installing Playwright Pytest from Virtual Environment Run test cases -- To run test cases from your 'tests' folder : "pytest --html=report.html --self-contained-html" +- To run test cases from your 'tests/e2e-test' folder : "pytest --html=report.html --self-contained-html" Create .env file in project root level with web app url and client credentials diff --git a/tests/e2e-test/requirements.txt b/tests/e2e-test/requirements.txt index 7aad0cfb..4e488e55 100644 --- a/tests/e2e-test/requirements.txt +++ b/tests/e2e-test/requirements.txt @@ -3,4 +3,5 @@ pytest-reporter-html1 python-dotenv pytest-check pytest-html -py \ No newline at end of file +py +beautifulsoup4 \ No newline at end of file diff --git a/tests/e2e-test/tests/conftest.py b/tests/e2e-test/tests/conftest.py index d356dc40..a6459bf2 100644 --- a/tests/e2e-test/tests/conftest.py +++ b/tests/e2e-test/tests/conftest.py @@ -1,53 +1,121 @@ -import os +""" +Pytest configuration for browser-based testing with Playwright and HTML report customization. +""" + +import io +import atexit +import logging +from pathlib import Path +from venv import logger import pytest -from config.constants import URL +from bs4 import BeautifulSoup from playwright.sync_api import sync_playwright -from py.xml import html # type: ignore + +from config.constants import URL + +# Global dictionary to store log streams for each test +LOG_STREAMS = {} @pytest.fixture(scope="session") def login_logout(): - # perform login and browser close once in a session - with sync_playwright() as p: - browser = p.chromium.launch(headless=False, args=["--start-maximized"]) + """ + Fixture to launch the browser, log in, and yield the page object. + Closes the browser after the session ends. + """ + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=False, args=["--start-maximized"]) context = browser.new_context(no_viewport=True) context.set_default_timeout(80000) page = context.new_page() - # Navigate to the login URL + page.goto(URL, wait_until="domcontentloaded") - # login to web url with username and password + + # Uncomment and complete the following to enable login # login_page = LoginPage(page) # load_dotenv() - # login_page.authenticate(os.getenv('user_name'), os.getenv('pass_word')) + # login_page.authenticate(os.getenv("user_name"), os.getenv("pass_word")) yield page - # perform close the browser + browser.close() @pytest.hookimpl(tryfirst=True) -def pytest_html_report_title(report): - report.title = "Automation_Content_Processing" - +def pytest_runtest_setup(item): + """ + Pytest hook to set up a log capture for each test. + """ + stream = io.StringIO() + handler = logging.StreamHandler(stream) + handler.setLevel(logging.INFO) -# Add a column for descriptions -def pytest_html_results_table_header(cells): - cells.insert(1, html.th("Description")) + logger = logging.getLogger() + logger.addHandler(handler) + LOG_STREAMS[item.nodeid] = (handler, stream) -def pytest_html_results_table_row(report, cells): - cells.insert( - 1, html.td(report.description if hasattr(report, "description") else "") - ) - -# Add logs and docstring to report @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): + """ + Pytest hook to add captured logs to the test report. + """ outcome = yield report = outcome.get_result() - report.description = str(item.function.__doc__) - os.makedirs("logs", exist_ok=True) - extra = getattr(report, "extra", []) - report.extra = extra + + handler, stream = LOG_STREAMS.get(item.nodeid, (None, None)) + + if handler and stream: + handler.flush() + log_output = stream.getvalue() + + logger = logging.getLogger() + logger.removeHandler(handler) + + report.description = f"
{log_output.strip()}
" + + LOG_STREAMS.pop(item.nodeid, None) + else: + report.description = "" + + +def pytest_collection_modifyitems(items): + """ + Modify test node IDs based on the test's parameterized 'prompt' value. + """ + for item in items: + if hasattr(item, "callspec"): + prompt = item.callspec.params.get("prompt") + if prompt: + item._nodeid = prompt + + +def rename_duration_column(): + """ + Modify the HTML report to rename 'Duration' column to 'Execution Time'. + Runs automatically after the test session. + """ + report_path = Path("report.html") + if not report_path.exists(): + logger.info("Report file not found, skipping column rename.") + return + + with report_path.open("r", encoding="utf-8") as file: + soup = BeautifulSoup(file, "html.parser") + + headers = soup.select("table#results-table thead th") + for th in headers: + if th.text.strip() == "Duration": + th.string = "Execution Time" + break + else: + print("'Duration' column not found in report.") + + with report_path.open("w", encoding="utf-8") as file: + file.write(str(soup)) + + +# Register HTML report column modification +atexit.register(rename_duration_column) diff --git a/tests/e2e-test/tests/test_contentProcessing_gp_tc.py b/tests/e2e-test/tests/test_contentProcessing_gp_tc.py index cbe99797..7fe90c2c 100644 --- a/tests/e2e-test/tests/test_contentProcessing_gp_tc.py +++ b/tests/e2e-test/tests/test_contentProcessing_gp_tc.py @@ -1,41 +1,72 @@ import logging - +import time import pytest from pages.HomePage import HomePage logger = logging.getLogger(__name__) +# Define step-wise test actions for Golden Path +golden_path_steps = [ + ("Validate home page is loaded", lambda home: home.validate_home_page()), + ("Select Invoice Schema", lambda home: home.select_schema("Invoice")), + ("Upload Invoice documents", lambda home: home.upload_files("Invoice")), + ("Refreshing the page until the 'Invoice' file status is updated to 'Completed'", lambda home: home.refresh()), + ( + "Validate extracted result for Invoice", + lambda home: home.validate_invoice_extracted_result(), + ), + ( + "Modify Extracted Data JSON & submit comments", + lambda home: home.modify_and_submit_extracted_data(), + ), + ("Validate process steps for Invoice", lambda home: home.validate_process_steps()), + ( + "Select Property Loss Damage Claim Form Schema", + lambda home: home.select_schema("Property"), + ), + ( + "Upload Property Loss Damage Claim Form documents", + lambda home: home.upload_files("Property"), + ), + ("Refreshing the page until the 'Claim Form' status is updated to 'Completed'", lambda home: home.refresh()), + ( + "Validate extracted result for Property Loss Damage Claim Form", + lambda home: home.validate_property_extracted_result(), + ), + ( + "Validate process steps for Property Loss Damage Claim Form", + lambda home: home.validate_process_steps(), + ), + ("Validate user able to delete file", lambda home: home.delete_files()), +] + +# Generate readable test step IDs +golden_path_ids = [ + f"{i+1:02d}. {desc}" for i, (desc, _) in enumerate(golden_path_steps) +] + -@pytest.mark.testcase_id("TC001") -def test_ContentProcessing_Golden_path_test(login_logout): - """Validate Golden path test case for Content Processing Accelerator""" +@pytest.mark.parametrize("description, action", golden_path_steps, ids=golden_path_ids) +def test_content_processing_steps(login_logout, description, action, request): + """ + Executes Golden Path content processing steps with individual log entries. + """ + request.node._nodeid = description page = login_logout - home_page = HomePage(page) - logger.info("Step 1: Validate home page is loaded.") - home_page.validate_home_page() - logger.info("Step 2: Select Invoice Schema.") - home_page.select_schema("Invoice") - logger.info("Step 3: Upload Invoice documents.") - home_page.upload_files("Invoice") - logger.info("Step 4: Refresh page till status is updated to Completed.") - home_page.refresh() - logger.info("Step 5: Validate extracted result for Invoice.") - home_page.validate_invoice_extracted_result() - logger.info("Step 6: Modify Extracted Data JSON & submit comments.") - home_page.modify_and_submit_extracted_data() - logger.info("Step 7: Validate process steps for Invoice") - home_page.validate_process_steps() - logger.info("Step 8: Select Property Loss Damage Claim Form Schema.") - home_page.select_schema("Property") - logger.info("Step 9: Upload Property Loss Damage Claim Form documents.") - home_page.upload_files("Property") - logger.info("Step 10: Refresh page till status is updated to Completed.") - home_page.refresh() - logger.info( - "Step 11: Validate extracted result for Property Loss Damage Claim Form." - ) - home_page.validate_property_extracted_result() - logger.info("Step 12: Validate process steps for Property Loss Damage Claim Form.") - home_page.validate_process_steps() - logger.info("Step 13: Validate Delete files.") - home_page.delete_files() + home = HomePage(page) + + logger.info(f"Running test step: {description}") + + start_time = time.time() + try: + action(home) + duration = time.time() - start_time + message = "Step passed: %s (Duration: %.2f seconds)" % (description, duration) + logger.info(message) + request.node._report_sections.append(("call", "log", message)) + + except Exception: + duration = time.time() - start_time + logger.error("Step failed: %s (Duration: %.2f seconds)", description, duration, exc_info=True) + raise + request.node._report_sections.append(("call", "log", f"Step passed: {description}"))