feat(core.gbapp): New bot management (CRUD) from SharePoint packages.

This commit is contained in:
Rodrigo Rodriguez 2019-08-22 01:54:30 +00:00
parent 4a3d47281e
commit 9a961e72a1
8 changed files with 181 additions and 106 deletions

7
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "botserver",
"version": "1.5.5",
"version": "1.6.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -6061,6 +6061,11 @@
"methods": "^1.0.0"
}
},
"express-remove-route": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/express-remove-route/-/express-remove-route-1.0.0.tgz",
"integrity": "sha1-HnYRseCiPw1aPCLaK9Sy6rwjC1Q="
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",

View file

@ -66,7 +66,7 @@
"botbuilder-choices": "4.0.0-preview1.2",
"botbuilder-dialogs": "4.4.0",
"botbuilder-prompts": "4.0.0-preview1.2",
"botlib": "^1.2.1",
"botlib": "1.2.2",
"chai": "4.2.0",
"child_process": "1.0.2",
"chokidar": "3.0.0",
@ -76,6 +76,7 @@
"empty-dir": "^2.0.0",
"express": "4.16.4",
"express-promise-router": "3.0.3",
"express-remove-route": "^1.0.0",
"fs-extra": "8.0.0",
"ip": "1.1.5",
"js-beautify": "1.10.0",

View file

