UNPKG

dockerode-compose

Version:

docker-compose in nodejs using dockerode

577 lines (549 loc) 17.9 kB
const tools = require('./tools'); const servicesTools = require('./servicesTools'); const fs = require('fs'); const yaml = require('js-yaml'); const path = require('path'); async function down(docker, projectName, recipe, output, options) { var services = []; var serviceNames = tools.sortServices(recipe); for (var serviceName of serviceNames) { let container = docker.getContainer(projectName + '_' + serviceName + '_1'); try { await container.stop(); } catch (e) {} try { await container.remove(); } catch (e) {} } return services; } async function up(docker, projectName, recipe, output, options) { var services = []; var serviceNames = tools.sortServices(recipe); const cwdPath = path.dirname(output.file); for (var serviceName of serviceNames) { var pathScope = {}; pathScope.file = output.file; var networksToAttach = []; var service = recipe.services[serviceName]; if (service.extends !== undefined) { if (service.extends.service !== undefined) { service = extendsServices(service, recipe, pathScope); } else { throw new Error('Service key in extends is required!'); } } if (service.build !== undefined) { var absolutePath = path.dirname(pathScope.file); var obj = {}; if (service.image !== undefined) { obj['t'] = service.image; } else { obj['t'] = projectName + '_' + serviceName; service.image = projectName + '_' + serviceName; } if (typeof service.build === 'object') { if (service.build.context !== undefined) { var buildContextPath = path.resolve( path.join(absolutePath, service.build.context) ); if (fs.existsSync(buildContextPath)) { if (service.build.args !== undefined) { var out = {}; if (Array.isArray(service.build.args)) { for (let arg_line of service.build.args) { var arg = arg_line.split('='); out[arg[0]] = arg[1]; } } else { var argNames = Object.keys(service.build.args); for (var argName of argNames) { out[argName] = service.build.args[argName]; } } obj['buildargs'] = out; } if (service.build.cache_from !== undefined) { obj['cachefrom'] = service.build.cache_from; } if (service.build.extra_hosts !== undefined) { obj['extrahosts'] = service.build.extra_hosts; } if (service.build.labels !== undefined) { if (service.build.labels.length > 0) { let labels = {}; for (let labelsb of service.build.labels) { let p = labelsb.split('='); if (p[1] === undefined) { p[1] = ''; } labels[p[0]] = p[1]; } obj['labels'] = labels; } else { obj['labels'] = service.build.labels; } } if (service.build.shm_size !== undefined) { // RE ARRAGE the function "convertSizeStringToByteValue" to a generic one obj['shmsize'] = servicesTools.convertSizeStringToByteValue([ { path: '', rate: service.build.shm_size }, ]).Rate; } if (service.build.target !== undefined) { obj['target'] = service.build.target; } if (service.build.dockerfile === undefined) { await servicesTools.buildDockerImage( docker, buildContextPath, obj, null, options ); } else { await servicesTools.buildDockerImage( docker, buildContextPath, obj, service.build.dockerfile, options ); } } else { throw new Error( `build path ${buildContextPath} either does not exist, is not accessible, or is not a valid URL.` ); } } else { throw new Error('Build context is required!'); } } else { var dockerFilePath = path.resolve( path.join(absolutePath, service.build) ); if (fs.existsSync(dockerFilePath)) { await servicesTools.buildDockerImage( docker, dockerFilePath, obj, null, options ); } else { throw new Error( `build path ${dockerFilePath} either does not exist, is not accessible, or is not a valid URL.` ); } } } var opts = { name: projectName + '_' + serviceName + '_1', Image: service.image, HostConfig: servicesTools.buildHostConfig( projectName, service, recipe, cwdPath ), Env: servicesTools.buildEnvVars(service), NetworkingConfig: { EndpointsConfig: {}, }, Labels: { 'com.docker.compose.project': projectName, 'com.docker.compose.service': serviceName, }, }; if (service.networks !== undefined) { servicesTools.buildNetworks(projectName, serviceName, service.networks, networksToAttach, opts, recipe.networks); } else { opts.HostConfig.NetworkMode = projectName + '_default'; opts.NetworkingConfig.EndpointsConfig[projectName + '_default'] = { IPAMConfig: null, Links: null, Aliases: [serviceName], }; } // Can be used VolumesFrom from API DIRECTLY inside HostConfig :( if (service.volumes_from) { for (var volume_from of service.volumes_from) { var vf = volume_from.split(':'); var svf = recipe.services[vf[0]]; servicesTools.buildVolumes(svf.volumes, opts); } } if (service.volumes) { servicesTools.buildVolumes(service.volumes, opts); } if (service.container_name !== undefined) { opts.name = service.container_name; } if (service.domainname !== undefined) { opts.Domainname = service.domainname; } if (service.hostname !== undefined) { opts.Hostname = service.hostname; } if (service.mac_address !== undefined) { opts.MacAddress = service.mac_address; } if (service.stdin_open !== undefined) { opts.OpenStdin = service.stdin_open; } if (service.stop_grace_period !== undefined) { let period = parseInt(service.stop_grace_period); if (service.stop_grace_period == period) { opts.StopTimeout = service.stop_grace_period; } else if ( service.stop_grace_period.includes('m') && service.stop_grace_period.includes('s') ) { let minutes = parseInt( service.stop_grace_period.substring( 0, service.stop_grace_period.indexOf('m') ) ); let seconds = parseInt( service.stop_grace_period.substring( service.stop_grace_period.indexOf('m') + 1, service.stop_grace_period.indexOf('s') ) ); opts.StopTimeout = minutes * 60 + seconds; } else { opts.StopTimeout = service.stop_grace_period.substring( 0, service.stop_grace_period.length - 2 ); } } if (service.stop_signal !== undefined) { opts.StopSignal = service.stop_signal; } if (service.expose !== undefined) { var ports = {}; for (var port of service.expose) { ports[port + '/tcp'] = {}; } opts.ExposedPorts = ports; } if (service.tty !== undefined) { opts.Tty = service.tty; } if (service.user !== undefined) { opts.User = service.user; } if (service.working_dir !== undefined) { opts.WorkingDir = service.working_dir; } if (service.labels !== undefined) { if (service.labels.length > 0) { let labels = {}; for (let labelsb of service.labels) { let p = labelsb.split('='); if (p[1] === undefined) { p[1] = ''; } labels[p[0]] = p[1]; } opts.Labels = labels; } else { opts.Labels = service.labels; } } if (service.healthcheck !== undefined) { let healthcheck = {}; healthcheck.Test = service.healthcheck.test; healthcheck.Interval = convertFancyDurationToMs( service.healthcheck.interval ); healthcheck.Timeout = convertFancyDurationToMs( service.healthcheck.timeout ); healthcheck.Retries = service.healthcheck.retries; healthcheck.StartPeriod = convertFancyDurationToMs( service.healthcheck.start_period ); opts.Healthcheck = healthcheck; } if (service.command !== undefined) { opts.Cmd = service.command; } if (service.entrypoint !== undefined) { if (Array.isArray(service.entrypoint)) { opts.Entrypoint = service.entrypoint; } else { let entrypoint = []; entrypoint.push(service.entrypoint); opts.Entrypoint = entrypoint; } } var container = await docker.createContainer(opts); if (networksToAttach.length > 1) { let networkNames = Object.keys(networksToAttach[0]); await findNetwork(output, networkNames[0]).disconnect({ Container: container.id, }); let networksToAttachSorted = tools.sortNetworksToAttach(networksToAttach); for (var networkToAttach of networksToAttachSorted) { let networkName = Object.keys(networkToAttach); await findNetwork(output, networkName).connect({ Container: container.id, EndpointConfig: networkToAttach[networkName], }); } } await container.start(); services.push(container); } return services; } var findNetwork = function (output, name) { for (var network of output.networks) { if (network.name == name) return network.network; } }; var convertFancyDurationToMs = function (value) { let interval = parseInt(value); if (value == interval) { return value; } else if (value.includes('m') && value.includes('s')) { let minutes = parseInt(value.substring(0, value.indexOf('m'))); let seconds = parseInt( value.substring(value.indexOf('m') + 1, value.indexOf('s')) ); return (minutes * 60 + seconds) * 1000 * 1000000; } else { return parseInt(value.substring(0, value.length - 2)) * 1000 * 1000000; } }; // https://github.com/compose-spec/compose-spec/blob/master/spec.md#extends var extendsServices = function (service, recipe, pathScope) { // https://github.com/compose-spec/compose-spec/blob/master/spec.md#finding-referenced-service if (service.extends.file === undefined) { // EXTENDS OF THE SAME RECIPE return buildExtendsService( service, service.extends.service, recipe, pathScope ); } else { // EXTENDS OF ANOTHER RECIPE var absolutePath = path.dirname(pathScope.file); var extendsRecipe = yaml.load( fs.readFileSync( path.resolve(path.join(absolutePath, service.extends.file)), 'utf8' ) ); return buildExtendsService( service, service.extends.service, extendsRecipe, pathScope ); } }; var buildExtendsService = function ( service, extendsServiceName, recipe, pathScope ) { var extend = false; var extendsRecipeServiceNames = Object.keys(recipe.services); for (var extendsRecipeServiceName of extendsRecipeServiceNames) { if (extendsRecipeServiceName == extendsServiceName) { var extendsService = recipe.services[extendsRecipeServiceName]; //deep copy //var oldService = JSON.parse(JSON.stringify(service)); var oldService = service; service = extendsService; var serviceKeys = Object.keys(service); for (let key of serviceKeys) { verifyRestrictions(key); if (key == 'extends') { extend = true; } } var oldServiceKeys = Object.keys(oldService); for (let key of oldServiceKeys) { if (key != 'extends') { mergingService(key, service, oldService); } } if (oldService.extends.file) { var absolutePath = path.dirname(pathScope.file); pathScope.file = path.resolve( path.join(absolutePath, oldService.extends.file) ); } if (extend) service = extendsServices(service, recipe, pathScope); return service; } } throw new Error('Extends service not found'); }; // https://github.com/compose-spec/compose-spec/blob/master/spec.md#restrictions var verifyRestrictions = function (key) { var restrictions = [ 'links', 'volumes_from', 'depends_on', 'ipc', 'pid', 'network_mode', //'net' ]; if (restrictions.includes(key)) { throw new Error('This extends service cannot be used as a base'); } }; // https://github.com/compose-spec/compose-spec/blob/master/spec.md#merging-service-definitions var mergingService = function (key, service, oldService) { var mappings = [ 'environment', //'healthcheck', 'labels', 'sysctls', 'extra_hosts', 'ulimits', ]; var objectMappings = { build: { args: '', labels: '', extra_hosts: '' }, deploy: { labels: '', update_config: '', rollback_config: '', restart_policy: '', resources: { limits: '' }, }, blkio_config: { device_read_bps: '', device_read_iops: '', device_write_bps: '', device_write_iops: '', }, logging: { options: '' }, }; var sequences = [ 'cap_add', 'cap_drop', //'configs', // not implemented yet 'device_cgroup_rules', 'expose', //'external_links', // not implemented yet 'ports', //'secrets', // not fully implemented yet 'security_opt', ]; var objectSequences = { deploy: { placement: { constraints: '', preferences: '' }, reservations: { generic_resources: '' }, }, }; // https://github.com/compose-spec/compose-spec/blob/master/spec.md#mappings - MAPPINGS if ( key == 'build' || key == 'deploy' || key == 'blkio_config' || key == 'logging' ) { // one object level missing in deploy resources var objectMappingsKeys = Object.keys(objectMappings[key]); for (let objectMappingsKey of objectMappingsKeys) { if (oldService[key][objectMappingsKey] !== undefined) { service[key][objectMappingsKey] = oldService[key][objectMappingsKey]; } } } else if (mappings.includes(key) && service[key] !== undefined) { // TRICKY TRICKY (bugs can appear because long and short syntaxes) if (Array.isArray(oldService[key]) || Array.isArray(service[key])) { if (!Array.isArray(service[key])) { let tempService = []; let envsNames = Object.keys(service[key]); for (let envName of envsNames) { tempService.push(envName + '=' + service[key][envName]); } service[key] = tempService; } if (!Array.isArray(oldService[key])) { var tempOldService = []; let envsNames = Object.keys(oldService[key]); for (let envName of envsNames) { tempOldService.push(envName + '=' + oldService[key][envName]); } oldService[key] = tempOldService; } for (let oldServiceLine of oldService[key]) { for (let serviceLine of service[key]) { if ( serviceLine.split('=')[0] == oldServiceLine.split('=')[0] || serviceLine.split(':')[0] == oldServiceLine.split(':')[0] ) { service[key].splice( service[key].indexOf(serviceLine), 1, oldServiceLine ); } } if (!service[key].includes(oldServiceLine)) { service[key].push(oldServiceLine); } } } else { Object.assign(service[key], oldService[key]); } // https://github.com/compose-spec/compose-spec/blob/master/spec.md#sequences - SEQUENCES } else if ( key == 'dns' || key == 'dns_search' || key == 'env_file' || key == 'tmpfs' ) { if (Array.isArray(oldService[key]) || Array.isArray(service[key])) { if (!Array.isArray(service[key])) { let tempService = []; if (service[key] !== undefined) { tempService.push(service[key]); } service[key] = tempService; } if (!Array.isArray(oldService[key])) { oldService[key] = [oldService[key]]; } service[key] = service[key].concat(oldService[key]); } else { service[key] = []; service[key].push(service[key]); service[key].push(oldService[key]); } } else if (key == 'deploy') { var objectSequencesKeys = Object.keys(objectSequences[key]); for (let objectSequencesKey of objectSequencesKeys) { if (oldService[key][objectSequencesKey] !== undefined) { service[key][objectSequencesKey] = oldService[key][objectSequencesKey]; } } } else if (sequences.includes(key) && service[key] !== undefined) { if (Array.isArray(oldService[key]) || Array.isArray(service[key])) { for (let oldServiceLine of oldService[key]) { if (!service[key].includes(oldServiceLine)) { service[key].push(oldServiceLine); } } } // else !!! for now all keys are arrays with future implementations can maybe change !!! } else { // https://github.com/compose-spec/compose-spec/blob/master/spec.md#scalars - SCALARS service[key] = oldService[key]; } }; module.exports = { down, up, };