Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Added provision resource checks before publish #6662

Merged
merged 8 commits into from
Apr 2, 2021
83 changes: 80 additions & 3 deletions extensions/azurePublish/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -505,6 +506,36 @@ export default async (composer: IExtensionRegistration): Promise<void> => {
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));

Expand All @@ -515,8 +546,6 @@ export default async (composer: IExtensionRegistration): Promise<void> => {
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);

Expand Down Expand Up @@ -673,6 +702,54 @@ export default async (composer: IExtensionRegistration): Promise<void> => {
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 => {
GeoffCoxMSFT marked this conversation as resolved.
Show resolved Hide resolved
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] = {};
Expand Down