Skip to content

Commit 877cb31

Browse files
authored
Merge pull request #61 from clue-labs/attach
Add `containerAttach()` and `containerAttachStream()` API methods
2 parents dac2915 + e3d9eca commit 877cb31

8 files changed

+322
-14
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ The following API endpoints resolve with a buffered string of the command output
172172
(STDOUT and/or STDERR):
173173

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

188189
```php
190+
$stream = $client->containerAttachStream($container);
189191
$stream = $client->containerLogsStream($container);
190192
$stream = $client->execStartStream($exec);
191193
```

examples/attach-stream.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
// this example shows how the containerAttachStream() call can be used to get the output of the given container.
4+
// demonstrates the streaming attach API, which can be used to dump the container output as it arrives
5+
//
6+
// $ docker run -it --rm --name=foo busybox sh
7+
// $ php examples/attach-stream.php foo
8+
9+
use Clue\CaretNotation\Encoder;
10+
use Clue\React\Docker\Client;
11+
12+
require __DIR__ . '/../vendor/autoload.php';
13+
14+
$container = isset($argv[1]) ? $argv[1] : 'foo';
15+
echo 'Dumping output of container "' . $container . '" (pass as argument to this example)' . PHP_EOL;
16+
17+
$loop = React\EventLoop\Factory::create();
18+
$client = new Client($loop);
19+
20+
// use caret notation for any control characters except \t, \r and \n
21+
$caret = new Encoder("\t\r\n");
22+
23+
$stream = $client->containerAttachStream($container, true, true);
24+
$stream->on('data', function ($data) use ($caret) {
25+
echo $caret->encode($data);
26+
});
27+
28+
$stream->on('error', function (Exception $e) {
29+
// will be called if either parameter is invalid
30+
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
31+
});
32+
33+
$stream->on('close', function () {
34+
echo 'CLOSED' . PHP_EOL;
35+
});
36+
37+
$loop->run();

examples/benchmark-attach.php

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
// This example executes a command within a new container and displays how fast
4+
// it can receive its output.
5+
//
6+
// $ php examples/benchmark-attach.php
7+
// $ php examples/benchmark-attach.php busybox echo -n hello
8+
//
9+
// Expect this to be noticeably faster than the (totally unfair) equivalent:
10+
//
11+
// $ docker run -i --rm --log-driver=none busybox dd if=/dev/zero bs=1M count=1000 status=none | dd of=/dev/null
12+
13+
use Clue\React\Docker\Client;
14+
15+
require __DIR__ . '/../vendor/autoload.php';
16+
17+
if (extension_loaded('xdebug')) {
18+
echo 'NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL;
19+
}
20+
21+
$image = 'busybox';
22+
$cmd = array('dd', 'if=/dev/zero', 'bs=1M', 'count=1000', 'status=none');
23+
24+
if (isset($argv[1])) {
25+
$image = $argv[1];
26+
$cmd = array_slice($argv, 2);
27+
}
28+
29+
$loop = React\EventLoop\Factory::create();
30+
$client = new Client($loop);
31+
32+
$client->containerCreate(array(
33+
'Image' => $image,
34+
'Cmd' => $cmd,
35+
'Tty' => false,
36+
'HostConfig' => array(
37+
'LogConfig' => array(
38+
'Type' => 'none'
39+
)
40+
)
41+
))->then(function ($container) use ($client, $loop) {
42+
$stream = $client->containerAttachStream($container['Id'], false, true);
43+
44+
// we're creating the container without a log, so first wait for attach stream before starting
45+
$loop->addTimer(0.1, function () use ($client, $container) {
46+
$client->containerStart($container['Id'])->then(null, 'printf');
47+
});
48+
49+
$start = microtime(true);
50+
$bytes = 0;
51+
$stream->on('data', function ($chunk) use (&$bytes) {
52+
$bytes += strlen($chunk);
53+
});
54+
55+
$stream->on('error', 'printf');
56+
57+
// show stats when stream ends
58+
$stream->on('close', function () use ($client, &$bytes, $start, $container) {
59+
$time = microtime(true) - $start;
60+
$client->containerRemove($container['Id'])->then(null, 'printf');
61+
62+
echo 'Received ' . $bytes . ' bytes in ' . round($time, 1) . 's => ' . round($bytes / $time / 1000000, 1) . ' MB/s' . PHP_EOL;
63+
});
64+
}, 'printf');
65+
66+
$loop->run();

examples/benchmark-exec.php

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
<?php
22

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

1416
use Clue\React\Docker\Client;
1517

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

18-
$container = 'asd';
20+
if (extension_loaded('xdebug')) {
21+
echo 'NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL;
22+
}
23+
24+
$container = 'foo';
1925
$cmd = array('dd', 'if=/dev/zero', 'bs=1M', 'count=1000');
2026