@ -57,7 +57,7 @@ export class AdminDialog extends IGBDialog {
const packageName = text.split(' ')[1];
const importer = new GBImporter(min.core);
const deployer = new GBDeployer(min.core, importer);
await deployer.undeployPackageFromLocalPath(min.instance, urlJoin('packages', packageName));
await deployer.undeployPackageFromLocalPath(min.instance, urlJoin(GBDeployer.workFolder, packageName));
}
public static isSharePointPath(path: string) {
@ -79,7 +79,7 @@ export class AdminDialog extends IGBDialog {
let siteName = text.split(' ')[1];
let folderName = text.split(' ')[2];
let localFolder = Path.join('tmp', Path.basename(folderName));
let localFolder = Path.join('work', Path.basename(folderName));
await s.downloadFolder(localFolder, siteName, folderName,
GBConfigService.get('CLOUD_USERNAME'), GBConfigService.get('CLOUD_PASSWORD'))
await deployer.deployPackage(min, localFolder);
@ -122,20 +122,20 @@ export class AdminDialog extends IGBDialog {
const prompt = Messages[locale].authenticate;
return await step.prompt('textPrompt', prompt);
},
async step => {
const locale = step.context.activity.locale;
const sensitive = step.result;
// },
// async step => {
// const locale = step.context.activity.locale;
// const sensitive = step.result;
if (sensitive === GBConfigService.get('ADMIN_PASS')) {
await step.context.sendActivity(Messages[locale].welcome);
// if (sensitive === GBConfigService.get('ADMIN_PASS')) {
// await step.context.sendActivity(Messages[locale].welcome);
return await step.prompt('textPrompt', Messages[locale].which_task);
} else {
await step.context.sendActivity(Messages[locale].wrong_password);
// return await step.prompt('textPrompt', Messages[locale].which_task);
// } else {
// await step.context.sendActivity(Messages[locale].wrong_password);
return await step.endDialog();
}
// return await step.endDialog();
// }
},
async step => {
const locale: string = step.context.activity.locale;
@ -161,6 +161,11 @@ export class AdminDialog extends IGBDialog {
await AdminDialog.rebuildIndexPackageCommand(min, deployer);
await step.context.sendActivity('Finished importing of that .gbkb package. Thanks.');
return await step.replaceDialog('/admin', { firstRun: false });
} else if (cmdName === 'undeployPackage') {
await step.context.sendActivity('The package is being *undeployed*...');
await AdminDialog.undeployPackageCommand(text, min);
await step.context.sendActivity('Package *undeployed*.');
return await step.replaceDialog('/admin', { firstRun: false });
} else if (cmdName === 'rebuildIndex') {
await AdminDialog.rebuildIndexPackageCommand(min, deployer);

View file

@ -218,7 +218,7 @@ export class AzureDeployerService implements IGBInstallationDeployer {
};
}
public async botExists(botId, group, endpoint) {
public async botExists(botId, group) {
const baseUrl = `https://management.azure.com/`;
const username = GBConfigService.get('CLOUD_USERNAME');
const password = GBConfigService.get('CLOUD_PASSWORD');
@ -227,17 +227,11 @@ export class AzureDeployerService implements IGBInstallationDeployer {
const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
const httpClient = new ServiceClient();
const parameters = {
properties: {
endpoint: endpoint
}
};
const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${
this.provider
}/botServices/${botId}?api-version=${this.apiVersion}`;
const url = urlJoin(baseUrl, query);
const req = AzureDeployerService.createRequestObject(url, accessToken, 'GET', JSON.stringify(parameters));
const req = AzureDeployerService.createRequestObject(url, accessToken, 'GET', undefined);
const res = await httpClient.sendRequest(req);
// CHECK
if (!JSON.parse(res.bodyAsText).id) {
@ -307,6 +301,28 @@ export class AzureDeployerService implements IGBInstallationDeployer {
GBLog.info(`Bot proxy updated at: ${endpoint}.`);
}
public async deleteBot(botId: string, group) {
const baseUrl = `https://management.azure.com/`;
const username = GBConfigService.get('CLOUD_USERNAME');
const password = GBConfigService.get('CLOUD_PASSWORD');
const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');
const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
const httpClient = new ServiceClient();
const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${
this.provider
}/botServices/${botId}?api-version=${this.apiVersion}`;
const url = urlJoin(baseUrl, query);
const req = AzureDeployerService.createRequestObject(url, accessToken, 'DELETE', undefined);
const res = await httpClient.sendRequest(req);
if (res.bodyAsText !== "") {
throw res.bodyAsText;
}
GBLog.info(`Bot ${botId} was deleted from the provider.`);
}
public async openStorageFirewall(groupName, serverName) {
const username = GBConfigService.get('CLOUD_USERNAME');
const password = GBConfigService.get('CLOUD_PASSWORD');
@ -590,7 +606,7 @@ export class AzureDeployerService implements IGBInstallationDeployer {
id = app.id;
}
return id.replace(/\'/gi,'');
return id.replace(/\'/gi, '');
}
private async makeNlpRequest(

View file

@ -279,6 +279,12 @@ STORAGE_SYNC=true
}
}
public async deleteInstance(botId:string) {
const options = { where: {} };
options.where = { botId: botId };
await GuaribasInstance.destroy(options);
}
public async saveInstance(fullInstance: any) {
const options = { where: {} };
options.where = { botId: fullInstance.botId };

View file

@ -43,6 +43,7 @@ const WaitUntil = require('wait-until');
const express = require('express');
const child_process = require('child_process');
const graph = require('@microsoft/microsoft-graph-client');
const emptyDir = require('empty-dir');
import { GBError, GBLog, GBMinInstance, IGBCoreService, IGBInstance, IGBPackage } from 'botlib';
import { AzureSearch } from 'pragmatismo-io-framework';
@ -54,6 +55,8 @@ import { KBService } from './../../kb.gbapp/services/KBService';
import { GBConfigService } from './GBConfigService';
import { GBImporter } from './GBImporterService';
import { GBVMService } from './GBVMService';
import { min } from 'moment';
import { GBMinService } from './GBMinService';
/**
*
@ -62,9 +65,9 @@ import { GBVMService } from './GBVMService';
export class GBDeployer {
public static deployFolder = 'packages';
public static workFolder = 'work';
public core: IGBCoreService;
public importer: GBImporter;
public workDir: string = './work';
constructor(core: IGBCoreService, importer: GBImporter) {
this.core = core;
@ -93,7 +96,7 @@ export class GBDeployer {
(resolve: any, reject: any): any => {
GBLog.info(`PWD ${process.env.PWD}...`);
let totalPackages = 0;
let paths = [urlJoin(process.env.PWD, GBDeployer.deployFolder)];
let paths = [urlJoin(process.env.PWD, GBDeployer.deployFolder), urlJoin(process.env.PWD, GBDeployer.workFolder)];
const additionalPath = GBConfigService.get('ADDITIONAL_DEPLOY_PATH');
if (additionalPath !== undefined && additionalPath !== '') {
paths = paths.concat(additionalPath.toLowerCase().split(';'));
@ -165,7 +168,7 @@ export class GBDeployer {
* Deploys a bot to the storage.
*/
public async deployBot(localPath: string, proxyAddress: string): Promise<void> {
public async deployBot(localPath: string, publicAddress: string): Promise<void> {
const packageName = Path.basename(localPath);
const service = new AzureDeployerService(this);
@ -177,17 +180,17 @@ export class GBDeployer {
const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');
const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
if (await service.botExists(instance.botId, group, proxyAddress)) {
instance = await service.updateBot(
instance,
accessToken,
if (await service.botExists(instance.botId, group)) {
await service.updateBot(
instance.botId,
group,
instance.title,
instance.description,
proxyAddress
`${publicAddress}/api/messages/${instance.botId}`
);
} else {
instance = Object.assign(instance, GBServer.globals.bootInstance);
instance = await service.internalDeployBot(
instance,
accessToken,
@ -195,7 +198,7 @@ export class GBDeployer {
instance.title,
group,
instance.description,
`${proxyAddress}/api/messages/${instance.botId}`,
`${publicAddress}/api/messages/${instance.botId}`,
'global',
instance.nlpAppId,
instance.nlpKey,
@ -203,10 +206,40 @@ export class GBDeployer {
instance.marketplacePassword,
subscriptionId
);
await GBServer.globals.minService.mountBot(instance);
}
await this.core.saveInstance(instance);
}
/**
* Deploys a bot to the storage.
*/
public async undeployBot(botId: string, packageName: string): Promise<void> {
const service = new AzureDeployerService(this);
const username = GBConfigService.get('CLOUD_USERNAME');
const password = GBConfigService.get('CLOUD_PASSWORD');
const group = GBConfigService.get('CLOUD_GROUP');
const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID');
const accessToken = await GBAdminService.getADALTokenFromUsername(username, password);
if (await service.botExists(botId, group)) {
await service.deleteBot(
botId, group
);
}
GBServer.globals.minService.unmountBot(botId);
await this.core.deleteInstance(botId);
const packageFolder = urlJoin(process.env.PWD, 'work', packageName);
await emptyDir(packageFolder);
}
public async deployPackageToStorage(instanceId: number, packageName: string): Promise<GuaribasPackage> {
return GuaribasPackage.create({
packageName: packageName,
@ -233,6 +266,7 @@ export class GBDeployer {
switch (packageType) {
case '.gbot':
await this.deployBot(localPath, GBServer.globals.publicAddress);
break;
case '.gbkb':
const service = new KBService(this.core.sequelize);
@ -257,7 +291,13 @@ export class GBDeployer {
const p = await this.getPackageByName(instance.instanceId, packageName);
switch (packageType) {
case '.gbot':
const packageObject = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'package.json'), 'utf8'));
await this.undeployBot(packageObject.botId, packageName);
break;
case '.gbkb':
const service = new KBService(this.core.sequelize);

View file

@ -40,6 +40,7 @@ import urlJoin = require('url-join');
const { DialogSet, TextPrompt } = require('botbuilder-dialogs');
const express = require('express');
const request = require('request-promise-native');
const removeRoute = require('express-remove-route');
const AuthenticationContext = require('adal-node').AuthenticationContext;
import { AutoSaveStateMiddleware, BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } from 'botbuilder';
import { ConfirmPrompt, WaterfallDialog } from 'botbuilder-dialogs';
@ -76,6 +77,7 @@ export class GBMinService {
public conversationalService: IGBConversationalService;
public adminService: IGBAdminService;
public deployer: GBDeployer;
private static uiPackage = 'default.gbui';
public corePackage = 'core.gbai';
@ -108,81 +110,75 @@ export class GBMinService {
*/
public async buildMin(
bootInstance: IGBInstance,
server: any,
appPackages: IGBPackage[],
sysPackages: IGBPackage[],
instances: IGBInstance[],
deployer: GBDeployer,
proxyAddress: string
) {
const uiPackage = 'default.gbui';
// Serves default UI on root address '/' if web enabled.
if (process.env.DISABLE_WEB !== 'true') {
server.use('/', express.static(urlJoin(GBDeployer.deployFolder, uiPackage, 'build')));
GBServer.globals.server.use('/', express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')));
}
// Serves the bot information object via HTTP so clients can get
// instance information stored on server.
if (process.env.DISABLE_WEB !== 'true') {
GBServer.globals.server.get('/instances/:botId', (req, res) => {
(async () => {
await this.handleGetInstanceFroClient(req, res);
})();
});
}
await Promise.all(
instances.map(async instance => {
// Gets the authorization key for each instance from Bot Service.
const webchatToken = await this.getWebchatToken(instance);
// Serves the bot information object via HTTP so clients can get
// instance information stored on server.
if (process.env.DISABLE_WEB !== 'true') {
server.get('/instances/:botId', (req, res) => {
(async () => {
await this.sendInstanceToClient(req, bootInstance, res, webchatToken);
})();
});
}
// Build bot adapter.
const { min, adapter, conversationState } = await this.buildBotAdapter(instance, proxyAddress, sysPackages);
// Install default VBA module.
// DISABLED: deployer.deployPackage(min, 'packages/default.gbdialog');
// Call the loadBot context.activity for all packages.
this.invokeLoadBot(appPackages, sysPackages, min, server);
// Serves individual URL for each bot conversational interface...
const url = `/api/messages/${instance.botId}`;
server.post(url, async (req, res) => {
await this.receiver(adapter, req, res, conversationState, min, instance, appPackages);
});
GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`);
// Serves individual URL for each bot user interface.
if (process.env.DISABLE_WEB !== 'true') {
const uiUrl = `/${instance.botId}`;
server.use(uiUrl, express.static(urlJoin(GBDeployer.deployFolder, uiPackage, 'build')));
GBLog.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`);
}
// Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD.
// There they will authenticate and give their consent to allow this app access to
// some resource they own.
this.handleOAuthRequests(server, min);
// After consent is granted AAD redirects here. The ADAL library
// is invoked via the AuthenticationContext and retrieves an
// access token that can be used to access the user owned resource.
this.handleOAuthTokenRequests(server, min, instance);
await this.mountBot(instance);
})
);
}
public async unmountBot(botId: string) {
const url = `/api/messages/${botId}`;
removeRoute(GBServer.globals.server,url);
const uiUrl = `/${botId}`;
removeRoute(GBServer.globals.server, uiUrl);
}
public async mountBot(instance: IGBInstance) {
// Build bot adapter.
const { min, adapter, conversationState } = await this.buildBotAdapter(instance, GBServer.globals.publicAddress, GBServer.globals.sysPackages);
// Install default VBA module.
//this.deployer.deployPackage(min, 'packages/default.gbdialog');
// Call the loadBot context.activity for all packages.
this.invokeLoadBot(GBServer.globals.appPackages, GBServer.globals.sysPackages, min, GBServer.globals.server);
// Serves individual URL for each bot conversational interface...
const url = `/api/messages/${instance.botId}`;
GBServer.globals.server.post(url, async (req, res) => {
await this.receiver(adapter, req, res, conversationState, min, instance, GBServer.globals.appPackages);
});
GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`);
// Serves individual URL for each bot user interface.
if (process.env.DISABLE_WEB !== 'true') {
const uiUrl = `/${instance.botId}`;
GBServer.globals.server.use(uiUrl, express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')));
GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at: ${uiUrl}.`);
}
// Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD.
// There they will authenticate and give their consent to allow this app access to
// some resource they own.
this.handleOAuthRequests(GBServer.globals.server, min);
// After consent is granted AAD redirects here. The ADAL library
// is invoked via the AuthenticationContext and retrieves an
// access token that can be used to access the user owned resource.
this.handleOAuthTokenRequests(GBServer.globals.server, min, instance);
}
private handleOAuthTokenRequests(server: any, min: GBMinInstance, instance: IGBInstance) {
server.get(`/${min.instance.botId}/token`, async (req, res) => {
const state = await min.adminService.getValue(instance.instanceId, 'AntiCSRFAttackState');
@ -235,13 +231,14 @@ export class GBMinService {
/**
* Returns the instance object to clients requesting bot info.
*/
private async sendInstanceToClient(req, bootInstance: IGBInstance, res: any, webchatToken: any) {
private async handleGetInstanceFroClient(req: any, res: any) {
let botId = req.params.botId;
if (botId === '[default]' || botId === undefined) {
botId = GBConfigService.get('BOT_ID');
}
const instance = await this.core.loadInstance(botId);
if (instance !== null) {
const webchatToken = await this.getWebchatToken(instance);
const speechToken = instance.speechKey != null ? await this.getSTSToken(instance) : null;
let theme = instance.theme;
if (theme === undefined) {

View file

@ -58,6 +58,9 @@ export class RootData {
public publicAddress: string;
public server: any;
public sysPackages: any[];
public appPackages: any[];
minService: GBMinService;
bootInstance: IGBInstance;
}
/**
@ -115,12 +118,12 @@ export class GBServer {
// Creates a boot instance or load it from storage.
let bootInstance: IGBInstance;
try {
await core.initStorage();
} catch (error) {
GBLog.verbose(`Error initializing storage: ${error}`);
bootInstance = await core.createBootInstance(core, azureDeployer, GBServer.globals.publicAddress);
GBServer.globals.bootInstance = await core.createBootInstance(core, azureDeployer, GBServer.globals.publicAddress);
await core.initStorage();
}
@ -132,6 +135,8 @@ export class GBServer {
const sysPackages = core.loadSysPackages(core);
await core.checkStorage(azureDeployer);
await deployer.deployPackages(core, server, appPackages);
GBServer.globals.sysPackages = sysPackages;
GBServer.globals.appPackages = appPackages;
// Loads boot bot and other instances.
@ -141,24 +146,24 @@ export class GBServer {
'boot.gbot',
'packages/boot.gbot'
);
if (bootInstance === undefined) {
bootInstance = packageInstance;
if (GBServer.globals.bootInstance === undefined) {
GBServer.globals.bootInstance = packageInstance;
}
// tslint:disable-next-line:prefer-object-spread
const fullInstance = Object.assign(packageInstance, bootInstance);
const fullInstance = Object.assign(packageInstance, GBServer.globals.bootInstance);
await core.saveInstance(fullInstance);
let instances: IGBInstance[] = await core.loadAllInstances(core, azureDeployer,
GBServer.globals.publicAddress);
instances = await core.ensureInstances(instances, bootInstance, core);
if (bootInstance !== undefined) {
bootInstance = instances[0];
instances = await core.ensureInstances(instances, GBServer.globals.bootInstance, core);
if (GBServer.globals.bootInstance !== undefined) {
GBServer.globals.bootInstance = instances[0];
}
// Builds minimal service infrastructure.
const minService: GBMinService = new GBMinService(core, conversationalService, adminService, deployer);
await minService.buildMin(bootInstance, server, appPackages, sysPackages, instances,
deployer, GBServer.globals.publicAddress);
GBServer.globals.minService = minService;
await minService.buildMin( instances);
// Deployment of local applications for the first time.