├── test ├── test.json ├── .eslintrc ├── test.js └── unit │ ├── docker_util_test.js │ └── links_test.js ├── lib ├── docker_util.js ├── init.js ├── demuxer.js ├── slave.js ├── run.js ├── links.js └── create-container.js ├── static ├── icon.png └── logo.png ├── .gitignore ├── .travis.yml ├── .jshintrc ├── .editorconfig ├── .eslintrc ├── package.json ├── config └── config.html ├── README.md └── index.js /test/test.json: -------------------------------------------------------------------------------- 1 | {"command":"sh","args":["-x","-c","echo 'hello'"]} -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/docker_util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('dockerode-optionator'); 4 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strider-CD/strider-docker-runner/HEAD/static/icon.png -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strider-CD/strider-docker-runner/HEAD/static/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ/WebStorm project files 2 | .idea/ 3 | 4 | # npm modules 5 | node_modules/ 6 | 7 | # npm debug output 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "node" 5 | 6 | sudo: required 7 | 8 | services: 9 | - docker 10 | 11 | env: 12 | - DOCKER_IP=127.0.0.1 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": false, 3 | "laxcomma": true, 4 | "undef": true, 5 | "browser": true, 6 | "unused": true, 7 | "globals": { 8 | "marked": false, 9 | "Promise": false, 10 | "_": false 11 | }, 12 | "node": true, 13 | "-W033": false, 14 | "-W024": false, 15 | "-W030": false, 16 | "-W069": false, 17 | "-W089": false, 18 | "-W098": false, 19 | "-W116": false, 20 | "-W065": false 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Strider Editor/IDE Settings 2 | # This file is used to promote consistent source code standards 3 | # amongst all Strider-CD contributors. 4 | # More information can be found here: http://editorconfig.org/ 5 | 6 | # General Settings 7 | root = true 8 | 9 | # Settings for all files 10 | [*] 11 | indent_style = space 12 | indent_size = 2 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.hbs] 19 | insert_final_newline = false 20 | 21 | [*.{diff,md}] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "script" 5 | }, 6 | "env": { 7 | "node": true, 8 | "es6": true 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": [2, 2], 13 | "brace-style": [2, "1tbs"], 14 | "quotes": [2, "single"], 15 | "semi": [2, "always"], 16 | "comma-style": [2, "last"], 17 | "one-var": [2, "never"], 18 | "strict": [2, "global"], 19 | "prefer-template": 2, 20 | "no-console": 0, 21 | "no-use-before-define": [2, "nofunc"], 22 | "no-underscore-dangle": 0, 23 | "no-constant-condition": 0, 24 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 25 | "func-style": [2, "declaration"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Docker = require('dockerode'); 4 | const dockerUtil = require('./docker_util'); 5 | const domain = require('domain'); 6 | 7 | module.exports = function (rawOpts, done) { 8 | const options = dockerUtil.normalizeOptions(rawOpts, process.env); 9 | 10 | const dockerDomain = domain.create(); 11 | dockerDomain.on('error', e => { 12 | e.connectOptions = options; 13 | done(e); 14 | }); 15 | 16 | dockerDomain.run(() => { 17 | try { 18 | const docker = new Docker(options); 19 | docker.listContainers(err => { 20 | if (err) err.connectOptions = options; 21 | done(err, docker); 22 | }); 23 | 24 | } catch (e) { 25 | e.connectOptions = options; 26 | done(e); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/demuxer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Taken from the docker-modem project, with a slight change to allow for 5 | * later "removeListener" 6 | * 7 | * ...okay, so what does it do? 8 | */ 9 | 10 | module.exports = demuxer; 11 | 12 | function demuxer(stream, stdout, stderr) { 13 | var header = null; 14 | 15 | function demux() { 16 | header = header || stream.read(8); 17 | while (header !== null) { 18 | const type = header.readUInt8(0); 19 | const payload = stream.read(header.readUInt32BE(4)); 20 | if (payload === null) break; 21 | if (type == 2) { 22 | stderr.write(payload); 23 | } else { 24 | stdout.write(payload); 25 | } 26 | header = stream.read(8); 27 | } 28 | } 29 | 30 | stream.on('readable', demux); 31 | return demux; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /lib/slave.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createContainer = require('./create-container'); 4 | const debug = require('debug')('strider-docker-runner:slave'); 5 | const resolveLinks = require('./links').resolveLinks; 6 | 7 | module.exports = createSlave; 8 | 9 | function createSlave(docker, config, done) { 10 | resolveLinks(docker, config.docker_links, (err, links) => { 11 | if (err) return done(err); 12 | 13 | debug('Creating container...'); 14 | createContainer({ 15 | Image: config.image, 16 | Env: config.env, 17 | AttachStdout: true, 18 | AttachStderr: true, 19 | OpenStdin: true, 20 | Tty: false, 21 | name: config.name, 22 | Binds: config.docker_volumeBinds, 23 | Links: links, 24 | Privileged: config.privileged, 25 | PublishAllPorts: config.publishPorts, 26 | Dns: config.dns 27 | }, docker, config, done); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const init = require('../lib/init'); 4 | const createSlave = require('../lib/slave'); 5 | 6 | if (!process.env.DOCKER_IP) { 7 | console.log('Need to specify (at least) DOCKER_IP'); 8 | process.exit(1); 9 | } 10 | 11 | function run(spawn, command, args, done) { 12 | spawn(command, args, {}, function (err, proc) { 13 | if (err) { 14 | throw err; 15 | } 16 | proc.stderr.on('data', function (data) { 17 | console.log('[err]', data.toString()); 18 | }); 19 | proc.stdout.on('data', function (data) { 20 | console.log('[out]', data.toString()); 21 | }); 22 | proc.on('exit', function (code) { 23 | console.log('[exit] with', code); 24 | done(code); 25 | }); 26 | }); 27 | } 28 | 29 | init({}, function (err, docker) { 30 | createSlave(docker, { 31 | image: 'strider/strider-docker-slave' 32 | }, function (err, spawn) { 33 | if (err) { 34 | throw err; 35 | } 36 | var command = 'git'; 37 | var args = ['clone', 'https://github.com/notablemind/loco.git', '.']; 38 | run(spawn, command, args, function () { 39 | run(spawn, 'echo', ['hello'], function () { 40 | process.exit(0); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strider-docker-runner", 3 | "version": "1.5.3", 4 | "description": "Run jobs inside of docker containers", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint *.js lib", 8 | "test": "npm run lint && node test/test.js", 9 | "tdd": "mocha -w --recursive test/unit" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Strider-CD/strider-docker-runner.git" 14 | }, 15 | "strider": { 16 | "id": "docker", 17 | "type": "runner", 18 | "title": "Docker", 19 | "webapp": "index.js", 20 | "icon": "icon.png", 21 | "config": true 22 | }, 23 | "keywords": [ 24 | "docker", 25 | "strider" 26 | ], 27 | "engines": { 28 | "node": ">=4.2" 29 | }, 30 | "author": "Jared Forsyth ", 31 | "license": "MIT", 32 | "dependencies": { 33 | "async": "^2.0.1", 34 | "debug": "^2.2.0", 35 | "dockerode": "^2.3.0", 36 | "dockerode-optionator": "^1.1.4", 37 | "event-stream": "^3.3.4", 38 | "lodash": "^4.14.1", 39 | "strider-runner-core": "^2.0.0", 40 | "strider-simple-runner": "^1.0.0" 41 | }, 42 | "devDependencies": { 43 | "chai": "^3.5.0", 44 | "eslint": "^3.2.2", 45 | "mocha": "^3.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/docker_util_test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , dockerUtil = require('../../lib/docker_util'); 3 | 4 | describe("dockerUtil#normalizeOptions()", function () { 5 | var fn = dockerUtil.normalizeOptions; 6 | describe("passing in nothing", function () { 7 | it("defaults to default socket path", function () { 8 | expect(fn({}, {})).to.deep.eq({ 9 | socketPath: "/var/run/docker.sock" 10 | }) 11 | }); 12 | }); 13 | describe("using environment variables DOCKER_IP and DOCKER_PORT", function () { 14 | it("returns expected dockerode connection structure", function () { 15 | var out = fn({}, { 16 | DOCKER_IP: "127.0.0.1", 17 | DOCKER_PORT: "4243" 18 | }); 19 | expect(out).to.deep.eq({ 20 | host: "127.0.0.1", 21 | port: "4243" 22 | }) 23 | }); 24 | }); 25 | 26 | describe("using environment variable DOCKER_HOST", function () { 27 | it("understands http://127.0.0.1:4243", function () { 28 | var out = fn({}, {DOCKER_HOST: "http://127.0.0.1:4243"}); 29 | expect(out).to.deep.eq({ 30 | host: "127.0.0.1", 31 | port: 4243 32 | }) 33 | }); 34 | it("understands unix:///var/run/docker.sock", function () { 35 | var out = fn({}, {DOCKER_HOST: "unix:///var/run/docker.sock"}); 36 | expect(out).to.deep.eq({ 37 | socketPath: "/var/run/docker.sock" 38 | }) 39 | }); 40 | it("understands tcp://127.0.0.1:4243", function () { 41 | var out = fn({}, {DOCKER_HOST: "tcp://127.0.0.1:4243"}); 42 | expect(out).to.deep.eq({ 43 | host: "127.0.0.1", 44 | port: 4243 45 | }) 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /config/config.html: -------------------------------------------------------------------------------- 1 |