2127
if (isset($argv[1])) {

examples/logs.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

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

610
use Clue\CaretNotation\Encoder;
711
use Clue\React\Docker\Client;
812

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

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

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

src/Client.php

+100
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,106 @@ public function containerUnpause($container)
672672
)->then(array($this->parser, 'expectEmpty'));
673673
}
674674

675+
/**
676+
* Attach to a container to read its output.
677+
*
678+
* This resolves with a string containing the container output, i.e. STDOUT
679+
* and STDERR as requested.
680+
*
681+
* Keep in mind that this means the whole string has to be kept in memory.
682+
* For a larger container output it's usually a better idea to use a streaming
683+
* approach, see `containerAttachStream()` for more details.
684+
* In particular, the same also applies for the `$stream` flag. It can be used
685+
* to follow the container output as long as the container is running.
686+
*
687+
* Note that this endpoint internally has to check the `containerInspect()`
688+
* endpoint first in order to figure out the TTY settings to properly decode
689+
* the raw container output.
690+
*
691+
* @param string $container container ID
692+
* @param bool $logs replay previous logs before attaching. Default false
693+
* @param bool $stream continue streaming. Default false
694+
* @param bool $stdout attach to stdout. Default true
695+
* @param bool $stderr attach to stderr. Default true
696+
* @return PromiseInterface Promise<string> container output string
697+
* @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach
698+
* @uses self::containerAttachStream()
699+
* @see self::containerAttachStream()
700+
*/
701+
public function containerAttach($container, $logs = false, $stream = false, $stdout = true, $stderr = true)
702+
{
703+
return $this->streamingParser->bufferedStream(
704+
$this->containerAttachStream($container, $logs, $stream, $stdout, $stderr)
705+
);
706+
}
707+
708+
/**
709+
* Attach to a container to read its output.
710+
*
711+
* This is a streaming API endpoint that returns a readable stream instance
712+
* containing the container output, i.e. STDOUT and STDERR as requested.
713+
*
714+
* This works for container output of arbitrary sizes as only small chunks have to
715+
* be kept in memory.
716+
*
717+
* This is particularly useful for the `$stream` flag. It can be used to
718+
* follow the container output as long as the container is running. Either
719+
* the `$stream` or `$logs` parameter must be `true` for this endpoint to do
720+
* anything meaningful.
721+
*
722+
* Note that by default the output of both STDOUT and STDERR will be emitted
723+
* as normal "data" events. You can optionally pass a custom event name which
724+
* will be used to emit STDERR data so that it can be handled separately.
725+
* Note that the normal streaming primitives likely do not know about this
726+
* event, so special care may have to be taken.
727+
* Also note that this option has no effect if the container has been
728+
* created with a TTY.
729+
*
730+
* Note that this endpoint internally has to check the `containerInspect()`
731+
* endpoint first in order to figure out the TTY settings to properly decode
732+
* the raw container output.
733+
*
734+
* Note that this endpoint intentionally does not expose the `$stdin` flag.
735+
* Access to STDIN will be exposed as a dedicated API endpoint in a future
736+
* version.
737+
*
738+
* @param string $container container ID
739+
* @param bool $logs replay previous logs before attaching. Default false
740+
* @param bool $stream continue streaming. Default false
741+
* @param bool $stdout attach to stdout. Default true
742+
* @param bool $stderr attach to stderr. Default true
743+
* @param string $stderrEvent custom event to emit for STDERR data (otherwise emits as "data")
744+
* @return ReadableStreamInterface container output stream
745+
* @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach
746+
* @see self::containerAttach()
747+
*/
748+
public function containerAttachStream($container, $logs = false, $stream = false, $stdout = true, $stderr = true, $stderrEvent = null)
749+
{
750+
$parser = $this->streamingParser;
751+
$browser = $this->browser;
752+
$url = $this->uri->expand(
753+
'/containers/{container}/attach{?logs,stream,stdout,stderr}',
754+
array(
755+
'container' => $container,
756+
'logs' => $this->boolArg($logs),
757+
'stream' => $this->boolArg($stream),
758+
'stdout' => $this->boolArg($stdout),
759+
'stderr' => $this->boolArg($stderr)
760+
)
761+
);
762+
763+
// first inspect container to check TTY setting, then attach with appropriate log parser
764+
return \React\Promise\Stream\unwrapReadable($this->containerInspect($container)->then(function ($info) use ($url, $browser, $parser, $stderrEvent) {
765+
$stream = $parser->parsePlainStream($browser->withOptions(array('streaming' => true))->post($url));
766+
767+
if (!$info['Config']['Tty']) {
768+
$stream = $parser->demultiplexStream($stream, $stderrEvent);
769+
}
770+
771+
return $stream;
772+
}));
773+
}
774+
675775
/**
676776
* Block until container id stops, then returns the exit code
677777
*

0 commit comments

Comments
 (0)