UNPKG

firmament-docker

Version:

Typescript classes for performing Docker operations

1,040 lines (994 loc) 42.1 kB
import {injectable, inject} from 'inversify'; import { Positive, FailureRetVal, CommandUtil, ProgressBar, Spawn, ForceErrorImpl, SafeJson, SpawnOptions2 } from 'firmament-yargs'; import {DockerContainerManagement} from '../interfaces/docker-container-management'; import {DockerImageManagement} from '../interfaces/docker-image-management'; import * as async from 'async'; import * as fs from 'fs'; import * as YAML from 'yamljs'; import * as path from 'path'; import * as tmp from 'tmp'; import * as mkdirp from 'mkdirp'; import * as touch from 'touch'; import * as _ from 'lodash'; import {RemoteCatalogGetter} from 'firmament-yargs'; import {DockerProvision} from '../interfaces/docker-provision'; import {DockerUtil} from '../interfaces/docker-util'; import { DockerMachineDriverOptions_openstack, DockerMachineDriverOptions_vmwarevsphere, DockerServiceDescription, DockerStackConfigTemplate, DockerVolumeDescription } from '../'; import {ProcessCommandJson} from 'firmament-bash/js/interfaces/process-command-json'; const fileExists = require('file-exists'); //const path = require('path'); //const templateCatalogUrl = '/home/jreeme/src/firmament-docker/docker/provisionTemplateCatalog.json'; //const templateCatalogUrl = 'https://raw.githubusercontent.com/jreeme/firmament-docker/manager/docker/provisionTemplateCatalog.json'; @injectable() export class DockerProvisionImpl extends ForceErrorImpl implements DockerProvision { private stackConfigTemplate: DockerStackConfigTemplate; private writeScripts = false; constructor(@inject('CommandUtil') private commandUtil: CommandUtil, @inject('Spawn') private spawn: Spawn, @inject('SafeJson') private safeJson: SafeJson, @inject('RemoteCatalogGetter') private remoteCatalogGetter: RemoteCatalogGetter, @inject('ProcessCommandJson') private processCommandJson: ProcessCommandJson, @inject('DockerUtil') public dockerUtil: DockerUtil, @inject('DockerImageManagement') private dockerImageManagement: DockerImageManagement, @inject('DockerContainerManagement') private dockerContainerManagement: DockerContainerManagement, @inject('Positive') private positive: Positive, @inject('ProgressBar') private progressBar: ProgressBar) { super(); } private validateDockerStackConfigTemplate(argv: any, dockerStackConfigTemplate: DockerStackConfigTemplate, cb: (err: Error, dockerStackConfigTemplate?: DockerStackConfigTemplate) => void) { const me = this; const dsct = dockerStackConfigTemplate; const dockerImageRegex = /.+?(:\d+?)?\/.+?:.+$/g; const dockerVolumesRegex = /:\//g; //Add dockerMachines.common options to dockerMachineDriverOptions Object.assign(dsct.dockerMachineDriverOptions, dsct.dockerMachines.common); //Don't need dsct.dockerMachines.common anymore delete dsct.dockerMachines.common; //Patch Docker Machines ==> If no 'engineLabels.affinity' set to nodeName //--Manager machine const manager = dsct.dockerMachines.manager; manager.engineLabels = manager.engineLabels || { role: 'manager', affinity: manager.nodeName }; manager.engineLabels.role = manager.engineLabels.role || 'manager'; manager.engineLabels.affinity = manager.engineLabels.affinity || manager.nodeName; //--Worker machines dsct.dockerMachines.workers = dsct.dockerMachines.workers || []; dsct.dockerMachines.workers.forEach((worker) => { worker.engineLabels = worker.engineLabels || { role: 'worker', affinity: worker.nodeName }; worker.engineLabels.role = worker.engineLabels.role || 'worker'; worker.engineLabels.affinity = worker.engineLabels.affinity || worker.nodeName; }); //Patch 'nfsConfig' block dsct.nfsConfig = dsct.nfsConfig || { nfsUser: undefined, nfsPassword: undefined, nfsSshKeyPath: undefined, nfsSshPort: 22, exportBaseDir: undefined, serverAddr: undefined, options: undefined }; dsct.nfsConfig.nfsUser = dsct.nfsConfig.nfsUser || undefined; dsct.nfsConfig.nfsPassword = dsct.nfsConfig.nfsPassword || undefined; dsct.nfsConfig.nfsSshKeyPath = dsct.nfsConfig.nfsSshKeyPath || undefined; dsct.nfsConfig.nfsSshPort = dsct.nfsConfig.nfsSshPort || undefined; dsct.nfsConfig.exportBaseDir = dsct.nfsConfig.exportBaseDir || undefined; dsct.nfsConfig.serverAddr = dsct.nfsConfig.serverAddr || undefined; dsct.nfsConfig.options = dsct.nfsConfig.options || undefined; //Patch 'dockerComposeYaml.volumes' block dsct.dockerComposeYaml.volumes = dsct.dockerComposeYaml.volumes || {}; for(const volume in dsct.dockerComposeYaml.volumes) { const v = <DockerVolumeDescription>dsct.dockerComposeYaml.volumes[volume]; if(v.driver_opts && v.driver_opts.type === 'nfs' && v.driver === 'local') { //Could be we need to 'fill in the blanks' for NFS volume //If device is missing, use the volume name v.driver_opts.device = v.driver_opts.device || volume; dockerVolumesRegex.lastIndex = 0; //If device doesn't start with ':/' then prepend 'nfsConfig.exportBaseDir' if(!dockerVolumesRegex.test(v.driver_opts.device)) { if(!dsct.nfsConfig) { return cb(new Error(`'${volume}.driver_opts.device' does not begin with ':/' but no 'nfsConfig' is specified`)); } if(!dsct.nfsConfig.exportBaseDir) { return cb(new Error(`'${volume}.driver_opts.device' does not begin with ':/' but no 'nfsConfig.exportBaseDir' is specified`)); } v.driver_opts.device = `:${dsct.nfsConfig.exportBaseDir}/${v.driver_opts.device}`; } //If there are no options then copy them from 'nfsConfig' (if they exist, otherwise error) if(!v.driver_opts.o) { if(!dsct.nfsConfig) { return cb(new Error(`'${volume}.driver_opts.o' does not exist but no 'nfsConfig' is specified`)); } if(!dsct.nfsConfig.serverAddr) { return cb(new Error(`'${volume}.driver_opts.o' does not exist but no 'nfsConfig.serverAddr' is specified`)); } if(!dsct.nfsConfig.options) { return cb(new Error(`'${volume}.driver_opts.o' does not exist but no 'nfsConfig.options' is specified`)); } v.driver_opts.o = dsct.nfsConfig.options; } //Build the options (driver_opts.o) const optionsHash = DockerProvisionImpl.optionsStringToHash(v.driver_opts.o); if(!optionsHash['addr'] && !dsct.nfsConfig.serverAddr) { return cb(new Error(`'${volume}.driver_opts.o[addr=]' does not exist but no 'nfsConfig.serverAddr' is specified`)); } //Only muck with 'addr' option since we know it's required. TODO: We could get clever and 'merge' nfsConfig.options & v.driver_opts.o optionsHash['addr'] = optionsHash['addr'] || dsct.nfsConfig.serverAddr; v.driver_opts.o = DockerProvisionImpl.optionsHashToString(optionsHash); } } //Patch 'dockerComposeYaml.services' block for(const service in dsct.dockerComposeYaml.services) { const s = <DockerServiceDescription>dsct.dockerComposeYaml.services[service]; dockerImageRegex.lastIndex = 0; if(!dockerImageRegex.test(s.image)) { if(!dsct.defaultDockerRegistry) { return cb(new Error(`Service '${service}' missing 'image' property and 'defaultDockerRegistry' is undefined`)); } if(!dsct.defaultDockerImageTag) { return cb(new Error(`Service '${service}' missing 'image' property and 'defaultDockerImageTag' is undefined`)); } if(s.image) { if(/.+?:\d+?/.test(s.image)) { s.image = `${dsct.defaultDockerRegistry}/${s.image}`; } else { s.image = `${dsct.defaultDockerRegistry}/${s.image}:${dsct.defaultDockerImageTag}`; } } else { s.image = `${dsct.defaultDockerRegistry}/${service}:${dsct.defaultDockerImageTag}`; } } const labels = {}; if(s.deploy.labels) { let traefikPortLabelPresent = 0; let frontendRuleLabelPresent = 0; const portRegex = /traefik\.(.*?\.|)port/g; const frontendRuleRegex = /traefik\.(.*?\.|)frontend.rule/g; s.deploy.labels.forEach((label) => { const tuple = label.split('='); portRegex.lastIndex = frontendRuleRegex.lastIndex = 0; if(portRegex.test(tuple[0])) { ++traefikPortLabelPresent; } if(frontendRuleRegex.test(tuple[0])) { ++frontendRuleLabelPresent; } labels[tuple[0]] = tuple[1]; }); if(labels['traefik.enable'] !== 'false') { if(!traefikPortLabelPresent) { return cb(new Error(`'traefik.??.port' label not present for service '${service}'`)); } if(!labels['traefik.backend'] && frontendRuleLabelPresent === 1) { labels['traefik.backend'] = service; } if(!frontendRuleLabelPresent) { if(!dsct.traefikZoneName) { return cb(new Error(`'traefik.??.frontend.rule' label and 'traefikZoneName' both undefined for service '${service}'`)); } labels['traefik.frontend.rule'] = `Host: ${service}.${dsct.traefikZoneName}`; } } s.deploy.labels.length = 0; for(const label in labels) { s.deploy.labels.push(`${label}=${labels[label]}`); } } } this.dockerUtil.writeJsonTemplateFile(dsct, '/tmp/tmp.json'); if(argv.noNfs) { return cb(null, dsct); } me.checkNfsMounts(dsct, (err, dsct) => { me.commandUtil.processExitIfError(err); cb(null, dsct); }); } private checkNfsMounts(dsct: DockerStackConfigTemplate, cb: (err: Error, dockerStackConfigTemplate: DockerStackConfigTemplate) => void) { const me = this; const localSpawnOptions: SpawnOptions2 = { suppressStdOut: false, suppressStdErr: false, cacheStdOut: true, cacheStdErr: true, suppressResult: false }; const remoteSpawnOptions: SpawnOptions2 = Object.assign({}, localSpawnOptions, { remoteHost: '', remoteUser: dsct.nfsConfig.nfsUser, remotePassword: dsct.nfsConfig.nfsPassword }); const volumes: DockerVolumeDescription[] = Object.keys(dsct.dockerComposeYaml.volumes).map((key) => dsct.dockerComposeYaml.volumes[key]); async.waterfall([ (cb) => { //Get a list of all the NFS servers this deployment wants const nfsServerHash = {}; volumes.forEach((v) => { if(v.driver_opts && v.driver_opts.type === 'nfs' && v.driver === 'local') { const host = DockerProvisionImpl.optionsStringToHash(v.driver_opts.o).addr; nfsServerHash[host] = nfsServerHash[host] || []; nfsServerHash[host].push(v.driver_opts.device.slice(1)); } } ); const nfsServers = Object.keys(nfsServerHash).map((key) => key); //Step (0) - Make sure NFS servers are available async.each( nfsServers, (nfsServer: string, cb: (err?: Error) => void) => { me.spawn.spawnShellCommandAsync( [ 'showmount', '-e', nfsServer ], localSpawnOptions, () => { }, (err: Error, result: string) => { if(err) { const resultObject = me.safeJson.safeParseSync(err.message); if(resultObject.obj.code !== 0) { //Try to install NFS server (makes a lot of assumptions) const cmds = [ [ 'apt-get', 'update' ], [ 'apt-get', 'install', '-y', 'nfs-kernel-server' ] ]; return me.remoteSpawnCmdArray( cmds, Object.assign({}, remoteSpawnOptions, {remoteHost: nfsServer}), (err) => { //TODO: Lots can go wrong here. Someday maybe look to see if RPC can route to host, etc. me.callbackAndExitIfError(err, cb); cb(); }); } } cb(); } ); }, (err: Error) => { me.callbackAndExitIfError(err, cb); cb(null, nfsServerHash); } ); }, (nfsServerHash, cb) => { //Step (1) - Assume all NFS servers are online and ready to rock, make sure needed volumes are exported //from each server const nfsServers = Object.keys(nfsServerHash).map((key) => key); async.each( nfsServers, (nfsServer: string, cb: (err?: Error) => void) => { me.spawn.spawnShellCommandAsync( [ 'showmount', '-e', nfsServer ], localSpawnOptions, () => { }, (err: Error, result: string) => { me.safeJson.safeParse(result, (err: Error, obj: {code: number, stdoutText: string}) => { if(obj.code) { return cb(new Error(`local 'showmount' FAILED`)); } const exportList = obj.stdoutText.split('\n').slice(1, -1).map((exportLine) => exportLine.split(/\s/)[0]); const volumes = nfsServerHash[nfsServer]; const addExportSpawnOptions = Object.assign({}, remoteSpawnOptions, {remoteHost: nfsServer}); async.series([ (cb) => { async.each( volumes, (volume: string, cb: (err?: Error) => void) => { const msg = `Checking NFS mount ${nfsServer}:${volume} ...`; if(exportList.indexOf(volume) !== -1) { me.commandUtil.log(`${msg} OK`); return cb(); } me.commandUtil.log(`${msg} NOT EXPORTED`); me.commandUtil.log(`Attempting to export NFS volume ${nfsServer}:${volume} ...`); if(!addExportSpawnOptions.remoteHost || !addExportSpawnOptions.remoteUser || !addExportSpawnOptions.remotePassword) { return cb(new Error(`nfsConfig [nfsHost && (nfsUser + nfsPassword) || nfsSshKeyPath] must be specified to export NFS volume. Cannot continue.`)); } const etcExportsEntry = `${volume} *(insecure,rw,sync,no_root_squash,no_subtree_check)`; const cmds = [ [ 'mkdir', '-p', volume ], [ 'chmod', '777', volume ], [ // Check for this entry in /etc/exports and add if not there `grep -q -F '${etcExportsEntry}' /etc/exports || echo '${etcExportsEntry}' >> /etc/exports` ] ]; me.remoteSpawnCmdArray(cmds, addExportSpawnOptions, cb); }, cb); }, (cb) => { const cmds = [ [ '/usr/sbin/exportfs', '-ra' ] ]; me.remoteSpawnCmdArray(cmds, addExportSpawnOptions, (err: Error) => { me.commandUtil.log(`Restarted NFS services on ${nfsServer}`); cb(err); }); } ], cb); }); }); }, cb); } ], (err: Error) => { cb(err, dsct); }); } private remoteSpawnCmdArray(cmds: any[], remoteSpawnOptions: SpawnOptions2, cb: (err: Error) => void) { const me = this; async.eachSeries(cmds, (cmd, cb) => { me.spawn.spawnShellCommandAsync( cmd, remoteSpawnOptions, (err: Error, result: string) => { me.commandUtil.log(result); }, (err: Error, result: string) => { me.commandUtil.log(result); cb(err); } ); }, (err: Error) => { cb(err); }); } private static optionsHashToString(hash: any): string { let retVal = ''; for(const key in hash) { retVal += key + ((hash[key]) ? `=${hash[key]},` : ','); } return retVal.slice(0, -1); } private static optionsStringToHash(optionsString: string): any { const hash = {}; optionsString.split(',').forEach((option) => { const optionWithValue = option.split('='); hash[optionWithValue[0]] = optionWithValue[1]; }); return hash; } extractYamlFromJson(argv: any, cb: () => void = null) { const me = this; const {fullInputPath, stackConfigTemplate} = me.getContainerConfigsFromJsonFile(argv.inputJsonFile); me.createOutputPath(fullInputPath, argv.outputYamlFile, '.yaml', (err, outputYamlPath, outputFileExists) => { me.callbackAndExitIfError(err, cb); if(outputFileExists && !me.positive.areYouSure( `Output file '${outputYamlPath}' already exists. Overwrite? [Y/n] `, 'Operation canceled.', true, FailureRetVal.TRUE)) { me.callbackAndExitWithError(err, cb); } const yaml = YAML.stringify(stackConfigTemplate.dockerComposeYaml, 8, 2); fs.writeFile(outputYamlPath, yaml, (err) => { me.callbackAndExitWithError(err, cb); }); }); } makeTemplate(argv: any, cb: () => void = null) { const me = this; me.composeAndWriteTemplate(argv.get, argv.dm, argv.yaml, argv.output, (err: Error, msg: string) => { me.callbackAndExitWithError(err, cb); }); } buildTemplate(argv: any, cb: (err?: Error) => void = null) { const me = this; const {fullInputPath, stackConfigTemplate} = me.getContainerConfigsFromJsonFile(argv.input); me.validateDockerStackConfigTemplate(argv, stackConfigTemplate, (err, stackConfigTemplate) => { if(err) { if(cb) { return cb(err); } me.commandUtil.processExitWithError(err, 'OK'); } me.stackConfigTemplate = stackConfigTemplate; switch(stackConfigTemplate.dockerMachineDriverOptions.driver) { case 'openstack': { const dmdo = (<DockerMachineDriverOptions_openstack>stackConfigTemplate.dockerMachineDriverOptions); if(argv.username) { dmdo.openstackUsername = argv.username; } if(argv.password) { dmdo.openstackPassword = argv.password; } break; } case 'vmwarevsphere': { const dmdo = (<DockerMachineDriverOptions_vmwarevsphere>stackConfigTemplate.dockerMachineDriverOptions); if(argv.username) { dmdo.vmwarevsphereUsername = argv.username; } if(argv.password) { dmdo.vmwarevspherePassword = argv.password; } break; } case 'amazonec2': case 'virtualbox': default: break; } me.commandUtil.log("Constructing Docker Stack described in: '" + fullInputPath + "'"); me.createDockerMachines(fullInputPath, stackConfigTemplate, argv, (err, result) => { if(cb) { return cb(); } me.commandUtil.processExitWithError(err, 'OK'); }); }); } private convertOptionsFromCamelToSnakeCase(options: any): string[] { const me = this; const retVal = []; const excludeProperties = ['nodeCount', 'nodeName']; for(const option in options) { if(excludeProperties.indexOf(option) !== -1) { continue; } if(option === 'engineLabels') { for(const engineLabel in options[option]) { retVal.push('--engine-label'); retVal.push(`${engineLabel}=${options[option][engineLabel]}`); } continue; } const optionKey = me.camelToSnake(option, '-'); const optionValue = options[option]; //There are some options --engine-opt & --engine-env that require a <space> instead of an <=> symbol //between Key & Value. This seems to be because the Value has an <=> symbol in it. //(e.g. --engine-opt dns=8.8.8.8 & --engine-env HTTP_PROXY=http://bananna-daiquiri.drink const keyValueSeparator = ((typeof optionValue !== 'string') || (optionValue.indexOf('=') === -1)) ? '=' : ' '; retVal.push(`--${optionKey}${keyValueSeparator}${optionValue}`); } return retVal; } private logErrAndResult(err: Error, result: string) { const me = this; if(err) { return me.commandUtil.log(err.message); } me.commandUtil.log((result || '').toString()); } //(alter vm.max_map_count in boot2docker ISO //https://github.com/boot2docker/boot2docker/issues/1216 private createDockerMachines(fullInputPath: string, stackConfigTemplate: DockerStackConfigTemplate, argv: any, cb: (err: Error, result: string) => void) { const me = this; cb = me.checkCallback(cb); const createOptions = me.convertOptionsFromCamelToSnakeCase(stackConfigTemplate.dockerMachineDriverOptions); async.waterfall([ (cb: (err: Error, managerMachineName: string, ip: string) => void) => { const managerMachineName = `${stackConfigTemplate.stackName}-${stackConfigTemplate.dockerMachines.manager.nodeName}`; const managerDockerMachineCmd = createOptions.slice(); managerDockerMachineCmd.push.apply(managerDockerMachineCmd, me.convertOptionsFromCamelToSnakeCase(stackConfigTemplate.dockerMachines.manager)); managerDockerMachineCmd.push(managerMachineName); me.createDockerMachine(managerDockerMachineCmd, cb); }, (managerMachineName: string, ip: string, cb: (err: Error, managerMachineName: string, ip: string) => void) => { const dockerMachineInitSwarmCmd = [ 'docker-machine', 'ssh', managerMachineName, 'docker', 'swarm', 'init', '--advertise-addr', ip ]; if(me.writeScripts) { fs.writeFileSync(`/home/jreeme/tmp/_init_swarm.sh`, dockerMachineInitSwarmCmd.join(' ')); return cb(null, managerMachineName, ip); } me.spawn.spawnShellCommandAsync(dockerMachineInitSwarmCmd, { cacheStdOut: true }, me.logErrAndResult.bind(me), (_err: Error) => { if(_err) { const {err, obj} = me.safeJson.safeParseSync(_err.message); if(!err && obj.code === 1) { return cb(null, managerMachineName, ip); } } cb(_err, managerMachineName, ip); }); }, (managerMachineName: string, ip: string, cb: (err: Error, managerMachineName: string, ip: string, joinToken: string) => void) => { const dockerMachineInitSwarmCmd = [ 'docker-machine', 'ssh', managerMachineName, 'docker', 'swarm', 'join-token', 'worker', '-q' ]; if(me.writeScripts) { fs.writeFileSync(`/home/jreeme/tmp/_join_swarm.sh`, dockerMachineInitSwarmCmd.join(' ')); return cb(null, managerMachineName, ip, 'joinToken'); } me.spawn.spawnShellCommandAsync(dockerMachineInitSwarmCmd, { cacheStdOut: true }, me.logErrAndResult.bind(me), (err: Error, result: any) => { const joinToken = me.safeJson.safeParseSync(result).obj.stdoutText.trim(); cb(null, managerMachineName, ip, joinToken); }); }, (managerMachineName: string, managerIp: string, joinToken: string, cb: (err: Error, managerMachineName: string, ip: string) => void) => { let fnArray = []; stackConfigTemplate.dockerMachines.workers.forEach((workerDockerMachine) => { for(let i = 0; i < workerDockerMachine.nodeCount; ++i) { const workerMachineName = `${stackConfigTemplate.stackName}-${workerDockerMachine.nodeName}-${i}`; const workerDockerMachineCmd = createOptions.slice(); workerDockerMachineCmd.push.apply(workerDockerMachineCmd, me.convertOptionsFromCamelToSnakeCase(workerDockerMachine)); workerDockerMachineCmd.push(workerMachineName); fnArray.push(async.apply(me.createWorkerDockerMachine.bind(me), workerDockerMachineCmd, managerIp, joinToken )); } }); async.parallel(fnArray, (err: Error) => { cb(err, managerMachineName, managerIp); }); }, (managerMachineName: string, ip: string, cb: (err: Error, env: any, ip: string) => void) => { const dockerMachineGetMasterEnvCmd = [ 'docker-machine', 'env', managerMachineName ]; me.spawn.spawnShellCommandAsync(dockerMachineGetMasterEnvCmd, { cacheStdOut: true }, (err, result) => { me.commandUtil.log(result.toString()); }, (err, result) => { const regex = /export (.*)/g; const envString = me.safeJson.safeParseSync(result).obj.stdoutText.trim(); let match: string[]; let env: any = {}; do { match = regex.exec(envString); if(match) { const keyValue = match[1].split('='); env[keyValue[0]] = keyValue[1].replace(/"/g, ''); } } while(match); cb(null, env, ip); }); }, (env: any, ip: string, cb: (err?: Error) => void) => { tmp.file({dir: path.dirname(fullInputPath)}, (err, tmpPath, fd, cleanupCb) => { try { if(argv.noPorts) { for(const serviceName in stackConfigTemplate.dockerComposeYaml.services) { const service = stackConfigTemplate.dockerComposeYaml.services[serviceName]; service.ports && delete service.ports; } } } catch(err) { me.commandUtil.error(`Failed to execute 'noPorts' option ${err}`); } const yaml = YAML.stringify(stackConfigTemplate.dockerComposeYaml, 8, 2).replace(/\$\{MASTER_IP\}/g, ip); if(me.commandUtil.callbackIfError(err)) { return; } fs.writeFile(tmpPath, yaml, (err: Error) => { if(err) { me.logErrAndResult(err, ''); return cb(err); } const dockerMachineDeployCmd = [ 'docker', 'stack', 'deploy', '-c', tmpPath, stackConfigTemplate.stackName ? stackConfigTemplate.stackName : stackConfigTemplate.clusterPrefix ]; me.spawn.spawnShellCommandAsync(dockerMachineDeployCmd, { env, cacheStdOut: true }, (err, result) => { me.commandUtil.log(result.toString()); }, (err, result) => { me.logErrAndResult(err, result); //const joinToken = me.safeJson.safeParseSync(result).obj.stdoutText.trim(); cleanupCb(); cb(); }); }); }); } ], (err: Error, result: string) => { me.logErrAndResult(err, result); cb(err, result); }); } private createWorkerDockerMachine(dockerMachineCmd: any[], managerIp, joinToken, cb: (err, result?) => void) { const me = this; me.createDockerMachine(dockerMachineCmd, (err: Error, workerMachineName: string, workerIp: string) => { if(err) { return cb(err); } const dockerMachineJoinSwarmCmd = [ 'docker-machine', 'ssh', workerMachineName, 'docker', 'swarm', 'join', '--advertise-addr', `${workerIp}:2377`, '--token', joinToken, `${managerIp}:2377` ]; if(me.writeScripts) { fs.writeFileSync(`/home/jreeme/tmp/_join_worker_${workerMachineName}.sh`, dockerMachineJoinSwarmCmd.join(' ')); return cb(null, ''); } me.spawn.spawnShellCommandAsync(dockerMachineJoinSwarmCmd, { cacheStdOut: true, cacheStdErr: true }, (err, result) => { me.commandUtil.log(result.toString()); }, (err: Error, result: string) => { cb(null, result); }); }); } private handleDockerMachineExecutionFailure(err: Error, cb: (err: Error) => void) { const me = this; me.safeJson.safeParse(err.message, (err: Error, obj: any) => { try { if(obj.code.code === 'ENOENT') { if(me.positive.areYouSure( `Looks like 'docker-machine' is not installed. Want me to try to install it?`, 'Operation canceled.', true, FailureRetVal.TRUE)) { const installDockerMachineJson = path.resolve(__dirname, '../../firmament-bash/install-docker-machine.json'); return me.processCommandJson.processAbsoluteUrl(installDockerMachineJson, (err) => { cb(new Error(`'docker-machine' installed. Try provisioning again.`)); }); } return cb(new Error('docker-machine installation canceled')); } cb(null); } catch(err) { cb(err); } }); } private createDockerMachine(dockerMachineCreateCmdOptions: string[], cb: (err: Error, machineName: string, ip: string) => void) { const me = this; async.waterfall([ (cb: (err: Error, machineName: string) => void) => { const dockerMachineCmd = [ 'docker-machine', 'create' ].concat(dockerMachineCreateCmdOptions); if(me.writeScripts) { fs.writeFileSync(`/home/jreeme/tmp/_create_${dockerMachineCmd[dockerMachineCmd.length - 1]}.sh`, dockerMachineCmd.join(' ')); return cb(null, ''); } me.spawn.spawnShellCommandAsync( dockerMachineCmd, {}, (err, result) => { me.commandUtil.log(result.toString()); }, (err) => { const machineName = dockerMachineCmd[dockerMachineCmd.length - 1]; if(err) { return me.handleDockerMachineExecutionFailure(err, (err: Error) => { me.commandUtil.processExitIfError(err); cb(err, machineName); }); } //Below is where we do any tweaks to the underlying docker-machine host. This host is sometimes a boot2docker //machine (VMWare, VirtualBox) and sometimes a cloud-init, usually Ubuntu, image (OpenStack, AWS). switch(me.stackConfigTemplate.dockerMachineDriverOptions.driver) { case('vmwarevsphere'): return me.finalConfig_VMWareVSphere(machineName, cb); case('openstack'): return me.finalConfig_OpenStack(machineName, cb); case('amazonec2'): return me.finalConfig_AmazonEC2(machineName, cb); case('virtualbox'): default: return me.finalConfig_VirtualBox(machineName, cb); } }); }, (machineName, cb) => { const dockerMachineGetIpCmd = [ 'docker-machine', 'ip', machineName ]; if(me.writeScripts) { fs.writeFileSync(`/home/jreeme/tmp/_get_managerIp.sh`, dockerMachineGetIpCmd.join(' ')); return cb(null, machineName, '0.0.0.0'); } me.spawn.spawnShellCommandAsync(dockerMachineGetIpCmd, { cacheStdOut: true }, me.logErrAndResult.bind(me), (err, result) => { const ip = me.safeJson.safeParseSync(result).obj.stdoutText.trim(); cb(err, {machineName, ip}); }); } ], (err, result: {machineName: string, ip: string}) => { const {machineName, ip} = result; cb(err, machineName, ip); }); }; private finalConfig_VirtualBox(machineName: string, cb: (err, machineName) => void) { this.adjustBoot2DockerProfile(machineName, cb); } private finalConfig_AmazonEC2(machineName: string, cb: (err, machineName) => void) { cb(null, machineName); } private finalConfig_OpenStack(machineName: string, cb: (err, machineName) => void) { cb(null, machineName); } private finalConfig_VMWareVSphere(machineName: string, cb: (err, machineName) => void) { this.adjustBoot2DockerProfile(machineName, cb); } //NOTE: The boot2docker profile file (living at /var/lib/boot2docker/profile on the VM host) is appropriate for //changing the way the docker daemon behaves. Settings for non-docker daemons need to be handled another way. private adjustBoot2DockerProfile(machineName: string, cb: (err, machineName) => void) { const me = this; //Need to up the vm.max_map_count to 262144 to support elasticsearch 5 //NETDEVICES="$(awk -F: '\''/eth.:|tr.:/{print $1}'\'' /proc/net/dev 2>/dev/null)" const profileLines = (me.stackConfigTemplate.hostMachineDnsServer) ? ` #Need to set vm.max_map_count to 262144 to support ElasticSearch (ES fails in production without it) sysctl -w vm.max_map_count=262144 sysctl -w net.ipv4.tcp_keepalive_time=600 ulimit -l unlimited #swapoff /dev/sda2 #sed -i '\\''/sda2/ s/^/#/'\\'' /etc/fstab NETDEVICES="$(awk -F: '\\''/eth.:|tr.:/{print $1}'\\'' /proc/net/dev 2>/dev/null)" for DEVICE in $NETDEVICES; do DHCP_PID_FILE=/var/run/udhcpc.$DEVICE.pid echo "Checking existence of $DHCP_PID_FILE ..." until [ -f $DHCP_PID_FILE ]; do echo "Waiting for $DHCP_PID_FILE to exist ..." sleep 1 done done echo "$DHCP_PID_FILE exists ..." RESOLV_CONF=/etc/resolv.conf echo "Checking existence of $RESOLV_CONF ..." until [ -f $RESOLV_CONF ]; do echo "Waiting for $RESOLV_CONF to exist ..." sleep 1 done echo "$RESOLV_CONF exists ..." DATE_OF_DHCP_PID_FILE=$(date -u -r $DHCP_PID_FILE +%s) DATE_OF_RESOLV_CONF=$(date -u -r $RESOLV_CONF +%s) NOW=$(date -u +%s) echo "$DHCP_PID_FILE last modified at $DATE_OF_DHCP_PID_FILE" echo "$RESOLV_CONF last modified at $DATE_OF_RESOLV_CONF" echo $(( \${DATE_OF_DHCP_PID_FILE}-\${DATE_OF_RESOLV_CONF} )) echo 'nameserver ${me.stackConfigTemplate.hostMachineDnsServer}' > /etc/resolv.conf #Try to protect against resolver restart or DHCP lease renewal rewriting our resolv.conf file chmod 444 /etc/resolv.conf ` : ` #Need to set vm.max_map_count to 262144 to support ElasticSearch (ES fails in production without it) sysctl -w vm.max_map_count=262144 sysctl -w net.ipv4.tcp_keepalive_time=600 ulimit -l unlimited #swapoff /dev/sda2 #sed -i '\\''/sda2/ s/^/#/'\\'' /etc/fstab `; const dockerMachineJoinSwarmCmd = [ `echo '${profileLines}' | sudo tee -a /var/lib/boot2docker/profile && sudo /etc/init.d/docker restart` ]; me.runCommandOnDockerMachineHost(machineName, dockerMachineJoinSwarmCmd, (err: Error, result: string) => { err && me.commandUtil.log(err.toString()); cb(err, machineName); }); } private runCommandOnDockerMachineHost(machineName: string, commandArray: string[], cb: (err: Error, result: string) => void) { const me = this; commandArray.unshift('docker-machine', 'ssh', machineName); me.spawn.spawnShellCommandAsync(commandArray, { cacheStdOut: true }, (err, result) => { me.commandUtil.log(result.toString()); }, cb); //const joinToken = me.safeJson.safeParseSync(result).obj.stdoutText.trim(); } private getContainerConfigsFromJsonFile(inputPath: string) { const me = this; const fullInputPath = me.commandUtil.getConfigFilePath(inputPath, '.json'); if(!fileExists.sync(fullInputPath)) { me.commandUtil.processExitWithError(new Error(`\n'${fullInputPath}' does not exist`)); } const stackConfigTemplate = <DockerStackConfigTemplate>me.safeJson.readFileSync(fullInputPath, undefined); return {fullInputPath, stackConfigTemplate}; } private composeAndWriteTemplate(catalogEntryName: string, dockerMachineHostType: string, dockerComposeYamlPath: string, outputTemplateFileName: string, cb: (err: Error, msg?: string) => void) { const me = this; me.createOutputPath(path.resolve(process.cwd(), 'tmp.txt'), outputTemplateFileName, '.json', (err, fullOutputPath, outputFileExists) => { if(catalogEntryName === undefined) { //Just write out the descriptors we have "baked in" to this application if(fileExists.sync(fullOutputPath) && !me.positive.areYouSure( `Config file '${fullOutputPath}' already exists. Overwrite? [Y/n] `, 'Operation canceled.', true, FailureRetVal.TRUE)) { return cb(null); } const dockerMachineWrapperPath = path.resolve(__dirname, `../../docker/docker-machine-wrappers/${dockerMachineHostType}.json`); const dockerMachineWrapper = me.safeJson.readFileSync(dockerMachineWrapperPath, undefined); const dockerComposeYaml = YAML.load(dockerComposeYamlPath); const jsonTemplate = Object.assign({}, dockerMachineWrapper, {dockerComposeYaml}); //let jsonTemplate = Object.assign({}, DockerDescriptors.dockerStackConfigTemplate, {dockerComposeYaml}); me.dockerUtil.writeJsonTemplateFile(jsonTemplate, fullOutputPath); cb(null, 'Template written.'); } else { cb(new Error('Provision from template not implemented.')); //Need to interact with the network to get templates /* me.remoteCatalogGetter.getCatalogFromUrl(templateCatalogUrl, (err, remoteCatalog) => { if (!argv.get.length) { //User specified --get with no template name so write available template names to console me.commandUtil.log('\nAvailable templates:\n'); remoteCatalog.entries.forEach(entry => { me.commandUtil.log('> ' + entry.name); }); me.commandUtil.processExit(); } else { //User specified a template, let's go get it let template: RemoteCatalogEntry = _.find(remoteCatalog.entries, entry => { return entry.name === argv.get; }); if (!template) { me.commandUtil.processExitWithError(new Error(`\nTemplate catalog '${argv.get}' does not exist.\n`)); } template.resources.forEach(resource => { try { let outputPath = path.resolve(process.cwd(), path.basename(resource.name)); fs.writeFileSync(outputPath, resource.text); } catch (err) { me.commandUtil.processExitWithError(err); } }); me.commandUtil.processExit(0, `\nTemplate '${template.name}' written.\n`); } });*/ } }); } private callbackAndExitIfError(err: Error, cb: () => void) { if(!err) { return; } if(cb) { cb(); } this.commandUtil.processExitWithError(err, 'OK'); } private callbackAndExitWithError(err: Error, cb: () => void) { if(cb) { cb(); } this.commandUtil.processExitWithError(err, 'OK'); } private camelToSnake(name, separator) { return name.replace(/([a-z]|(?:[A-Z]+))([A-Z]|$)/g, function(_, $1, $2) { return $1 + ($2 && (separator || '_') + $2); }).toLowerCase(); } private createOutputPath(inPathFragment: string, outPathFragment: string, extension: string, cb: (err: Error, createdOutputPath: string, exists?: boolean) => void) { if(path.extname(outPathFragment) !== extension) { outPathFragment += extension; } if(!path.isAbsolute(outPathFragment)) { outPathFragment = path.resolve(path.dirname(inPathFragment), outPathFragment); } const pathDirname = path.dirname(outPathFragment); mkdirp(pathDirname, (err) => { if(err) { return cb(err, outPathFragment); } //Check existence of file fileExists(outPathFragment, (err, exists) => { touch(outPathFragment, (err) => { if(err || exists) { return cb(err, outPathFragment, exists); } fs.unlink(outPathFragment, (err) => { return cb(err, outPathFragment, exists); }); }); }); }); }; }