diff --git a/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json b/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json new file mode 100644 index 0000000000..511420d5d8 --- /dev/null +++ b/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json @@ -0,0 +1,221 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "appInsightsName": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, + "appInsightsLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "qnaMakerServiceName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-qna')]" + }, + "qnaMakerServiceSku": { + "type": "string", + "defaultValue": "S0" + }, + "qnaMakerServiceLocation": { + "type": "string", + "defaultValue": "westus" + }, + "qnaMakerSearchName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-search')]" + }, + "qnaMakerSearchSku": { + "type": "string", + "defaultValue": "standard" + }, + "qnaMakerSearchLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "qnaMakerWebAppName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-qnahost')]" + }, + "qnaMakerWebAppLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "qnaMakerSearchName": "[toLower(replace(parameters('qnaMakerSearchName'), '_', ''))]", + "qnaMakerWebAppName": "[replace(parameters('qnaMakerWebAppName'), '_', '')]" + }, + "resources": [ + { + "apiVersion": "2018-02-01", + "name": "1d41002f-62a1-49f3-bd43-2f3f32a19cbb", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } + } + }, + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "app insights", + "type": "Microsoft.Insights/components", + "kind": "web", + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "location": "[parameters('appInsightsLocation')]", + "properties": { + "Application_Type": "web" + } + }, + { + "comments": "Cognitive service key for all QnA Maker knowledgebases.", + "type": "Microsoft.CognitiveServices/accounts", + "kind": "QnAMaker", + "apiVersion": "2017-04-18", + "name": "[parameters('qnaMakerServiceName')]", + "location": "[parameters('qnaMakerServiceLocation')]", + "sku": { + "name": "[parameters('qnaMakerServiceSku')]" + }, + "properties": { + "apiProperties": { + "qnaRuntimeEndpoint": "[concat('https://',reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", + "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]", + "[resourceId('microsoft.insights/components/', parameters('appInsightsName'))]" + ] + }, + { + "comments": "Search service for QnA Maker service.", + "type": "Microsoft.Search/searchServices", + "apiVersion": "2015-08-19", + "name": "[variables('qnaMakerSearchName')]", + "location": "[parameters('qnaMakerSearchLocation')]", + "sku": { + "name": "[parameters('qnaMakerSearchSku')]" + }, + "properties": { + "replicaCount": 1, + "partitionCount": 1, + "hostingMode": "default" + } + }, + { + "comments": "Web app for QnA Maker service.", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('qnaMakerWebAppName')]", + "location": "[parameters('qnaMakerWebAppLocation')]", + "properties": { + "enabled": true, + "name": "[variables('qnaMakerWebAppName')]", + "hostingEnvironment": "", + "serverFarmId": "[concat('/subscriptions/', Subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('servicePlanName'))]", + "siteConfig": { + "cors": { + "allowedOrigins": [ + "*" + ] + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "resources": [ + { + "apiVersion": "2016-08-01", + "name": "appsettings", + "type": "config", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", + "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]", + "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]" + ], + "properties": { + "AzureSearchName": "[variables('qnaMakerSearchName')]", + "AzureSearchAdminKey": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName')), '2015-08-19').primaryKey]", + "UserAppInsightsKey": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').InstrumentationKey]", + "UserAppInsightsName": "[parameters('appInsightsName')]", + "UserAppInsightsAppId": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').AppId]", + "PrimaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-PrimaryEndpointKey')]", + "SecondaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-SecondaryEndpointKey')]", + "DefaultAnswer": "No good match found in KB.", + "QNAMAKER_EXTENSION_VERSION": "latest" + } + } + ] + } + ], + "outputs": { + "qna": { + "type": "object", + "value": { + "endpoint": "[concat('https://', reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]", + "subscriptionKey": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('qnaMakerServiceName')),'2017-04-18').key1]" + } + } + } + } + \ No newline at end of file diff --git a/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/template-with-preexisting-rg.json b/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/template-with-preexisting-rg.json index d385e94444..81c8531adc 100644 --- a/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/template-with-preexisting-rg.json +++ b/Composer/plugins/samples/assets/shared/scripts/DeploymentTemplates/template-with-preexisting-rg.json @@ -34,10 +34,6 @@ "type": "bool", "defaultValue": true }, - "shouldCreateQnAResource": { - "type": "bool", - "defaultValue": true - }, "cosmosDbName": { "type": "string", "defaultValue": "[resourceGroup().name]" @@ -134,38 +130,6 @@ "luisServiceLocation": { "type": "string", "defaultValue": "[resourceGroup().location]" - }, - "qnaMakerServiceName": { - "type": "string", - "defaultValue": "[concat(parameters('name'), '-qna')]" - }, - "qnaMakerServiceSku": { - "type": "string", - "defaultValue": "S0" - }, - "qnaMakerServiceLocation": { - "type": "string", - "defaultValue": "westus" - }, - "qnaMakerSearchName": { - "type": "string", - "defaultValue": "[concat(parameters('name'), '-search')]" - }, - "qnaMakerSearchSku": { - "type": "string", - "defaultValue": "standard" - }, - "qnaMakerSearchLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "qnaMakerWebAppName": { - "type": "string", - "defaultValue": "[concat(parameters('name'), '-qnahost')]" - }, - "qnaMakerWebAppLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]" } }, "variables": { @@ -178,9 +142,7 @@ "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", "storageAccountName": "[toLower(take(replace(replace(parameters('storageAccountName'), '-', ''), '_', ''), 24))]", - "LuisAuthoringAccountName": "[concat(parameters('luisServiceName'), '-Authoring')]", - "qnaMakerSearchName": "[toLower(replace(parameters('qnaMakerSearchName'), '_', ''))]", - "qnaMakerWebAppName": "[replace(parameters('qnaMakerWebAppName'), '_', '')]" + "LuisAuthoringAccountName": "[concat(parameters('luisServiceName'), '-Authoring')]" }, "resources": [ { @@ -388,92 +350,6 @@ "name": "[parameters('luisServiceRunTimeSku')]" }, "condition": "[parameters('shouldCreateLuisResource')]" - }, - { - "comments": "Cognitive service key for all QnA Maker knowledgebases.", - "type": "Microsoft.CognitiveServices/accounts", - "kind": "QnAMaker", - "apiVersion": "2017-04-18", - "name": "[parameters('qnaMakerServiceName')]", - "location": "[parameters('qnaMakerServiceLocation')]", - "sku": { - "name": "[parameters('qnaMakerServiceSku')]" - }, - "properties": { - "apiProperties": { - "qnaRuntimeEndpoint": "[concat('https://',reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", - "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]", - "[resourceId('microsoft.insights/components/', parameters('appInsightsName'))]" - ], - "condition": "[parameters('shouldCreateQnAResource')]" - }, - { - "comments": "Search service for QnA Maker service.", - "type": "Microsoft.Search/searchServices", - "apiVersion": "2015-08-19", - "name": "[variables('qnaMakerSearchName')]", - "location": "[parameters('qnaMakerSearchLocation')]", - "sku": { - "name": "[parameters('qnaMakerSearchSku')]" - }, - "properties": { - "replicaCount": 1, - "partitionCount": 1, - "hostingMode": "default" - }, - "condition": "[parameters('shouldCreateQnAResource')]" - }, - { - "comments": "Web app for QnA Maker service.", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('qnaMakerWebAppName')]", - "location": "[parameters('qnaMakerWebAppLocation')]", - "properties": { - "enabled": true, - "name": "[variables('qnaMakerWebAppName')]", - "hostingEnvironment": "", - "serverFarmId": "[concat('/subscriptions/', Subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('servicePlanName'))]", - "siteConfig": { - "cors": { - "allowedOrigins": [ - "*" - ] - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "condition": "[parameters('shouldCreateQnAResource')]", - "resources": [ - { - "apiVersion": "2016-08-01", - "name": "appsettings", - "type": "config", - "dependsOn": [ - "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", - "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]", - "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]" - ], - "properties": { - "AzureSearchName": "[variables('qnaMakerSearchName')]", - "AzureSearchAdminKey": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName')), '2015-08-19').primaryKey]", - "UserAppInsightsKey": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').InstrumentationKey]", - "UserAppInsightsName": "[parameters('appInsightsName')]", - "UserAppInsightsAppId": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').AppId]", - "PrimaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-PrimaryEndpointKey')]", - "SecondaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-SecondaryEndpointKey')]", - "DefaultAnswer": "No good match found in KB.", - "QNAMAKER_EXTENSION_VERSION": "latest" - }, - "condition": "[parameters('shouldCreateQnAResource')]" - } - ] } ], "outputs": { @@ -509,13 +385,6 @@ "endpoint": "[if(parameters('shouldCreateLuisResource'), reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('luisServiceName'))).endpoint, '')]", "authoringEndpoint": "[if(parameters('shouldCreateAuthoringResource'), reference(resourceId('Microsoft.CognitiveServices/accounts', variables('LuisAuthoringAccountName'))).endpoint, '')]" } - }, - "qna": { - "type": "object", - "value": { - "endpoint": "[if(parameters('shouldCreateQnAResource'), concat('https://', reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0]), '')]", - "subscriptionKey": "[if(parameters('shouldCreateQnAResource'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('qnaMakerServiceName')),'2017-04-18').key1, '')]" - } } } } diff --git a/Composer/plugins/samples/assets/shared/scripts/provisionComposer.js b/Composer/plugins/samples/assets/shared/scripts/provisionComposer.js index 9e85606907..fbf4f2bdd9 100644 --- a/Composer/plugins/samples/assets/shared/scripts/provisionComposer.js +++ b/Composer/plugins/samples/assets/shared/scripts/provisionComposer.js @@ -43,6 +43,10 @@ const usage = () => { 'customArmTemplate', 'Path to runtime ARM template. By default it will use an Azure WebApp template. Pass `DeploymentTemplates/function-template-with-preexisting-rg.json` for Azure Functions or your own template for a custom deployment.', ], + [ + 'qnaTemplate', + 'Path to qna template. By default it will use `DeploymentTemplates/qna-template.json`' + ] ]; const instructions = [ @@ -53,11 +57,11 @@ const usage = () => { ``, chalk.bold(`Basic Usage:`), chalk.greenBright(`node provisionComposer --subscriptionId=`) + - chalk.yellow('') + - chalk.greenBright(' --name=') + - chalk.yellow('') + - chalk.greenBright(' --appPassword=') + - chalk.yellow('<16 character password>'), + chalk.yellow('') + + chalk.greenBright(' --name=') + + chalk.yellow('') + + chalk.greenBright(' --appPassword=') + + chalk.yellow('<16 character password>'), ``, chalk.bold(`All options:`), ...options.map((option) => { @@ -98,6 +102,8 @@ var tenantId = argv.tenantId ? argv.tenantId : ''; const templatePath = argv.customArmTemplate || path.join(__dirname, 'DeploymentTemplates', 'template-with-preexisting-rg.json'); +const qnaTemplatePath = + argv.qnaTemplate || path.join(__dirname, 'DeploymentTemplates', 'qna-template.json'); const BotProjectDeployLoggerType = { // Logger Type for Provision @@ -206,6 +212,18 @@ const getTenantId = async (accessToken) => { } }; +/** + * + * @param {*} appId the appId of application registration + * @param {*} appPwd the app password of application registration + * @param {*} location the locaiton of all resources + * @param {*} name the name of resource group + * @param {*} shouldCreateAuthoringResource + * @param {*} shouldCreateLuisResource + * @param {*} useAppInsights + * @param {*} useCosmosDb + * @param {*} useStorage + */ const getDeploymentTemplateParam = ( appId, appPwd, @@ -213,7 +231,6 @@ const getDeploymentTemplateParam = ( name, shouldCreateAuthoringResource, shouldCreateLuisResource, - shouldCreateQnAResource, useAppInsights, useCosmosDb, useStorage @@ -225,13 +242,65 @@ const getDeploymentTemplateParam = ( botId: pack(name), shouldCreateAuthoringResource: pack(shouldCreateAuthoringResource), shouldCreateLuisResource: pack(shouldCreateLuisResource), - shouldCreateQnAResource: pack(shouldCreateQnAResource), useAppInsights: pack(useAppInsights), useCosmosDb: pack(useCosmosDb), useStorage: pack(useStorage), }; }; +/** + * Get QnA template param + */ +const getQnaTemplateParam = ( + location, + name +) => { + return { + appServicePlanLocation: pack(location), + name: pack(name) + }; +}; + +/** + * Validate the qna template and the qna template param + */ +const validateQnADeployment = async (client, resourceGroupName, deployName, templateParam) => { + logger({ + status: BotProjectDeployLoggerType.PROVISION_INFO, + message: '> Validating QnA deployment ...', + }); + + const templateFile = await readFile(qnaTemplatePath, { encoding: 'utf-8' }); + const deployParam = { + properties: { + template: JSON.parse(templateFile), + parameters: templateParam, + mode: 'Incremental', + }, + }; + return await client.deployments.validate(resourceGroupName, deployName, deployParam); +}; + +/** + * Create a QnA resource deployment + * @param {*} client + * @param {*} resourceGroupName + * @param {*} deployName + * @param {*} templateParam + */ +const createQnADeployment = async (client, resourceGroupName, deployName, templateParam) => { + const templateFile = await readFile(qnaTemplatePath, { encoding: 'utf-8' }); + const deployParam = { + properties: { + template: JSON.parse(templateFile), + parameters: templateParam, + mode: 'Incremental', + }, + }; + + return await client.deployments.createOrUpdate(resourceGroupName, deployName, deployParam); +}; + /** * Validate the deployment using the Azure API */ @@ -347,6 +416,12 @@ const create = async ( createStorage = true, createAppInsights = true ) => { + + // App insights is a dependency of QnA + if (createQnAResource) { + createAppInsights = true; + } + // If tenantId is empty string, get tenanId from API if (!tenantId) { const token = await creds.getToken(); @@ -422,7 +497,6 @@ const create = async ( location, name, createLuisAuthoringResource, - createQnAResource, createLuisResource, createAppInsights, createCosmosDb, @@ -486,6 +560,75 @@ const create = async ( return provisionFailed(); } + var qnaResult = null; + + // Create qna resources, the reason why seperate the qna resources from others: https://github.com/Azure/azure-sdk-for-js/issues/10186 + if (createQnAResource) { + const qnaDeployName = new Date().getTime().toString(); + const qnaDeploymentTemplateParam = getQnaTemplateParam( + location, + name + ); + const qnaValidation = await validateQnADeployment(client, resourceGroupName, qnaDeployName, qnaDeploymentTemplateParam); + if (qnaValidation.error) { + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR, + message: `! Error: ${qnaValidation.error.message}`, + }); + if (qnaValidation.error.details) { + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR_DETAILS, + message: JSON.stringify(qnaValidation.error.details, null, 2), + }); + } + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR, + message: `+ To delete this resource group, run 'az group delete -g ${resourceGroupName} --no-wait'`, + }); + return provisionFailed(); + } + + // Create qna deloyment + logger({ + status: BotProjectDeployLoggerType.PROVISION_INFO, + message: `> Deploying QnA Resources (this could take a while)...`, + }); + const spinner = ora().start(); + try { + const qnaDeployment = await createQnADeployment(client, resourceGroupName, qnaDeployName, qnaDeploymentTemplateParam); + // Handle errors + if (qnaDeployment._response.status != 200) { + spinner.fail(); + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR, + message: `! QnA Template is not valid with provided parameters. Review the log for more information.`, + }); + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR, + message: `! Error: ${qnaValidation.error}`, + }); + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR, + message: `+ To delete this resource group, run 'az group delete -g ${resourceGroupName} --no-wait'`, + }); + return provisionFailed(); + } + } catch (err) { + spinner.fail(); + logger({ + status: BotProjectDeployLoggerType.PROVISION_ERROR, + message: getErrorMesssage(err), + }); + return provisionFailed(); + } + + const qnaDeploymentOutput = await client.deployments.get(resourceGroupName, qnaDeployName); + if (qnaDeploymentOutput && qnaDeploymentOutput.properties && qnaDeploymentOutput.properties.outputs) { + const qnaOutputResult = qnaDeploymentOutput.properties.outputs; + qnaResult = unpackObject(qnaOutputResult); + } + } + // If application insights created, update the application insights settings in azure bot service if (createAppInsights) { logger({ @@ -574,10 +717,10 @@ const create = async ( if (failedOperations) { failedOperations.forEach((operation) => { switch ( - operation && - operation.properties && - operation.properties.statusMessage.error.code && - operation.properties.targetResource + operation && + operation.properties && + operation.properties.statusMessage.error.code && + operation.properties.targetResource ) { case 'MissingRegistrationForLocation': logger({ @@ -609,6 +752,14 @@ const create = async ( }); } } + + // Merge qna outputs with other resources' outputs + if (createQnAResource) { + if (qnaResult) { + Object.assign(updateResult, qnaResult); + } + } + return updateResult; };