Skip to content

Commit 8efb578

Browse files
committed
Merge pull request #9 from clue-labs/streaming
Add streaming API
2 parents 061d2e9 + 4b21ae4 commit 8efb578

13 files changed

Lines changed: 856 additions & 12 deletions

README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ The `Client` is responsible for assembling and sending HTTP requests to the Dock
7373
It requires a `Browser` object bound to the main `EventLoop` in order to handle async requests and a base URL.
7474
The recommended way to create a `Client` is using the `Factory` (see above).
7575

76+
#### Commands
77+
7678
All public methods on the `Client` resemble the API described in the [Remote API documentation](https://docs.docker.com/reference/api/docker_remote_api_v1.15/) like this:
7779

7880
```php
@@ -92,6 +94,8 @@ $client->version();
9294

9395
Listing all available commands is out of scope here, please refer to the [Remote API documentation](https://docs.docker.com/reference/api/docker_remote_api_v1.15/) or the class outline.
9496

97+
#### Promises
98+
9599
Sending requests is async (non-blocking), so you can actually send multiple requests in parallel.
96100
Docker will respond to each request with a response message, the order is not guaranteed.
97101
Sending requests uses a [Promise](https://github.com/reactphp/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):
@@ -107,6 +111,128 @@ $client->version()->then(
107111
});
108112
```
109113

114+
#### TAR streaming
115+
116+
The following API endpoints resolve with a string in the [TAR file format](https://en.wikipedia.org/wiki/Tar_%28computing%29):
117+
118+
```php
119+
$client->containerExport($container);
120+
$client->containerCopy($container, $config);
121+
```
122+
123+
Keep in mind that this means the whole string has to be kept in memory.
124+
This is easy to get started and works reasonably well for smaller files/containers.
125+
126+
For bigger containers it's usually a better idea to use a streaming approach,
127+
where only small chunks have to be kept in memory.
128+
This works for (any number of) files of arbitrary sizes.
129+
The following API endpoints complement the default Promise-based API and return
130+
a [`Stream`](https://github.com/reactphp/stream) instance instead:
131+
132+
```php
133+
$stream = $client->containerExportStream($image);
134+
$stream = $client->containerCopyStream($image, $config);
135+
```
136+
137+
Accessing individual files in the TAR file format string or stream is out of scope
138+
for this library.
139+
Several libraries are available, one that is known to work is [clue/tar-react](https://github.com/clue/php-tar-react).
140+
141+
See also the [copy example](examples/copy.php) and the [export example](examples/export.php).
142+
143+
#### JSON streaming
144+
145+
The following API endpoints take advantage of [JSON streaming](https://en.wikipedia.org/wiki/JSON_Streaming):
146+
147+
```php
148+
$client->imageCreate();
149+
$client->imagePush();
150+
```
151+
152+
What this means is that these endpoints actually emit any number of progress
153+
events (individual JSON objects).
154+
At the HTTP level, a common response message could look like this:
155+
156+
```
157+
HTTP/1.1 200 OK
158+
Content-Type: application/json
159+
160+
{"status":"loading","current":1,"total":10}
161+
{"status":"loading","current":2,"total":10}
162+
163+
{"status":"loading","current":10,"total":10}
164+
{"status":"done","total":10}
165+
```
166+
167+
The user-facing API hides this fact by resolving with an array of all individual
168+
progress events once the stream ends:
169+
170+
```php
171+
$client->imageCreate('clue/streamripper')->then(
172+
function ($data) {
173+
// $data is an array of *all* elements in the JSON stream
174+
},
175+
function ($error) {
176+
// an error occurred (possibly after receiving *some* elements)
177+
178+
if ($error instanceof Io\JsonProgressException) {
179+
// a progress message (usually the last) contains an error message
180+
} else {
181+
// any other error, like invalid request etc.
182+
}
183+
}
184+
);
185+
```
186+
187+
Keep in mind that due to resolving with an array of all progress events,
188+
this API has to keep all event objects in memory until the Promise resolves.
189+
This is easy to get started and usually works reasonably well for the above
190+
API endpoints.
191+
192+
If you're dealing with lots of concurrent requests (100+) or
193+
if you want to access the individual progress events as they happen, you
194+
should consider using a streaming approach instead,
195+
where only individual progress event objects have to be kept in memory.
196+
The following API endpoints complement the default Promise-based API and return
197+
a [`Stream`](https://github.com/reactphp/stream) instance instead:
198+
199+
```php
200+
$stream = $client->imageCreateStream();
201+
$stream = $client->imagePushStream();
202+
```
203+
204+
The resulting stream will emit the following events:
205+
206+
* `progress`: for *each* element in the update stream
207+
* `error`: once if an error occurs, will close() stream then
208+
* Will emit an [`Io\JsonProgressException`](#jsonprogressexception) if an individual progress message contains an error message
209+
* Any other `Exception` in case of an transport error, like invalid request etc.
210+
* `close`: once the stream ends (either finished or after "error")
211+
212+
Please note that the resulting stream does not emit any "data" events, so
213+
you will not be able to pipe() its events into another `WritableStream`.
214+
215+
```php
216+
$stream = $client->imageCreateStream('clue/redis-benchmark');
217+
$stream->on('progress', function ($data) {
218+
// data will be emitted for *each* complete element in the JSON stream
219+
echo $data['status'] . PHP_EOL;
220+
});
221+
$stream->on('close', function () {
222+
// the JSON stream just ended, this could(?) be a good thing
223+
echo 'Ended' . PHP_EOL;
224+
});
225+
```
226+
227+
See also the [pull example](examples/pull.php) and the [push example](examples/push.php).
228+
229+
### JsonProgressException
230+
231+
The `Io\JsonProgressException` will be thrown by [JSON streaming](#json-streaming)
232+
endpoints if an individual progress message contains an error message.
233+
234+
The `getData()` method can be used to obtain the progress message.
235+
110236
## Install
111237

112238
The recommended way to install this library is [through composer](http://getcomposer.org). [New to composer?](http://getcomposer.org/doc/00-intro.md)

composer.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
"require": {
1717
"php": ">=5.3",
1818
"react/event-loop": "~0.3.0|~0.4.0",
19-
"clue/buzz-react": "~0.2.0",
20-
"react/promise": "~1.0|~2.0"
19+
"clue/buzz-react": "~0.3.0",
20+
"react/promise": "~1.0|~2.0",
21+
"clue/json-stream": "~0.1.0"
22+
},
23+
"require-dev": {
24+
"clue/tar-react": "~0.1.0",
25+
"clue/caret-notation": "~0.2.0"
2126
}
2227
}

examples/copy.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
// this example shows how the containerCopy() call returns a TAR stream,
3+
// how it can be passed to a TAR decoder and how we can then pipe each
4+
// individual file to the console output.
5+
6+
require __DIR__ . '/../vendor/autoload.php';
7+
8+
use React\EventLoop\Factory as LoopFactory;
9+
use Clue\React\Docker\Factory;
10+
use Clue\React\Tar\Decoder;
11+
use React\Stream\ReadableStreamInterface;
12+
use Clue\CaretNotation\Encoder;
13+
14+
$container = isset($argv[1]) ? $argv[1] : 'asd';
15+
$file = isset($argv[2]) ? $argv[2] : '/etc/passwd';
16+
echo 'Container "' . $container . '" dumping "' . $file . '" (pass as arguments to this example)' . PHP_EOL;
17+
18+
$loop = LoopFactory::create();
19+
20+
$factory = new Factory($loop);
21+
$client = $factory->createClient();
22+
23+
$stream = $client->containerCopyStream($container, array('Resource' => $file));
24+
25+
$tar = new Decoder();
26+
27+
// use caret notation for any control characters expect \t, \r and \n
28+
$caret = new Encoder("\t\r\n");
29+
30+
$tar->on('entry', function ($header, ReadableStreamInterface $file) use ($caret) {
31+
// write each entry to the console output
32+
echo '########## ' . $caret->encode($header['filename']) . ' ##########' . PHP_EOL;
33+
$file->on('data', function ($chunk) use ($caret) {
34+
echo $caret->encode($chunk);
35+
});
36+
});
37+
38+
$tar->on('error', function ($e = null) {
39+
// should not be invoked, unless the stream is somehow interrupted
40+
echo 'ERROR processing tar stream' . PHP_EOL . $e;
41+
});
42+
$stream->on('error', function ($e = null) {
43+
// will be called if either parameter is invalid
44+
echo 'ERROR requesting stream' . PHP_EOL . $e;
45+
});
46+
47+
$stream->pipe($tar);
48+
49+
$loop->run();

examples/export.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
// this example shows how the containerExport() call returns a TAR stream
3+
// and how we it can be piped into a output tar file.
4+
5+
require __DIR__ . '/../vendor/autoload.php';
6+
7+
use React\EventLoop\StreamSelectLoop;
8+
use Clue\React\Docker\Factory;
9+
use React\Stream\Stream;
10+
11+
$container = isset($argv[1]) ? $argv[1] : 'asd';
12+
$target = isset($argv[2]) ? $argv[2] : ($container . '.tar');
13+
echo 'Exporting whole container "' . $container . '" to "' . $target .'" (pass as arguments to this example)' . PHP_EOL;
14+
15+
$loop = new StreamSelectLoop();
16+
17+
$factory = new Factory($loop);
18+
$client = $factory->createClient();
19+
20+
$stream = $client->containerExportStream($container);
21+
22+
$stream->on('error', function ($e = null) {
23+
// will be called if the container is invalid/does not exist
24+
echo 'ERROR requesting stream' . PHP_EOL . $e;
25+
});
26+
27+
$out = new Stream(fopen($target, 'w'), $loop);
28+
$stream->pipe($out);
29+
30+
$loop->run();

examples/pull.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
// this example shows how the imageCreateStream() call can be used to pull a given image.
3+
// demonstrates the JSON streaming API, individual progress events will be printed as they happen.
4+
5+
require __DIR__ . '/../vendor/autoload.php';
6+
7+
use React\EventLoop\Factory as LoopFactory;
8+
use Clue\React\Docker\Factory;
9+
10+
$image = isset($argv[1]) ? $argv[1] : 'clue/redis-benchmark';
11+
echo 'Pulling image "' . $image . '" (pass as argument to this example)' . PHP_EOL;
12+
13+
$loop = LoopFactory::create();
14+
15+
$factory = new Factory($loop);
16+
$client = $factory->createClient();
17+
18+
$stream = $client->imageCreateStream($image);
19+
20+
$stream->on('progress', function ($progress) {
21+
echo 'progress: '. json_encode($progress) . PHP_EOL;
22+
});
23+
24+
$stream->on('close', function () {
25+
echo 'stream closed' . PHP_EOL;
26+
});
27+
28+
$loop->run();

examples/push.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
// this example shows how the imagePush() call can be used to publish a given image.
3+
// this requires authorization and this example includes some invalid defaults.
4+
5+
require __DIR__ . '/../vendor/autoload.php';
6+
7+
use React\EventLoop\Factory as LoopFactory;
8+
use Clue\React\Docker\Factory;
9+
10+
$image = isset($argv[1]) ? $argv[1] : 'asd';
11+
$auth = json_decode('{"username": "string", "password": "string", "email": "string", "serveraddress" : "string", "auth": ""}');
12+
echo 'Pushing image "' . $image . '" (pass as argument to this example)' . PHP_EOL;
13+
14+
$loop = LoopFactory::create();
15+
16+
$factory = new Factory($loop);
17+
$client = $factory->createClient();
18+
19+
$client->imagePush($image, null, null, $auth)->then(
20+
function ($response) {
21+
echo 'response: ' . json_encode($response) . PHP_EOL;
22+
},
23+
'var_dump'
24+
);
25+
26+
$loop->run();

0 commit comments

Comments
 (0)