Skip to content

Commit 992b18a

Browse files
committed
Add execStartStream() API endpoint
1 parent c73b287 commit 992b18a

5 files changed

Lines changed: 167 additions & 9 deletions

File tree

examples/exec-stream.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
// this example executes some commands within the given running container and
3+
// displays the streaming output as it happens.
4+
5+
require __DIR__ . '/../vendor/autoload.php';
6+
7+
use React\EventLoop\Factory as LoopFactory;
8+
use Clue\React\Docker\Factory;
9+
use React\Stream\Stream;
10+
11+
$container = 'asd';
12+
//$cmd = array('echo', 'hello world');
13+
//$cmd = array('sleep', '2');
14+
$cmd = array('sh', '-c', 'echo -n hello && sleep 1 && echo world && sleep 1 && env');
15+
//$cmd = array('cat', 'invalid-path');
16+
17+
if (isset($argv[1])) {
18+
$container = $argv[1];
19+
$cmd = array_slice($argv, 2);
20+
}
21+
22+
$loop = LoopFactory::create();
23+
24+
$factory = new Factory($loop);
25+
$client = $factory->createClient();
26+
27+
$out = new Stream(STDOUT, $loop);
28+
$out->pause();
29+
30+
$client->execCreate($container, array('Cmd' => $cmd, 'AttachStdout' => true, 'AttachStderr' => true, 'Tty' => true))->then(function ($info) use ($client, $out) {
31+
$stream = $client->execStartStream($info['Id'], array('Tty' => true));
32+
$stream->pipe($out);
33+
34+
$stream->on('error', 'printf');
35+
36+
// exit with error code of executed command once it closes
37+
$stream->on('close', function () use ($client, $info) {
38+
$client->execInspect($info['Id'])->then(function ($info) {
39+
exit($info['ExitCode']);
40+
}, 'printf');
41+
});
42+
}, 'printf');
43+
44+
$loop->run();

src/Client.php

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,9 @@ public function execCreate($container, $config)
907907
* as set up in the `execCreate()` call.
908908
*
909909
* Keep in mind that this means the whole string has to be kept in memory.
910+
* If you want to access the individual output chunks as they happen or
911+
* for bigger command outputs, it's usually a better idea to use a streaming
912+
* approach, see `execStartStream()` for more details.
910913
*
911914
* If detach is true, this API returns after starting the exec command.
912915
* Otherwise, this API sets up an interactive session with the exec command.
@@ -915,18 +918,51 @@ public function execCreate($container, $config)
915918
* @param array $config (see link)
916919
* @return PromiseInterface Promise<string> buffered exec data
917920
* @link https://docs.docker.com/reference/api/docker_remote_api_v1.15/#exec-start
921+
* @uses self::execStartStream()
922+
* @see self::execStartStream()
918923
*/
919924
public function execStart($exec, $config = array())
920925
{
921-
return $this->postJson(
922-
$this->uri->expand(
923-
'/exec/{exec}/start',
926+
return $this->streamingParser->bufferedStream(
927+
$this->execStartStream($exec, $config)
928+
);
929+
}
930+
931+
/**
932+
* Starts a previously set up exec instance id.
933+
*
934+
* This is a streaming API endpoint that returns a readable stream instance
935+
* containing the command output, i.e. STDOUT and STDERR as set up in the
936+
* `execCreate()` call.
937+
*
938+
* This works for command output of any size as only small chunks have to
939+
* be kept in memory.
940+
*
941+
* If detach is true, this API returns after starting the exec command.
942+
* Otherwise, this API sets up an interactive session with the exec command.
943+
*
944+
* @param string $exec exec ID
945+
* @param array $config (see link)
946+
* @return ReadableStreamInterface stream of exec data
947+
* @link https://docs.docker.com/reference/api/docker_remote_api_v1.15/#exec-start
948+
* @see self::execStart()
949+
*/
950+
public function execStartStream($exec, $config = array())
951+
{
952+
return $this->streamingParser->parsePlainStream(
953+
$this->browser->withOptions(array('streaming' => true))->post(
954+
$this->uri->expand(
955+
'/exec/{exec}/start',
956+
array(
957+
'exec' => $exec
958+
)
959+
),
924960
array(
925-
'exec' => $exec
926-
)
927-
),
928-
$config
929-
)->then(array($this->parser, 'expectPlain'));
961+
'Content-Type' => 'application/json'
962+
),
963+
$this->json($config)
964+
)
965+
);
930966
}
931967

