diff --git a/.env.example b/.env.example index a8fab76..0f836a1 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ AZURE_OPENAI_ENDPOINT="https://.openai.azure.com/" AZURE_OPENAI_API_KEY="" # Environment variable obtained from Azure Cosmos DB for MongoDB vCore AZURE_COSMOS_CONNECTION_STRING="" +AZURE_COSMOS_USERNAME="" +AZURE_COSMOS_PASSWORD="" # Environment variables you set to be used by the code AZURE_COSMOS_DATABASE_NAME="" AZURE_COSMOS_COLLECTION_NAME="" diff --git a/infra/core/database/cosmos/mongo/cosmos-mongo-cluster.bicep b/infra/core/database/cosmos/mongo/cosmos-mongo-cluster.bicep index f426b9d..5f5f10d 100644 --- a/infra/core/database/cosmos/mongo/cosmos-mongo-cluster.bicep +++ b/infra/core/database/cosmos/mongo/cosmos-mongo-cluster.bicep @@ -4,29 +4,45 @@ param name string param location string = resourceGroup().location param tags object = {} +@description('Username for admin user') param administratorLogin string -param sku string -param storage int -param nodeCount int @secure() +@description('Password for admin user') +@minLength(8) +@maxLength(128) param administratorLoginPassword string -param highAvailabilityMode bool = false -param allowAzureIPsFirewall bool = false +@description('Whether to allow all IPs or not. Warning: No IP addresses will be blocked and any host on the Internet can access the coordinator in this server group. It is strongly recommended to use this rule only temporarily and only on test clusters that do not contain sensitive data.') param allowAllIPsFirewall bool = false +@description('Whether to allow Azure internal IPs or not') +param allowAzureIPsFirewall bool = false +@description('IP addresses to allow access to the cluster from') param allowedSingleIPs array = [] +@description('Mode to create the mongo cluster') +param createMode string = 'Default' +@description('Whether high availability is enabled on the node group') +param highAvailabilityMode bool = false +@description('Number of nodes in the node group') +param nodeCount int +@description('Deployed Node type in the node group') +param nodeType string = 'Shard' +@description('SKU defines the CPU and memory that is provisioned for each node') +param sku string +@description('Disk storage size for the node group in GB') +param storage int -resource mognoCluster 'Microsoft.DocumentDB/mongoClusters@2023-03-01-preview' = { +resource mognoCluster 'Microsoft.DocumentDB/mongoClusters@2024-02-15-preview' = { name: name tags: tags location: location properties: { administratorLogin: administratorLogin administratorLoginPassword: administratorLoginPassword + createMode: createMode nodeGroupSpecs: [ { diskSizeGB: storage enableHa: highAvailabilityMode - kind: 'Shard' + kind: nodeType nodeCount: nodeCount sku: sku } diff --git a/infra/core/database/mysql/flexibleserver.bicep b/infra/core/database/mysql/flexibleserver.bicep deleted file mode 100644 index 47d509d..0000000 --- a/infra/core/database/mysql/flexibleserver.bicep +++ /dev/null @@ -1,65 +0,0 @@ -metadata description = 'Creates an Azure Database for MySQL - Flexible Server.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param highAvailabilityMode string = 'Disabled' -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// MySQL version -param version string - -resource mysqlServer 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: highAvailabilityMode - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output domainName string = mysqlServer.properties.fullyQualifiedDomainName diff --git a/infra/core/database/postgresql/aca-service.bicep b/infra/core/database/postgresql/aca-service.bicep deleted file mode 100644 index 3ed4b9a..0000000 --- a/infra/core/database/postgresql/aca-service.bicep +++ /dev/null @@ -1,49 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param containerAppsEnvironmentId string - -resource postgres 'Microsoft.App/containerApps@2023-04-01-preview' = { - name: name - location: location - tags: tags - properties: { - environmentId: containerAppsEnvironmentId - configuration: { - service: { - type: 'postgres' - } - } - } -} - -/* -resource pgsqlCli 'Microsoft.App/containerApps@2023-04-01-preview' = { - name: '${name}-cli' - location: location - properties: { - environmentId: containerAppsEnvironmentId - template: { - serviceBinds: [ - { - serviceId: postgres.id - } - ] - containers: [ - { - name: 'psql' - image: 'mcr.microsoft.com/k8se/services/postgres:14' - command: [ '/bin/sleep', 'infinity' ] - } - ] - scale: { - minReplicas: 1 - maxReplicas: 1 - } - } - } -} -*/ - -output id string = postgres.id diff --git a/infra/core/database/postgresql/flexibleserver.bicep b/infra/core/database/postgresql/flexibleserver.bicep deleted file mode 100644 index 3647524..0000000 --- a/infra/core/database/postgresql/flexibleserver.bicep +++ /dev/null @@ -1,64 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// PostgreSQL version -param version string - -// Latest official version 2022-12-01 does not have Bicep types available -resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: 'Disabled' - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output domainName string = postgresServer.properties.fullyQualifiedDomainName diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep deleted file mode 100644 index 64477a7..0000000 --- a/infra/core/database/sqlserver/sqlserver.bicep +++ /dev/null @@ -1,129 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param appUser string = 'appUser' -param databaseName string -param keyVaultName string -param sqlAdmin string = 'sqlAdmin' -param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' - -@secure() -param sqlAdminPassword string -@secure() -param appUserPassword string - -resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { - name: name - location: location - tags: tags - properties: { - version: '12.0' - minimalTlsVersion: '1.2' - publicNetworkAccess: 'Enabled' - administratorLogin: sqlAdmin - administratorLoginPassword: sqlAdminPassword - } - - resource database 'databases' = { - name: databaseName - location: location - } - - resource firewall 'firewallRules' = { - name: 'Azure Services' - properties: { - // Allow all clients - // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". - // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. - startIpAddress: '0.0.0.1' - endIpAddress: '255.255.255.254' - } - } -} - -resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { - name: '${name}-deployment-script' - location: location - kind: 'AzureCLI' - properties: { - azCliVersion: '2.37.0' - retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running - timeout: 'PT5M' // Five minutes - cleanupPreference: 'OnSuccess' - environmentVariables: [ - { - name: 'APPUSERNAME' - value: appUser - } - { - name: 'APPUSERPASSWORD' - secureValue: appUserPassword - } - { - name: 'DBNAME' - value: databaseName - } - { - name: 'DBSERVER' - value: sqlServer.properties.fullyQualifiedDomainName - } - { - name: 'SQLCMDPASSWORD' - secureValue: sqlAdminPassword - } - { - name: 'SQLADMIN' - value: sqlAdmin - } - ] - - scriptContent: ''' -wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 -tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . - -cat < ./initDb.sql -drop user ${APPUSERNAME} -go -create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' -go -alter role db_owner add member ${APPUSERNAME} -go -SCRIPT_END - -./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql - ''' - } -} - -resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'sqlAdminPassword' - properties: { - value: sqlAdminPassword - } -} - -resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'appUserPassword' - properties: { - value: appUserPassword - } -} - -resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: '${connectionString}; Password=${appUserPassword}' - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' -output connectionStringKey string = connectionStringKey -output databaseName string = sqlServer::database.name diff --git a/infra/core/gateway/apim-api-policy.xml b/infra/core/gateway/apim-api-policy.xml deleted file mode 100644 index c3a3605..0000000 --- a/infra/core/gateway/apim-api-policy.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - {0} - - - PUT - GET - POST - DELETE - PATCH - - -
*
-
- -
*
-
-
- - - - - - - Call to the @(context.Api.Name) - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Failed to process the @(context.Api.Name) - - - - - - - - - - - - - - - - - - We're Sorry. An unexpected error has occurred. If this continues please contact Tech Support. - - -
diff --git a/infra/core/gateway/apim.bicep b/infra/core/gateway/apim.bicep deleted file mode 100644 index 64c958c..0000000 --- a/infra/core/gateway/apim.bicep +++ /dev/null @@ -1,78 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The email address of the owner of the service') -@minLength(1) -param publisherEmail string = 'noreply@microsoft.com' - -@description('The name of the owner of the service') -@minLength(1) -param publisherName string = 'n/a' - -@description('The pricing tier of this API Management service') -@allowed([ - 'Consumption' - 'Developer' - 'Standard' - 'Premium' -]) -param sku string = 'Consumption' - -@description('The instance size of this API Management service.') -@allowed([ 0, 1, 2 ]) -param skuCount int = 0 - -@description('Azure Application Insights Name') -param applicationInsightsName string - -resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = { - name: name - location: location - tags: union(tags, { 'azd-service-name': name }) - sku: { - name: sku - capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount) - } - properties: { - publisherEmail: publisherEmail - publisherName: publisherName - // Custom properties are not supported for Consumption SKU - customProperties: sku == 'Consumption' ? {} : { - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false' - } - } -} - -resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) { - name: 'app-insights-logger' - parent: apimService - properties: { - credentials: { - instrumentationKey: applicationInsights.properties.InstrumentationKey - } - description: 'Logger to Azure Application Insights' - isBuffered: false - loggerType: 'applicationInsights' - resourceId: applicationInsights.id - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName -} - -output apimServiceName string = apimService.name diff --git a/infra/core/host/aks-agent-pool.bicep b/infra/core/host/aks-agent-pool.bicep deleted file mode 100644 index f6ff43a..0000000 --- a/infra/core/host/aks-agent-pool.bicep +++ /dev/null @@ -1,17 +0,0 @@ -param clusterName string - -@description('The agent pool name') -param name string - -@description('The agent pool configuration') -param config object - -resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-01-02-preview' existing = { - name: clusterName -} - -resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2023-01-02-preview' = { - parent: aksCluster - name: name - properties: config -} diff --git a/infra/core/host/aks-managed-cluster.bicep b/infra/core/host/aks-managed-cluster.bicep deleted file mode 100644 index b189af8..0000000 --- a/infra/core/host/aks-managed-cluster.bicep +++ /dev/null @@ -1,139 +0,0 @@ -@description('The name for the AKS managed cluster') -param name string - -@description('The name of the resource group for the managed resources of the AKS cluster') -param nodeResourceGroupName string = '' - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('Kubernetes Version') -param kubernetesVersion string = '1.25.5' - -@description('Whether RBAC is enabled for local accounts') -param enableRbac bool = true - -// Add-ons -@description('Whether web app routing (preview) add-on is enabled') -param webAppRoutingAddon bool = true - -// AAD Integration -@description('Enable Azure Active Directory integration') -param enableAad bool = false - -@description('Enable RBAC using AAD') -param enableAzureRbac bool = false - -@description('The Tenant ID associated to the Azure Active Directory') -param aadTenantId string = '' - -@description('The load balancer SKU to use for ingress into the AKS cluster') -@allowed([ 'basic', 'standard' ]) -param loadBalancerSku string = 'standard' - -@description('Network plugin used for building the Kubernetes network.') -@allowed([ 'azure', 'kubenet', 'none' ]) -param networkPlugin string = 'azure' - -@description('Network policy used for building the Kubernetes network.') -@allowed([ 'azure', 'calico' ]) -param networkPolicy string = 'azure' - -@description('If set to true, getting static credentials will be disabled for this cluster.') -param disableLocalAccounts bool = false - -@description('The managed cluster SKU.') -@allowed([ 'Free', 'Paid', 'Standard' ]) -param sku string = 'Free' - -@description('Configuration of AKS add-ons') -param addOns object = {} - -@description('The log analytics workspace id used for logging & monitoring') -param workspaceId string = '' - -@description('The node pool configuration for the System agent pool') -param systemPoolConfig object - -@description('The DNS prefix to associate with the AKS cluster') -param dnsPrefix string = '' - -resource aks 'Microsoft.ContainerService/managedClusters@2023-02-01' = { - name: name - location: location - tags: tags - identity: { - type: 'SystemAssigned' - } - sku: { - name: 'Base' - tier: sku - } - properties: { - nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' - kubernetesVersion: kubernetesVersion - dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix - enableRBAC: enableRbac - aadProfile: enableAad ? { - managed: true - enableAzureRBAC: enableAzureRbac - tenantID: aadTenantId - } : null - agentPoolProfiles: [ - systemPoolConfig - ] - networkProfile: { - loadBalancerSku: loadBalancerSku - networkPlugin: networkPlugin - networkPolicy: networkPolicy - } - disableLocalAccounts: disableLocalAccounts && enableAad - addonProfiles: addOns - ingressProfile: { - webAppRouting: { - enabled: webAppRoutingAddon - } - } - } -} - -var aksDiagCategories = [ - 'cluster-autoscaler' - 'kube-controller-manager' - 'kube-audit-admin' - 'guard' -] - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'aks-diagnostics' - scope: aks - properties: { - workspaceId: workspaceId - logs: [for category in aksDiagCategories: { - category: category - enabled: true - }] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -@description('The resource name of the AKS cluster') -output clusterName string = aks.name - -@description('The AKS cluster identity') -output clusterIdentity object = { - clientId: aks.properties.identityProfile.kubeletidentity.clientId - objectId: aks.properties.identityProfile.kubeletidentity.objectId - resourceId: aks.properties.identityProfile.kubeletidentity.resourceId -} diff --git a/infra/core/host/aks.bicep b/infra/core/host/aks.bicep deleted file mode 100644 index f2f4206..0000000 --- a/infra/core/host/aks.bicep +++ /dev/null @@ -1,213 +0,0 @@ -@description('The name for the AKS managed cluster') -param name string - -@description('The name for the Azure container registry (ACR)') -param containerRegistryName string - -@description('The name of the connected log analytics workspace') -param logAnalyticsName string = '' - -@description('The name of the keyvault to grant access') -param keyVaultName string - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('AKS add-ons configuration') -param addOns object = { - azurePolicy: { - enabled: true - config: { - version: 'v2' - } - } - keyVault: { - enabled: true - config: { - enableSecretRotation: 'true' - rotationPollInterval: '2m' - } - } - openServiceMesh: { - enabled: false - config: {} - } - omsAgent: { - enabled: true - config: {} - } - applicationGateway: { - enabled: false - config: {} - } -} - -@allowed([ - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The System Pool Preset sizing') -param systemPoolType string = 'CostOptimised' - -@allowed([ - '' - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The User Pool Preset sizing') -param agentPoolType string = '' - -// Configure system / user agent pools -@description('Custom configuration of system node pool') -param systemPoolConfig object = {} -@description('Custom configuration of user node pool') -param agentPoolConfig object = {} - -// Configure AKS add-ons -var omsAgentConfig = (!empty(logAnalyticsName) && !empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? union( - addOns.omsAgent, - { - config: { - logAnalyticsWorkspaceResourceID: logAnalytics.id - } - } -) : {} - -var addOnsConfig = union( - (!empty(addOns.azurePolicy) && addOns.azurePolicy.enabled) ? { azurepolicy: addOns.azurePolicy } : {}, - (!empty(addOns.keyVault) && addOns.keyVault.enabled) ? { azureKeyvaultSecretsProvider: addOns.keyVault } : {}, - (!empty(addOns.openServiceMesh) && addOns.openServiceMesh.enabled) ? { openServiceMesh: addOns.openServiceMesh } : {}, - (!empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? { omsagent: omsAgentConfig } : {}, - (!empty(addOns.applicationGateway) && addOns.applicationGateway.enabled) ? { ingressApplicationGateway: addOns.applicationGateway } : {} -) - -// Link to existing log analytics workspace when available -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = if (!empty(logAnalyticsName)) { - name: logAnalyticsName -} - -var systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType] - -// Create the primary AKS cluster resources and system node pool -module managedCluster 'aks-managed-cluster.bicep' = { - name: 'managed-cluster' - params: { - name: name - location: location - tags: tags - systemPoolConfig: union( - { name: 'npsystem', mode: 'System' }, - nodePoolBase, - systemPoolSpec - ) - addOns: addOnsConfig - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - } -} - -var hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType) -var agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : empty(agentPoolType) ? {} : nodePoolPresets[agentPoolType] - -// Create additional user agent pool when specified -module agentPool 'aks-agent-pool.bicep' = if (hasAgentPool) { - name: 'aks-node-pool' - params: { - clusterName: managedCluster.outputs.clusterName - name: 'npuserpool' - config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec) - } -} - -// Creates container registry (ACR) -module containerRegistry 'container-registry.bicep' = { - name: 'container-registry' - params: { - name: containerRegistryName - location: location - tags: tags - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - } -} - -// Grant ACR Pull access from cluster managed identity to container registry -module containerRegistryAccess '../security/registry-access.bicep' = { - name: 'cluster-container-registry-access' - params: { - containerRegistryName: containerRegistry.outputs.name - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Give the AKS Cluster access to KeyVault -module clusterKeyVaultAccess '../security/keyvault-access.bicep' = { - name: 'cluster-keyvault-access' - params: { - keyVaultName: keyVaultName - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Helpers for node pool configuration -var nodePoolBase = { - osType: 'Linux' - maxPods: 30 - type: 'VirtualMachineScaleSets' - upgradeSettings: { - maxSurge: '33%' - } -} - -var nodePoolPresets = { - CostOptimised: { - vmSize: 'Standard_B4ms' - count: 1 - minCount: 1 - maxCount: 3 - enableAutoScaling: true - availabilityZones: [] - } - Standard: { - vmSize: 'Standard_DS2_v2' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } - HighSpec: { - vmSize: 'Standard_D4s_v3' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } -} - -// Module outputs -@description('The resource name of the AKS cluster') -output clusterName string = managedCluster.outputs.clusterName - -@description('The AKS cluster identity') -output clusterIdentity object = managedCluster.outputs.clusterIdentity - -@description('The resource name of the ACR') -output containerRegistryName string = containerRegistry.outputs.name - -@description('The login server for the container registry') -output containerRegistryLoginServer string = containerRegistry.outputs.loginServer diff --git a/infra/core/host/appservice-appsettings.bicep b/infra/core/host/appservice-appsettings.bicep index 2286e1f..f4b22f8 100644 --- a/infra/core/host/appservice-appsettings.bicep +++ b/infra/core/host/appservice-appsettings.bicep @@ -1,7 +1,9 @@ +metadata description = 'Updates app settings for an Azure App Service.' @description('The name of the app service resource within the current resource group scope') param name string @description('The app settings to be applied to the app service') +@secure() param appSettings object resource appService 'Microsoft.Web/sites@2022-03-01' existing = { diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index 1fb9410..20ef570 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -8,6 +8,7 @@ param applicationInsightsName string = '' param appServicePlanId string param keyVaultName string = '' param managedIdentity bool = !empty(keyVaultName) +param virtualNetworkSubnetId string = '' // Runtime Properties @allowed([ @@ -22,6 +23,9 @@ param kind string = 'app,linux' // Microsoft.Web/sites/config param allowedOrigins array = [] +param additionalScopes array = [] +param additionalAllowedAudiences array = [] +param allowedApplications array = [] param alwaysOn bool = true param appCommandLine string = '' @secure() @@ -36,33 +40,59 @@ param scmDoBuildDuringDeployment bool = false param use32BitWorkerProcess bool = false param ftpsState string = 'FtpsOnly' param healthCheckPath string = '' +param clientAppId string = '' +param serverAppId string = '' +@secure() +param clientSecretSettingName string = '' +param authenticationIssuerUri string = '' +@allowed([ 'Enabled', 'Disabled' ]) +param publicNetworkAccess string = 'Enabled' +param enableUnauthenticatedAccess bool = false + +var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ] +var loginEndpoint = environment().authentication.loginEndpoint +var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint +var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins + +// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly +// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent +var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ] +var requiredAudiences = [ 'api://${serverAppId}' ] + +var coreConfig = { + linuxFxVersion: linuxFxVersion + alwaysOn: alwaysOn + ftpsState: ftpsState + appCommandLine: appCommandLine + numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null + minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null + minTlsVersion: '1.2' + use32BitWorkerProcess: use32BitWorkerProcess + functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null + healthCheckPath: healthCheckPath + cors: { + allowedOrigins: union(allMsftAllowedOrigins, allowedOrigins) + } +} + +var appServiceProperties = { + serverFarmId: appServicePlanId + siteConfig: coreConfig + clientAffinityEnabled: clientAffinityEnabled + httpsOnly: true + // Always route traffic through the vnet + // See https://learn.microsoft.com/azure/app-service/configure-vnet-integration-routing#configure-application-routing + vnetRouteAllEnabled: !empty(virtualNetworkSubnetId) + virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null + publicNetworkAccess: publicNetworkAccess +} resource appService 'Microsoft.Web/sites@2022-03-01' = { name: name location: location tags: tags kind: kind - properties: { - serverFarmId: appServicePlanId - siteConfig: { - linuxFxVersion: linuxFxVersion - alwaysOn: alwaysOn - ftpsState: ftpsState - minTlsVersion: '1.2' - appCommandLine: appCommandLine - numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null - minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null - use32BitWorkerProcess: use32BitWorkerProcess - functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null - healthCheckPath: healthCheckPath - cors: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } - clientAffinityEnabled: clientAffinityEnabled - httpsOnly: true - } - + properties: appServiceProperties identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } resource configAppSettings 'config' = { @@ -72,7 +102,7 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) ENABLE_ORYX_BUILD: string(enableOryxBuild) }, - runtimeName == 'python' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {}, + runtimeName == 'python' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true' } : {}, !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) } @@ -103,6 +133,41 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { allow: false } } + + resource configAuth 'config' = if (!(empty(clientAppId))) { + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: enableUnauthenticatedAccess ? 'AllowAnonymous' : 'RedirectToLoginPage' + redirectToProvider: 'azureactivedirectory' + } + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + clientId: clientAppId + clientSecretSettingName: clientSecretSettingName + openIdIssuer: authenticationIssuerUri + } + login: { + loginParameters: [ 'scope=${join(union(requiredScopes, additionalScopes), ' ')}' ] + } + validation: { + allowedAudiences: union(requiredAudiences, additionalAllowedAudiences) + defaultAuthorizationPolicy: { + allowedApplications: allowedApplications + } + } + } + } + login: { + tokenStore: { + enabled: true + } + } + } + } } resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { @@ -113,6 +178,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing name: applicationInsightsName } +output id string = appService.id output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' output name string = appService.name output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep index c444f40..2e37e04 100644 --- a/infra/core/host/appserviceplan.bicep +++ b/infra/core/host/appserviceplan.bicep @@ -1,3 +1,4 @@ +metadata description = 'Creates an Azure App Service plan.' param name string param location string = resourceGroup().location param tags object = {} diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep deleted file mode 100644 index 38d740d..0000000 --- a/infra/core/host/container-app-upsert.bicep +++ /dev/null @@ -1,81 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param containerAppsEnvironmentName string -param containerName string = 'main' -param containerRegistryName string - -@description('Minimum number of replicas to run') -@minValue(1) -param containerMinReplicas int = 1 -@description('Maximum number of replicas to run') -@minValue(1) -param containerMaxReplicas int = 10 - -param secrets array = [] -param env array = [] -param external bool = true -param targetPort int = 80 -param exists bool - -@description('User assigned identity name') -param identityName string - -@description('Enabled Ingress for container app') -param ingressEnabled bool = true - -// Dapr Options -@description('Enable Dapr') -param daprEnabled bool = false -@description('Dapr app ID') -param daprAppId string = containerName -@allowed([ 'http', 'grpc' ]) -@description('Protocol used by Dapr to connect to the app, e.g. http or grpc') -param daprAppProtocol string = 'http' - -// Service options -@description('PostgreSQL service ID') -param postgresServiceId string = '' - -@description('CPU cores allocated to a single container instance, e.g. 0.5') -param containerCpuCoreCount string = '0.5' - -@description('Memory allocated to a single container instance, e.g. 1Gi') -param containerMemory string = '1.0Gi' - -resource existingApp 'Microsoft.App/containerApps@2022-03-01' existing = if (exists) { - name: name -} - -module app 'container-app.bicep' = { - name: '${deployment().name}-update' - params: { - name: name - location: location - tags: tags - identityName: identityName - ingressEnabled: ingressEnabled - containerName: containerName - containerAppsEnvironmentName: containerAppsEnvironmentName - containerRegistryName: containerRegistryName - containerCpuCoreCount: containerCpuCoreCount - containerMemory: containerMemory - containerMinReplicas: containerMinReplicas - containerMaxReplicas: containerMaxReplicas - daprEnabled: daprEnabled - daprAppId: daprAppId - daprAppProtocol: daprAppProtocol - postgresServiceId: postgresServiceId - secrets: secrets - external: external - env: env - imageName: exists ? existingApp.properties.template.containers[0].image : '' - targetPort: targetPort - } -} - -output defaultDomain string = app.outputs.defaultDomain -output imageName string = app.outputs.imageName -output name string = app.outputs.name -output uri string = app.outputs.uri diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep deleted file mode 100644 index e514f45..0000000 --- a/infra/core/host/container-app.bicep +++ /dev/null @@ -1,133 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param containerAppsEnvironmentName string -param containerName string = 'main' -param containerRegistryName string - -@description('Minimum number of replicas to run') -@minValue(1) -param containerMinReplicas int = 1 -@description('Maximum number of replicas to run') -@minValue(1) -param containerMaxReplicas int = 10 - -param secrets array = [] -param env array = [] -param external bool = true -param imageName string -param targetPort int = 80 - -@description('User assigned identity name') -param identityName string - -@description('Enabled Ingress for container app') -param ingressEnabled bool = true - -// Dapr Options -@description('Enable Dapr') -param daprEnabled bool = false -@description('Dapr app ID') -param daprAppId string = containerName -@allowed([ 'http', 'grpc' ]) -@description('Protocol used by Dapr to connect to the app, e.g. http or grpc') -param daprAppProtocol string = 'http' - -// Service options -@description('PostgreSQL service ID') -param postgresServiceId string = '' - -@description('CPU cores allocated to a single container instance, e.g. 0.5') -param containerCpuCoreCount string = '0.5' - -@description('Memory allocated to a single container instance, e.g. 1Gi') -param containerMemory string = '1.0Gi' - -resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { - name: identityName -} - -module containerRegistryAccess '../security/registry-access.bicep' = { - name: '${deployment().name}-registry-access' - params: { - containerRegistryName: containerRegistryName - principalId: userIdentity.properties.principalId - } -} - -resource app 'Microsoft.App/containerApps@2023-04-01-preview' = { - name: name - location: location - tags: tags - // It is critical that the identity is granted ACR pull access before the app is created - // otherwise the container app will throw a provision error - // This also forces us to use an user assigned managed identity since there would no way to - // provide the system assigned identity with the ACR pull access before the app is created - dependsOn: [ containerRegistryAccess ] - identity: { - type: 'UserAssigned' - userAssignedIdentities: { '${userIdentity.id}': {} } - } - properties: { - managedEnvironmentId: containerAppsEnvironment.id - configuration: { - activeRevisionsMode: 'single' - ingress: ingressEnabled ? { - external: external - targetPort: targetPort - transport: 'auto' - } : null - dapr: daprEnabled ? { - enabled: true - appId: daprAppId - appProtocol: daprAppProtocol - appPort: ingressEnabled ? targetPort : 0 - } : { enabled: false } - secrets: secrets - registries: [ - { - server: '${containerRegistry.name}.azurecr.io' - identity: userIdentity.id - } - ] - } - template: { - serviceBinds: !empty(postgresServiceId) ? [ - { - serviceId: postgresServiceId - name: 'postgres' - } - ] : [] - containers: [ - { - image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: containerName - env: env - resources: { - cpu: json(containerCpuCoreCount) - memory: containerMemory - } - } - ] - scale: { - minReplicas: containerMinReplicas - maxReplicas: containerMaxReplicas - } - } - } -} - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = { - name: containerAppsEnvironmentName -} - -// 2022-02-01-preview needed for anonymousPullEnabled -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { - name: containerRegistryName -} - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output imageName string = imageName -output name string = app.name -output uri string = 'https://${app.properties.configuration.ingress.fqdn}' diff --git a/infra/core/host/container-apps-environment.bicep b/infra/core/host/container-apps-environment.bicep deleted file mode 100644 index 9c0126e..0000000 --- a/infra/core/host/container-apps-environment.bicep +++ /dev/null @@ -1,35 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param daprEnabled bool = false -param logAnalyticsWorkspaceName string -param applicationInsightsName string = '' - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' = { - name: name - location: location - tags: tags - properties: { - appLogsConfiguration: { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: logAnalyticsWorkspace.properties.customerId - sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey - } - } - daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' - } -} - -resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { - name: logAnalyticsWorkspaceName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)){ - name: applicationInsightsName -} - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output name string = containerAppsEnvironment.name -output id string = containerAppsEnvironment.id diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep deleted file mode 100644 index 087a4ae..0000000 --- a/infra/core/host/container-apps.bicep +++ /dev/null @@ -1,34 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param containerAppsEnvironmentName string -param containerRegistryName string -param logAnalyticsWorkspaceName string -param applicationInsightsName string = '' - -module containerAppsEnvironment 'container-apps-environment.bicep' = { - name: '${name}-container-apps-environment' - params: { - name: containerAppsEnvironmentName - location: location - tags: tags - logAnalyticsWorkspaceName: logAnalyticsWorkspaceName - applicationInsightsName: applicationInsightsName - } -} - -module containerRegistry 'container-registry.bicep' = { - name: '${name}-container-registry' - params: { - name: containerRegistryName - location: location - tags: tags - } -} - -output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain -output environmentName string = containerAppsEnvironment.outputs.name -output environmentId string = containerAppsEnvironment.outputs.id -output registryLoginServer string = containerRegistry.outputs.loginServer -output registryName string = containerRegistry.outputs.name diff --git a/infra/core/host/container-registry.bicep b/infra/core/host/container-registry.bicep deleted file mode 100644 index c0ba201..0000000 --- a/infra/core/host/container-registry.bicep +++ /dev/null @@ -1,67 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param adminUserEnabled bool = true -param anonymousPullEnabled bool = false -param dataEndpointEnabled bool = false -param encryption object = { - status: 'disabled' -} -param networkRuleBypassOptions string = 'AzureServices' -param publicNetworkAccess string = 'Enabled' -param sku object = { - name: 'Basic' -} -param zoneRedundancy string = 'Disabled' - -@description('The log analytics workspace id used for logging & monitoring') -param workspaceId string = '' - -// 2022-02-01-preview needed for anonymousPullEnabled -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { - name: name - location: location - tags: tags - sku: sku - properties: { - adminUserEnabled: adminUserEnabled - anonymousPullEnabled: anonymousPullEnabled - dataEndpointEnabled: dataEndpointEnabled - encryption: encryption - networkRuleBypassOptions: networkRuleBypassOptions - publicNetworkAccess: publicNetworkAccess - zoneRedundancy: zoneRedundancy - } -} - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'registry-diagnostics' - scope: containerRegistry - properties: { - workspaceId: workspaceId - logs: [ - { - category: 'ContainerRegistryRepositoryEvents' - enabled: true - } - { - category: 'ContainerRegistryLoginEvents' - enabled: true - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - timeGrain: 'PT1M' - } - ] - } -} - -output loginServer string = containerRegistry.properties.loginServer -output name string = containerRegistry.name diff --git a/infra/core/host/functions.bicep b/infra/core/host/functions.bicep deleted file mode 100644 index 28a581b..0000000 --- a/infra/core/host/functions.bicep +++ /dev/null @@ -1,82 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) -param storageAccountName string - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Function Settings -@allowed([ - '~4', '~3', '~2', '~1' -]) -param extensionVersion string = '~4' - -// Microsoft.Web/sites Properties -param kind string = 'functionapp,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = true -param use32BitWorkerProcess bool = false - -module functions 'appservice.bicep' = { - name: '${name}-functions' - params: { - name: name - location: location - tags: tags - allowedOrigins: allowedOrigins - alwaysOn: alwaysOn - appCommandLine: appCommandLine - applicationInsightsName: applicationInsightsName - appServicePlanId: appServicePlanId - appSettings: union(appSettings, { - AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' - FUNCTIONS_EXTENSION_VERSION: extensionVersion - FUNCTIONS_WORKER_RUNTIME: runtimeName - }) - clientAffinityEnabled: clientAffinityEnabled - enableOryxBuild: enableOryxBuild - functionAppScaleLimit: functionAppScaleLimit - keyVaultName: keyVaultName - kind: kind - linuxFxVersion: linuxFxVersion - managedIdentity: managedIdentity - minimumElasticInstanceCount: minimumElasticInstanceCount - numberOfWorkers: numberOfWorkers - runtimeName: runtimeName - runtimeVersion: runtimeVersion - runtimeNameAndVersion: runtimeNameAndVersion - scmDoBuildDuringDeployment: scmDoBuildDuringDeployment - use32BitWorkerProcess: use32BitWorkerProcess - } -} - -resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { - name: storageAccountName -} - -output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' -output name string = functions.outputs.name -output uri string = functions.outputs.uri diff --git a/infra/core/host/staticwebapp.bicep b/infra/core/host/staticwebapp.bicep deleted file mode 100644 index 91c2d0d..0000000 --- a/infra/core/host/staticwebapp.bicep +++ /dev/null @@ -1,21 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'Free' - tier: 'Free' -} - -resource web 'Microsoft.Web/staticSites@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - properties: { - provider: 'Custom' - } -} - -output name string = web.name -output uri string = 'https://${web.properties.defaultHostname}' diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep index b7af2c1..d082e66 100644 --- a/infra/core/monitor/applicationinsights-dashboard.bicep +++ b/infra/core/monitor/applicationinsights-dashboard.bicep @@ -1,3 +1,4 @@ +metadata description = 'Creates a dashboard for an Application Insights instance.' param name string param applicationInsightsName string param location string = resourceGroup().location diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep deleted file mode 100644 index f76b292..0000000 --- a/infra/core/monitor/applicationinsights.bicep +++ /dev/null @@ -1,30 +0,0 @@ -param name string -param dashboardName string -param location string = resourceGroup().location -param tags object = {} - -param logAnalyticsWorkspaceId string - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: name - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalyticsWorkspaceId - } -} - -module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = { - name: 'application-insights-dashboard' - params: { - name: dashboardName - location: location - applicationInsightsName: applicationInsights.name - } -} - -output connectionString string = applicationInsights.properties.ConnectionString -output instrumentationKey string = applicationInsights.properties.InstrumentationKey -output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep deleted file mode 100644 index 770544c..0000000 --- a/infra/core/monitor/loganalytics.bicep +++ /dev/null @@ -1,21 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: name - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -output id string = logAnalytics.id -output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep index 96ba11e..b402763 100644 --- a/infra/core/monitor/monitoring.bicep +++ b/infra/core/monitor/monitoring.bicep @@ -1,31 +1,50 @@ +metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' param logAnalyticsName string param applicationInsightsName string -param applicationInsightsDashboardName string +param applicationInsightsDashboardName string = '' param location string = resourceGroup().location param tags object = {} +@allowed([ 'Enabled', 'Disabled' ]) +param publicNetworkAccess string = 'Enabled' -module logAnalytics 'loganalytics.bicep' = { +module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.4.0' = { name: 'loganalytics' params: { name: logAnalyticsName location: location tags: tags + skuName: 'PerGB2018' + dataRetention: 30 + publicNetworkAccessForIngestion: publicNetworkAccess + publicNetworkAccessForQuery: publicNetworkAccess + useResourcePermissions: true } } -module applicationInsights 'applicationinsights.bicep' = { +module applicationInsights 'br/public:avm/res/insights/component:0.3.1' = { name: 'applicationinsights' params: { name: applicationInsightsName location: location tags: tags - dashboardName: applicationInsightsDashboardName - logAnalyticsWorkspaceId: logAnalytics.outputs.id + workspaceResourceId: logAnalytics.outputs.resourceId + publicNetworkAccessForIngestion: publicNetworkAccess + publicNetworkAccessForQuery: publicNetworkAccess + } +} + +module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(applicationInsightsDashboardName)) { + name: 'application-insights-dashboard' + params: { + name: applicationInsightsDashboardName + location: location + applicationInsightsName: applicationInsights.name } } output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsId string = applicationInsights.outputs.resourceId output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey output applicationInsightsName string = applicationInsights.outputs.name -output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceId string = logAnalytics.outputs.resourceId output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/networking/cdn-endpoint.bicep b/infra/core/networking/cdn-endpoint.bicep deleted file mode 100644 index e92ee89..0000000 --- a/infra/core/networking/cdn-endpoint.bicep +++ /dev/null @@ -1,51 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The name of the CDN profile resource') -@minLength(1) -param cdnProfileName string - -@description('Delivery policy rules') -param deliveryPolicyRules array = [] - -@description('The origin URL for the endpoint') -@minLength(1) -param originUrl string - -resource endpoint 'Microsoft.Cdn/profiles/endpoints@2022-05-01-preview' = { - parent: cdnProfile - name: name - location: location - tags: tags - properties: { - originHostHeader: originUrl - isHttpAllowed: false - isHttpsAllowed: true - queryStringCachingBehavior: 'UseQueryString' - optimizationType: 'GeneralWebDelivery' - origins: [ - { - name: replace(originUrl, '.', '-') - properties: { - hostName: originUrl - originHostHeader: originUrl - priority: 1 - weight: 1000 - enabled: true - } - } - ] - deliveryPolicy: { - rules: deliveryPolicyRules - } - } -} - -resource cdnProfile 'Microsoft.Cdn/profiles@2022-05-01-preview' existing = { - name: cdnProfileName -} - -output id string = endpoint.id -output name string = endpoint.name -output uri string = 'https://${endpoint.properties.hostName}' diff --git a/infra/core/networking/cdn-profile.bicep b/infra/core/networking/cdn-profile.bicep deleted file mode 100644 index 8d70f54..0000000 --- a/infra/core/networking/cdn-profile.bicep +++ /dev/null @@ -1,33 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The pricing tier of this CDN profile') -@allowed([ - 'Custom_Verizon' - 'Premium_AzureFrontDoor' - 'Premium_Verizon' - 'StandardPlus_955BandWidth_ChinaCdn' - 'StandardPlus_AvgBandWidth_ChinaCdn' - 'StandardPlus_ChinaCdn' - 'Standard_955BandWidth_ChinaCdn' - 'Standard_Akamai' - 'Standard_AvgBandWidth_ChinaCdn' - 'Standard_AzureFrontDoor' - 'Standard_ChinaCdn' - 'Standard_Microsoft' - 'Standard_Verizon' -]) -param sku string = 'Standard_Microsoft' - -resource profile 'Microsoft.Cdn/profiles@2022-05-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: sku - } -} - -output id string = profile.id -output name string = profile.name diff --git a/infra/core/networking/cdn.bicep b/infra/core/networking/cdn.bicep deleted file mode 100644 index 2177c19..0000000 --- a/infra/core/networking/cdn.bicep +++ /dev/null @@ -1,42 +0,0 @@ -// Module to create a CDN profile with a single endpoint -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the CDN endpoint resource') -param cdnEndpointName string - -@description('Name of the CDN profile resource') -param cdnProfileName string - -@description('Delivery policy rules') -param deliveryPolicyRules array = [] - -@description('Origin URL for the CDN endpoint') -param originUrl string - -module cdnProfile 'cdn-profile.bicep' = { - name: 'cdn-profile' - params: { - name: cdnProfileName - location: location - tags: tags - } -} - -module cdnEndpoint 'cdn-endpoint.bicep' = { - name: 'cdn-endpoint' - params: { - name: cdnEndpointName - location: location - tags: tags - cdnProfileName: cdnProfile.outputs.name - originUrl: originUrl - deliveryPolicyRules: deliveryPolicyRules - } -} - -output endpointName string = cdnEndpoint.outputs.name -output endpointId string = cdnEndpoint.outputs.id -output profileName string = cdnProfile.outputs.name -output profileId string = cdnProfile.outputs.id -output uri string = cdnEndpoint.outputs.uri diff --git a/infra/core/search/search-services.bicep b/infra/core/search/search-services.bicep deleted file mode 100644 index 399a8f3..0000000 --- a/infra/core/search/search-services.bicep +++ /dev/null @@ -1,62 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'standard' -} - -param authOptions object = {} -param disableLocalAuth bool = false -param disabledDataExfiltrationOptions array = [] -param encryptionWithCmk object = { - enforcement: 'Unspecified' -} -@allowed([ - 'default' - 'highDensity' -]) -param hostingMode string = 'default' -param networkRuleSet object = { - bypass: 'None' - ipRules: [] -} -param partitionCount int = 1 -@allowed([ - 'enabled' - 'disabled' -]) -param publicNetworkAccess string = 'enabled' -param replicaCount int = 1 -@allowed([ - 'disabled' - 'free' - 'standard' -]) -param semanticSearch string = 'disabled' - -resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { - name: name - location: location - tags: tags - identity: { - type: 'SystemAssigned' - } - properties: { - authOptions: authOptions - disableLocalAuth: disableLocalAuth - disabledDataExfiltrationOptions: disabledDataExfiltrationOptions - encryptionWithCmk: encryptionWithCmk - hostingMode: hostingMode - networkRuleSet: networkRuleSet - partitionCount: partitionCount - publicNetworkAccess: publicNetworkAccess - replicaCount: replicaCount - semanticSearch: semanticSearch - } - sku: sku -} - -output id string = search.id -output endpoint string = 'https://${name}.search.windows.net/' -output name string = search.name diff --git a/infra/core/security/registry-access.bicep b/infra/core/security/registry-access.bicep deleted file mode 100644 index e17e404..0000000 --- a/infra/core/security/registry-access.bicep +++ /dev/null @@ -1,18 +0,0 @@ -param containerRegistryName string -param principalId string - -var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - -resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry // Use when specifying a scope that is different than the deployment scope - name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) - properties: { - roleDefinitionId: acrPullRole - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { - name: containerRegistryName -} diff --git a/infra/core/security/role.bicep b/infra/core/security/role.bicep index dca01e1..0b30cfd 100644 --- a/infra/core/security/role.bicep +++ b/infra/core/security/role.bicep @@ -1,3 +1,4 @@ +metadata description = 'Creates a role assignment for a service principal.' param principalId string @allowed([ diff --git a/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep deleted file mode 100644 index 53d449b..0000000 --- a/infra/core/storage/storage-account.bicep +++ /dev/null @@ -1,61 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@allowed([ - 'Cool' - 'Hot' - 'Premium' ]) -param accessTier string = 'Hot' -param allowBlobPublicAccess bool = true -param allowCrossTenantReplication bool = true -param allowSharedKeyAccess bool = true -param containers array = [] -param defaultToOAuthAuthentication bool = false -param deleteRetentionPolicy object = {} -@allowed([ 'AzureDnsZone', 'Standard' ]) -param dnsEndpointType string = 'Standard' -param kind string = 'StorageV2' -param minimumTlsVersion string = 'TLS1_2' -param networkAcls object = { - bypass: 'AzureServices' - defaultAction: 'Allow' -} -@allowed([ 'Enabled', 'Disabled' ]) -param publicNetworkAccess string = 'Enabled' -param sku object = { name: 'Standard_LRS' } - -resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { - name: name - location: location - tags: tags - kind: kind - sku: sku - properties: { - accessTier: accessTier - allowBlobPublicAccess: allowBlobPublicAccess - allowCrossTenantReplication: allowCrossTenantReplication - allowSharedKeyAccess: allowSharedKeyAccess - defaultToOAuthAuthentication: defaultToOAuthAuthentication - dnsEndpointType: dnsEndpointType - minimumTlsVersion: minimumTlsVersion - networkAcls: networkAcls - publicNetworkAccess: publicNetworkAccess - } - - resource blobServices 'blobServices' = if (!empty(containers)) { - name: 'default' - properties: { - deleteRetentionPolicy: deleteRetentionPolicy - } - resource container 'containers' = [for container in containers: { - name: container.name - properties: { - publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' - } - }] - } -} - -output name string = storage.name -output primaryEndpoints object = storage.properties.primaryEndpoints diff --git a/infra/main.bicep b/infra/main.bicep index 60319e2..ed4d777 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -11,6 +11,9 @@ param location string @description('Id of the user or app to assign application roles') param principalId string = '' +@description('SKU to use for App Service Plan') +param appServiceSku string + var mongoClusterName = '${uniqueString(resourceGroup.id)}-mvcore' var mongoAdminUser = 'admin${uniqueString(resourceGroup.id)}' @secure() @@ -109,7 +112,7 @@ module appServicePlan 'core/host/appserviceplan.bicep' = { location: location tags: tags sku: { - name: 'B1' + name: appServiceSku } reserved: true } @@ -133,12 +136,12 @@ module mongoCluster 'core/database/cosmos/mongo/cosmos-mongo-cluster.bicep' = { module keyVaultSecrets './core/security/keyvault-secret.bicep' = { dependsOn: [ mongoCluster ] - name: 'keyvault-secret-mongo-connstr' + name: 'keyvault-secret-mongo-password' scope: resourceGroup params: { - name: 'mongoConnectionStr' + name: 'mongoAdminPassword' keyVaultName: keyVault.outputs.name - secretValue: replace(replace(mongoCluster.outputs.connectionStringKey, '', mongoAdminUser), '', mongoAdminPassword) + secretValue: mongoAdminPassword } } @@ -153,10 +156,12 @@ module web 'core/host/appservice.bicep' = { appServicePlanId: appServicePlan.outputs.id appCommandLine: 'entrypoint.sh' runtimeName: 'python' - runtimeVersion: '3.10' + runtimeVersion: '3.12' scmDoBuildDuringDeployment: true ftpsState: 'Disabled' managedIdentity: true + use32BitWorkerProcess: appServiceSku == 'F1' + alwaysOn: appServiceSku != 'F1' appSettings: { AZURE_OPENAI_DEPLOYMENT_NAME: openAIDeploymentName AZURE_OPENAI_ENDPOINT: openAi.outputs.endpoint @@ -165,7 +170,9 @@ module web 'core/host/appservice.bicep' = { AZURE_OPENAI_EMBEDDINGS_MODEL_NAME: embeddingModelName AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: embeddingDeploymentName AZURE_OPENAI_API_KEY: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=cognitiveServiceKey)' - AZURE_COSMOS_CONNECTION_STRING: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=mongoConnectionStr)' + AZURE_COSMOS_PASSWORD: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=mongoAdminPassword)' + AZURE_COSMOS_CONNECTION_STRING: mongoCluster.outputs.connectionStringKey + AZURE_COSMOS_USERNAME: mongoAdminUser AZURE_COSMOS_DATABASE_NAME: 'lc_database' AZURE_COSMOS_COLLECTION_NAME: 'lc_collection' } diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 79b8212..f7b250c 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -13,6 +13,9 @@ }, "mongoAdminPassword": { "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} mongoAdminPassword)" + }, + "appServiceSku": { + "value": "${AZURE_APP_SERVICE_SKU=B1}" } } } diff --git a/CBD_Mongo_vCore.ipynb b/rag-azure-openai-cosmosdb-langchain-notebook.ipynb similarity index 53% rename from CBD_Mongo_vCore.ipynb rename to rag-azure-openai-cosmosdb-langchain-notebook.ipynb index 6ef6403..280798c 100644 --- a/CBD_Mongo_vCore.ipynb +++ b/rag-azure-openai-cosmosdb-langchain-notebook.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -46,7 +46,7 @@ "True" ] }, - "execution_count": 2, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } @@ -57,7 +57,7 @@ "\n", "from dotenv import load_dotenv\n", "\n", - "load_dotenv()" + "load_dotenv(\".env\", override=True)" ] }, { @@ -71,19 +71,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Save the `api_type`, `api_base`, `api_version`, and `api_key` as global variables to avoid the need to supply them later in code." + "Save the `api_type`, `base_url`, `api_version`, and `api_key` as global variables to avoid the need to supply them later in code." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import openai\n", "\n", "openai.api_type = os.getenv(\"OPENAI_API_TYPE\", \"azure\")\n", - "openai.api_base = os.getenv(\"AZURE_OPENAI_ENDPOINT\", \"https://.openai.azure.com/\")\n", + "openai.base_url = os.getenv(\"AZURE_OPENAI_ENDPOINT\", \"https://.openai.azure.com/\")\n", "openai.api_version = os.getenv(\"OPENAI_API_VERSION\", \"2023-09-15-preview\")\n", "openai.api_key = os.getenv(\"OPENAI_API_KEY\", \"\")" ] @@ -97,23 +97,30 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/tmp/ipykernel_11099/3617200806.py:9: UserWarning: You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n", + "/var/folders/db/x_x115ns61154jxycm1mwr780000gn/T/ipykernel_29265/1114347389.py:14: UserWarning: You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n", " mongo_client = MongoClient(mongo_connection_string)\n" ] } ], "source": [ "from pymongo import MongoClient\n", + "from urllib.parse import quote_plus\n", "\n", "# Read and Store Environment variables\n", "mongo_connection_string = os.getenv(\"AZURE_COSMOS_CONNECTION_STRING\", \"\")\n", + "mongo_username = quote_plus(os.getenv(\"AZURE_COSMOS_USERNAME\"))\n", + "mongo_password = quote_plus(os.getenv(\"AZURE_COSMOS_PASSWORD\"))\n", + "mongo_connection_string = mongo_connection_string.replace(\"\", mongo_username).replace(\n", + " \"\", mongo_password\n", + ")\n", + "\n", "collection_name = os.getenv(\"AZURE_COSMOS_COLLECTION_NAME\", \"collectionName\")\n", "database_name = os.getenv(\"AZURE_COSMOS_DATABASE_NAME\", \"DatabaseName\")\n", "\n", @@ -136,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -146,21 +153,18 @@ "\n", "SOURCE_FILE_NAME = \"./src/data/food_items.json\"\n", "\n", + "\n", "def read_data(file_path) -> list[Document]:\n", " # Load JSON file\n", " with open(file_path) as file:\n", " json_data = json.load(file)\n", "\n", - "\n", " documents = []\n", " absolute_path = os.path.abspath(file_path)\n", " # Process each item in the JSON data\n", " for idx, item in enumerate(json_data):\n", " documents.append(\n", - " Document(\n", - " page_content=json.dumps(item),\n", - " metadata={'source': absolute_path, 'seq_num': idx+1}\n", - " )\n", + " Document(page_content=json.dumps(item), metadata={\"source\": absolute_path, \"seq_num\": idx + 1})\n", " )\n", "\n", " return documents" @@ -168,7 +172,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -177,14 +181,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "page_content='{\"category\": \"Smoothies\", \"name\": \"Jimmy Jam Smoothie\", \"description\": \"Berries n kale, strawberries, bananas, blueberries kale, tropical fruit blend, and dragon fruit. Our fruity tasty smoothies are blended to perfection.\", \"price\": \"5.49 USD\"}' metadata={'source': '/home/john/repos/Cosmic-Food-RAG-app/src/data/food_items.json', 'seq_num': 2}\n" + "page_content='{\"category\": \"Smoothies\", \"name\": \"Jimmy Jam Smoothie\", \"description\": \"Berries n kale, strawberries, bananas, blueberries kale, tropical fruit blend, and dragon fruit. Our fruity tasty smoothies are blended to perfection.\", \"price\": \"5.49 USD\"}' metadata={'source': '/Users/john0isaac/Developer/Cosmic-Food-RAG-app/src/data/food_items.json', 'seq_num': 2}\n" ] } ], @@ -202,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -226,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -246,14 +250,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/home/john/repos/Cosmic-Food-RAG-app/.venv/lib/python3.10/site-packages/langchain_community/vectorstores/azure_cosmos_db.py:146: UserWarning: You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n", + "/Users/john0isaac/Developer/Cosmic-Food-RAG-app/.venv/lib/python3.10/site-packages/langchain_community/vectorstores/azure_cosmos_db.py:146: UserWarning: You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n", " client: MongoClient = MongoClient(connection_string, appname=appname)\n" ] } @@ -262,7 +266,7 @@ "from langchain_community.vectorstores.azure_cosmos_db import AzureCosmosDBVectorSearch\n", "\n", "# Run this to connect to the vector store\n", - "vector_store: AzureCosmosDBVectorSearch = AzureCosmosDBVectorSearch.from_connection_string(\n", + "vector_store: AzureCosmosDBVectorSearch = AzureCosmosDBVectorSearch.from_connection_string(\n", " connection_string=mongo_connection_string,\n", " namespace=f\"{database_name}.{collection_name}\",\n", " embedding=azure_openai_embeddings,\n", @@ -278,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -292,7 +296,7 @@ " 'ok': 1}" ] }, - "execution_count": 11, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -312,9 +316,7 @@ "ef_construction = 64\n", "\n", "# Create the collection and the index\n", - "vector_store.create_index(\n", - " num_lists, dimensions, similarity_algorithm, kind, m, ef_construction\n", - ")" + "vector_store.create_index(num_lists, dimensions, similarity_algorithm, kind, m, ef_construction)" ] }, { @@ -326,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -352,14 +354,14 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "from langchain_openai import AzureChatOpenAI\n", "\n", "openai_chat_model = os.getenv(\"AZURE_OPENAI_CHAT_MODEL_NAME\", \"gpt-35-turbo\")\n", - "openai_chat_deployment= os.getenv(\"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME\", \"chat-gpt\")\n", + "openai_chat_deployment = os.getenv(\"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME\", \"chat-gpt\")\n", "\n", "azure_openai_chat: AzureChatOpenAI = AzureChatOpenAI(\n", " model=openai_chat_model,\n", @@ -369,14 +371,16 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Why did the tomato turn red? Because it saw the salad dressing!\n" + "Why did the tomato turn red?\n", + "\n", + "Because it saw the salad dressing!\n" ] } ], @@ -388,45 +392,27 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Education is the backbone of society. It is the foundation that enables individuals to acquire knowledge and skills, and it is through education that individuals are able to grow and develop. Education is not just about acquiring knowledge; it is also about learning how to think critically, how to communicate effectively, and how to become productive members of society. In this essay, I will explore the importance of education, the challenges facing education, and the future of education.\n", - "\n", - "The Importance of Education\n", - "\n", - "Education is important because it provides individuals with the knowledge and skills they need to succeed in life. Education enables individuals to acquire the skills they need to enter the workforce and to become productive members of society. It also provides individuals with the knowledge they need to make informed decisions about their lives, including decisions about their health, finances, and relationships.\n", - "\n", - "Education is also important because it promotes social mobility. Education provides individuals with the opportunity to move up the social ladder, regardless of their background. Education is the great equalizer, and it is through education that individuals can overcome the barriers of poverty, discrimination, and inequality.\n", - "\n", - "Challenges Facing Education\n", - "\n", - "Despite the importance of education, there are many challenges facing education today. One of the biggest challenges is the lack of funding. Education is expensive, and many schools and universities struggle to provide the resources they need to provide a quality education to their students. This lack of funding often leads to overcrowded classrooms, outdated materials and technology, and a lack of opportunities for students.\n", + "Education is a fundamental right of every individual and plays a crucial role in determining their future success. It is the process of acquiring knowledge, skills, values, and attitudes that enable individuals to lead a meaningful and fulfilling life. Education is not only important for personal development but also for the development of society as a whole.\n", "\n", - "Another challenge facing education is the lack of access to education for marginalized communities. Many individuals, particularly those living in poverty or in rural areas, do not have access to quality education. This lack of access often perpetuates the cycle of poverty and inequality, as individuals without education are unable to secure jobs that pay a living wage.\n", + "Education provides individuals with the necessary skills and knowledge to succeed in their chosen field. It equips them with the ability to think critically, solve problems, and make informed decisions. Education also helps individuals to develop their creativity and imagination, which is essential in today's rapidly changing world.\n", "\n", - "Finally, there is also a challenge of the quality of education. While some schools and universities provide a quality education, others do not. This often leads to a disparity in the quality of education that students receive, and this disparity can lead to a lack of opportunities for some individuals.\n", + "Moreover, education plays a vital role in promoting societal development. It helps to create a more educated and informed population, which can contribute to the growth and development of the economy. Education also promotes social cohesion and helps to reduce social inequalities by providing equal opportunities to all individuals.\n", "\n", - "The Future of Education\n", + "However, access to education is still a challenge for many people, especially in developing countries. Lack of access to education can lead to poverty, unemployment, and limited opportunities for personal growth. Therefore, there is a need to provide equal access to education for everyone, regardless of their background or economic status.\n", "\n", - "Despite the challenges facing education, there is also a bright future for education. With advances in technology, education is becoming more accessible than ever before. Online learning platforms, such as Coursera and Khan Academy, are providing individuals with access to quality education from the comfort of their own homes.\n", - "\n", - "In addition, there is also a growing emphasis on experiential learning. Experiential learning involves learning through doing, and it is becoming an increasingly popular method of education. This method of education provides students with the opportunity to apply what they have learned in real-world situations, which can lead to a deeper understanding of the material.\n", - "\n", - "Finally, there is also a growing emphasis on lifelong learning. Lifelong learning involves the idea that education is not just something that happens in school; it is something that happens throughout our lives. Lifelong learning encourages individuals to continue learning and growing throughout their lives, which can lead to personal and professional growth.\n", - "\n", - "Conclusion\n", - "\n", - "Education is the foundation of society. It provides individuals with the knowledge and skills they need to succeed in life, and it is through education that individuals can overcome the barriers of poverty, discrimination, and inequality. While there are many challenges facing education, there is also a bright future for education. With advances in technology, experiential learning, and lifelong learning, education is becoming more accessible and more effective than ever before. As we move forward, it is important to continue to invest in education and to ensure that everyone has access to quality education." + "In conclusion, education is a basic human right that should be accessible to all individuals. It is essential for personal development, societal growth, and economic prosperity. Therefore, it is the responsibility of governments, institutions, and individuals to ensure that everyone has access to quality education." ] } ], "source": [ - "chat_response = azure_openai_chat.astream(\"Write a 500 words essay about education.\")\n", + "chat_response = azure_openai_chat.astream(\"Write a 200 words essay about education.\")\n", "\n", "async for response in chat_response:\n", " print(response.content, end=\"\")" @@ -441,37 +427,57 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "from langchain.prompts import ChatPromptTemplate\n", - "from langchain_core.prompts import MessagesPlaceholder\n", - "\n", - "history_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " MessagesPlaceholder(variable_name=\"chat_history\"),\n", - " (\"user\", \"{input}\"),\n", - " (\n", - " \"user\",\n", - " \"\"\"Given the above conversation,\n", - " generate a search query to look up to get information relevant to the conversation\"\"\",\n", - " ),\n", - " ]\n", - ")\n", "\n", - "context_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", \"Answer the user's questions based on the below context:\\n\\n{context}\"),\n", - " MessagesPlaceholder(variable_name=\"chat_history\"),\n", - " (\"user\", \"{input}\"),\n", - " ]\n", - ")" + "\n", + "REPHRASE_PROMPT = \"\"\"\\\n", + "Given the following conversation and a follow up question, rephrase the follow up \\\n", + "question to be a standalone question.\n", + "\n", + "Chat History:\n", + "{chat_history}\n", + "Follow Up Input: {question}\n", + "Standalone Question:\"\"\"\n", + "\n", + "CONTEXT_PROMPT = \"\"\"\\\n", + "You are a restaurant chatbot, tasked with answering any question about \\\n", + "food dishes from the contex.\n", + "\n", + "Generate a response of 80 words or less for the \\\n", + "given question based solely on the provided search results (name, description, price, and category). \\\n", + "You must only use information from the provided search results. Use an unbiased and \\\n", + "fun tone. Do not repeat text. Your response must be solely based on the provided context.\n", + "\n", + "If there is nothing in the context relevant to the question at hand, just say \"Hmm, \\\n", + "I'm not sure.\" Don't try to make up an answer.\n", + "\n", + "Anything between the following `context` html blocks is retrieved from a knowledge \\\n", + "bank, not part of the conversation with the user. \n", + "\n", + "\n", + " {context} \n", + "\n", + "\n", + "REMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm \\\n", + "not sure.\" Don't try to make up an answer. Anything between the preceding 'context' \\\n", + "html blocks is retrieved from a knowledge bank, not part of the conversation with the \\\n", + "user.\\\n", + "\n", + "User Question: {input}\n", + "\n", + "Chatbot Response:\"\"\"\n", + "\n", + "rephrase_prompt_template = ChatPromptTemplate.from_template(REPHRASE_PROMPT)\n", + "context_prompt_template = ChatPromptTemplate.from_template(CONTEXT_PROMPT)" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -482,28 +488,30 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2024-07-19 16:25:27,396] {logger.py:101} INFO - You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n", + "[2024-07-19 16:25:27,415] {logger.py:101} INFO - You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n", + "[2024-07-19 16:25:27,433] {logger.py:101} INFO - You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb\n" + ] + } + ], "source": [ - "from langchain.chains.combine_documents import create_stuff_documents_chain\n", - "from langchain.chains.history_aware_retriever import create_history_aware_retriever\n", - "from langchain.chains.retrieval import create_retrieval_chain\n", - "from langchain_core.runnables import Runnable\n", + "from quartapp.approaches.rag import get_data_points\n", "\n", + "# Vector Store Retriever\n", "vector_store_retriever = vector_store.as_retriever(\n", " search_type=search_type, search_kwargs={\"k\": limit, \"score_threshold\": score_threshold}\n", ")\n", - "\n", - "\n", - "retriever_chain = create_history_aware_retriever(azure_openai_chat, vector_store_retriever, history_prompt)\n", - "\n", - "context_chain = create_stuff_documents_chain(llm=azure_openai_chat, prompt=context_prompt)\n", - "\n", - "rag_chain: Runnable = create_retrieval_chain(\n", - " retriever=retriever_chain,\n", - " combine_docs_chain=context_chain,\n", - ")" + "# Rephrase Chain\n", + "rephrase_chain = rephrase_prompt_template | azure_openai_chat\n", + "# Context Chain\n", + "context_chain = context_prompt_template | azure_openai_chat" ] }, { @@ -515,137 +523,137 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 17, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2024-07-19 16:25:34,499] {_client.py:1026} INFO - HTTP Request: POST https://build-24-openai.openai.azure.com//openai/deployments/chat-gpt/chat/completions?api-version=2023-09-15-preview \"HTTP/1.1 200 OK\"\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "Yes, we have two vegan options: the Beyond Burger and the Tofu Salad Sandwich.\n" + "What vegan options do you offer?\n" ] } ], "source": [ - "first_question = \"Do you have any vegan options?\"\n", - "chat_history = []\n", - "response = rag_chain.invoke({\"input\": first_question, \"chat_history\": chat_history})\n", - "print(response['answer'])" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "chat_history.extend([HumanMessage(content=first_question), response[\"answer\"]])\n", + "# 1. Rephrase the question\n", + "messages = [{\"content\": \"Do you have any vegan options?\", \"role\": \"user\"}]\n", "\n", - "second_question = \"What did I just ask you about?\"\n", - "response = rag_chain.invoke({\"input\": second_question, \"chat_history\": chat_history})" + "rephrased_question = rephrase_chain.invoke({\"chat_history\": messages[:-1], \"question\": messages[-1]})\n", + "print(rephrased_question.content)" ] }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 18, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2024-07-19 16:25:37,722] {_client.py:1026} INFO - HTTP Request: POST https://build-24-openai.openai.azure.com//openai/deployments/text-embedding/embeddings?api-version=2023-09-15-preview \"HTTP/1.1 200 OK\"\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "You asked if there are any vegan options available.\n" + "[DataPoint(name='Beyond Burger', description='Served with Romaine lettuce, tomato, pickle, vegan mayonnaise, ketchup, and mustard on a toasted bun. Sandwich made with whole wheat bread. Can be made as a wrap in a whole wheat tortilla. Served with kettle potato chips or corn tortilla chips.', price='9.0 USD', category='Sandwiches', collection=None), DataPoint(name='Tofu Salad Sandwich', description='Served with Romaine lettuce, tomato, vegan mayonnaise, and mustard. Sandwich made with whole wheat bread. Can be made as a wrap in a whole wheat tortilla. Served with kettle potato chips or corn tortilla chips.', price='9.0 USD', category='Sandwiches', collection=None), DataPoint(name=\"Boca Chik'n Sandwich\", description='Served with Romaine lettuce, tomato, pickle, vegan mayonnaise, ketchup, and mustard on a toasted bun. Sandwich made with whole wheat bread. Can be made as a wrap in a whole wheat tortilla. Served with kettle potato chips or corn tortilla chips.', price='9.0 USD', category='Sandwiches', collection=None)]\n" ] } ], "source": [ - "print(response['answer'])" + "# 2. Get the context from the database and format it to remove the embeddings\n", + "vector_context = vector_store_retriever.invoke(str(rephrased_question.content))\n", + "data_points = get_data_points(vector_context)\n", + "print(data_points)" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 19, "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2024-07-19 16:25:41,757] {_client.py:1026} INFO - HTTP Request: POST https://build-24-openai.openai.azure.com//openai/deployments/chat-gpt/chat/completions?api-version=2023-09-15-preview \"HTTP/1.1 200 OK\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We have several vegan options available, including the Beyond Burger, Tofu Salad Sandwich, and Boca Chik'n Sandwich. All of these sandwiches come with vegan mayonnaise, and can be made as a wrap in a whole wheat tortilla. They are each served with kettle potato chips or corn tortilla chips.\n" + ] + } + ], "source": [ - "# Test with Gradio" + "# 3. Generate a response based on the context\n", + "response = context_chain.invoke({\"context\": [dp.to_dict() for dp in data_points], \"input\": rephrased_question.content})\n", + "print(response.content)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ - "%pip install ipywidgets gradio" + "# 4. Store the chat history and the response\n", + "messages.append({\"content\": response.content, \"role\": \"assistant\"})" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2024-07-19 16:25:48,395] {_client.py:1026} INFO - HTTP Request: POST https://build-24-openai.openai.azure.com//openai/deployments/chat-gpt/chat/completions?api-version=2023-09-15-preview \"HTTP/1.1 200 OK\"\n", + "[2024-07-19 16:25:48,942] {_client.py:1026} INFO - HTTP Request: POST https://build-24-openai.openai.azure.com//openai/deployments/text-embedding/embeddings?api-version=2023-09-15-preview \"HTTP/1.1 200 OK\"\n", + "[2024-07-19 16:25:50,271] {_client.py:1026} INFO - HTTP Request: POST https://build-24-openai.openai.azure.com//openai/deployments/chat-gpt/chat/completions?api-version=2023-09-15-preview \"HTTP/1.1 200 OK\"\n" + ] + } + ], "source": [ - "import gradio as gr\n", - "\n", + "# Test with another question to see if the chat history is maintained\n", + "messages.append({\"content\": \"what is the price of the first dish?\", \"role\": \"user\"})\n", "\n", - "def setup_gradio_interface(chain): \n", - " with gr.Blocks() as demo_interface:\n", - " chatbot = gr.Chatbot(label=\"Food Ordering System\")\n", - " chat_history = gr.State([])\n", - " lc_chat_history = gr.State([])\n", - " msg = gr.Textbox(label=\"Your question\")\n", - " gr.ClearButton([msg, chatbot])\n", - " \n", - " def fetch_response(message, chat_history, lc_chat_history):\n", - " response = chain.invoke({\"question\": message, \"chat_history\": lc_chat_history})\n", - " lc_chat_history.append((message, response['answer']))\n", - " chat_history.append([message, response[\"answer\"]])\n", - " return \"\", chat_history, lc_chat_history\n", - "\n", - " msg.submit(fetch_response, inputs=[msg, chatbot, lc_chat_history], outputs=[msg, chatbot, lc_chat_history])\n", - " \n", - " return demo_interface" + "rephrased_question = rephrase_chain.invoke({\"chat_history\": messages[:-1], \"question\": messages[-1]})\n", + "vector_context = vector_store_retriever.invoke(str(rephrased_question.content))\n", + "data_points = get_data_points(vector_context)\n", + "response = context_chain.invoke({\"context\": [dp.to_dict() for dp in data_points], \"input\": rephrased_question.content})" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Running on local URL: http://127.0.0.1:7860\n", - "\n", - "To create a public link, set `share=True` in `launch()`.\n" + "Rephrased Question: What is the price of the Beyond Burger?\n", + "LLM Response: The price of the Beyond Burger is 9.0 USD. It's a delicious vegan burger served with Romaine lettuce, tomato, pickle, vegan mayonnaise, ketchup, and mustard on a toasted bun. You can also choose to have it made as a wrap in a whole wheat tortilla. It comes with your choice of kettle potato chips or corn tortilla chips.\n" ] - }, - { - "data": { - "text/html": [ - "
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "food_ordering_demo = setup_gradio_interface(rag_chain)\n", - "food_ordering_demo.launch()" + "print(\"Rephrased Question: \", rephrased_question.content)\n", + "print(\"LLM Response: \", response.content)" ] }, { @@ -692,35 +700,23 @@ " \"m\": 4,\n", " \"efConstruction\": 16,\n", " \"similarity\": \"COS\",\n", - " \"dimensions\": 1536\n", - " }\n", + " \"dimensions\": 1536,\n", + " },\n", " }\n", - " ]\n", + " ],\n", "}\n", - "db.command(createIndexCommand)\n" + "db.command(createIndexCommand)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Score: 1.000] Ashunti`Way Smoothie: Fruit n greens, mango bananas, tropical fruit blend, dragon fruit mix, mango, bananas, pineapples, apples, and spinach. Special green with strawberry bananas juice blend . Our fruity tasty smoothies are blended to perfection.\n", - "[Score: 0.986] Dayton 500 Smoothie: Tropical fruit blend, dragon fruit mix, mango, bananas, pineapples, apples. Special green juice blend. Our fruity tasty smoothies are blended to perfection.\n", - "[Score: 0.973] Tongue Teaser Smoothie: Tropical fruit blend, dragon fruit, pineapples, bananas, mango, apples, spinach, ginger powder. Special green blend, pineapple and ginger smoothies. Our fruity tasty smoothies are blended to perfection.\n", - "[Score: 0.967] Tejay Impact Smoothie: Tropical fruit blend, dragon fruit mix, mango, bananas, pineapples, apples, and spinach. Special blue juice blend smoothies.\n", - "[Score: 0.961] Jimmy Jam Smoothie: Berries n kale, strawberries, bananas, blueberries kale, tropical fruit blend, and dragon fruit. Our fruity tasty smoothies are blended to perfection.\n" - ] - } - ], + "outputs": [], "source": [ - "search_pipeline = [ \n", - " { \"$search\": { \"cosmosSearch\": { \"query\": docs[0][\"description\"], \"k\": 5, \"path\": \"embeddings\", \"efSearch\": 100 }}} , \n", - " { \"$project\": { \"similarityScore\": { \"$meta\": \"searchScore\" }, \"_id\":0, \"name\":1, \"description\":1 } }\n", + "search_pipeline = [\n", + " {\"$search\": {\"cosmosSearch\": {\"query\": docs[0][\"description\"], \"k\": 5, \"path\": \"embeddings\", \"efSearch\": 100}}},\n", + " {\"$project\": {\"similarityScore\": {\"$meta\": \"searchScore\"}, \"_id\": 0, \"name\": 1, \"description\": 1}},\n", "]\n", "\n", "results = collection.aggregate(search_pipeline)\n", @@ -746,7 +742,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/quartapp/config_base.py b/src/quartapp/config_base.py index 6c8ba6c..fb04245 100644 --- a/src/quartapp/config_base.py +++ b/src/quartapp/config_base.py @@ -1,6 +1,7 @@ import json import os from abc import ABC, abstractmethod +from urllib.parse import quote_plus from langchain_core.documents import Document from pydantic.v1 import SecretStr @@ -15,13 +16,23 @@ from quartapp.approaches.setup import Setup +def read_and_parse_connection_string() -> str: + mongo_connection_string = os.getenv("AZURE_COSMOS_CONNECTION_STRING", "YOUR-COSMOS-DB-CONNECTION-STRING") + mongo_username = quote_plus(os.getenv("AZURE_COSMOS_USERNAME", "YOUR-COSMOS-DB-USERNAME")) + mongo_password = quote_plus(os.getenv("AZURE_COSMOS_PASSWORD", "YOUR-COSMOS-DB-PASSWORD")) + mongo_connection_string = mongo_connection_string.replace("", mongo_username).replace( + "", mongo_password + ) + return mongo_connection_string + + class AppConfigBase(ABC): def __init__(self) -> None: openai_embeddings_model = os.getenv("AZURE_OPENAI_EMBEDDINGS_MODEL_NAME", "text-embedding-ada-002") openai_embeddings_deployment = os.getenv("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME", "text-embedding") openai_chat_model = os.getenv("AZURE_OPENAI_CHAT_MODEL_NAME", "gpt-35-turbo") openai_chat_deployment = os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "chat-gpt") - connection_string = os.getenv("AZURE_COSMOS_CONNECTION_STRING", "") + connection_string = read_and_parse_connection_string() database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME", "") collection_name = os.getenv("AZURE_COSMOS_COLLECTION_NAME", "") index_name = os.getenv("AZURE_COSMOS_INDEX_NAME", "") diff --git a/tests/test_app_endpoints.py b/tests/test_app_endpoints.py index e57e197..8b95196 100644 --- a/tests/test_app_endpoints.py +++ b/tests/test_app_endpoints.py @@ -40,7 +40,6 @@ async def test_favicon(client): favicon_file = f.read() assert response.status_code == 200 - assert response.content_type == "image/vnd.microsoft.icon" assert response.headers["Content-Length"] == str(len(favicon_file)) assert favicon_file == await response.data