clue/docker-react

Simple async/streaming Docker client

v0.2.0 2015-08-11 22:20 UTC

README

Simple async/streaming access to the Docker API, built on top of React PHP.

Docker is a popular open source platform to run and share applications within isolated, lightweight containers. The Docker Remote API allows you to control and monitor your containers and images. Among others, it can be used to list existing images, download new images, execute arbitrary commands within isolated containers, stop running containers and much more.

  • Async execution of Actions - Send any number of actions (commands) to your Docker daemon in parallel and process their responses as soon as results come in. The Promise-based design provides a sane interface to working with out of bound responses.
  • Lightweight, SOLID design - Provides a thin abstraction that is just good enough and does not get in your way. This library is merely a very thin wrapper around the Remote API.
  • Good test coverage - Comes with an automated tests suite and is regularly tested in the real world

Table of contents

Quickstart example

Once installed, you can use the following code to access the Docker API of your local docker daemon:

$loop = React\EventLoop\Factory::create();
$factory = new Factory($loop);
$client = $factory->createClient();

$client->imageSearch('clue')->then(function ($images) {
    var_dump($images);
});

$loop->run();

See also the examples.

Usage

Factory

The Factory is responsible for creating your Client instance. It also registers everything with the main EventLoop.

$loop = React\EventLoop\Factory::create();
$factory = new Factory($loop);

If you need custom DNS, SSL/TLS or proxy settings, you can explicitly pass a custom Browser instance:

$factory = new Factory($loop, $browser);

createClient()

The createClient($url = null) method can be used to create a new Client. It helps with constructing a Browser object for the given remote URL.

// create client with default URL (unix:///var/run/docker.sock)
$client = $factory->createClient();

// explicitly use given UNIX socket path
$client = $factory->createClient('unix:///var/run/docker.sock');

// connect via TCP/IP
$client = $factory->createClient('http://10.0.0.2:8000/');

Client

The Client is responsible for assembling and sending HTTP requests to the Docker API. It requires a Browser object bound to the main EventLoop in order to handle async requests and a base URL. The recommended way to create a Client is using the Factory (see above).

Commands

All public methods on the Client resemble the API described in the Remote API documentation like this:

$client->containerList($all, $size);
$client->containerCreate($config, $name);
$client->containerStart($name);
$client->containerKill($name, $signal);
$client->containerRemove($name, $v, $force);

$client->imageList($all);
$client->imageSearch($term);
$client->imageCreate($fromImage, $fromSrc, $repo, $tag, $registry, $registryAuth);

$client->info();
$client->version();

// many, many more…

Listing all available commands is out of scope here, please refer to the Remote API documentation or the class outline.

Each of these commands supports async operation and either resolves with its results or rejects with an Exception. Please see the following section about promises for more details.

Promises

Sending requests is async (non-blocking), so you can actually send multiple requests in parallel. Docker will respond to each request with a response message, the order is not guaranteed. Sending requests uses a Promise-based interface that makes it easy to react to when a request is fulfilled (i.e. either successfully resolved or rejected with an error):

$client->version()->then(
    function ($result) {
        var_dump('Result received', $result);
    },
    function (Exception $error) {
        var_dump('There was an error', $error->getMessage());
    }
});

If this looks strange to you, you can also use the more traditional blocking API.

Blocking

As stated above, this library provides you a powerful, async API by default.

If, however, you want to integrate this into your traditional, blocking environment, you should look into also using clue/block-react.

The resulting blocking code could look something like this:

use Clue\React\Block;

$loop = React\EventLoop\Factory::create();
$factory = new Factory($loop);
$client = $factory->createClient();

$promise = $client->imageInspect('busybox');

try {
    $results = Block\await($promise, $loop);
    // resporesults successfully received
} catch (Exception $e) {
    // an error occured while performing the request
}

Similarly, you can also process multiple commands concurrently and await an array of results:

$promises = array(
    $client->imageInspect('busybox'),
    $client->imageInspect('ubuntu'),
);

$inspections = Block\awaitAll($promises, $loop);

Please refer to clue/block-react for more details.

Command streaming

The following API endpoint resolves with a buffered string of the command output (STDOUT and/or STDERR):

$client->execStart($exec);

Keep in mind that this means the whole string has to be kept in memory. If you want to access the individual output chunks as they happen or for bigger command outputs, it's usually a better idea to use a streaming approach.

This works for (any number of) commands of arbitrary sizes. The following API endpoint complements the default Promise-based API and returns a Stream instance instead:

$stream = $client->execStartStream($exec);

The resulting stream is a well-behaving readable stream that will emit the normal stream events:

$stream = $client->execStartStream($exec, $tty);
$stream->on('data', function ($data) {
    // data will be emitted in multiple chunk
    echo $data;
});
$stream->on('close', function () {
    // the stream just ended, this could(?) be a good thing
    echo 'Ended' . PHP_EOL;
});

Note that by default the output of both STDOUT and STDERR will be emitted as normal data events. You can optionally pass a custom event name which will be used to emit STDERR data so that it can be handled separately. Note that the normal streaming primitives likely do not know about this event, so special care may have to be taken. Also note that this option has no effect if you execute with a TTY.

$stream = $client->execStartStream($exec, $tty, 'stderr');
$stream->on('data', function ($data) {
    echo 'STDOUT data: ' . $data;
});
$stream->on('stderr', function ($data) {
    echo 'STDERR data: ' . $data;
});

See also the streaming exec example and the exec benchmark example.

The TTY mode should be set depending on whether your command needs a TTY or not. Note that toggling the TTY mode affects how/whether you can access the STDERR stream and also has a significant impact on performance for larger streams (relevant for 100 MiB and above). See also the TTY mode on the execStart*() call.

Running this benchmark on my personal (rather mediocre) VM setup reveals that the benchmark achieves a throughput of ~300 MiB/s while the (totally unfair) comparison script using the plain Docker client only yields ~100 MiB/s. Instead of me posting more details here, I encourage you to re-run the benchmark yourself and adjust it to better suite your problem domain. The key takeway here is: PHP is faster than you probably thought.

TAR streaming

The following API endpoints resolve with a string in the TAR file format:

$client->containerExport($container);
$client->containerCopy($container, $config);

Keep in mind that this means the whole string has to be kept in memory. This is easy to get started and works reasonably well for smaller files/containers.

For bigger containers it's usually a better idea to use a streaming approach, where only small chunks have to be kept in memory. This works for (any number of) files of arbitrary sizes. The following API endpoints complement the default Promise-based API and return a Stream instance instead:

$stream = $client->containerExportStream($image);
$stream = $client->containerCopyStream($image, $config);

Accessing individual files in the TAR file format string or stream is out of scope for this library. Several libraries are available, one that is known to work is clue/tar-react.

See also the copy example and the export example.

JSON streaming

The following API endpoints take advantage of JSON streaming:

$client->imageCreate();
$client->imagePush();
$client->events();

What this means is that these endpoints actually emit any number of progress events (individual JSON objects). At the HTTP level, a common response message could look like this:

HTTP/1.1 200 OK
Content-Type: application/json

{"status":"loading","current":1,"total":10}
{"status":"loading","current":2,"total":10}
…
{"status":"loading","current":10,"total":10}
{"status":"done","total":10}

The user-facing API hides this fact by resolving with an array of all individual progress events once the stream ends:

$client->imageCreate('clue/streamripper')->then(
    function ($data) {
        // $data is an array of *all* elements in the JSON stream
    },
    function ($error) {
        // an error occurred (possibly after receiving *some* elements)
        
        if ($error instanceof Io\JsonProgressException) {
            // a progress message (usually the last) contains an error message
        } else {
            // any other error, like invalid request etc.
        }
    }
);

Keep in mind that due to resolving with an array of all progress events, this API has to keep all event objects in memory until the Promise resolves. This is easy to get started and usually works reasonably well for the above API endpoints.

If you're dealing with lots of concurrent requests (100+) or if you want to access the individual progress events as they happen, you should consider using a streaming approach instead, where only individual progress event objects have to be kept in memory. The following API endpoints complement the default Promise-based API and return a Stream instance instead:

$stream = $client->imageCreateStream();
$stream = $client->imagePushStream();
$stream = $client->eventsStream();

The resulting stream will emit the following events:

  • progress: for each element in the update stream
  • error: once if an error occurs, will close() stream then
    • Will emit an Io\JsonProgressException if an individual progress message contains an error message
    • Any other Exception in case of an transport error, like invalid request etc.
  • close: once the stream ends (either finished or after "error")

Please note that the resulting stream does not emit any "data" events, so you will not be able to pipe() its events into another WritableStream.

$stream = $client->imageCreateStream('clue/redis-benchmark');
$stream->on('progress', function ($data) {
    // data will be emitted for *each* complete element in the JSON stream
    echo $data['status'] . PHP_EOL;
});
$stream->on('close', function () {
    // the JSON stream just ended, this could(?) be a good thing
    echo 'Ended' . PHP_EOL;
});

See also the pull example and the push example.

JsonProgressException

The Io\JsonProgressException will be thrown by JSON streaming endpoints if an individual progress message contains an error message.

The getData() method can be used to obtain the progress message.

Install

The recommended way to install this library is through composer. New to composer?

{
    "require": {
        "clue/docker-react": "~0.2.0"
    }
}

License

MIT