Docker Runner

2 |

3 | Jobs are run inside docker containers! 4 |

5 | 6 | 7 | 8 |

9 | Use the following to bind host directories to volumes in a docker container: 10 |

11 | 12 |

13 | Link running docker containers into the test container: 14 |

15 | 16 | 20 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strider-docker-runner 2 | 3 | Run jobs inside of a Docker container. 4 | 5 | [![Build Status](https://travis-ci.org/Strider-CD/strider-docker-runner.svg?branch=master)](https://travis-ci.org/Strider-CD/strider-docker-runner) 6 | 7 | ## Installation 8 | 9 | `cd` into strider deployment and run `npm install strider-docker-runner` 10 | 11 | If you need to install Docker, see the [official installation instructions](https://docs.docker.com/installation/) 12 | 13 | The default image is `strider/strider-docker-slave` -- it is recommended to `docker pull strider/strider-docker-slave` directly on the Docker host, however the plugin will do this for you as of [PR#22](https://github.com/Strider-CD/strider-docker-runner/pull/22). 14 | 15 | If Docker is running on the same machine as Strider, you do not need to add any additional environment variables -- the plugin will try to use `unix:///var/run/docker.sock` to communicate with Docker. Make sure Strider has permission to do so, otherwise you will get errors when running a build. 16 | 17 | If Docker is running on a remote machine, you will need to use the [Docker Remote API](https://docs.docker.com/reference/api/docker_remote_api/) and let Strider know about it by setting `DOCKER_HOST` accordingly. e.g. `DOCKER_HOST=http://127.0.0.1:4243 strider` You can also override this value in the runner config per-project branch. 18 | 19 | Once Strider is running, go to your project's plugin config page and you will be able to select the Docker runner. You can also configure the Runner to use a different, custom base image. You may even combine this feature with [strider-docker-build](https://github.com/Strider-CD/strider-docker-build) to fully automate changes to the base image. 20 | 21 | ## Configuration Environment Variables 22 | 23 | It uses the standard Docker environment variable `DOCKER_HOST` 24 | 25 | Examples: 26 | 27 | ``` 28 | DOCKER_HOST="http://127.0.0.1:4243" 29 | DOCKER_HOST="unix:///var/run/docker.sock" 30 | DOCKER_HOST="tcp://127.0.0.1:4243" 31 | ``` 32 | 33 | You are not required to set `DOCKER_HOST` globally. You may choose to configure this value per project through the plugin config page. 34 | 35 | If DOCKER_HOST is not set and the value is also not configured in the plugin config page, it defaults to `unix:///var/run/docker.sock` 36 | 37 | If you are working with remote docker containers, it is advised to use [Dockers TLS security options](http://docs.docker.com/articles/https/). For client setup please see the [official documentation](http://docs.docker.com/articles/https/#client-modes) 38 | 39 | Example 40 | ``` 41 | DOCKER_HOST="tcp://127.0.0.1:4243" 42 | DOCKER_TLS_VERIFY=1 43 | DOCKER_CERT_PATH=~/stridercd/certs 44 | ``` 45 | Be sure to [enable TLS on the Daemon](http://docs.docker.com/articles/https/#daemon-modes) as well. More information at http://docs.docker.com/articles/https/ 46 | 47 | ## Verification 48 | 49 | See this comment in the "prepare" phase telling you that **docker is alive** 50 | 51 | ![](https://cloud.githubusercontent.com/assets/112170/3838066/871cff0c-1dfc-11e4-9fce-430447bafffa.png) 52 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const createSlave = require('./slave'); 5 | const debug = require('debug')('strider-docker-runner:run'); 6 | const initDocker = require('./init'); 7 | const processJob = require('strider-runner-core').process; 8 | const util = require('util'); 9 | 10 | module.exports = function (job, provider, plugins, config, next) { 11 | initDocker(config.branchConfig.runner.config, (err, docker) => { 12 | if (err) 13 | return next(new Error(`Cannot connect to Docker with options ${util.format(err.connectOptions)}\nActual Error: ${err.stack}`)); 14 | 15 | // Construct the environment for the container, by merging all environment variable declarations of all plugins. 16 | const env = []; 17 | _.each(plugins, plugin => { 18 | if (plugin.env) { 19 | _.each(plugin.env, (value, key) => { 20 | env.push(`${key}=${value}`); 21 | }); 22 | } 23 | }); 24 | 25 | const slaveConfig = _.extend({ 26 | id: job._id, 27 | dataDir: config.dataDir, 28 | image: 'strider/strider-docker-slave', 29 | env: env, 30 | name: `strider-${job._id}` 31 | }, config.branchConfig.runner.config); 32 | slaveConfig.image = slaveConfig.image || 'strider/strider-docker-slave'; 33 | 34 | if (slaveConfig.dns && slaveConfig.dns.length > 6) { 35 | slaveConfig.dns = slaveConfig.dns.split(','); 36 | } 37 | 38 | if (slaveConfig.docker_volumeBinds && slaveConfig.docker_volumeBinds.indexOf(',') > -1) { 39 | slaveConfig.docker_volumeBinds = slaveConfig 40 | .docker_volumeBinds 41 | .split(',') 42 | .map(volBinding => { 43 | return volBinding.trim(); 44 | }); 45 | } 46 | 47 | if (slaveConfig.docker_links) { 48 | slaveConfig.docker_links = slaveConfig.docker_links.trim(); 49 | if (slaveConfig.docker_links.indexOf(',') > -1) { 50 | slaveConfig.docker_links = slaveConfig 51 | .docker_links 52 | .split(',') 53 | .map(link => { 54 | return link.trim(); 55 | }); 56 | } else if (slaveConfig.docker_links) { 57 | slaveConfig.docker_links = [slaveConfig.docker_links]; 58 | } 59 | } 60 | slaveConfig.docker_links = slaveConfig.docker_links || []; 61 | 62 | config.io.emit('job.status.command.comment', job._id, { 63 | comment: `Creating docker container from ${slaveConfig.image}`, 64 | plugin: 'docker', 65 | time: new Date() 66 | }); 67 | 68 | createSlave(docker, slaveConfig, (err, spawn, kill) => { 69 | if (err) { 70 | debug('Error creating slave', err); 71 | return next(err); 72 | } 73 | 74 | // spawn is a function that allows us to execute shell commands inside the docker container. 75 | config.spawn = spawn; 76 | 77 | processJob(job, provider, plugins, config, err => { 78 | debug('Job done. Killing container.'); 79 | 80 | if (err) { 81 | debug('There was an error processing the job.', err); 82 | 83 | // Attempt to kill the container, no matter what. 84 | kill(() => { 85 | return next(new Error(err)); 86 | }); 87 | } 88 | 89 | const originalError = err; 90 | kill(err => { 91 | if (err) debug('Failed to kill docker container!', err); 92 | next(originalError); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /lib/links.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | const debug = require('debug')('strider-docker-runner:links'); 5 | const linkLabels = ['com.stridercd.link', 'com.docker.compose.service']; 6 | 7 | // Parse docker link syntax into sanitized name, alias pair 8 | // e.g. redis:db -> [redis, db] 9 | // mongo -> [mongo, mongo] 10 | // /db_1 -> [db_1, db_1] 11 | // Returns undefined on parse failure 12 | function parseLink(link) { 13 | if (typeof link !== 'string') return; 14 | 15 | const parts = link.split(':'); 16 | if (parts.length > 2 || !parts[0].length) return; 17 | 18 | const name = parts[0].replace(/^\//, '').trim(); 19 | if (!name.length) return; 20 | 21 | const alias = parts.length === 1 ? name : parts[1].trim(); 22 | if (!alias.length) return; 23 | 24 | return [name, alias]; 25 | } 26 | 27 | // List all containers matching a label = value filter 28 | function filterContainers(docker, label, value, done) { 29 | const opts = { 30 | filters: JSON.stringify({ 31 | label: [`${label}=${value}`] 32 | }) 33 | }; 34 | docker.listContainers(opts, done); 35 | } 36 | 37 | // Find the first label with containers matching value and return the containers 38 | // Errors are considered a non-match and are never returned. 39 | // Callback is called with undefined if no labels matched any containers. 40 | function findLabeledContainers(docker, labels, value, done) { 41 | // Hack reduce error to work like find 42 | async.reduce(labels, undefined, (found, label, done) => { 43 | filterContainers(docker, label, value, (err, containers) => { 44 | if (found) return done(found); 45 | if (containers && containers.length > 0) { 46 | debug('[runner:docker] found containers with label', label); 47 | return done(containers); 48 | } 49 | debug('[runner:docker] no containers with label', label); 50 | done(); 51 | }); 52 | }, containers => { 53 | done(undefined, containers); 54 | }); 55 | } 56 | 57 | // Find the first label with a container matching value and return the container 58 | // Errors are considered a non-match and are never returned. 59 | // Callback is called with undefined if no labels matched any container. 60 | function findLabeledContainer(docker, labels, value, done) { 61 | findLabeledContainers(docker, labels, value, (err, containers) => { 62 | if (containers && containers.length > 0) { 63 | return done(undefined, containers[0]); 64 | } 65 | done(); 66 | }); 67 | } 68 | 69 | // Resolves strider docker runner links into docker links using names and labels 70 | // First checks for a container with the given name, then searches for 71 | // the first container matching a set of predefined labels. 72 | // Callback called with an error if the link cannot be parsed or no matching 73 | // container is found. 74 | function resolveLinks(docker, links, done) { 75 | async.map(links, function (link, done) { 76 | 77 | let parsed = parseLink(link); 78 | if (!parsed) return done(new Error(`Invalid link: ${link}`)); 79 | 80 | function resolve(name) { 81 | const resolved = [name, parsed[1]].join(':'); 82 | debug('[runner:docker] resolved link', link, resolved); 83 | return done(undefined, resolved); 84 | } 85 | 86 | // Try to find a container by name (or id) 87 | const name = parsed[0]; 88 | docker.getContainer(name).inspect((err, container) => { 89 | if (!err && container) return resolve(name); 90 | debug('[runner:docker] no container with name', name); 91 | // Try to find a container by label 92 | findLabeledContainer(docker, linkLabels, name, (err, container) => { 93 | if (!err && container) return resolve(container.Id); 94 | debug('[runner:docker] no container with label', name); 95 | done(new Error(`No container found for link: ${link}`)); 96 | }); 97 | }); 98 | }, done); 99 | } 100 | 101 | module.exports = { 102 | parseLink: parseLink, 103 | filterContainers: filterContainers, 104 | findLabeledContainers: findLabeledContainers, 105 | findLabeledContainer: findLabeledContainer, 106 | resolveLinks: resolveLinks 107 | }; 108 | -------------------------------------------------------------------------------- /lib/create-container.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | const debug = require('debug')('strider-docker-runner:create-container'); 5 | const demuxer = require('./demuxer'); 6 | const es = require('event-stream'); 7 | const EventEmitter = require('events').EventEmitter; 8 | const inspect = require('util').inspect; 9 | const stream = require('stream'); 10 | 11 | function isImageLocally(docker, image, done) { 12 | const imageSplitted = image.split(':'); 13 | const withoutTag = imageSplitted.reduce((accum, current, index) => { 14 | if (index < imageSplitted.length - 1) { 15 | return `${accum}:${current}`; 16 | } else { 17 | return accum; 18 | } 19 | }); 20 | const fullname = image === withoutTag ? `${image}:latest` : image; 21 | 22 | docker.listImages({filter: withoutTag}, function (err, images) { 23 | if (err) return done(err); 24 | 25 | const found = images.some(function (img) { 26 | return img.RepoTags && img.RepoTags.indexOf(fullname) >= 0; 27 | }); 28 | 29 | done(null, found); 30 | }); 31 | } 32 | 33 | function pull(docker, image, done) { 34 | docker.pull(image, (err, streamc) => { 35 | if (err) return done(err); 36 | 37 | streamc 38 | .pipe(es.map((data, cb) => { 39 | let json_data = null; 40 | 41 | try { 42 | json_data = JSON.parse(data.toString()); 43 | } catch (error) { 44 | json_data = { 45 | type: 'stdout', 46 | data: data.toString() 47 | }; 48 | } 49 | 50 | cb(null, json_data); 51 | })) 52 | .on('data', event => { 53 | debug(`pull event: ${inspect(event)}`); 54 | }) 55 | .on('end', () => { 56 | done(); 57 | }); 58 | }); 59 | } 60 | 61 | function create(createOptions, docker, config, done) { 62 | docker.createContainer(createOptions, (err, container) => { 63 | if (err) return done(new Error(err)); 64 | 65 | debug('[runner:docker] container id', container.id); 66 | 67 | container.attach({ 68 | stream: true, stdin: true, 69 | stdout: true, stderr: true 70 | }, attached); 71 | 72 | function attached(err, streamc) { 73 | if (err) return done(err); 74 | if (!streamc) return done(new Error('Failed to attach container stream')); 75 | 76 | // start, and wait for it to be done 77 | container.start(err => { 78 | if (err) return done(new Error(err)); 79 | 80 | container.wait((err, data) => { 81 | debug('done with the container', err, data); 82 | container.stop(() => { 83 | debug('Stopped the container!'); 84 | }); 85 | }); 86 | 87 | done(err, spawn.bind(null, streamc), kill); 88 | }); 89 | } 90 | 91 | function kill(done) { 92 | container.remove({ 93 | force: true, // Stop container and remove 94 | v: true // Remove any attached volumes 95 | }, done); 96 | } 97 | 98 | function spawn(streamc, command, args, options, done) { 99 | const proc = new EventEmitter(); 100 | proc.kill = function () { 101 | streamc.write(`${JSON.stringify({type: 'kill'})}\n`); 102 | }; 103 | proc.stdout = new stream.PassThrough(); 104 | proc.stderr = new stream.PassThrough(); 105 | proc.stdin = streamc; 106 | var stdout = new stream.PassThrough(); 107 | var stderr = new stream.PassThrough(); 108 | 109 | done(null, proc); 110 | 111 | const demux = demuxer(streamc, stdout, stderr); 112 | 113 | stdout 114 | .pipe(es.split()) 115 | .pipe(es.parse()) 116 | .pipe(es.mapSync(function (data) { 117 | debug('got an event', data); 118 | if (data.event === 'stdout') { 119 | proc.stdout.write(data.data); 120 | } 121 | if (data.event === 'stderr') { 122 | proc.stderr.write(data.data); 123 | } 124 | if (data.event === 'exit') { 125 | proc.emit('exit', data.code); 126 | streamc.removeListener('readable', demux); 127 | stdout.unpipe(); 128 | } 129 | })); 130 | 131 | debug('running command', command); 132 | debug('with args', args); 133 | 134 | streamc.write(`${JSON.stringify({command: command, args: args, type: 'start'})}\n`); 135 | } 136 | }); 137 | } 138 | 139 | module.exports = function (createOptions, docker, config, done) { 140 | async.waterfall([ 141 | // First check if we already have the image stored locally. 142 | callback => { 143 | debug('Checking if image exists locally...'); 144 | isImageLocally(docker, createOptions.Image, callback); 145 | }, 146 | 147 | // If the image isn't stored locally, pull it. 148 | (isLocally, callback) => { 149 | if (isLocally) { 150 | debug('Image is already locally'); 151 | return callback(); 152 | } 153 | debug(`Unable to find image "${createOptions.Image}" locally`); 154 | pull(docker, createOptions.Image, callback); 155 | }, 156 | 157 | // Create the container. 158 | () => { 159 | debug('Creating container...'); 160 | create(createOptions, docker, config, done); 161 | } 162 | ]); 163 | }; 164 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('strider-docker-runner'); 4 | const initDocker = require('./lib/init'); 5 | const runDocker = require('./lib/run'); 6 | const Runner = require('strider-simple-runner').Runner; 7 | 8 | function create(emitter, config, context, done) { 9 | config = config || {}; 10 | config.processJob = runDocker; 11 | 12 | const runner = new Runner(emitter, config); 13 | runner.id = 'docker'; 14 | 15 | debug('Overriding runner.processJob'); 16 | runner.processJob = function processJob(job, config, next) { 17 | debug('Running docker job...'); 18 | 19 | const self = this; 20 | const now = new Date(); 21 | 22 | const oldnext = next; 23 | next = () => { 24 | delete self.callbackMap[job._id]; 25 | oldnext(); 26 | }; 27 | this.callbackMap[job._id] = next; 28 | 29 | const dirs = { 30 | base: '/home/strider/workspace', 31 | data: '/home/strider/workspace', 32 | cache: '/home/strider/workspace' 33 | }; 34 | 35 | self.jobdata.get(job._id).started = now; 36 | self.emitter.emit('browser.update', job.project.name, 'job.status.started', [job._id, now]); 37 | debug(`[runner:${self.id}] Job started. Project: ${job.project.name} Job ID: ${job._id}`); 38 | debug('Initializing plugins...'); 39 | self.plugins(job.project.creator, config, job, dirs, (err, workers) => { 40 | if (err) { 41 | let jobdata = self.jobdata.pop(job._id); 42 | if (!jobdata) return next(err); 43 | jobdata.errored = true; 44 | jobdata.error = { 45 | message: err.message, 46 | stack: err.stack 47 | }; 48 | // self.emitter.emit('browser.update', job.project.name, 'job.status.errored', [job._id, jobdata.error]) 49 | delete jobdata.data; 50 | jobdata.finished = new Date(); 51 | self.emitter.emit('job.done', jobdata); 52 | 53 | debug(`[runner:${self.id}] Job done with error. Project: ${job.project.name} Job ID: ${job._id}`); 54 | return next(err); 55 | } 56 | 57 | const env = {}; 58 | if (config.envKeys) { 59 | env.STRIDER_SSH_PUB = config.pubkey; 60 | env.STRIDER_SSH_PRIV = config.privkey; 61 | } 62 | self.config.processJob(job, workers.provider, workers.jobplugins, { 63 | cachier: Function.prototype, 64 | baseDir: dirs.base, 65 | dataDir: dirs.data, 66 | cacheDir: dirs.cache, 67 | io: self.config.io, 68 | branchConfig: config, 69 | env: env, 70 | log: debug, 71 | error: debug 72 | }, err => { 73 | var jobdata = self.jobdata.pop(job._id); 74 | if (!jobdata) return next(err); 75 | 76 | // Mark jobs as finished. 77 | delete jobdata.data; 78 | jobdata.finished = new Date(); 79 | 80 | if (err) { 81 | jobdata.errored = true; 82 | jobdata.error = { 83 | message: err.message, 84 | stack: err.stack 85 | }; 86 | self.emitter.emit('browser.update', job.project.name, 'job.status.errored', [job._id, jobdata.error]); 87 | 88 | debug(`[runner:${self.id}] Job done with error. Project: ${job.project.name} Job ID: ${job._id}`); 89 | return next(err); 90 | } 91 | 92 | delete jobdata.data; 93 | jobdata.finished = new Date(); 94 | self.emitter.emit('job.done', jobdata); 95 | debug(`[runner:${self.id}] Job done without error. Project: ${job.project.name} Job ID: ${job._id}`); 96 | return next(); 97 | }); 98 | }); 99 | }; 100 | 101 | debug('Fixing job queue handler'); 102 | runner.queue.handler = runner.processJob.bind(runner); 103 | 104 | runner.loadExtensions(context.extensionPaths, err => { 105 | done(err, runner); 106 | }); 107 | } 108 | 109 | /** 110 | * List all running containers and check if their names are prefixed with "strider-". 111 | * If so, then they were probably started during a previous run and were not shut down properly. 112 | * In that case, we try to clean them up now. 113 | * 114 | * Note that this process does and can not know about containers that were started on any other but 115 | * the default host. Meaning with a different host given in the job configuration. 116 | */ 117 | function cleanup() { 118 | debug('Cleaning up...'); 119 | initDocker({}, (err, docker) => { 120 | if (err) { 121 | debug(err); 122 | return; 123 | } 124 | 125 | docker.listContainers((err, containers) => { 126 | if (err) { 127 | debug(err); 128 | return; 129 | } 130 | 131 | debug(`Found ${containers.length} running containers.`); 132 | 133 | containers.forEach(function (containerInfo) { 134 | if (!containerInfo.Names[0].match(/^\/strider-/)) { 135 | debug(`Container "${containerInfo.Names[0]}" is not a strider runner. Skipping.`); 136 | return; 137 | } 138 | 139 | debug(`Attempting to clean up container "${containerInfo.Names[0]}"...`); 140 | docker.getContainer(containerInfo.Id) 141 | .remove({ 142 | force: true, // Stop container and remove 143 | v: true // Remove any attached volumes 144 | }, err => { 145 | if (err) { 146 | debug(err); 147 | return; 148 | } 149 | 150 | debug(`Cleaned up container "${containerInfo.Names[0]}".`); 151 | }); 152 | }); 153 | }); 154 | }); 155 | } 156 | // Cleanup on initial module load. 157 | cleanup(); 158 | 159 | module.exports = { 160 | create: create, 161 | config: { 162 | host: String, 163 | port: Number, 164 | socketPath: String, 165 | dns: String, 166 | docker_host: String, 167 | docker_volumeBinds: String, 168 | docker_links: String 169 | } 170 | }; 171 | -------------------------------------------------------------------------------- /test/unit/links_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const links = require('../../lib/links'); 5 | 6 | describe('links#parseLink()', function () { 7 | var fn = links.parseLink; 8 | 9 | it('returns undefined for invalid links', function () { 10 | expect(fn()).to.be.undefined; 11 | expect(fn('')).to.be.undefined; 12 | expect(fn(':')).to.be.undefined; 13 | expect(fn('a:')).to.be.undefined; 14 | expect(fn(':b')).to.be.undefined; 15 | expect(fn('a:b:c')).to.be.undefined; 16 | }); 17 | 18 | it('uses the name as alias when no alias given', function () { 19 | expect(fn('abc')).to.deep.equal(['abc', 'abc']); 20 | }); 21 | 22 | it('splits a name:alias pair', function () { 23 | expect(fn('a:b')).to.deep.equal(['a', 'b']); 24 | }); 25 | 26 | it('strips the leading slash from the name', function () { 27 | expect(fn('/a:b')).to.deep.equal(['a', 'b']); 28 | }); 29 | }); 30 | 31 | describe('links#filterContainers()', function () { 32 | 33 | it('calls docker.listContainers with a label/value filter', function (done) { 34 | var docker = { 35 | listContainers: function (opts, done) { 36 | expect(opts).to.deep.equal({filters: '{"label":["a=b"]}'}); 37 | done(); 38 | } 39 | }; 40 | links.filterContainers(docker, 'a', 'b', done); 41 | }); 42 | }); 43 | 44 | describe('links#findLabeledContainers()', function () { 45 | 46 | it('swallows errors', function (done) { 47 | var docker = { 48 | listContainers: function (opts, done) { 49 | done(new Error()); 50 | } 51 | }; 52 | links.findLabeledContainers(docker, ['a'], 'b', done); 53 | }); 54 | 55 | it('returns undefined if no containers match', function (done) { 56 | var docker = { 57 | listContainers: function (opts, done) { 58 | done(null, []); 59 | } 60 | }; 61 | links.findLabeledContainers(docker, ['a'], 'b', function (err, containers) { 62 | expect(err).to.not.exist; 63 | expect(containers).to.not.exist; 64 | done(); 65 | }); 66 | }); 67 | 68 | it('finds the first label with containers matching the value and returns the containers', function (done) { 69 | var docker = { 70 | listContainers: function (opts, done) { 71 | if (opts.filters !== '{"label":["b=v"]}') return done(null, []); 72 | done(null, ['a', 'b']); 73 | } 74 | }; 75 | links.findLabeledContainers(docker, ['a', 'b', 'c'], 'v', function (err, containers) { 76 | expect(err).to.not.exist; 77 | expect(containers).to.deep.equal(['a', 'b']); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('does not ask docker for containers after it finds a match', function (done) { 83 | var calls = 0; 84 | var docker = { 85 | listContainers: function (opts, done) { 86 | calls++; 87 | done(null, ['a', 'b']); 88 | } 89 | }; 90 | links.findLabeledContainers(docker, ['a', 'b', 'c'], 'v', function (err, containers) { 91 | expect(err).to.be.undefined; 92 | expect(containers).to.deep.equal(['a', 'b']); 93 | expect(calls).to.equal(1); 94 | done(); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('links#findLabeledContainer()', function () { 100 | 101 | it('returns undefined if no containers match', function (done) { 102 | var docker = { 103 | listContainers: function (opts, done) { 104 | done(null, []); 105 | } 106 | }; 107 | links.findLabeledContainer(docker, ['a'], 'b', function (err, container) { 108 | expect(err).to.be.undefined; 109 | expect(container).to.be.undefined; 110 | done(); 111 | }); 112 | }); 113 | 114 | it('returns the first matching container', function (done) { 115 | var docker = { 116 | listContainers: function (opts, done) { 117 | done(null, ['a', 'b']); 118 | } 119 | }; 120 | links.findLabeledContainer(docker, ['a'], 'v', function (err, containers) { 121 | expect(err).to.be.undefined; 122 | expect(containers).to.equal('a'); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('links#resolveLinks()', function () { 129 | 130 | it('returns an error if no containers match a link', function (done) { 131 | var docker = { 132 | listContainers: function (opts, done) { 133 | done(null, []); 134 | }, 135 | getContainer: function () { 136 | return { 137 | inspect: function (done) { 138 | return done(new Error()); 139 | } 140 | }; 141 | } 142 | }; 143 | links.resolveLinks(docker, ['a'], function (err) { 144 | expect(err).to.be.an.instanceof(Error); 145 | done(); 146 | }); 147 | }); 148 | 149 | it('resolves links by name', function (done) { 150 | var docker = { 151 | listContainers: function (opts, done) { 152 | done(null, []); 153 | }, 154 | getContainer: function (name) { 155 | return { 156 | inspect: function (done) { 157 | return done(null, name); 158 | } 159 | }; 160 | } 161 | }; 162 | links.resolveLinks(docker, ['a', 'b'], function (err, links) { 163 | expect(err).to.not.exist; 164 | expect(links).to.deep.equal(['a:a', 'b:b']); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('resolves links with the com.stridercd.link label', function (done) { 170 | var docker = { 171 | listContainers: function (opts, done) { 172 | var parts = JSON.parse(opts.filters).label[0].split('='); 173 | var label = parts[0]; 174 | var value = parts[1]; 175 | if (label === 'com.stridercd.link') return done(null, [{Id: value}]); 176 | done(null, []); 177 | }, 178 | getContainer: function () { 179 | return { 180 | inspect: function (done) { 181 | return done(); 182 | } 183 | }; 184 | } 185 | }; 186 | links.resolveLinks(docker, ['a', 'b'], function (err, links) { 187 | expect(err).to.not.exist; 188 | expect(links).to.deep.equal(['a:a', 'b:b']); 189 | done(); 190 | }); 191 | }); 192 | 193 | it('resolves links with the com.docker.compose.service label', function (done) { 194 | var docker = { 195 | listContainers: function (opts, done) { 196 | var parts = JSON.parse(opts.filters).label[0].split('='); 197 | var label = parts[0]; 198 | var value = parts[1]; 199 | if (label === 'com.docker.compose.service') return done(null, [{Id: value}]); 200 | done(null, []); 201 | }, 202 | getContainer: function () { 203 | return { 204 | inspect: function (done) { 205 | return done(); 206 | } 207 | }; 208 | } 209 | }; 210 | links.resolveLinks(docker, ['a', 'b'], function (err, links) { 211 | expect(err).to.not.exist; 212 | expect(links).to.deep.equal(['a:a', 'b:b']); 213 | done(); 214 | }); 215 | }); 216 | 217 | }); 218 | --------------------------------------------------------------------------------