Skip to content

Commit

Permalink
Merge pull request #61 from clue-labs/attach
Browse files Browse the repository at this point in the history
Add `containerAttach()` and `containerAttachStream()` API methods
  • Loading branch information
clue authored Mar 21, 2020
2 parents dac2915 + e3d9eca commit 877cb31
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 14 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ The following API endpoints resolve with a buffered string of the command output
(STDOUT and/or STDERR):

```php
$client->containerAttach($container);
$client->containerLogs($container);
$client->execStart($exec);
```
Expand All @@ -186,6 +187,7 @@ The following API endpoints complement the default Promise-based API and return
a [`Stream`](https://github.com/reactphp/stream) instance instead:

```php
$stream = $client->containerAttachStream($container);
$stream = $client->containerLogsStream($container);
$stream = $client->execStartStream($exec);
```
Expand Down
37 changes: 37 additions & 0 deletions examples/attach-stream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

// this example shows how the containerAttachStream() call can be used to get the output of the given container.
// demonstrates the streaming attach API, which can be used to dump the container output as it arrives
//
// $ docker run -it --rm --name=foo busybox sh
// $ php examples/attach-stream.php foo

use Clue\CaretNotation\Encoder;
use Clue\React\Docker\Client;

require __DIR__ . '/../vendor/autoload.php';

$container = isset($argv[1]) ? $argv[1] : 'foo';
echo 'Dumping output of container "' . $container . '" (pass as argument to this example)' . PHP_EOL;

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

// use caret notation for any control characters except \t, \r and \n
$caret = new Encoder("\t\r\n");

$stream = $client->containerAttachStream($container, true, true);
$stream->on('data', function ($data) use ($caret) {
echo $caret->encode($data);
});

$stream->on('error', function (Exception $e) {
// will be called if either parameter is invalid
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
});

$stream->on('close', function () {
echo 'CLOSED' . PHP_EOL;
});

$loop->run();
66 changes: 66 additions & 0 deletions examples/benchmark-attach.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

// This example executes a command within a new container and displays how fast
// it can receive its output.
//
// $ php examples/benchmark-attach.php
// $ php examples/benchmark-attach.php busybox echo -n hello
//
// Expect this to be noticeably faster than the (totally unfair) equivalent:
//
// $ docker run -i --rm --log-driver=none busybox dd if=/dev/zero bs=1M count=1000 status=none | dd of=/dev/null

use Clue\React\Docker\Client;

require __DIR__ . '/../vendor/autoload.php';

if (extension_loaded('xdebug')) {
echo 'NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL;
}

$image = 'busybox';
$cmd = array('dd', 'if=/dev/zero', 'bs=1M', 'count=1000', 'status=none');

if (isset($argv[1])) {
$image = $argv[1];
$cmd = array_slice($argv, 2);
}

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

$client->containerCreate(array(
'Image' => $image,
'Cmd' => $cmd,
'Tty' => false,
'HostConfig' => array(
'LogConfig' => array(
'Type' => 'none'
)
)
))->then(function ($container) use ($client, $loop) {
$stream = $client->containerAttachStream($container['Id'], false, true);

// we're creating the container without a log, so first wait for attach stream before starting
$loop->addTimer(0.1, function () use ($client, $container) {
$client->containerStart($container['Id'])->then(null, 'printf');
});

$start = microtime(true);
$bytes = 0;
$stream->on('data', function ($chunk) use (&$bytes) {
$bytes += strlen($chunk);
});

$stream->on('error', 'printf');

// show stats when stream ends
$stream->on('close', function () use ($client, &$bytes, $start, $container) {
$time = microtime(true) - $start;
$client->containerRemove($container['Id'])->then(null, 'printf');

echo 'Received ' . $bytes . ' bytes in ' . round($time, 1) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL;
});
}, 'printf');

$loop->run();
14 changes: 10 additions & 4 deletions examples/benchmark-exec.php
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
<?php

// This simple example executes a command within the given running container and
// This example executes a command within the given running container and
// displays how fast it can receive its output.
//
// Before starting the benchmark, you have to start a container first, such as:
//
// $ docker run -it --rm --name=asd busybox sh
// $ docker run -it --rm --name=foo busybox sh
// $ php examples/benchmark-exec.php
// $ php examples/benchmark-exec.php foo echo -n hello
//
// Expect this to be significantly faster than the (totally unfair) equivalent:
//
// $ docker exec asd dd if=/dev/zero bs=1M count=1000 | dd of=/dev/null
// $ docker exec foo dd if=/dev/zero bs=1M count=1000 | dd of=/dev/null

use Clue\React\Docker\Client;

require __DIR__ . '/../vendor/autoload.php';

$container = 'asd';
if (extension_loaded('xdebug')) {
echo 'NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL;
}

$container = 'foo';
$cmd = array('dd', 'if=/dev/zero', 'bs=1M', 'count=1000');

if (isset($argv[1])) {
Expand Down
6 changes: 5 additions & 1 deletion examples/logs.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

// this example shows how the containerLogs() call can be used to get the logs of the given container.
// demonstrates the deferred logs API, which can be used to dump the logs in one go
//
// $ docker run -d --name=foo busybox ps
// $ php examples/logs.php foo
// $ docker rm foo

use Clue\CaretNotation\Encoder;
use Clue\React\Docker\Client;

require __DIR__ . '/../vendor/autoload.php';

$container = isset($argv[1]) ? $argv[1] : 'asd';
$container = isset($argv[1]) ? $argv[1] : 'foo';
echo 'Dumping logs (last 100 lines) of container "' . $container . '" (pass as argument to this example)' . PHP_EOL;

$loop = React\EventLoop\Factory::create();
Expand Down
100 changes: 100 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,106 @@ public function containerUnpause($container)
)->then(array($this->parser, 'expectEmpty'));
}

