Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add containerAttach() and containerAttachStream() API methods #61

Merged
merged 2 commits into from
Mar 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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