UNPKG

whaler

Version:

Define and run multi-container applications with Docker

591 lines (499 loc) 22.9 kB
'use strict'; const fs = require('fs').promises; const path = require('path'); const parseEnv = require('../../lib/parse-env'); module.exports = exports; module.exports.__cmd = require('./cmd'); /** * @param whaler */ async function exports (whaler) { whaler.on('create', async ctx => { const whalerConfig = await whaler.config(); let appName = ctx.options['ref']; let serviceName = null; const parts = ctx.options['ref'].split('.'); if (2 == parts.length) { appName = parts[1]; serviceName = parts[0]; } const { default: docker } = await whaler.fetch('docker'); const { default: storage } = await whaler.fetch('apps'); const app = await storage.get(appName); let appConfig = app.config; if (ctx.options['config']) { appConfig = await whaler.emit('config', { name: appName, file: ctx.options['config'] }); } if (serviceName) { if (!appConfig['data']['services'][serviceName]) { throw new Error('Config for `' + ctx.options['ref'] + '` not found.'); } } const containers = {}; let services = Object.keys(appConfig['data']['services']); if (serviceName) { services = [serviceName]; } const vars = await whaler.emit('vars', {}); let whalerNetwork = docker.getNetwork('whaler_nw'); try { await whalerNetwork.inspect(); } catch (e) { whalerNetwork = await docker.createNetwork({ 'Name': 'whaler_nw', 'CheckDuplicate': true }); } let appNetwork = docker.getNetwork('whaler_nw.' + appName); try { await appNetwork.inspect(); } catch (e) { const nwConfig = whalerConfig['network'] || {}; appNetwork = await docker.createNetwork({ 'Name': 'whaler_nw.' + appName, 'Driver': nwConfig['driver'] || 'bridge', 'Options': nwConfig['options'] || {}, 'CheckDuplicate': true }); } const keys = Object.keys(appConfig['data']['services']); const volumesCfg = appConfig['data']['volumes'] || {}; for (let name of services) { const config = appConfig['data']['services'][name]; whaler.info('Creating `%s.%s` container.', name, appName); config['env'] = config['env'] || []; config['env'].push('WHALER_APP=' + appName); config['env'].push('WHALER_SERVICE=' + name); if (config['env_file']) { if (!Array.isArray(config['env_file'])) { config['env_file'] = [ config['env_file'] ]; } for (let envFile of config['env_file']) { try { const content = await fs.readFile(envFile, 'utf8'); const env = parseEnv(content || ''); for (let key in env) { vars[key] = env[key]; } } catch (e) {} } } for (let v in vars) { let exists = false; if (config['env'].length) { for (let env of config['env']) { const arr = env.split('='); if (-1 !== (arr[0] + '=').indexOf(v + '=')) { exists = true; } } } if (!exists) { config['env'].push(v + '=' + vars[v]); } } config['labels'] = config['labels'] || {}; for (let l in config['labels']) { config['labels'][l] = JSON.stringify(config['labels'][l]); } config['labels']['whaler.app'] = appName; config['labels']['whaler.service'] = name; const index = keys.indexOf(name); config['labels']['whaler.position'] = JSON.stringify({ after: keys[index - 1] || null, before: keys[index + 1] || null }); let waitMode = 'noninteractive'; if (config['wait']) { config['labels']['whaler.wait'] = config['wait'].toString(); if ('interactive' === process.env.WHALER_WAIT_MODE) { if ('interactive' === process.env.WHALER_FRONTEND && process.stdout.isTTY) { waitMode = 'interactive'; } } } let tty = false; let attachStdin = false; if ('interactive' === waitMode) { tty = true; attachStdin = true; } const createOpts = { 'name': name + '.' + appName, 'Hostname': name + '.' + appName, 'Image': config['image'] || 'whaler_' + appName + '_' + name, 'Tty': tty, 'OpenStdin': attachStdin, 'AttachStdin': attachStdin, 'AttachStdout': true, 'AttachStderr': true, 'Env': config['env'], 'Labels': config['labels'], 'ExposedPorts': {}, 'HostConfig': { 'Binds': [ '/var/lib/whaler/bin/bridge:/usr/bin/@me', '/var/lib/whaler/bin/bridge:/usr/bin/@whaler' ], 'RestartPolicy': {}, 'PortBindings': {}, 'VolumesFrom': null, 'ExtraHosts': null } }; if (config['restart']) { if ('string' === typeof config['restart']) { config['restart'] = { name: config['restart'], max_retry: 0 }; } const availableRestartPolicy = ['always', 'unless-stopped', 'on-failure']; createOpts['HostConfig']['RestartPolicy'] = { 'Name': -1 !== availableRestartPolicy.indexOf(config['restart']['name']) ? config['restart']['name'] : '' }; if ('on-failure' == config['restart']['name']) { createOpts['HostConfig']['RestartPolicy']['MaximumRetryCount'] = config['restart']['max_retry'] || 0; } } let logging = config['logging'] || whalerConfig['log'] || null; if (logging) { createOpts['HostConfig']['LogConfig'] = { 'Type': logging['driver'] || 'json-file', 'Config': logging['options'] || {} }; } let imageId = null; try { const image = docker.getImage(createOpts['Image']); const info = await image.inspect(); imageId = info['Id']; } catch (e) {} let buildContext = config['build'] || null; if (config['dockerfile']) { if (!buildContext) { buildContext = [ { Dockerfile: config['dockerfile'] } ]; } else if ('string' === typeof buildContext) { buildContext = [ buildContext, { Dockerfile: config['dockerfile'] } ]; } else if (Array.isArray(buildContext)) { buildContext.push({ Dockerfile: config['dockerfile'] }); } else { let dockerfile = 'Dockerfile'; if ('string' === typeof buildContext['dockerfile']) { dockerfile = buildContext['dockerfile']; } if (!buildContext['context']) { buildContext['context'] = []; } else if ('string' === typeof buildContext['context']) { buildContext['context'] = [ buildContext['context'] ]; } const df = {}; df[dockerfile] = config['dockerfile']; buildContext['context'].push(df); } } let platform = ''; if ('string' === typeof config['platform']) { platform = config['platform']; } if (buildContext) { let file = null; let dockerfile = null; if ('string' === typeof buildContext) { let build = buildContext; if (build && !path.isAbsolute(build)) { build = path.join(path.dirname(appConfig['file']), path.normalize(build)); } file = await docker.createTarPack(build); } else { let context = null; if (Array.isArray(buildContext)) { context = buildContext; } else { context = buildContext['context'] || null; if ('string' === typeof buildContext['dockerfile']) { dockerfile = buildContext['dockerfile']; } } if (!context) { throw new Error('Context must be specified!'); } if ('string' === typeof context) { if (!path.isAbsolute(context)) { context = path.join(path.dirname(appConfig['file']), path.normalize(context)); } } else if (Array.isArray(context)) { for (let i = 0; i < context.length; i++) { if ('string' === typeof context[i] && !path.isAbsolute(context[i])) { context[i] = path.join(path.dirname(appConfig['file']), path.normalize(context[i])); } } } file = await docker.createTarPack({ context, dockerfile }); } let pull = true; if ('object' === typeof config['build'] && config['build'].hasOwnProperty('pull')) { pull = config['build']['pull']; } let buildargs = null; if ('object' === typeof config['build'] && config['build'].hasOwnProperty('args')) { buildargs = {}; for (let arg of config['build']['args']) { const arr = arg.split('='); buildargs[arr[0]] = arr[1]; } buildargs = JSON.stringify(buildargs); } let target = null; if ('object' === typeof config['build'] && config['build'].hasOwnProperty('target')) { target = config['build']['target']; } const authconfig = await whaler.emit('create:authconfig', createOpts); await docker.followBuildImage(file, { pull, platform, dockerfile, buildargs, target, authconfig, t: createOpts['Image'] }); } else { try { const authconfig = await whaler.emit('create:authconfig', createOpts); await docker.followPull(createOpts['Image'], { platform, authconfig }); } catch (e) {} } const image = docker.getImage(createOpts['Image']); const info = await image.inspect(); if (imageId && imageId != info['Id']) { try { const image = docker.getImage(imageId); await image.remove(); } catch (e) {} } if (config['workdir']) { createOpts['WorkingDir'] = config['workdir']; } if (config['entrypoint']) { if (config['entrypoint'].indexOf('\n') !== -1) { const dir = '/var/lib/whaler/volumes/' + appName + '/' + name; const entrypoint = dir + '/entrypoint'; await mkdirp(dir); await fs.writeFile(entrypoint, config['entrypoint'], { mode: '755' }); createOpts['HostConfig']['Binds'].push(entrypoint +':/usr/bin/@entrypoint'); config['entrypoint'] = '/usr/bin/@entrypoint'; } createOpts['Entrypoint'] = config['entrypoint']; } if (config['cmd']) { if ('string' === typeof config['cmd']) { if (config['cmd'].indexOf('\n') !== -1) { const dir = '/var/lib/whaler/volumes/' + appName + '/' + name; const cmd = dir + '/cmd'; await mkdirp(dir); await fs.writeFile(cmd, config['cmd'], { mode: '755' }); createOpts['HostConfig']['Binds'].push(cmd +':/usr/bin/@cmd'); config['cmd'] = '/usr/bin/@cmd'; } let hasEntrypoint = !!info['Config']['Entrypoint'] && info['Config']['Entrypoint'].length; if (createOpts['Entrypoint']) { hasEntrypoint = !!createOpts['Entrypoint'] && createOpts['Entrypoint'].length; } if (hasEntrypoint) { config['cmd'] = docker.util.parseCmd(config['cmd']); } else { config['cmd'] = ['/bin/sh', '-c', config['cmd']]; } } createOpts['Cmd'] = config['cmd']; } else if (info['Config']['Cmd']) { createOpts['Cmd'] = info['Config']['Cmd']; } let volumes = []; if (info['ContainerConfig']['Volumes']) { volumes = Object.keys(info['ContainerConfig']['Volumes']); } if (config['volumes_from']) { let volumesFrom = []; for (let name of config['volumes_from']) { const arr = name.split(':'); if ('container' === arr[0]) { arr.shift(); } else { arr[0] = arr[0] + '.' + appName; } // const len = arr.length; // const accessLevel = arr[len - 1]; // if (2 == len && -1 === ['ro', 'rw', 'z', 'Z'].indexOf(accessLevel)) { // arr.pop(); // } volumesFrom.push(arr.join(':')); if (volumes.length) { const container = docker.getContainer(arr[0]); const info = await container.inspect(); const removeVolumes = []; if (info['Mounts'] && info['Mounts'].length) { for (let mount of info['Mounts']) { removeVolumes.push(mount['Destination']); } } volumes = volumes.filter((el) => { return removeVolumes.indexOf(el) < 0; }); } } createOpts['HostConfig']['VolumesFrom'] = volumesFrom; } if (config['volumes']) { for (let volume of config['volumes']) { const arr = volume.split(':'); if (arr.length == 1) { const index = volumes.indexOf(arr[0]); if (-1 === index) { volumes.push(arr[0]); } } else { let accessLevel = null; if (3 == arr.length) { accessLevel = arr.pop(); // if (-1 === ['ro', 'rw', 'z', 'Z'].indexOf(accessLevel)) { // accessLevel = null; // } } if (arr[0] in volumesCfg) { let volumeCfg = volumesCfg[arr[0]] || {}; if (volumeCfg['external']) { arr[0] = volumeCfg['external']['name'] || arr[0]; let appVolume = docker.getVolume(arr[0]); await appVolume.inspect(); } else { if (!/^[a-z0-9-]+$/.test(arr[0])) { throw new Error('Application volume name `' + arr[0] + '` includes invalid characters, only `[a-z0-9-]` are allowed.'); } arr[0] = 'whaler_vlm.' + appName + '.' + (volumeCfg['name'] || arr[0]); volumeCfg['labels'] = volumeCfg['labels'] || {}; for (let l in volumeCfg['labels']) { volumeCfg['labels'][l] = JSON.stringify(volumeCfg['labels'][l]); } let appVolume = docker.getVolume(arr[0]); try { await appVolume.inspect(); } catch (e) { appVolume = await docker.createVolume({ 'Name': arr[0], 'Driver': volumeCfg['driver'] || 'local', 'DriverOpts': volumeCfg['driver_opts'] || {}, 'Labels': volumeCfg['labels'] }); } } } else { if (!path.isAbsolute(arr[0])) { arr[0] = path.join(path.dirname(appConfig['file']), path.normalize(arr[0])); } } if (volumes.length) { const index = volumes.indexOf(arr[1]); if (-1 !== index) { volumes.splice(index, 1); } } if (accessLevel) { arr.push(accessLevel); } createOpts['HostConfig']['Binds'].push(arr.join(':')); } } } if (volumes.length) { for (let volume of volumes) { const v = '/var/lib/whaler/volumes/' + appName + '/' + name + volume; createOpts['HostConfig']['Binds'].push(v + ':' + volume); } } if (config['ports']) { for (let value of config['ports']) { const arr = value.split(':'); let port = arr[1]; let hostPort = arr[0]; let hostIp = ''; if (3 === arr.length) { port = arr[2]; hostPort = arr[1]; hostIp = arr[0]; } if (-1 == port.indexOf('/tcp') || -1 == port.indexOf('/udp')) { port += '/tcp'; } createOpts['ExposedPorts'][port] = {}; createOpts['HostConfig']['PortBindings'][port] = [ { 'HostIp': hostIp, 'HostPort': hostPort } ]; } } if (config['extra_hosts']) { createOpts['HostConfig']['ExtraHosts'] = []; for (let value of config['extra_hosts']) { const arr = value.split(':'); if (3 === arr.length && 'container' === arr[1]) { const container = docker.getContainer(arr[2]); const info = await container.inspect(); createOpts['HostConfig']['ExtraHosts'].push(arr[0] + ':' + info['NetworkSettings']['Networks']['bridge']['IPAddress']); } else { createOpts['HostConfig']['ExtraHosts'].push(value); } } } //const container = await docker.createContainer(createOpts); const container = await whaler.emit('create:container', createOpts); if (whalerNetwork) { await whalerNetwork.connect({ 'Container': container.id }); } if (appNetwork) { await appNetwork.connect({ 'Container': container.id, 'EndpointConfig': { 'Aliases': [name] } }); } whaler.info('Container `%s.%s` created.', name, appName); containers[name] = container; } ctx.result = containers; }); // TODO: experimental whaler.on('create:container', async ctx => { const { default: docker } = await whaler.fetch('docker'); ctx.result = await docker.createContainer(ctx.options); }); // TODO: experimental whaler.on('create:authconfig', async ctx => { try { const { default: config } = await whaler.import(process.env.HOME + '/.docker/config.json'); const serveraddress = ctx.options['Image'].split('/')[0]; if (config['auths'][serveraddress]) { if (config['auths'][serveraddress]['auth']) { let [ username, password ] = Buffer.from(config['auths'][serveraddress]['auth'], 'base64').toString().split(':'); ctx.result = { username, password, serveraddress }; } } } catch (e) { ctx.error = e; } }); } // PRIVATE async function mkdirp (dir) { try { await fs.stat(dir); return; } catch (e) {} await fs.mkdir(dir, { recursive: true }); }