/**
* Attach to a container to read its output.
*
* This resolves with a string containing the container output, i.e. STDOUT
* and STDERR as requested.
*
* Keep in mind that this means the whole string has to be kept in memory.
* For a larger container output it's usually a better idea to use a streaming
* approach, see `containerAttachStream()` for more details.
* In particular, the same also applies for the `$stream` flag. It can be used
* to follow the container output as long as the container is running.
*
* Note that this endpoint internally has to check the `containerInspect()`
* endpoint first in order to figure out the TTY settings to properly decode
* the raw container output.
*
* @param string $container container ID
* @param bool $logs replay previous logs before attaching. Default false
* @param bool $stream continue streaming. Default false
* @param bool $stdout attach to stdout. Default true
* @param bool $stderr attach to stderr. Default true
* @return PromiseInterface Promise<string> container output string
* @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach
* @uses self::containerAttachStream()
* @see self::containerAttachStream()
*/
public function containerAttach($container, $logs = false, $stream = false, $stdout = true, $stderr = true)
{
return $this->streamingParser->bufferedStream(
$this->containerAttachStream($container, $logs, $stream, $stdout, $stderr)
);
}

/**
* Attach to a container to read its output.
*
* This is a streaming API endpoint that returns a readable stream instance
* containing the container output, i.e. STDOUT and STDERR as requested.
*
* This works for container output of arbitrary sizes as only small chunks have to
* be kept in memory.
*
* This is particularly useful for the `$stream` flag. It can be used to
* follow the container output as long as the container is running. Either
* the `$stream` or `$logs` parameter must be `true` for this endpoint to do
* anything meaningful.
*
* 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 the container has been
* created with a TTY.
*
* Note that this endpoint internally has to check the `containerInspect()`
* endpoint first in order to figure out the TTY settings to properly decode
* the raw container output.
*
* Note that this endpoint intentionally does not expose the `$stdin` flag.
* Access to STDIN will be exposed as a dedicated API endpoint in a future
* version.
*
* @param string $container container ID
* @param bool $logs replay previous logs before attaching. Default false
* @param bool $stream continue streaming. Default false
* @param bool $stdout attach to stdout. Default true
* @param bool $stderr attach to stderr. Default true
* @param string $stderrEvent custom event to emit for STDERR data (otherwise emits as "data")
* @return ReadableStreamInterface container output stream
* @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach
* @see self::containerAttach()
*/
public function containerAttachStream($container, $logs = false, $stream = false, $stdout = true, $stderr = true, $stderrEvent = null)
{
$parser = $this->streamingParser;
$browser = $this->browser;
$url = $this->uri->expand(
'/containers/{container}/attach{?logs,stream,stdout,stderr}',
array(
'container' => $container,
'logs' => $this->boolArg($logs),
'stream' => $this->boolArg($stream),
'stdout' => $this->boolArg($stdout),
'stderr' => $this->boolArg($stderr)
)
);

// first inspect container to check TTY setting, then attach with appropriate log parser
return \React\Promise\Stream\unwrapReadable($this->containerInspect($container)->then(function ($info) use ($url, $browser, $parser, $stderrEvent) {
$stream = $parser->parsePlainStream($browser->withOptions(array('streaming' => true))->post($url));

if (!$info['Config']['Tty']) {
$stream = $parser->demultiplexStream($stream, $stderrEvent);
}

return $stream;
}));
}

/**
* Block until container id stops, then returns the exit code
*
Expand Down
Loading

0 comments on commit 877cb31

Please sign in to comment.