Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
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