diff --git a/extensions/azurePublish/src/node/index.ts b/extensions/azurePublish/src/node/index.ts index 322eaed867..d5e5cdf4de 100644 --- a/extensions/azurePublish/src/node/index.ts +++ b/extensions/azurePublish/src/node/index.ts @@ -3,6 +3,7 @@ import path from 'path'; +import formatMessage from 'format-message'; import md5 from 'md5'; import { copy, rmdir, emptyDir, readJson, pathExists, writeJson, mkdirSync, writeFileSync } from 'fs-extra'; import { Debugger } from 'debug'; @@ -20,7 +21,7 @@ import { authConfig, ResourcesItem } from '../types'; import { AzureResourceTypes, AzureResourceDefinitions } from './resourceTypes'; import { mergeDeep } from './mergeDeep'; -import { BotProjectDeploy, getAbsSettings, isProfileComplete } from './deploy'; +import { BotProjectDeploy, getAbsSettings } from './deploy'; import { BotProjectProvision } from './provision'; import { BackgroundProcessManager } from './backgroundProcessManager'; import { ProvisionConfig } from './provision'; @@ -505,6 +506,36 @@ export default async (composer: IExtensionRegistration): Promise => { const resourcekey = md5([project.name, name, environment].join()); try { + // verify the profile has been provisioned at least once + if (!this.isProfileProvisioned(config)) { + throw new Error( + formatMessage( + 'There was a problem publishing {projectName}/{profileName}. The profile has not been provisioned yet.', + { projectName: project.name, profileName } + ) + ); + } + + // verify the publish profile has the required resources configured + const resources = await this.getResources(project, user); + + const missingResourceNames = resources.reduce((result, resource) => { + if (resource.required && !this.isResourceProvisionedInProfile(resource, config)) { + result.push(resource.text); + } + return result; + }, []); + + if (missingResourceNames.length > 0) { + const missingResourcesText = missingResourceNames.join(','); + throw new Error( + formatMessage( + 'There was a problem publishing {projectName}/{profileName}. These required resources have not been provisioned: {missingResourcesText}', + { projectName: project.name, profileName, missingResourcesText } + ) + ); + } + // authenticate with azure const accessToken = config.accessToken || (await getAccessToken(authConfig.arm)); @@ -515,8 +546,6 @@ export default async (composer: IExtensionRegistration): Promise => { if (!settings) { throw new Error('Required field `settings` is missing from publishing profile.'); } - // verify publish profile - isProfileComplete(config); this.asyncPublish({ ...config, accessToken, luResources, qnaResources, abs }, project, resourcekey, jobId); @@ -673,6 +702,54 @@ export default async (composer: IExtensionRegistration): Promise => { return recommendedResources; }; + private isProfileProvisioned = (profile: PublishConfig): boolean => { + //TODO: Post-migration we can check for profile?.tenantId + return profile?.resourceGroup && profile?.subscriptionId && profile?.region; + }; + + // While the provisioning process may return more information for various resources than is checked here, + // this tries to verify the minimum settings are present and that cannot be empty strings. + private isResourceProvisionedInProfile = (resource: ResourcesItem, profile: PublishConfig): boolean => { + switch (resource.key) { + case AzureResourceTypes.APPINSIGHTS: + // InstrumentationKey is Pascal-cased for some unknown reason + return profile?.settings?.applicationInsights?.InstrumentationKey; + case AzureResourceTypes.APP_REGISTRATION: + // MicrosoftAppId and MicrosoftAppPassword are Pascal-cased for some unknown reason + return profile?.settings?.MicrosoftAppId && profile?.settings?.MicrosoftAppPassword; + case AzureResourceTypes.BLOBSTORAGE: + // name is not checked (not in schema.ts) + // container property is not checked (empty may be a valid value) + return profile?.settings?.blobStorage?.connectionString; + case AzureResourceTypes.BOT_REGISTRATION: + return profile?.botName; + case AzureResourceTypes.COSMOSDB: + // collectionId is not checked (not in schema.ts) + // databaseId and containerId are not checked (empty may be a valid value) + return profile?.settings?.cosmosDB?.authKey && profile?.settings?.cosmosDB?.cosmosDBEndpoint; + case AzureResourceTypes.LUIS_AUTHORING: + // region is not checked (empty may be a valid value) + return profile?.settings?.luis?.authoringKey && profile?.settings?.luis?.authoringEndpoint; + case AzureResourceTypes.LUIS_PREDICTION: + // region is not checked (empty may be a valid value) + return profile?.settings?.luis?.endpointKey && profile?.settings?.luis?.endpoint; + case AzureResourceTypes.QNA: + // endpoint is not checked (it is in schema.ts and provision() returns the value, but it is not set in the config) + // qnaRegion is not checked (empty may be a valid value) + return profile?.settings?.qna?.subscriptionKey; + case AzureResourceTypes.SERVICE_PLAN: + // no settings exist to verify the service plan was created + return true; + case AzureResourceTypes.AZUREFUNCTIONS: + case AzureResourceTypes.WEBAPP: + return profile?.hostname; + default: + throw new Error( + formatMessage('Azure resource type {resourceKey} is not handled.', { resourceKey: resource.key }) + ); + } + }; + private addProvisionHistory = (botId: string, profileName: string, newValue: ProcessStatus) => { if (!this.provisionHistories[botId]) { this.provisionHistories[botId] = {};