UNPKG

botium-core

Version:
544 lines (483 loc) 19.7 kB
const fs = require('fs') const util = require('util') const async = require('async') const path = require('path') const yaml = require('write-yaml') const mustache = require('mustache') const findRoot = require('find-root') const _ = require('lodash') const debug = require('debug')('botium-DockerContainer') const debugContainerOutput = require('debug')('botium-DockerContainerOutput') const Capabilities = require('../Capabilities') const Events = require('../Events') const BaseContainer = require('./BaseContainer') const DockerCmd = require('./DockerCmd') const DockerMocks = require('./DockerMocks') const BotiumMockCommand = require('../mocks/BotiumMockCommand') const TcpPortUtils = require('../helpers/TcpPortUtils') const SyslogServer = require('../helpers/SyslogServer') const ProcessUtils = require('../helpers/ProcessUtils') const SafeFileCopy = require('../helpers/SafeFileCopy') const botiumPackageRootDir = findRoot() module.exports = class DockerContainer extends BaseContainer { Validate () { return super.Validate().then(() => { this._AssertCapabilityExists(Capabilities.DOCKERCOMPOSEPATH) this._AssertCapabilityExists(Capabilities.STARTCMD) this._AssertCapabilityExists(Capabilities.DOCKERIMAGE) this._AssertOneCapabilityExists(Capabilities.DOCKERSYSLOGPORT, Capabilities.DOCKERSYSLOGPORT_RANGE) if (this.caps[Capabilities.DOCKERMACHINE]) { this._AssertCapabilityExists(Capabilities.DOCKERMACHINEPATH) } if (this.caps[Capabilities.FACEBOOK_API]) { this._AssertCapabilityExists(Capabilities.FACEBOOK_WEBHOOK_PORT) this._AssertCapabilityExists(Capabilities.FACEBOOK_WEBHOOK_PATH) this._AssertOneCapabilityExists(Capabilities.FACEBOOK_PUBLISHPORT, Capabilities.FACEBOOK_PUBLISHPORT_RANGE) } if (this.caps[Capabilities.SLACK_API]) { this._AssertCapabilityExists(Capabilities.SLACK_EVENT_PORT) this._AssertCapabilityExists(Capabilities.SLACK_EVENT_PATH) this._AssertCapabilityExists(Capabilities.SLACK_OAUTH_PORT) this._AssertCapabilityExists(Capabilities.SLACK_OAUTH_PATH) this._AssertOneCapabilityExists(Capabilities.SLACK_PUBLISHPORT, Capabilities.SLACK_PUBLISHPORT_RANGE) } if (this.caps[Capabilities.BOTFRAMEWORK_API]) { this._AssertCapabilityExists(Capabilities.BOTFRAMEWORK_APP_ID) this._AssertCapabilityExists(Capabilities.BOTFRAMEWORK_CHANNEL_ID) this._AssertCapabilityExists(Capabilities.BOTFRAMEWORK_WEBHOOK_PORT) this._AssertCapabilityExists(Capabilities.BOTFRAMEWORK_WEBHOOK_PATH) this._AssertOneCapabilityExists(Capabilities.BOTFRAMEWORK_PUBLISHPORT, Capabilities.BOTFRAMEWORK_PUBLISHPORT_RANGE) } }) } Build () { if (this.caps[Capabilities.FACEBOOK_API]) { debug('Adding Facebook Mock to Docker compose') this.fbMock = new DockerMocks.Facebook() } if (this.caps[Capabilities.SLACK_API]) { debug('Adding Slack Mock to Docker compose') this.slackMock = new DockerMocks.Slack() } if (this.caps[Capabilities.BOTFRAMEWORK_API]) { debug('Adding BotFramework Mock to Docker compose') this.botframeworkMock = new DockerMocks.BotFramework() } return new Promise((resolve, reject) => { this.dockerConfig = { projectname: this.caps[Capabilities.PROJECTNAME], dockercomposepath: this.caps[Capabilities.DOCKERCOMPOSEPATH], uniquecontainernames: this.caps[Capabilities.DOCKERUNIQUECONTAINERNAMES], composefiles: [] } async.series([ (baseComplete) => { super.Build().then(() => baseComplete()).catch(baseComplete) }, (dockerIpFound) => { if (this.caps[Capabilities.DOCKERMACHINE]) { ProcessUtils.childProcessRun(this.caps[Capabilities.DOCKERMACHINEPATH], ['ip'], false) .then((output) => { if (output.stdout && output.stdout.length > 0) { this.dockerIp = `${output.stdout[0]}`.trim() if (this.dockerIp) { debug(`Found docker-machine ip ${this.dockerIp}.`) return dockerIpFound() } } dockerIpFound(`No docker-machine ip found in command output ${output}`) }).catch(dockerIpFound) } else { this.dockerIp = '127.0.0.1' dockerIpFound() } }, (dockerfileCreated) => { const dockerfileBotium = path.resolve(this.repo.workingDirectory, 'Dockerfile.botium') fs.stat(dockerfileBotium, (err, stats) => { if (!err && stats.isFile()) { debug(`Dockerfile ${dockerfileBotium} already present, using it.`) dockerfileCreated() } else { const templateFile = path.resolve(botiumPackageRootDir, 'src', 'Dockerfile.botium.template') fs.readFile(templateFile, 'utf8', (err, data) => { if (err) return dockerfileCreated(`Reading docker template file ${templateFile} failed: ${err}`) const viewData = { STARTCMD: this.caps[Capabilities.STARTCMD], DOCKERIMAGE: this.caps[Capabilities.DOCKERIMAGE] } const convertedFile = mustache.render(data, viewData) fs.writeFile(dockerfileBotium, convertedFile, (err) => { if (err) return dockerfileCreated(`Writing dockerfile ${dockerfileBotium} failed: ${err}`) this.cleanupTasks.push((cb) => { fs.stat(dockerfileBotium, (err, stats) => { if (!err && stats.isFile()) { fs.unlink(dockerfileBotium, cb) } else { cb() } }) }) dockerfileCreated() }) }) } }) }, (dockercomposeMainUsed) => { const dockercomposeMain = path.resolve(botiumPackageRootDir, 'src', 'docker-compose.botium.yml') const dockercomposeBotium = path.resolve(this.tempDirectory, 'docker-compose.botium.yml') SafeFileCopy(dockercomposeMain, dockercomposeBotium) .then(() => { this.dockerConfig.composefiles.push(dockercomposeBotium) dockercomposeMainUsed() }) .catch((err) => dockercomposeMainUsed(`Copying docker compose template file ${dockercomposeMain} failed: ${err}`)) }, (syslogPortSelected) => { if (this.caps[Capabilities.DOCKERSYSLOGPORT]) { this.syslogPort = this.caps[Capabilities.DOCKERSYSLOGPORT] if (this.caps[Capabilities.BOTIUMGRIDSLOT]) { this.publishPort += this.caps[Capabilities.BOTIUMGRIDSLOT] } syslogPortSelected() } else { TcpPortUtils.GetFreePortInRange('127.0.0.1', this.caps[Capabilities.DOCKERSYSLOGPORT_RANGE]) .then((syslogPort) => { this.syslogPort = syslogPort syslogPortSelected() }) .catch(syslogPortSelected) } }, (facebookPortSelected) => { if (this.fbMock) { this.fbMock.SelectPublishPort(this.dockerIp, this.caps).then(() => facebookPortSelected()).catch(facebookPortSelected) } else { facebookPortSelected() } }, (slackPortSelected) => { if (this.slackMock) { this.slackMock.SelectPublishPort(this.dockerIp, this.caps).then(() => slackPortSelected()).catch(slackPortSelected) } else { slackPortSelected() } }, (botframeworkPortSelected) => { if (this.botframeworkMock) { this.botframeworkMock.SelectPublishPort(this.dockerIp, this.caps).then(() => botframeworkPortSelected()).catch(botframeworkPortSelected) } else { botframeworkPortSelected() } }, (facebookMockPrepared) => { if (this.fbMock) { this.fbMock.PrepareDocker(path.resolve(this.tempDirectory, 'fbmock')).then(() => facebookMockPrepared()).catch(facebookMockPrepared) } else { facebookMockPrepared() } }, (slackMockPrepared) => { if (this.slackMock) { this.slackMock.PrepareDocker(path.resolve(this.tempDirectory, 'slackmock')).then(() => slackMockPrepared()).catch(slackMockPrepared) } else { slackMockPrepared() } }, (botframeworkMockPrepared) => { if (this.botframeworkMock) { this.botframeworkMock.PrepareDocker(path.resolve(this.tempDirectory, 'botframeworkmock')).then(() => botframeworkMockPrepared()).catch(botframeworkMockPrepared) } else { botframeworkMockPrepared() } }, (dockercomposeUsed) => { const promises = [] if (this.fbMock) { promises.push(this.fbMock.GetDockerCompose().then((f) => { this.dockerConfig.composefiles.push(f) })) } if (this.slackMock) { promises.push(this.slackMock.GetDockerCompose().then((f) => { this.dockerConfig.composefiles.push(f) })) } if (this.botframeworkMock) { promises.push(this.botframeworkMock.GetDockerCompose().then((f) => { this.dockerConfig.composefiles.push(f) })) } Promise.all(promises).then(() => dockercomposeUsed()) }, (dockercomposeEnvUsed) => { const sysLog = { driver: 'syslog', options: { 'syslog-address': `udp://127.0.0.1:${this.syslogPort}` } } const composeEnv = { version: '2', services: { botium: { build: { context: this.repo.workingDirectory }, logging: _.cloneDeep(sysLog), volumes: [ `${this.repo.workingDirectory}:/usr/src/app` ] } } } if (this.envs) { composeEnv.services.botium.environment = this._cleanDockerEnv(this.envs) } if (this.fbMock) { this.fbMock.FillDockerEnv(composeEnv, this.caps, sysLog) } if (this.slackMock) { this.slackMock.FillDockerEnv(composeEnv, this.caps, sysLog) } if (this.botframeworkMock) { this.botframeworkMock.FillDockerEnv(composeEnv, this.caps, sysLog) } this.dockercomposeEnvFile = path.resolve(this.tempDirectory, 'docker-env.yml') debug(`Writing docker compose environment to ${this.dockercomposeEnvFile} - ${JSON.stringify(composeEnv)}`) yaml(this.dockercomposeEnvFile, composeEnv, (err) => { if (err) return dockercomposeEnvUsed(`Writing docker file ${this.dockercomposeEnvFile} failed: ${err}`) this.dockerConfig.composefiles.push(this.dockercomposeEnvFile) dockercomposeEnvUsed() }) }, (dockercomposeOverrideUsed) => { const dockercomposeOverride = path.resolve(this.repo.workingDirectory, 'docker-compose.botium.override.yml') fs.stat(dockercomposeOverride, (err, stats) => { if (!err && stats.isFile()) { debug(`Docker-Compose file ${dockercomposeOverride} present, using it.`) this.dockerConfig.composefiles.push(dockercomposeOverride) } dockercomposeOverrideUsed() }) }, (dockercomposeLocalOverrideUsed) => { const dockercomposeOverride = path.resolve(process.cwd(), 'docker-compose.botium.override.yml') fs.stat(dockercomposeOverride, (err, stats) => { if (!err && stats.isFile()) { debug(`Docker-Compose file ${dockercomposeOverride} present, using it.`) this.dockerConfig.composefiles.push(dockercomposeOverride) } dockercomposeLocalOverrideUsed() }) }, (dockerReady) => { this.dockerConfig.composefiles = _.uniq(this.dockerConfig.composefiles) debug(this.dockerConfig) this.dockerCmd = new DockerCmd(this.dockerConfig) this.dockerCmd.setupContainer() .then(() => { dockerReady() }) .catch((err) => { dockerReady(`Cannot build docker containers: ${util.inspect(err)}`) }) } ], (err) => { if (err) { return reject(new Error(`Cannot build docker containers: ${util.inspect(err)}`)) } resolve(this) }) }) } Start () { this.eventEmitter.emit(Events.CONTAINER_STARTING, this) return new Promise((resolve, reject) => { async.series([ (baseComplete) => { super.Start().then(() => baseComplete()).catch(baseComplete) }, (syslogStarted) => { let waitFor = Promise.resolve() if (this.syslogServer) { waitFor = this.syslogServer.stop() } waitFor.then(() => { this.syslogFile = path.resolve(this.tempDirectory, 'docker-containers-log.txt') this.syslogServer = new SyslogServer() this.syslogServer.on('message', (value) => { debugContainerOutput(value.message) fs.appendFile(this.syslogFile, value.message, () => { }) }) this.syslogServer.on('error', (err) => { debug(`Error from syslog server: ${util.inspect(err)}`) }) this.syslogServer.start({ port: this.syslogPort }) .then(() => syslogStarted()) .catch((err) => { syslogStarted(`Cannot start syslog server: ${util.inspect(err)}`) }) }).catch((err) => { syslogStarted(`Cannot stop running syslog server: ${util.inspect(err)}`) }) }, (dockerStarted) => { if (!this.dockerCmd) return dockerStarted('not built') this.dockerCmd.startContainer() .then(() => dockerStarted()) .catch((err) => { dockerStarted(`Cannot start docker containers: ${util.inspect(err)}`) }) }, (facebookMockupOnline) => { if (this.fbMock) { this.fbMock.Start(this).then(() => facebookMockupOnline()).catch(facebookMockupOnline) } else { facebookMockupOnline() } }, (slackMockupOnline) => { if (this.slackMock) { this.slackMock.Start(this).then(() => slackMockupOnline()).catch(slackMockupOnline) } else { slackMockupOnline() } }, (botframeworkMockupOnline) => { if (this.botframeworkMock) { this.botframeworkMock.Start(this).then(() => botframeworkMockupOnline()).catch(botframeworkMockupOnline) } else { botframeworkMockupOnline() } } ], (err) => { if (err) { this.eventEmitter.emit(Events.CONTAINER_START_ERROR, this, err) return reject(new Error(`Start failed ${util.inspect(err)}`)) } this.eventEmitter.emit(Events.CONTAINER_STARTED, this) resolve(this) }) }) } UserSays (mockMsg) { return new Promise((resolve, reject) => { if (this.fbMock && this.fbMock.socket) { this.fbMock.socket.emit(BotiumMockCommand.MOCKCMD_SENDTOBOT, mockMsg) this.eventEmitter.emit(Events.MESSAGE_SENTTOBOT, this, mockMsg) resolve(this) } else if (this.slackMock && this.slackMock.socket) { this.slackMock.socket.emit(BotiumMockCommand.MOCKCMD_SENDTOBOT, mockMsg) this.eventEmitter.emit(Events.MESSAGE_SENTTOBOT, this, mockMsg) resolve(this) } else if (this.botframeworkMock && this.botframeworkMock.socket) { this.botframeworkMock.socket.emit(BotiumMockCommand.MOCKCMD_SENDTOBOT, mockMsg) this.eventEmitter.emit(Events.MESSAGE_SENTTOBOT, this, mockMsg) resolve(this) } else { this.eventEmitter.emit(Events.MESSAGE_SENDTOBOT_ERROR, this, 'No Mock online') reject(new Error('No Mock online')) } }) } Stop () { this.eventEmitter.emit(Events.CONTAINER_STOPPING, this) return new Promise((resolve, reject) => { async.series([ (baseComplete) => { super.Stop().then(() => baseComplete()).catch(baseComplete) }, (facebookStopDone) => { if (this.fbMock) { this.fbMock.Stop().then(() => facebookStopDone()).catch(facebookStopDone) } else { facebookStopDone() } }, (slackStopDone) => { if (this.slackMock) { this.slackMock.Stop().then(() => slackStopDone()).catch(slackStopDone) } else { slackStopDone() } }, (botframeworkStopDone) => { if (this.botframeworkMock) { this.botframeworkMock.Stop(this).then(() => botframeworkStopDone()).catch(botframeworkStopDone) } else { botframeworkStopDone() } }, (dockerStopped) => { if (!this.dockerCmd) return dockerStopped() this.dockerCmd.stopContainer() .then(() => { dockerStopped() }) .catch((err) => { dockerStopped(`Cannot stop docker containers: ${util.inspect(err)}`) }) }, (syslogStopped) => { if (!this.syslogServer) return syslogStopped() this.syslogServer.stop() .then(() => { this.syslogServer = null syslogStopped() }) .catch((err) => { syslogStopped(`Cannot stop syslog server: ${util.inspect(err)}`) }) } ], (err) => { if (err) { this.eventEmitter.emit(Events.CONTAINER_STOP_ERROR, this, err) return reject(new Error(`Stop failed ${util.inspect(err)}`)) } this.eventEmitter.emit(Events.CONTAINER_STOPPED, this) resolve(this) }) }) } Clean () { this.eventEmitter.emit(Events.CONTAINER_CLEANING, this) return new Promise((resolve, reject) => { async.series([ (dockerStopped) => { if (this.dockerCmd) { this.dockerCmd.teardownContainer() .then(() => { dockerStopped() }) .catch((err) => { debug(`Cannot teardown docker containers: ${util.inspect(err)}`) dockerStopped() }) } else { dockerStopped() } }, (baseComplete) => { super.Clean().then(() => baseComplete()).catch(baseComplete) } ], (err) => { if (err) { this.eventEmitter.emit(Events.CONTAINER_CLEAN_ERROR, this, err) return reject(new Error(`Cleanup failed ${util.inspect(err)}`)) } this.eventEmitter.emit(Events.CONTAINER_CLEANED, this) resolve(this) }) }) } _cleanDockerEnv (envs) { const res = Object.assign({}, envs) Object.keys(res).forEach((key) => { const val = res[key] if (typeof val === typeof true) res[key] = `${val}` }) return res } }