932968
/**

src/Io/StreamingParser.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ public function parsePlainStream(PromiseInterface $promise)
8484
}));
8585
}
8686

87+
/**
88+
* Returns a promise which resolves with the buffered stream contents of the given stream
89+
*
90+
* @param ReadableStreamInterface $stream
91+
* @return PromiseInterface Promise<string, Exception>
92+
*/
93+
public function bufferedStream(ReadableStreamInterface $stream)
94+
{
95+
return Stream\buffer($stream);
96+
}
97+
8798
/**
8899
* Returns a promise which resolves with an array of all "progress" events
89100
*

tests/ClientTest.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use RingCentral\Psr7\Response;
77
use Psr\Http\Message\ResponseInterface;
88
use Psr\Http\Message\RequestInterface;
9+
use React\Promise;
910

1011
class ClientTest extends TestCase
1112
{
@@ -388,11 +389,26 @@ public function testExecStart()
388389
{
389390
$data = 'hello world';
390391
$config = array();
391-
$this->expectRequestFlow('post', '/exec/123/start', $this->createResponse($data), 'expectPlain');
392+
$stream = $this->getMock('React\Stream\ReadableStreamInterface');
393+
394+
$this->expectRequest('POST', '/exec/123/start', $this->createResponse($data));
395+
$this->streamingParser->expects($this->once())->method('parsePlainStream')->will($this->returnValue($stream));
396+
$this->streamingParser->expects($this->once())->method('bufferedStream')->with($this->equalTo($stream))->willReturn(Promise\resolve($data));
392397

393398
$this->expectPromiseResolveWith($data, $this->client->execStart(123, $config));
394399
}
395400

401+
public function testExecStartStream()
402+
{
403+
$config = array();
404+
$stream = $this->getMock('React\Stream\ReadableStreamInterface');
405+
406+
$this->expectRequest('POST', '/exec/123/start', $this->createResponse());
407+
$this->streamingParser->expects($this->once())->method('parsePlainStream')->will($this->returnValue($stream));
408+
409+
$this->assertSame($stream, $this->client->execStartStream(123, $config));
410+
}
411+
396412
public function testExecResize()
397413
{
398414
$this->expectRequestFlow('POST', '/exec/123/resize?w=800&h=600', $this->createResponse(), 'expectEmpty');

tests/FunctionalClientTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use React\EventLoop\Factory as LoopFactory;
55
use Clue\React\Docker\Factory;
66
use Clue\React\Block;
7+
use Clue\React\Promise\Stream;
78

89
class FunctionalClientTest extends TestCase
910
{
@@ -157,6 +158,56 @@ public function testExecInspectAfterRunning($exec)
157158
$this->assertEquals(0, $info['ExitCode']);
158159
}
159160

161+
/**
162+
* @depends testStartRunning
163+
* @param string $container
164+
*/
165+
public function testExecStreamEmptyOutputWhileRunning($container)
166+
{
167+
$promise = $this->client->execCreate($container, array(
168+
'Cmd' => array('true'),
169+
'AttachStdout' => true,
170+
'AttachStderr' => true,
171+
'Tty' => true
172+
));
173+
$exec = Block\await($promise, $this->loop);
174+
175+
$this->assertTrue(is_array($exec));
176+
$this->assertTrue(is_string($exec['Id']));
177+
178+
$stream = $this->client->execStartStream($exec['Id'], array('Tty' => true));
179+
$stream->on('end', $this->expectCallableOnce());
180+
181+
$output = Block\await(Stream\buffer($stream), $this->loop);
182+
183+
$this->assertEquals('', $output);
184+
}
185+
186+
/**
187+
* @depends testStartRunning
188+
* @param string $container
189+
*/
190+
public function testExecStreamEmptyOutputBecauseOfDetachWhileRunning($container)
191+
{
192+
$promise = $this->client->execCreate($container, array(
193+
'Cmd' => array('sleep', '10'),
194+
'AttachStdout' => true,
195+
'AttachStderr' => true,
196+
'Tty' => true
197+
));
198+
$exec = Block\await($promise, $this->loop);
199+
200+
$this->assertTrue(is_array($exec));
201+
$this->assertTrue(is_string($exec['Id']));
202+
203+
$stream = $this->client->execStartStream($exec['Id'], array('Tty' => true, 'Detach' => true));
204+
$stream->on('end', $this->expectCallableOnce());
205+
206+
$output = Block\await(Stream\buffer($stream), $this->loop);
207+
208+
$this->assertEquals('', $output);
209+
}
210+
160211
/**
161212
* @depends testStartRunning
162213
* @param string $container

0 commit comments

Comments
 (0)