Wednesday, June 8, 2016

Runninng Docker Commands via Gulp

There is a saying: "Laziness is the mother of all bad habits." However, for us--programmers--I find another saying to be true more often: "Laziness is the driver of all progress."

While working on a couple projects, which I'll blog about separately in posts to come, I found myself executing the same Docker commands over and over again. Being a lazy programmer I decided to automate it, which led to the birth of a new library: docker-cmd-js.


Intro: Beyond the Bits and Bolts 

First, let me take a step back and talk about why I started using Docker in the first place.

The nature of the project I'm currently working on requires a lot of integration tests. These tests run against different databases (relational and nosql, different versions). The amount of databases that I'd need to test against could be rather large. Installing all these databases locally would be painful. Besides I'd have to maintain the databases state for the tests to run properly. With Docker I could start up a virtual machine with any amount of containers with any databases I need and test against these.

To be completely fair, I didn't discover Docker on my own. My good friend Matt Klein, who I recruited to help me with the project suggested I start using Docker. He also wrote an initial draft of docker-cmd-js. At first this code lived inside the project, but I decided to contribute to the community and open source the code.

This proved to be a good decision for several reasons:
  • I had to improve quality of docker-cmd-js to make it presentable.
  • I learned a bit more about TypeScript (yes, the source code is in TypeScript)
  • It forced me to get familiar with publishing packages on NPM (which I found to be very easy).
  • Open sourcing the code also gave me a reason to try my hand at blogging, which I've never done before.


Automating Using Gulp

Gulp is a tool that lets a developer automate tasks. Great! So can I simply create a task that would start a container by writing something like this?

var spawnSync = require('child_process').spawnSync;

function run(command) {
    var items = command.split(' ');
    var r = spawnSync(items[0], items.slice(1));
    return {
        stdOut: r.stdout.toString(),
        stdErr: r.stderr.toString()
    }
}

gulp.task('startVm', ()=> {
    return run('docker-machine create --driver virtualbox default');
});

The idea is right, but this won't work with all Docker commands. It took me a little bit of research to find out that running Docker commands inside cmd.exe on Windows requires setting up a bunch of environment variables beforehand. Besides, using spawnSync, like in the example above has few problems too:
  • The command output won't be seen until the command is executed, which, for lengthy tasks, can be frustrating.
  • The synchronous nature of the code does not let commands run in parallel.

docker-cmd-js takes care of all these issues. It sets all needed environment variables and returns a promise when the command is done, which lets commands run in parallel. To start a virtual machine, write this:

var dockerCmd = require('docker-cmd-js');
var cmd = new dockerCmd.Cmd();

gulp.task('startVm', (done)=> {
    cmd.run('docker-machine create --driver virtualbox default').then(
        ()=> { done(); },
        (err)=> { done.fail(err); }
    );
});

I also took the liberty and added some "shortcut" methods to the library that will let commands run quicker. For example, the command above will fail if the machine is already running. That's not desired in most cases. I just want to make sure it is running and continue with other Docker commands. For this one could simply write:

gulp.task('startVm', (done)=> {
    cmd.machine.start().then(
        ()=> { done(); },
        (err)=> { done.fail(err); }
    );
});


Gotcha - "mysql" Container Works Only After a Delay!

A lot of programmers have stumbled across this issue. You'd start a container with a simple Dockerfile:

FROM mysql

ENV MYSQL_ROOT_PASSWORD=rootPassword
ENV MYSQL_USER=testUser
ENV MYSQL_PASSWORD=password

...and have this container linked with another one (say a nodejs app) only to find out that your app returns an error. It cannot connect to the database. However, if you wait about 7 seconds, the app will connect just fine.

This is due to the fact that mysql service inside the container takes about 7 seconds to spin up after the container starts running. This issue is being tackled. You can track it on Github. Meanwhile I created a method that makes sure of a port availability. This workaround might not work in some situations, but it works great for mysql and similar major use-cases. See the usage below:

cmd.image.build('mysql_test')
    .then(() => cmd.container.start('mysql_test', { publish: '3306:3306' }))
    .then(() => cmd.container.waitForPort({ port: 3306, timeoutMs: 15 * 1000 })) // 15 seconds
    .then(() => runMySqlTests());

There are more helpful methods like the one above. See the full list on the Github repository page.
docker-cmd-js is available through NPM: npm install docker-cmd-js.

I hope people will find this package useful. Any feedback or contributions to the code are welcome!