├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Client.php └── Io ├── ReadableDemultiplexStream.php ├── ReadableJsonStream.php ├── ResponseParser.php └── StreamingParser.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.5.0 (2024-05-03) 4 | 5 | * Feature: Full PHP 8.3 compatibility. 6 | (#85 by @yadaiio) 7 | 8 | * Minor documentation improvements. 9 | (#83 and #86 by @yadaiio) 10 | 11 | * Update test suite to support new Promise v3 and new reactphp/http `v1.10.0`. 12 | (#82 and #84 by @clue and #87 by @SimonFrings) 13 | 14 | ## 1.4.0 (2022-12-01) 15 | 16 | * Feature: Add support for PHP 8.1 and PHP 8.2. 17 | (#78 by @dinooo13) 18 | 19 | * Feature: Forward compatibility with upcoming Promise v3. 20 | (#76 by @clue) 21 | 22 | * Feature: Simplify usage by supporting new default loop. 23 | (#71 by @clue) 24 | 25 | ```php 26 | // old (still supported) 27 | $client = new Clue\React\Docker\Client($loop); 28 | 29 | // new (using default loop) 30 | $client = new Clue\React\Docker\Client(); 31 | ``` 32 | 33 | * Feature: Add commit API endpoint. 34 | (#74 by @dinooo13) 35 | 36 | ```php 37 | $client->containerCommit($container)->then(function (array $image) { 38 | var_dump($image); 39 | }, function (Exception $e) { 40 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 41 | }); 42 | ``` 43 | 44 | * Improve documentation and examples, update to use new reactphp/async package and new HTTP and Socket API. 45 | (#70 by @PaulRotmann, #72 by @SimonFrings, #73 by @clue and #77 by @dinooo13) 46 | 47 | * Improve test suite and ensure 100% code coverage. 48 | (#80, #81 by @clue and #79 by @dinooo13) 49 | 50 | ## 1.3.0 (2020-12-17) 51 | 52 | * Feature: Update to reactphp/http v1.0.0. 53 | (#64 by @clue) 54 | 55 | * Improve test suite and add `.gitattributes` to exclude dev files from exports. 56 | Add PHP 8 support, update to PHPUnit 9 and simplify test setup. 57 | (#62, #65, #66 and #67 by @SimonFrings) 58 | 59 | ## 1.2.0 (2020-03-31) 60 | 61 | * Feature: Add `containerAttach()` and `containerAttachStream()` API methods. 62 | (#61 by @clue) 63 | 64 | * Improve test suite and fix failing tests with new Docker Engine API. 65 | (#60 by @clue) 66 | 67 | ## 1.1.0 (2020-02-11) 68 | 69 | * Feature: Add network API methods. 70 | (#57 by @tjoussen) 71 | 72 | * Improve test suite by testing against PHP 7.4 and simplify test matrix 73 | and add support / sponsorship info. 74 | (#58 and #59 by @clue) 75 | 76 | ## 1.0.0 (2019-09-19) 77 | 78 | * First stable release, now following SemVer! 79 | See [**release announcement**](https://clue.engineering/2019/introducing-reactphp-docker). 80 | 81 | * Feature: Update all ReactPHP dependencies to latest versions and 82 | significantly improve performance (see included benchmark examples). 83 | (#51 and #56 by @clue) 84 | 85 | * Feature / BC break: Replace `Factory` with simplified `Client` constructor. 86 | (#49 by @clue) 87 | 88 | ```php 89 | // old 90 | $factory = new Clue\React\Docker\Factory($loop); 91 | $client = $factory->createClient($url); 92 | 93 | // new 94 | $client = new Clue\React\Docker\Client($loop, $url); 95 | ``` 96 | 97 | * Feature / BC break: Change JSON stream to always report `data` events instead of `progress`, 98 | follow strict stream semantics, support backpressure and improve error handling. 99 | (#27 and #50 by @clue) 100 | 101 | ```php 102 | // old: all JSON streams use custom "progress" event 103 | $stream = $client->eventsStream(); 104 | $stream->on('progress', function ($data) { 105 | var_dump($data); 106 | }); 107 | 108 | // new: all streams use default "data" event 109 | $stream = $client->eventsStream(); 110 | $stream->on('data', function ($data) { 111 | var_dump($data); 112 | }); 113 | 114 | // new: stream follows stream semantics and supports stream composition 115 | $stream = $client->eventsStream(); 116 | $stream->pipe($logger); 117 | ``` 118 | 119 | * Feature / BC break: Add `containerArchive()` and `containerArchiveStream()` methods and 120 | remove deprecated `containerCopy()` and `containerCopyStream()` and 121 | remove deprecated HostConfig parameter from `containerStart()`. 122 | (#42, #48 and #55 by @clue) 123 | 124 | ```php 125 | // old 126 | $client->containerCopy($container, array('Resource' => $path)); 127 | 128 | // new 129 | $client->containerArchive($container, $path); 130 | ``` 131 | 132 | * Feature / BC break: Change `execCreate()` method to accept plain params instead of config object. 133 | (#38 and #39 by @clue) 134 | 135 | * Feature / BC break: Change `execStart()` method to resolve with buffered string contents. 136 | (#35 and #40) 137 | 138 | * Feature: Add `execStartDetached()` method to resolve without waiting for exec data. 139 | (#38 by @clue) 140 | 141 | * Feature: Add `execStartStream()` method to return stream of exec data. 142 | (#37 and #40) 143 | 144 | * Feature: Add `execInspect()` method. 145 | (#34 by @clue) 146 | 147 | * Feature: Add `containerLogs()` and `containerLogsStream()` methods. 148 | (#53 and #54 by @clue) 149 | 150 | * Feature: Add `containerStats()` and `containerStatsStream()` methods. 151 | (#52 by @clue) 152 | 153 | * Feature: Add `events()` and `eventsStream()` methods 154 | (#32 by @clue) 155 | 156 | * Feature: Add `containerRename()` method. 157 | (#43 by @clue) 158 | 159 | * Feature: Timeout `$t` is optional for `containerStop()` and `containerRestart()`. 160 | (#28 by @clue) 161 | 162 | * Fix: The `containerResize()` and `execResize()` to issue `POST` request to resize TTY. 163 | (#29 and #30 by @clue) 164 | 165 | * Improve test suite by adding PHPUnit to `require-dev`, support PHPUnit 7 - legacy PHPUnit 4 166 | and test against legacy PHP 5.3 through PHP 7.3, 167 | improve documentation and update project homepage. 168 | (#31, #46 and #47 by @clue) 169 | 170 | ## 0.2.0 (2015-08-11) 171 | 172 | * Feature: Add streaming API for existing endpoints (TAR and JSON streaming). 173 | ([#9](https://github.com/clue/php-docker-react/pull/9)) 174 | * JSON streaming endpoints now resolve with an array of progress messages 175 | * Reject Promise if progress messages indicate an error 176 | 177 | * Feature: Omit empty URI parameters and refactor to use URI templates internally 178 | ([#23](https://github.com/clue/php-docker-react/pull/23)) 179 | 180 | * Improved documentation, more SOLID code base and updated dependencies. 181 | 182 | ## 0.1.0 (2014-12-08) 183 | 184 | * First tagged release 185 | 186 | ## 0.0.0 (2014-11-26) 187 | 188 | * Initial concept 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Christian Lück 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/reactphp-docker 2 | 3 | [![CI status](https://github.com/clue/reactphp-docker/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-docker/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/docker-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/docker-react) 5 | 6 | Async, event-driven access to the [Docker Engine API](https://docs.docker.com/develop/sdk/), built on top of [ReactPHP](https://reactphp.org/). 7 | 8 | [Docker](https://www.docker.com/) is a popular open source platform 9 | to run and share applications within isolated, lightweight containers. 10 | The [Docker Engine API](https://docs.docker.com/develop/sdk/) 11 | allows you to control and monitor your containers and images. 12 | Among others, it can be used to list existing images, download new images, 13 | execute arbitrary commands within isolated containers, stop running containers and much more. 14 | This lightweight library provides an efficient way to work with the Docker Engine API 15 | from within PHP. It enables you to work with its images and containers or use 16 | its event-driven model to react to changes and events happening. 17 | 18 | * **Async execution of Actions** - 19 | Send any number of actions (commands) to your Docker daemon in parallel and 20 | process their responses as soon as results come in. 21 | The Promise-based design provides a *sane* interface to working with out of order responses. 22 | * **Lightweight, SOLID design** - 23 | Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough) 24 | and does not get in your way. 25 | This library is merely a very thin wrapper around the [Docker Engine API](https://docs.docker.com/develop/sdk/). 26 | * **Good test coverage** - 27 | Comes with an automated tests suite and is regularly tested in the *real world*. 28 | 29 | **Table of contents** 30 | 31 | * [Support us](#support-us) 32 | * [Quickstart example](#quickstart-example) 33 | * [Usage](#usage) 34 | * [Client](#client) 35 | * [Commands](#commands) 36 | * [Promises](#promises) 37 | * [Blocking](#blocking) 38 | * [Command streaming](#command-streaming) 39 | * [TAR streaming](#tar-streaming) 40 | * [JSON streaming](#json-streaming) 41 | * [Install](#install) 42 | * [Tests](#tests) 43 | * [License](#license) 44 | 45 | ## Support us 46 | 47 | We invest a lot of time developing, maintaining and updating our awesome 48 | open-source projects. You can help us sustain this high-quality of our work by 49 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 50 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 51 | for details. 52 | 53 | Let's take these projects to the next level together! 🚀 54 | 55 | ## Quickstart example 56 | 57 | Once [installed](#install), you can use the following code to access the 58 | Docker API of your local docker daemon: 59 | 60 | ```php 61 | imageSearch('clue')->then(function (array $images) { 68 | var_dump($images); 69 | }, function (Exception $e) { 70 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 71 | }); 72 | ``` 73 | 74 | See also the [examples](examples/). 75 | 76 | ## Usage 77 | 78 | ### Client 79 | 80 | The `Client` is responsible for assembling and sending HTTP requests to the Docker Engine API. 81 | 82 | ```php 83 | $client = new Clue\React\Docker\Client(); 84 | ``` 85 | 86 | This class takes an optional `LoopInterface|null $loop` parameter that can be used to 87 | pass the event loop instance to use for this object. You can use a `null` value 88 | here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). 89 | This value SHOULD NOT be given unless you're sure you want to explicitly use a 90 | given event loop instance. 91 | 92 | If your Docker Engine API is not accessible using the default `unix:///var/run/docker.sock` 93 | Unix domain socket path, you may optionally pass an explicit URL like this: 94 | 95 | ```php 96 | // explicitly use given UNIX socket path 97 | $client = new Clue\React\Docker\Client(null, 'unix:///var/run/docker.sock'); 98 | 99 | // or connect via TCP/IP to a remote Docker Engine API 100 | $client = new Clue\React\Docker\Client(null, 'http://10.0.0.2:8000/'); 101 | ``` 102 | 103 | #### Commands 104 | 105 | All public methods on the `Client` resemble the API described in the [Docker Engine API documentation](https://docs.docker.com/develop/sdk/) like this: 106 | 107 | ```php 108 | $client->containerList($all, $size); 109 | $client->containerCreate($config, $name); 110 | $client->containerStart($name); 111 | $client->containerKill($name, $signal); 112 | $client->containerRemove($name, $v, $force); 113 | 114 | $client->imageList($all); 115 | $client->imageSearch($term); 116 | $client->imageCreate($fromImage, $fromSrc, $repo, $tag, $registry, $registryAuth); 117 | 118 | $client->info(); 119 | $client->version(); 120 | 121 | // many, many more… 122 | ``` 123 | 124 | Listing all available commands is out of scope here, please refer to the 125 | [Docker Engine API documentation](https://docs.docker.com/develop/sdk/) 126 | or the [class outline](src/Client.php). 127 | 128 | Each of these commands supports async operation and either *resolves* with its *results* 129 | or *rejects* with an `Exception`. 130 | Please see the following section about [promises](#promises) for more details. 131 | 132 | #### Promises 133 | 134 | Sending requests is async (non-blocking), so you can actually send multiple requests in parallel. 135 | Docker will respond to each request with a response message, the order is not guaranteed. 136 | Sending requests uses a [Promise](https://github.com/reactphp/promise)-based 137 | interface that makes it easy to react to when a command is completed 138 | (i.e. either successfully fulfilled or rejected with an error): 139 | 140 | ```php 141 | $client->version()->then( 142 | function ($result) { 143 | var_dump('Result received', $result); 144 | }, 145 | function (Exception $e) { 146 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 147 | } 148 | }); 149 | ``` 150 | 151 | If this looks strange to you, you can also use the more traditional [blocking API](#blocking). 152 | 153 | #### Blocking 154 | 155 | As stated above, this library provides you a powerful, async API by default. 156 | 157 | You can also integrate this into your traditional, blocking environment by using 158 | [reactphp/async](https://github.com/reactphp/async). This allows you to simply 159 | await commands on the client like this: 160 | 161 | ```php 162 | use function React\Async\await; 163 | 164 | $client = new Clue\React\Docker\Client(); 165 | 166 | $promise = $client->imageInspect('busybox'); 167 | 168 | try { 169 | $results = await($promise); 170 | // results successfully received 171 | } catch (Exception $e) { 172 | // an error occurred while performing the request 173 | } 174 | ``` 175 | 176 | Similarly, you can also process multiple commands concurrently and await an array of results: 177 | 178 | ```php 179 | use function React\Async\await; 180 | use function React\Promise\all; 181 | 182 | $promises = array( 183 | $client->imageInspect('busybox'), 184 | $client->imageInspect('ubuntu'), 185 | ); 186 | 187 | $inspections = await(all($promises)); 188 | ``` 189 | 190 | This is made possible thanks to fibers available in PHP 8.1+ and our 191 | compatibility API that also works on all supported PHP versions. 192 | Please refer to [reactphp/async](https://github.com/reactphp/async#readme) for more details. 193 | 194 | #### Command streaming 195 | 196 | The following API endpoints resolve with a buffered string of the command output 197 | (STDOUT and/or STDERR): 198 | 199 | ```php 200 | $client->containerAttach($container); 201 | $client->containerLogs($container); 202 | $client->execStart($exec); 203 | ``` 204 | 205 | Keep in mind that this means the whole string has to be kept in memory. 206 | If you want to access the individual output chunks as they happen or 207 | for bigger command outputs, it's usually a better idea to use a streaming 208 | approach. 209 | 210 | This works for (any number of) commands of arbitrary sizes. 211 | The following API endpoints complement the default Promise-based API and return 212 | a [`Stream`](https://github.com/reactphp/stream) instance instead: 213 | 214 | ```php 215 | $stream = $client->containerAttachStream($container); 216 | $stream = $client->containerLogsStream($container); 217 | $stream = $client->execStartStream($exec); 218 | ``` 219 | 220 | The resulting stream is a well-behaving readable stream that will emit 221 | the normal stream events: 222 | 223 | ```php 224 | $stream = $client->execStartStream($exec, $tty); 225 | 226 | $stream->on('data', function ($data) { 227 | // data will be emitted in multiple chunk 228 | echo $data; 229 | }); 230 | 231 | $stream->on('error', function (Exception $e) { 232 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 233 | }); 234 | 235 | $stream->on('close', function () { 236 | // the stream just ended, this could(?) be a good thing 237 | echo 'Ended' . PHP_EOL; 238 | }); 239 | ``` 240 | 241 | Note that by default the output of both STDOUT and STDERR will be emitted 242 | as normal `data` events. You can optionally pass a custom event name which 243 | will be used to emit STDERR data so that it can be handled separately. 244 | Note that the normal streaming primitives likely do not know about this 245 | event, so special care may have to be taken. 246 | Also note that this option has no effect if you execute with a TTY. 247 | 248 | ```php 249 | $stream = $client->execStartStream($exec, $tty, 'stderr'); 250 | 251 | $stream->on('data', function ($data) { 252 | echo 'STDOUT data: ' . $data; 253 | }); 254 | 255 | $stream->on('stderr', function ($data) { 256 | echo 'STDERR data: ' . $data; 257 | }); 258 | ``` 259 | 260 | See also the [streaming exec example](examples/exec-stream.php) and the [exec benchmark example](examples/benchmark-exec.php). 261 | 262 | The TTY mode should be set depending on whether your command needs a TTY 263 | or not. Note that toggling the TTY mode affects how/whether you can access 264 | the STDERR stream and also has a significant impact on performance for 265 | larger streams (relevant for hundreds of megabytes and more). See also the TTY 266 | mode on the `execStart*()` call. 267 | 268 | Running the provided benchmark example on a range of systems, it suggests that 269 | this library can process several gigabytes per second and may in fact outperform 270 | the Docker client and seems to be limited only by the Docker Engine implementation. 271 | Instead of posting more details here, you're encouraged to re-run the benchmarks 272 | yourself and see for yourself. 273 | The key takeway here is: *PHP is faster than you probably thought*. 274 | 275 | #### TAR streaming 276 | 277 | The following API endpoints resolve with a string in the [TAR file format](https://en.wikipedia.org/wiki/Tar_%28computing%29): 278 | 279 | ```php 280 | $client->containerExport($container); 281 | $client->containerArchive($container, $path); 282 | ``` 283 | 284 | Keep in mind that this means the whole string has to be kept in memory. 285 | This is easy to get started and works reasonably well for smaller files/containers. 286 | 287 | For bigger containers it's usually a better idea to use a streaming approach, 288 | where only small chunks have to be kept in memory. 289 | This works for (any number of) files of arbitrary sizes. 290 | The following API endpoints complement the default Promise-based API and return 291 | a [`Stream`](https://github.com/reactphp/stream) instance instead: 292 | 293 | ```php 294 | $stream = $client->containerExportStream($image); 295 | $stream = $client->containerArchiveStream($container, $path); 296 | ``` 297 | 298 | Accessing individual files in the TAR file format string or stream is out of scope 299 | for this library. 300 | Several libraries are available, one that is known to work is [clue/reactphp-tar](https://github.com/clue/reactphp-tar). 301 | 302 | See also the [archive example](examples/archive.php) and the [export example](examples/export.php). 303 | 304 | #### JSON streaming 305 | 306 | The following API endpoints take advantage of [JSON streaming](https://en.wikipedia.org/wiki/JSON_Streaming): 307 | 308 | ```php 309 | $client->imageCreate(); 310 | $client->imagePush(); 311 | $client->events(); 312 | ``` 313 | 314 | What this means is that these endpoints actually emit any number of progress 315 | events (individual JSON objects). 316 | At the HTTP level, a common response message could look like this: 317 | 318 | ``` 319 | HTTP/1.1 200 OK 320 | Content-Type: application/json 321 | 322 | {"status":"loading","current":1,"total":10} 323 | {"status":"loading","current":2,"total":10} 324 | … 325 | {"status":"loading","current":10,"total":10} 326 | {"status":"done","total":10} 327 | ``` 328 | 329 | The user-facing API hides this fact by resolving with an array of all individual 330 | progress events once the stream ends: 331 | 332 | ```php 333 | $client->imageCreate('clue/streamripper')->then( 334 | function (array $data) { 335 | // $data is an array of *all* elements in the JSON stream 336 | var_dump($data); 337 | }, 338 | function (Exception $e) { 339 | // an error occurred (possibly after receiving *some* elements) 340 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 341 | } 342 | ); 343 | ``` 344 | 345 | Keep in mind that due to resolving with an array of all progress events, 346 | this API has to keep all event objects in memory until the Promise resolves. 347 | This is easy to get started and usually works reasonably well for the above 348 | API endpoints. 349 | 350 | If you're dealing with lots of concurrent requests (100+) or 351 | if you want to access the individual progress events as they happen, you 352 | should consider using a streaming approach instead, 353 | where only individual progress event objects have to be kept in memory. 354 | The following API endpoints complement the default Promise-based API and return 355 | a [`Stream`](https://github.com/reactphp/stream) instance instead: 356 | 357 | ```php 358 | $stream = $client->imageCreateStream(); 359 | $stream = $client->imagePushStream(); 360 | $stream = $client->eventsStream(); 361 | $stream = $client->containerStatsStream($container); 362 | ``` 363 | 364 | The resulting stream will emit the following events: 365 | 366 | * `data`: for *each* element in the update stream 367 | * `error`: once if an error occurs, will close() stream then 368 | * Will emit a `RuntimeException` if an individual progress message contains an error message 369 | or any other `Exception` in case of an transport error, like invalid request etc. 370 | * `close`: once the stream ends (either finished or after "error") 371 | 372 | ```php 373 | $stream = $client->imageCreateStream('clue/redis-benchmark'); 374 | 375 | $stream->on('data', function (array $data) { 376 | // data will be emitted for *each* complete element in the JSON stream 377 | echo $data['status'] . PHP_EOL; 378 | }); 379 | 380 | $stream->on('error', function (Exception $e) { 381 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 382 | }); 383 | 384 | $stream->on('close', function () { 385 | // the JSON stream just ended, this could(?) be a good thing 386 | echo 'Ended' . PHP_EOL; 387 | }); 388 | ``` 389 | 390 | See also the [pull example](examples/pull.php) and the [push example](examples/push.php). 391 | 392 | ## Install 393 | 394 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 395 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 396 | 397 | This project follows [SemVer](https://semver.org/). 398 | This will install the latest supported version: 399 | 400 | ```bash 401 | composer require clue/docker-react:^1.5 402 | ``` 403 | 404 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 405 | 406 | This project aims to run on any platform and thus does not require any PHP 407 | extensions and supports running on legacy PHP 5.3 through current PHP 8+. 408 | It's *highly recommended to use the latest supported PHP version* for this project. 409 | 410 | ## Tests 411 | 412 | To run the test suite, you first need to clone this repo and then install all 413 | dependencies [through Composer](https://getcomposer.org/): 414 | 415 | ```bash 416 | composer install 417 | ``` 418 | 419 | To run the test suite, go to the project root and run: 420 | 421 | ```bash 422 | vendor/bin/phpunit 423 | ``` 424 | 425 | ## License 426 | 427 | This project is released under the permissive [MIT license](LICENSE). 428 | 429 | > Did you know that I offer custom development services and issuing invoices for 430 | sponsorships of releases and for contributions? Contact me (@clue) for details. 431 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/docker-react", 3 | "description": "Async, event-driven access to the Docker Engine API, built on top of ReactPHP.", 4 | "keywords": ["Docker", "container", "ReactPHP", "async"], 5 | "homepage": "https://github.com/clue/reactphp-docker", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@clue.engineering" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "clue/json-stream": "^0.1", 16 | "react/event-loop": "^1.2", 17 | "react/http": "^1.11", 18 | "react/promise": "^3.2 || ^2.11 || ^1.3", 19 | "react/promise-stream": "^1.6", 20 | "react/socket": "^1.16", 21 | "react/stream": "^1.4", 22 | "rize/uri-template": "^0.3" 23 | }, 24 | "require-dev": { 25 | "clue/caret-notation": "^0.2", 26 | "clue/tar-react": "^0.2", 27 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", 28 | "react/async": "^4.2 || ^3 || ^2" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Clue\\React\\Docker\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Clue\\Tests\\React\\Docker\\": "tests/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | browser = $browser->withBase($url); 88 | $this->parser = new ResponseParser(); 89 | $this->streamingParser = new StreamingParser(); 90 | $this->uri = new UriTemplate(); 91 | } 92 | 93 | /** 94 | * Ping the docker server 95 | * 96 | * @return PromiseInterface Promise "OK" 97 | * @link https://docs.docker.com/engine/api/v1.40/#operation/SystemPing 98 | */ 99 | public function ping() 100 | { 101 | return $this->browser->get('_ping')->then(array($this->parser, 'expectPlain')); 102 | } 103 | 104 | /** 105 | * Display system-wide information 106 | * 107 | * @return PromiseInterface Promise system info (see link) 108 | * @link https://docs.docker.com/engine/api/v1.40/#operation/SystemInfo 109 | */ 110 | public function info() 111 | { 112 | return $this->browser->get('info')->then(array($this->parser, 'expectJson')); 113 | } 114 | 115 | /** 116 | * Show the docker version information 117 | * 118 | * @return PromiseInterface Promise version info (see link) 119 | * @link https://docs.docker.com/engine/api/v1.40/#operation/SystemVersion 120 | */ 121 | public function version() 122 | { 123 | return $this->browser->get('version')->then(array($this->parser, 'expectJson')); 124 | } 125 | 126 | /** 127 | * Get container events from docker 128 | * 129 | * This is a JSON streaming API endpoint that resolves with an array of all 130 | * individual progress events. 131 | * 132 | * If you need to access the individual progress events as they happen, you 133 | * should consider using `eventsStream()` instead. 134 | * 135 | * Note that this method will buffer all events until the stream closes. 136 | * This means that you SHOULD pass a timestamp for `$until` so that this 137 | * method only polls the given time interval and then resolves. 138 | * 139 | * The optional `$filters` parameter can be used to only get events for 140 | * certain event types, images and/or containers etc. like this: 141 | * 142 | * $filters = array( 143 | * 'image' => array('ubuntu', 'busybox'), 144 | * 'event' => array('create') 145 | * ); 146 | * 147 | * 148 | * @param float|null $since timestamp used for polling 149 | * @param float|null $until timestamp used for polling 150 | * @param array $filters (optional) filters to apply (requires Docker Engine API v1.16+ / Docker v1.4+) 151 | * @return PromiseInterface Promise array of event objects 152 | * @link https://docs.docker.com/engine/api/v1.40/#operation/SystemEvents 153 | * @uses self::eventsStream() 154 | * @see self::eventsStream() 155 | */ 156 | public function events($since = null, $until = null, $filters = array()) 157 | { 158 | return $this->streamingParser->deferredStream( 159 | $this->eventsStream($since, $until, $filters) 160 | ); 161 | } 162 | 163 | /** 164 | * Get container events from docker 165 | * 166 | * This is a JSON streaming API endpoint that returns a stream instance. 167 | * 168 | * The resulting stream will emit the following events: 169 | * - data: for *each* element in the update stream 170 | * - error: once if an error occurs, will close() stream then 171 | * - close: once the stream ends (either finished or after "error") 172 | * 173 | * The optional `$filters` parameter can be used to only get events for 174 | * certain event types, images and/or containers etc. like this: 175 | * 176 | * $filters = array( 177 | * 'image' => array('ubuntu', 'busybox'), 178 | * 'event' => array('create') 179 | * ); 180 | * 181 | * 182 | * @param float|null $since timestamp used for polling 183 | * @param float|null $until timestamp used for polling 184 | * @param array $filters (optional) filters to apply (requires Docker Engine API v1.16+ / Docker v1.4+) 185 | * @return ReadableStreamInterface stream of event objects 186 | * @link https://docs.docker.com/engine/api/v1.40/#operation/SystemEvents 187 | * @see self::events() 188 | */ 189 | public function eventsStream($since = null, $until = null, $filters = array()) 190 | { 191 | return $this->streamingParser->parseJsonStream( 192 | $this->browser->requestStreaming( 193 | 'GET', 194 | $this->uri->expand( 195 | 'events{?since,until,filters}', 196 | array( 197 | 'since' => $since, 198 | 'until' => $until, 199 | 'filters' => $filters ? json_encode($filters) : null 200 | ) 201 | ) 202 | ) 203 | ); 204 | } 205 | 206 | /** 207 | * List containers 208 | * 209 | * @param boolean $all 210 | * @param boolean $size 211 | * @return PromiseInterface Promise list of container objects 212 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerList 213 | */ 214 | public function containerList($all = false, $size = false) 215 | { 216 | return $this->browser->get( 217 | $this->uri->expand( 218 | 'containers/json{?all,size}', 219 | array( 220 | 'all' => $this->boolArg($all), 221 | 'size' => $this->boolArg($size) 222 | ) 223 | ) 224 | )->then(array($this->parser, 'expectJson')); 225 | } 226 | 227 | /** 228 | * Create a container 229 | * 230 | * @param array $config e.g. `array('Image' => 'busybox', 'Cmd' => 'date')` (see link) 231 | * @param string|null $name (optional) name to assign to this container 232 | * @return PromiseInterface Promise container properties `array('Id' => $containerId', 'Warnings' => array())` 233 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerCreate 234 | */ 235 | public function containerCreate($config, $name = null) 236 | { 237 | return $this->postJson( 238 | $this->uri->expand( 239 | 'containers/create{?name}', 240 | array( 241 | 'name' => $name 242 | ) 243 | ), 244 | $config 245 | )->then(array($this->parser, 'expectJson')); 246 | } 247 | 248 | /** 249 | * Return low-level information on the container id 250 | * 251 | * @param string $container container ID 252 | * @return PromiseInterface Promise container properties 253 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerInspect 254 | */ 255 | public function containerInspect($container) 256 | { 257 | return $this->browser->get( 258 | $this->uri->expand( 259 | 'containers/{container}/json', 260 | array( 261 | 'container' => $container 262 | ) 263 | ) 264 | )->then(array($this->parser, 'expectJson')); 265 | } 266 | 267 | /** 268 | * List processes running inside the container id 269 | * 270 | * @param string $container container ID 271 | * @param string|null $ps_args (optional) ps arguments to use (e.g. aux) 272 | * @return PromiseInterface Promise 273 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerTop 274 | */ 275 | public function containerTop($container, $ps_args = null) 276 | { 277 | return $this->browser->get( 278 | $this->uri->expand( 279 | 'containers/{container}/top{?ps_args}', 280 | array( 281 | 'container' => $container, 282 | 'ps_args' => $ps_args 283 | ) 284 | ) 285 | )->then(array($this->parser, 'expectJson')); 286 | } 287 | 288 | /** 289 | * Get stdout and stderr logs from the container id 290 | * 291 | * This resolves with a string containing the log output, i.e. STDOUT 292 | * and STDERR as requested. 293 | * 294 | * Keep in mind that this means the whole string has to be kept in memory. 295 | * For bigger container logs it's usually a better idea to use a streaming 296 | * approach, see containerLogsStream() for more details. 297 | * In particular, the same also applies for the $follow flag. It can be used 298 | * to follow the container log messages as long as the container is running. 299 | * 300 | * Note that this endpoint works only for containers with the "json-file" or 301 | * "journald" logging drivers. 302 | * 303 | * Note that this endpoint internally has to check the `containerInspect()` 304 | * endpoint first in order to figure out the TTY settings to properly decode 305 | * the raw log output. 306 | * 307 | * @param string $container container ID 308 | * @param boolean $follow 1/True/true or 0/False/false, return stream. Default false 309 | * @param boolean $stdout 1/True/true or 0/False/false, show stdout log. Default true 310 | * @param boolean $stderr 1/True/true or 0/False/false, show stderr log. Default true 311 | * @param int $since UNIX timestamp (integer) to filter logs. Specifying a timestamp will only output log-entries since that timestamp. Default: 0 (unfiltered) (requires API v1.19+ / Docker v1.7+) 312 | * @param boolean $timestamps 1/True/true or 0/False/false, print timestamps for every log line. Default false 313 | * @param int|null $tail Output specified number of lines at the end of logs: all or . Default all 314 | * @return PromiseInterface Promise log output string 315 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerLogs 316 | * @uses self::containerLogsStream() 317 | * @see self::containerLogsStream() 318 | */ 319 | public function containerLogs($container, $follow = false, $stdout = true, $stderr = true, $since = 0, $timestamps = false, $tail = null) 320 | { 321 | return $this->streamingParser->bufferedStream( 322 | $this->containerLogsStream($container, $follow, $stdout, $stderr, $since, $timestamps, $tail) 323 | ); 324 | } 325 | 326 | /** 327 | * Get stdout and stderr logs from the container id 328 | * 329 | * This is a streaming API endpoint that returns a readable stream instance 330 | * containing the the log output, i.e. STDOUT and STDERR as requested. 331 | * 332 | * This works for container logs of arbitrary sizes as only small chunks have to 333 | * be kept in memory. 334 | * 335 | * This is particularly useful for the $follow flag. It can be used 336 | * to follow the container log messages as long as the container is running. 337 | * 338 | * Note that by default the output of both STDOUT and STDERR will be emitted 339 | * as normal "data" events. You can optionally pass a custom event name which 340 | * will be used to emit STDERR data so that it can be handled separately. 341 | * Note that the normal streaming primitives likely do not know about this 342 | * event, so special care may have to be taken. 343 | * Also note that this option has no effect if the container has been 344 | * created with a TTY. 345 | * 346 | * Note that this endpoint works only for containers with the "json-file" or 347 | * "journald" logging drivers. 348 | * 349 | * Note that this endpoint internally has to check the `containerInspect()` 350 | * endpoint first in order to figure out the TTY settings to properly decode 351 | * the raw log output. 352 | * 353 | * @param string $container container ID 354 | * @param boolean $follow 1/True/true or 0/False/false, return stream. Default false 355 | * @param boolean $stdout 1/True/true or 0/False/false, show stdout log. Default true 356 | * @param boolean $stderr 1/True/true or 0/False/false, show stderr log. Default true 357 | * @param int $since UNIX timestamp (integer) to filter logs. Specifying a timestamp will only output log-entries since that timestamp. Default: 0 (unfiltered) (requires API v1.19+ / Docker v1.7+) 358 | * @param boolean $timestamps 1/True/true or 0/False/false, print timestamps for every log line. Default false 359 | * @param int|null $tail Output specified number of lines at the end of logs: all or . Default all 360 | * @param string $stderrEvent custom event to emit for STDERR data (otherwise emits as "data") 361 | * @return ReadableStreamInterface log output stream 362 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerLogs 363 | * @see self::containerLogs() 364 | */ 365 | public function containerLogsStream($container, $follow = false, $stdout = true, $stderr = true, $since = 0, $timestamps = false, $tail = null, $stderrEvent = null) 366 | { 367 | $parser = $this->streamingParser; 368 | $browser = $this->browser; 369 | $url = $this->uri->expand( 370 | 'containers/{container}/logs{?follow,stdout,stderr,since,timestamps,tail}', 371 | array( 372 | 'container' => $container, 373 | 'follow' => $this->boolArg($follow), 374 | 'stdout' => $this->boolArg($stdout), 375 | 'stderr' => $this->boolArg($stderr), 376 | 'since' => ($since === 0) ? null : $since, 377 | 'timestamps' => $this->boolArg($timestamps), 378 | 'tail' => $tail 379 | ) 380 | ); 381 | 382 | // first inspect container to check TTY setting, then request logs with appropriate log parser 383 | return \React\Promise\Stream\unwrapReadable($this->containerInspect($container)->then(function ($info) use ($url, $browser, $parser, $stderrEvent) { 384 | $stream = $parser->parsePlainStream($browser->requestStreaming('GET', $url)); 385 | 386 | if (!$info['Config']['Tty']) { 387 | $stream = $parser->demultiplexStream($stream, $stderrEvent); 388 | } 389 | 390 | return $stream; 391 | })); 392 | } 393 | 394 | /** 395 | * Inspect changes on container id's filesystem 396 | * 397 | * @param string $container container ID 398 | * @return PromiseInterface Promise 399 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerChanges 400 | */ 401 | public function containerChanges($container) 402 | { 403 | return $this->browser->get( 404 | $this->uri->expand( 405 | 'containers/{container}/changes', 406 | array( 407 | 'container' => $container 408 | ) 409 | ) 410 | )->then(array($this->parser, 'expectJson')); 411 | } 412 | 413 | /** 414 | * Export the contents of container id 415 | * 416 | * This resolves with a string in the TAR file format containing all files 417 | * in the container. 418 | * 419 | * Keep in mind that this means the whole string has to be kept in memory. 420 | * For bigger containers it's usually a better idea to use a streaming 421 | * approach, see containerExportStream() for more details. 422 | * 423 | * Accessing individual files in the TAR file format string is out of scope 424 | * for this library. Several libraries are available, one that is known to 425 | * work is clue/reactphp-tar (see links). 426 | * 427 | * @param string $container container ID 428 | * @return PromiseInterface Promise tar stream 429 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerExport 430 | * @link https://github.com/clue/reactphp-tar 431 | * @see self::containerExportStream() 432 | */ 433 | public function containerExport($container) 434 | { 435 | return $this->browser->get( 436 | $this->uri->expand( 437 | 'containers/{container}/export', 438 | array( 439 | 'container' => $container 440 | ) 441 | ) 442 | )->then(array($this->parser, 'expectPlain')); 443 | } 444 | 445 | /** 446 | * Export the contents of container id 447 | * 448 | * This returns a stream in the TAR file format containing all files 449 | * in the container. 450 | * 451 | * This works for containers of arbitrary sizes as only small chunks have to 452 | * be kept in memory. 453 | * 454 | * Accessing individual files in the TAR file format stream is out of scope 455 | * for this library. Several libraries are available, one that is known to 456 | * work is clue/reactphp-tar (see links). 457 | * 458 | * The resulting stream is a well-behaving readable stream that will emit 459 | * the normal stream events. 460 | * 461 | * @param string $container container ID 462 | * @return ReadableStreamInterface tar stream 463 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerExport 464 | * @link https://github.com/clue/reactphp-tar 465 | * @see self::containerExport() 466 | */ 467 | public function containerExportStream($container) 468 | { 469 | return $this->streamingParser->parsePlainStream( 470 | $this->browser->requestStreaming( 471 | 'GET', 472 | $this->uri->expand( 473 | 'containers/{container}/export', 474 | array( 475 | 'container' => $container 476 | ) 477 | ) 478 | ) 479 | ); 480 | } 481 | 482 | /** 483 | * Returns a container’s resource usage statistics. 484 | * 485 | * This is a JSON API endpoint that resolves with a single stats info. 486 | * 487 | * If you want to monitor live stats events as they happen, you 488 | * should consider using `imageStatsStream()` instead. 489 | * 490 | * Available as of Docker Engine API v1.19 (Docker v1.7), use `containerStatsStream()` on legacy versions 491 | * 492 | * @param string $container container ID 493 | * @return PromiseInterface Promise JSON stats 494 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerStats 495 | * @see self::containerStatsStream() 496 | */ 497 | public function containerStats($container) 498 | { 499 | return $this->browser->get( 500 | $this->uri->expand( 501 | 'containers/{container}/stats{?stream}', 502 | array( 503 | 'container' => $container, 504 | 'stream' => 0 505 | ) 506 | ) 507 | )->then(array($this->parser, 'expectJson')); 508 | } 509 | 510 | /** 511 | * Returns a live stream of a container’s resource usage statistics. 512 | * 513 | * The resulting stream will emit the following events: 514 | * - data: for *each* element in the stats stream 515 | * - error: once if an error occurs, will close() stream then 516 | * - close: once the stream ends (either finished or after "error") 517 | * 518 | * Available as of Docker Engine API v1.17 (Docker v1.5) 519 | * 520 | * @param string $container container ID 521 | * @return ReadableStreamInterface JSON stats stream 522 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerStats 523 | * @see self::containerStats() 524 | */ 525 | public function containerStatsStream($container) 526 | { 527 | return $this->streamingParser->parseJsonStream( 528 | $this->browser->requestStreaming( 529 | 'GET', 530 | $this->uri->expand( 531 | 'containers/{container}/stats', 532 | array( 533 | 'container' => $container 534 | ) 535 | ) 536 | ) 537 | ); 538 | } 539 | 540 | /** 541 | * Resize the TTY of container id 542 | * 543 | * @param string $container container ID 544 | * @param int $w TTY width 545 | * @param int $h TTY height 546 | * @return PromiseInterface Promise 547 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerResize 548 | */ 549 | public function containerResize($container, $w, $h) 550 | { 551 | return $this->browser->post( 552 | $this->uri->expand( 553 | 'containers/{container}/resize{?w,h}', 554 | array( 555 | 'container' => $container, 556 | 'w' => $w, 557 | 'h' => $h, 558 | ) 559 | ) 560 | )->then(array($this->parser, 'expectEmpty')); 561 | } 562 | 563 | /** 564 | * Start the container id 565 | * 566 | * @param string $container container ID 567 | * @return PromiseInterface Promise 568 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerStart 569 | */ 570 | public function containerStart($container) 571 | { 572 | return $this->browser->post( 573 | $this->uri->expand( 574 | 'containers/{container}/start', 575 | array( 576 | 'container' => $container 577 | ) 578 | ) 579 | )->then(array($this->parser, 'expectEmpty')); 580 | } 581 | 582 | /** 583 | * Stop the container id 584 | * 585 | * @param string $container container ID 586 | * @param null|int $t (optional) number of seconds to wait before killing the container 587 | * @return PromiseInterface Promise 588 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerStop 589 | */ 590 | public function containerStop($container, $t = null) 591 | { 592 | return $this->browser->post( 593 | $this->uri->expand( 594 | 'containers/{container}/stop{?t}', 595 | array( 596 | 'container' => $container, 597 | 't' => $t 598 | ) 599 | ) 600 | )->then(array($this->parser, 'expectEmpty')); 601 | } 602 | 603 | /** 604 | * Restart the container id 605 | * 606 | * @param string $container container ID 607 | * @param null|int $t (optional) number of seconds to wait before killing the container 608 | * @return PromiseInterface Promise 609 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerRestart 610 | */ 611 | public function containerRestart($container, $t = null) 612 | { 613 | return $this->browser->post( 614 | $this->uri->expand( 615 | 'containers/{container}/restart{?t}', 616 | array( 617 | 'container' => $container, 618 | 't' => $t 619 | ) 620 | ) 621 | )->then(array($this->parser, 'expectEmpty')); 622 | } 623 | 624 | /** 625 | * Kill the container id 626 | * 627 | * @param string $container container ID 628 | * @param string|int|null $signal (optional) signal name or number 629 | * @return PromiseInterface Promise 630 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerKill 631 | */ 632 | public function containerKill($container, $signal = null) 633 | { 634 | return $this->browser->post( 635 | $this->uri->expand( 636 | 'containers/{container}/kill{?signal}', 637 | array( 638 | 'container' => $container, 639 | 'signal' => $signal 640 | ) 641 | ) 642 | )->then(array($this->parser, 'expectEmpty')); 643 | } 644 | 645 | /** 646 | * Rename the container id 647 | * 648 | * Requires Docker Engine API v1.17+ / Docker v1.5+ 649 | * 650 | * @param string $container container ID 651 | * @param string $name new name for the container 652 | * @return PromiseInterface Promise 653 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerRename 654 | */ 655 | public function containerRename($container, $name) 656 | { 657 | return $this->browser->post( 658 | $this->uri->expand( 659 | 'containers/{container}/rename{?name}', 660 | array( 661 | 'container' => $container, 662 | 'name' => $name 663 | ) 664 | ) 665 | )->then(array($this->parser, 'expectEmpty')); 666 | } 667 | 668 | /** 669 | * Pause the container id 670 | * 671 | * @param string $container container ID 672 | * @return PromiseInterface Promise 673 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerPause 674 | */ 675 | public function containerPause($container) 676 | { 677 | return $this->browser->post( 678 | $this->uri->expand( 679 | 'containers/{container}/pause', 680 | array( 681 | 'container' => $container 682 | ) 683 | ) 684 | )->then(array($this->parser, 'expectEmpty')); 685 | } 686 | 687 | /** 688 | * Unpause the container id 689 | * 690 | * @param string $container container ID 691 | * @return PromiseInterface Promise 692 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerUnpause 693 | */ 694 | public function containerUnpause($container) 695 | { 696 | return $this->browser->post( 697 | $this->uri->expand( 698 | 'containers/{container}/unpause', 699 | array( 700 | 'container' => $container 701 | ) 702 | ) 703 | )->then(array($this->parser, 'expectEmpty')); 704 | } 705 | 706 | /** 707 | * Attach to a container to read its output. 708 | * 709 | * This resolves with a string containing the container output, i.e. STDOUT 710 | * and STDERR as requested. 711 | * 712 | * Keep in mind that this means the whole string has to be kept in memory. 713 | * For a larger container output it's usually a better idea to use a streaming 714 | * approach, see `containerAttachStream()` for more details. 715 | * In particular, the same also applies for the `$stream` flag. It can be used 716 | * to follow the container output as long as the container is running. 717 | * 718 | * Note that this endpoint internally has to check the `containerInspect()` 719 | * endpoint first in order to figure out the TTY settings to properly decode 720 | * the raw container output. 721 | * 722 | * @param string $container container ID 723 | * @param bool $logs replay previous logs before attaching. Default false 724 | * @param bool $stream continue streaming. Default false 725 | * @param bool $stdout attach to stdout. Default true 726 | * @param bool $stderr attach to stderr. Default true 727 | * @return PromiseInterface Promise container output string 728 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach 729 | * @uses self::containerAttachStream() 730 | * @see self::containerAttachStream() 731 | */ 732 | public function containerAttach($container, $logs = false, $stream = false, $stdout = true, $stderr = true) 733 | { 734 | return $this->streamingParser->bufferedStream( 735 | $this->containerAttachStream($container, $logs, $stream, $stdout, $stderr) 736 | ); 737 | } 738 | 739 | /** 740 | * Attach to a container to read its output. 741 | * 742 | * This is a streaming API endpoint that returns a readable stream instance 743 | * containing the container output, i.e. STDOUT and STDERR as requested. 744 | * 745 | * This works for container output of arbitrary sizes as only small chunks have to 746 | * be kept in memory. 747 | * 748 | * This is particularly useful for the `$stream` flag. It can be used to 749 | * follow the container output as long as the container is running. Either 750 | * the `$stream` or `$logs` parameter must be `true` for this endpoint to do 751 | * anything meaningful. 752 | * 753 | * Note that by default the output of both STDOUT and STDERR will be emitted 754 | * as normal "data" events. You can optionally pass a custom event name which 755 | * will be used to emit STDERR data so that it can be handled separately. 756 | * Note that the normal streaming primitives likely do not know about this 757 | * event, so special care may have to be taken. 758 | * Also note that this option has no effect if the container has been 759 | * created with a TTY. 760 | * 761 | * Note that this endpoint internally has to check the `containerInspect()` 762 | * endpoint first in order to figure out the TTY settings to properly decode 763 | * the raw container output. 764 | * 765 | * Note that this endpoint intentionally does not expose the `$stdin` flag. 766 | * Access to STDIN will be exposed as a dedicated API endpoint in a future 767 | * version. 768 | * 769 | * @param string $container container ID 770 | * @param bool $logs replay previous logs before attaching. Default false 771 | * @param bool $stream continue streaming. Default false 772 | * @param bool $stdout attach to stdout. Default true 773 | * @param bool $stderr attach to stderr. Default true 774 | * @param string $stderrEvent custom event to emit for STDERR data (otherwise emits as "data") 775 | * @return ReadableStreamInterface container output stream 776 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach 777 | * @see self::containerAttach() 778 | */ 779 | public function containerAttachStream($container, $logs = false, $stream = false, $stdout = true, $stderr = true, $stderrEvent = null) 780 | { 781 | $parser = $this->streamingParser; 782 | $browser = $this->browser; 783 | $url = $this->uri->expand( 784 | 'containers/{container}/attach{?logs,stream,stdout,stderr}', 785 | array( 786 | 'container' => $container, 787 | 'logs' => $this->boolArg($logs), 788 | 'stream' => $this->boolArg($stream), 789 | 'stdout' => $this->boolArg($stdout), 790 | 'stderr' => $this->boolArg($stderr) 791 | ) 792 | ); 793 | 794 | // first inspect container to check TTY setting, then attach with appropriate log parser 795 | return \React\Promise\Stream\unwrapReadable($this->containerInspect($container)->then(function ($info) use ($url, $browser, $parser, $stderrEvent) { 796 | $stream = $parser->parsePlainStream($browser->requestStreaming('POST', $url)); 797 | 798 | if (!$info['Config']['Tty']) { 799 | $stream = $parser->demultiplexStream($stream, $stderrEvent); 800 | } 801 | 802 | return $stream; 803 | })); 804 | } 805 | 806 | /** 807 | * Block until container id stops, then returns the exit code 808 | * 809 | * @param string $container container ID 810 | * @return PromiseInterface Promise `array('StatusCode' => 0)` (see link) 811 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerWait 812 | */ 813 | public function containerWait($container) 814 | { 815 | return $this->browser->post( 816 | $this->uri->expand( 817 | 'containers/{container}/wait', 818 | array( 819 | 'container' => $container 820 | ) 821 | ) 822 | )->then(array($this->parser, 'expectJson')); 823 | } 824 | 825 | /** 826 | * Remove the container id from the filesystem 827 | * 828 | * @param string $container container ID 829 | * @param boolean $v Remove the volumes associated to the container. Default false 830 | * @param boolean $force Kill then remove the container. Default false 831 | * @return PromiseInterface Promise 832 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerRemove 833 | */ 834 | public function containerRemove($container, $v = false, $force = false) 835 | { 836 | return $this->browser->delete( 837 | $this->uri->expand( 838 | 'containers/{container}{?v,force}', 839 | array( 840 | 'container' => $container, 841 | 'v' => $this->boolArg($v), 842 | 'force' => $this->boolArg($force) 843 | ) 844 | ) 845 | )->then(array($this->parser, 'expectEmpty')); 846 | } 847 | 848 | /** 849 | * Get a tar archive of a resource in the filesystem of container id. 850 | * 851 | * This resolves with a string in the TAR file format containing all files 852 | * specified in the given $path. 853 | * 854 | * Keep in mind that this means the whole string has to be kept in memory. 855 | * For bigger containers it's usually a better idea to use a streaming approach, 856 | * see containerArchiveStream() for more details. 857 | * 858 | * Accessing individual files in the TAR file format string is out of scope 859 | * for this library. Several libraries are available, one that is known to 860 | * work is clue/reactphp-tar (see links). 861 | * 862 | * Available as of Docker Engine API v1.20 (Docker v1.8) 863 | * 864 | * @param string $container container ID 865 | * @param string $resource path to file or directory to archive 866 | * @return PromiseInterface Promise tar stream 867 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerArchive 868 | * @link https://github.com/clue/reactphp-tar 869 | * @see self::containerArchiveStream() 870 | */ 871 | public function containerArchive($container, $path) 872 | { 873 | return $this->browser->get( 874 | $this->uri->expand( 875 | 'containers/{container}/archive{?path}', 876 | array( 877 | 'container' => $container, 878 | 'path' => $path 879 | ) 880 | ) 881 | )->then(array($this->parser, 'expectPlain')); 882 | } 883 | 884 | /** 885 | * Get a tar archive of a resource in the filesystem of container id. 886 | * 887 | * This returns a stream in the TAR file format containing all files 888 | * specified in the given $path. 889 | * 890 | * This works for (any number of) files of arbitrary sizes as only small chunks have to 891 | * be kept in memory. 892 | * 893 | * Accessing individual files in the TAR file format stream is out of scope 894 | * for this library. Several libraries are available, one that is known to 895 | * work is clue/reactphp-tar (see links). 896 | * 897 | * The resulting stream is a well-behaving readable stream that will emit 898 | * the normal stream events. 899 | * 900 | * Available as of Docker Engine API v1.20 (Docker v1.8) 901 | * 902 | * @param string $container container ID 903 | * @param string $path path to file or directory to archive 904 | * @return ReadableStreamInterface tar stream 905 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerArchive 906 | * @link https://github.com/clue/reactphp-tar 907 | * @see self::containerArchive() 908 | */ 909 | public function containerArchiveStream($container, $path) 910 | { 911 | return $this->streamingParser->parsePlainStream( 912 | $this->browser->requestStreaming( 913 | 'GET', 914 | $this->uri->expand( 915 | 'containers/{container}/archive{?path}', 916 | array( 917 | 'container' => $container, 918 | 'path' => $path 919 | ) 920 | ) 921 | ) 922 | ); 923 | } 924 | 925 | /** 926 | * List images 927 | * 928 | * @param boolean $all 929 | * @return PromiseInterface Promise list of image objects 930 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageList 931 | * @todo support $filters param 932 | */ 933 | public function imageList($all = false) 934 | { 935 | return $this->browser->get( 936 | $this->uri->expand( 937 | 'images/json{?all}', 938 | array( 939 | 'all' => $this->boolArg($all) 940 | ) 941 | ) 942 | )->then(array($this->parser, 'expectJson')); 943 | } 944 | 945 | /** 946 | * Create an image, either by pulling it from the registry or by importing it 947 | * 948 | * This is a JSON streaming API endpoint that resolves with an array of all 949 | * individual progress events. 950 | * 951 | * If you want to access the individual progress events as they happen, you 952 | * should consider using `imageCreateStream()` instead. 953 | * 954 | * Pulling a private image from a remote registry will likely require authorization, so make 955 | * sure to pass the $registryAuth parameter, see `self::authHeaders()` for 956 | * more details. 957 | * 958 | * @param string|null $fromImage name of the image to pull 959 | * @param string|null $fromSrc source to import, - means stdin 960 | * @param string|null $repo repository 961 | * @param string|null $tag (optional) (obsolete) tag, use $repo and $fromImage in the "name:tag" instead 962 | * @param string|null $registry the registry to pull from 963 | * @param array|null $registryAuth AuthConfig object (to send as X-Registry-Auth header) 964 | * @return PromiseInterface Promise stream of message objects 965 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageCreate 966 | * @uses self::imageCreateStream() 967 | */ 968 | public function imageCreate($fromImage = null, $fromSrc = null, $repo = null, $tag = null, $registry = null, $registryAuth = null) 969 | { 970 | return $this->streamingParser->deferredStream( 971 | $this->imageCreateStream($fromImage, $fromSrc, $repo, $tag, $registry, $registryAuth) 972 | ); 973 | } 974 | 975 | /** 976 | * Create an image, either by pulling it from the registry or by importing it 977 | * 978 | * This is a JSON streaming API endpoint that returns a stream instance. 979 | * 980 | * The resulting stream will emit the following events: 981 | * - data: for *each* element in the update stream 982 | * - error: once if an error occurs, will close() stream then 983 | * - close: once the stream ends (either finished or after "error"). 984 | * 985 | * Pulling a private image from a remote registry will likely require authorization, so make 986 | * sure to pass the $registryAuth parameter, see `self::authHeaders()` for 987 | * more details. 988 | * 989 | * @param string|null $fromImage name of the image to pull 990 | * @param string|null $fromSrc source to import, - means stdin 991 | * @param string|null $repo repository 992 | * @param string|null $tag (optional) (obsolete) tag, use $repo and $fromImage in the "name:tag" instead 993 | * @param string|null $registry the registry to pull from 994 | * @param array|null $registryAuth AuthConfig object (to send as X-Registry-Auth header) 995 | * @return ReadableStreamInterface 996 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageCreate 997 | * @see self::imageCreate() 998 | * @uses self::authHeaders() 999 | */ 1000 | public function imageCreateStream($fromImage = null, $fromSrc = null, $repo = null, $tag = null, $registry = null, $registryAuth = null) 1001 | { 1002 | return $this->streamingParser->parseJsonStream( 1003 | $this->browser->requestStreaming( 1004 | 'POST', 1005 | $this->uri->expand( 1006 | 'images/create{?fromImage,fromSrc,repo,tag,registry}', 1007 | array( 1008 | 'fromImage' => $fromImage, 1009 | 'fromSrc' => $fromSrc, 1010 | 'repo' => $repo, 1011 | 'tag' => $tag, 1012 | 'registry' => $registry 1013 | ) 1014 | ), 1015 | $this->authHeaders($registryAuth) 1016 | ) 1017 | ); 1018 | } 1019 | 1020 | /** 1021 | * Return low-level information on the image name 1022 | * 1023 | * @param string $image image ID 1024 | * @return PromiseInterface Promise image properties 1025 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageInspect 1026 | */ 1027 | public function imageInspect($image) 1028 | { 1029 | return $this->browser->get( 1030 | $this->uri->expand( 1031 | 'images/{image}/json', 1032 | array( 1033 | 'image' => $image 1034 | ) 1035 | ) 1036 | )->then(array($this->parser, 'expectJson')); 1037 | } 1038 | 1039 | /** 1040 | * Return the history of the image name 1041 | * 1042 | * @param string $image image ID 1043 | * @return PromiseInterface Promise list of image history objects 1044 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageHistory 1045 | */ 1046 | public function imageHistory($image) 1047 | { 1048 | return $this->browser->get( 1049 | $this->uri->expand( 1050 | 'images/{image}/history', 1051 | array( 1052 | 'image' => $image 1053 | ) 1054 | ) 1055 | )->then(array($this->parser, 'expectJson')); 1056 | } 1057 | 1058 | /** 1059 | * Push the image name on the registry 1060 | * 1061 | * This is a JSON streaming API endpoint that resolves with an array of all 1062 | * individual progress events. 1063 | * 1064 | * If you need to access the individual progress events as they happen, you 1065 | * should consider using `imagePushStream()` instead. 1066 | * 1067 | * Pushing to a remote registry will likely require authorization, so make 1068 | * sure to pass the $registryAuth parameter, see `self::authHeaders()` for 1069 | * more details. 1070 | * 1071 | * @param string $image image ID 1072 | * @param string|null $tag (optional) the tag to associate with the image on the registry 1073 | * @param string|null $registry (optional) the registry to push to (e.g. `registry.acme.com:5000`) 1074 | * @param array|null $registryAuth (optional) AuthConfig object (to send as X-Registry-Auth header) 1075 | * @return PromiseInterface Promise list of image push messages 1076 | * @uses self::imagePushStream() 1077 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImagePush 1078 | */ 1079 | public function imagePush($image, $tag = null, $registry = null, $registryAuth = null) 1080 | { 1081 | return $this->streamingParser->deferredStream( 1082 | $this->imagePushStream($image, $tag, $registry, $registryAuth) 1083 | ); 1084 | } 1085 | 1086 | /** 1087 | * Push the image name on the registry 1088 | * 1089 | * This is a JSON streaming API endpoint that returns a stream instance. 1090 | * 1091 | * The resulting stream will emit the following events: 1092 | * - data: for *each* element in the update stream 1093 | * - error: once if an error occurs, will close() stream then 1094 | * - close: once the stream ends (either finished or after "error") 1095 | * 1096 | * Pushing to a remote registry will likely require authorization, so make 1097 | * sure to pass the $registryAuth parameter, see `self::authHeaders()` for 1098 | * more details. 1099 | * 1100 | * @param string $image image ID 1101 | * @param string|null $tag (optional) the tag to associate with the image on the registry 1102 | * @param string|null $registry (optional) the registry to push to (e.g. `registry.acme.com:5000`) 1103 | * @param array|null $registryAuth (optional) AuthConfig object (to send as X-Registry-Auth header) 1104 | * @return ReadableStreamInterface stream of image push messages 1105 | * @uses self::authHeaders() 1106 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImagePush 1107 | */ 1108 | public function imagePushStream($image, $tag = null, $registry = null, $registryAuth = null) 1109 | { 1110 | return $this->streamingParser->parseJsonStream( 1111 | $this->browser->requestStreaming( 1112 | 'POST', 1113 | $this->uri->expand( 1114 | 'images{/registry}/{image}/push{?tag}', 1115 | array( 1116 | 'registry' => $registry, 1117 | 'image' => $image, 1118 | 'tag' => $tag 1119 | ) 1120 | ), 1121 | $this->authHeaders($registryAuth) 1122 | ) 1123 | ); 1124 | } 1125 | 1126 | /** 1127 | * Tag the image name into a repository 1128 | * 1129 | * @param string $image image ID 1130 | * @param string $repo The repository to tag in 1131 | * @param string|null $tag The new tag name 1132 | * @param boolean $force 1/True/true or 0/False/false, default false 1133 | * @return PromiseInterface Promise 1134 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageTag 1135 | */ 1136 | public function imageTag($image, $repo, $tag = null, $force = false) 1137 | { 1138 | return $this->browser->post( 1139 | $this->uri->expand( 1140 | 'images/{image}/tag{?repo,tag,force}', 1141 | array( 1142 | 'image' => $image, 1143 | 'repo' => $repo, 1144 | 'tag' => $tag, 1145 | 'force' => $this->boolArg($force) 1146 | ) 1147 | ) 1148 | )->then(array($this->parser, 'expectEmpty')); 1149 | } 1150 | 1151 | /** 1152 | * Remove the image name from the filesystem 1153 | * 1154 | * @param string $image image ID 1155 | * @param boolean $force 1/True/true or 0/False/false, default false 1156 | * @param boolean $noprune 1/True/true or 0/False/false, default false 1157 | * @return PromiseInterface Promise 1158 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageRemove 1159 | */ 1160 | public function imageRemove($image, $force = false, $noprune = false) 1161 | { 1162 | return $this->browser->delete( 1163 | $this->uri->expand( 1164 | 'images/{image}{?force,noprune}', 1165 | array( 1166 | 'image' => $image, 1167 | 'force' => $this->boolArg($force), 1168 | 'noprune' => $this->boolArg($noprune) 1169 | ) 1170 | ) 1171 | )->then(array($this->parser, 'expectEmpty')); 1172 | } 1173 | 1174 | /** 1175 | * Search for an image on Docker Hub. 1176 | * 1177 | * @param string $term term to search 1178 | * @return PromiseInterface Promise list of image search result objects 1179 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ImageSearch 1180 | */ 1181 | public function imageSearch($term) 1182 | { 1183 | return $this->browser->get( 1184 | $this->uri->expand( 1185 | 'images/search{?term}', 1186 | array( 1187 | 'term' => $term 1188 | ) 1189 | ) 1190 | )->then(array($this->parser, 'expectJson')); 1191 | } 1192 | 1193 | /** 1194 | * Create a new image from a container 1195 | * 1196 | * @param ?string $container The ID or name of the container to commit 1197 | * @param ?string $repo Repository name for the created image 1198 | * @param ?string $tag Tag name for the created image 1199 | * @param ?string $comment Commit message 1200 | * @param ?string $author Author of the image (e.g., John Hannibal Smith ) 1201 | * @param bool $pause Whether to pause the container before committing, default: true 1202 | * @param ?string $changes Dockerfile's instructions to apply while committing 1203 | * @param array $config The container configuration (body of the request) 1204 | * @return PromiseInterface 1205 | * @link https://docs.docker.com/engine/api/v1.41/#operation/ImageCommit 1206 | */ 1207 | public function containerCommit($container, $repo = null, $tag = null, $comment = null, $author = null, $pause = true, $changes = null, array $config = array()) 1208 | { 1209 | return $this->postJson( 1210 | $this->uri->expand( 1211 | 'commit{?container,repo,tag,comment,author,pause,changes}', 1212 | array( 1213 | 'container' => $container, 1214 | 'repo' => $repo, 1215 | 'tag' => $tag, 1216 | 'comment' => $comment, 1217 | 'author' => $author, 1218 | 'pause' => $pause, 1219 | 'changes' => $changes, 1220 | ) 1221 | ), 1222 | $config 1223 | )->then(array($this->parser, 'expectJson')); 1224 | } 1225 | 1226 | /** 1227 | * Sets up an exec instance in a running container id 1228 | * 1229 | * The $command should be given as an array of strings (the command plus 1230 | * arguments). Alternatively, you can also pass a single command line string 1231 | * which will then be wrapped in a shell process. 1232 | * 1233 | * The TTY mode should be set depending on whether your command needs a TTY 1234 | * or not. Note that toggling the TTY mode affects how/whether you can access 1235 | * the STDERR stream and also has a significant impact on performance for 1236 | * larger streams (relevant for 100 MiB and above). See also the TTY mode 1237 | * on the `execStart*()` call: 1238 | * - create=false, start=false: 1239 | * STDOUT/STDERR are multiplexed into separate streams + quite fast. 1240 | * This is the default mode, also for `docker exec`. 1241 | * - create=true, start=true: 1242 | * STDOUT and STDERR are mixed into a single stream + relatively slow. 1243 | * This is how `docker exec -t` works internally. 1244 | * - create=false, start=true 1245 | * STDOUT is streamed, STDERR can not be accessed at all + fastest mode. 1246 | * This looks strange to you? It probably is. See also the benchmarking example. 1247 | * - create=true, start=false 1248 | * STDOUT/STDERR are multiplexed into separate streams + relatively slow 1249 | * This looks strange to you? It probably is. Consider using the first option instead. 1250 | * 1251 | * @param string $container container ID 1252 | * @param string|array $cmd Command to run specified as an array of strings or a single command string 1253 | * @param boolean $tty TTY mode 1254 | * @param boolean $stdin attaches to STDIN of the exec command 1255 | * @param boolean $stdout attaches to STDOUT of the exec command 1256 | * @param boolean $stderr attaches to STDERR of the exec command 1257 | * @param string|int $user user-specific exec, otherwise defaults to main container user (requires Docker Engine API v1.19+ / Docker v1.7+) 1258 | * @param boolean $privileged privileged exec with all capabilities enabled (requires Docker Engine API v1.19+ / Docker v1.7+) 1259 | * @return PromiseInterface Promise with exec ID in the form of `array("Id" => $execId)` 1260 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ContainerExec 1261 | */ 1262 | public function execCreate($container, $cmd, $tty = false, $stdin = false, $stdout = true, $stderr = true, $user = '', $privileged = false) 1263 | { 1264 | if (!is_array($cmd)) { 1265 | $cmd = array('sh', '-c', (string)$cmd); 1266 | } 1267 | 1268 | return $this->postJson( 1269 | $this->uri->expand( 1270 | 'containers/{container}/exec', 1271 | array( 1272 | 'container' => $container 1273 | ) 1274 | ), 1275 | array( 1276 | 'Cmd' => $cmd, 1277 | 'Tty' => !!$tty, 1278 | 'AttachStdin' => !!$stdin, 1279 | 'AttachStdout' => !!$stdout, 1280 | 'AttachStderr' => !!$stderr, 1281 | 'User' => $user, 1282 | 'Privileged' => !!$privileged, 1283 | ) 1284 | )->then(array($this->parser, 'expectJson')); 1285 | } 1286 | 1287 | /** 1288 | * Starts a previously set up exec instance id. 1289 | * 1290 | * This resolves with a string of the command output, i.e. STDOUT and STDERR 1291 | * as set up in the `execCreate()` call. 1292 | * 1293 | * Keep in mind that this means the whole string has to be kept in memory. 1294 | * If you want to access the individual output chunks as they happen or 1295 | * for bigger command outputs, it's usually a better idea to use a streaming 1296 | * approach, see `execStartStream()` for more details. 1297 | * 1298 | * @param string $exec exec ID 1299 | * @param boolean $tty tty mode 1300 | * @return PromiseInterface Promise buffered exec data 1301 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ExecStart 1302 | * @uses self::execStartStream() 1303 | * @see self::execStartStream() 1304 | * @see self::execStartDetached() 1305 | */ 1306 | public function execStart($exec, $tty = false) 1307 | { 1308 | return $this->streamingParser->bufferedStream( 1309 | $this->execStartStream($exec, $tty) 1310 | ); 1311 | } 1312 | 1313 | /** 1314 | * Starts a previously set up exec instance id. 1315 | * 1316 | * This resolves after starting the exec command, but without waiting for 1317 | * the command output (detached mode). 1318 | * 1319 | * @param string $exec exec ID 1320 | * @param boolean $tty tty mode 1321 | * @return PromiseInterface Promise 1322 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ExecStart 1323 | * @see self::execStart() 1324 | * @see self::execStartStream() 1325 | */ 1326 | public function execStartDetached($exec, $tty = false) 1327 | { 1328 | return $this->browser->post( 1329 | $this->uri->expand( 1330 | 'exec/{exec}/start', 1331 | array( 1332 | 'exec' => $exec 1333 | ) 1334 | ), 1335 | array( 1336 | 'Content-Type' => 'application/json' 1337 | ), 1338 | $this->json(array( 1339 | 'Detach' => true, 1340 | 'Tty' => !!$tty 1341 | )) 1342 | )->then(array($this->parser, 'expectEmpty')); 1343 | } 1344 | 1345 | /** 1346 | * Starts a previously set up exec instance id. 1347 | * 1348 | * This is a streaming API endpoint that returns a readable stream instance 1349 | * containing the command output, i.e. STDOUT and STDERR as set up in the 1350 | * `execCreate()` call. 1351 | * 1352 | * This works for command output of any size as only small chunks have to 1353 | * be kept in memory. 1354 | * 1355 | * Note that by default the output of both STDOUT and STDERR will be emitted 1356 | * as normal "data" events. You can optionally pass a custom event name which 1357 | * will be used to emit STDERR data so that it can be handled separately. 1358 | * Note that the normal streaming primitives likely do not know about this 1359 | * event, so special care may have to be taken. 1360 | * Also note that this option has no effect if you execute with a TTY. 1361 | * 1362 | * @param string $exec exec ID 1363 | * @param boolean $tty tty mode 1364 | * @param string $stderrEvent custom event to emit for STDERR data (otherwise emits as "data") 1365 | * @return ReadableStreamInterface stream of exec data 1366 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ExecStart 1367 | * @see self::execStart() 1368 | * @see self::execStartDetached() 1369 | */ 1370 | public function execStartStream($exec, $tty = false, $stderrEvent = null) 1371 | { 1372 | $stream = $this->streamingParser->parsePlainStream( 1373 | $this->browser->requestStreaming( 1374 | 'POST', 1375 | $this->uri->expand( 1376 | 'exec/{exec}/start', 1377 | array( 1378 | 'exec' => $exec 1379 | ) 1380 | ), 1381 | array( 1382 | 'Content-Type' => 'application/json' 1383 | ), 1384 | $this->json(array( 1385 | 'Tty' => !!$tty 1386 | )) 1387 | ) 1388 | ); 1389 | 1390 | // this is a multiplexed stream unless this is started with a TTY 1391 | if (!$tty) { 1392 | $stream = $this->streamingParser->demultiplexStream($stream, $stderrEvent); 1393 | } 1394 | 1395 | return $stream; 1396 | } 1397 | 1398 | /** 1399 | * Resizes the tty session used by the exec command id. 1400 | * 1401 | * This API is valid only if tty was specified as part of creating and starting the exec command. 1402 | * 1403 | * @param string $exec exec ID 1404 | * @param int $w TTY width 1405 | * @param int $h TTY height 1406 | * @return PromiseInterface Promise 1407 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ExecResize 1408 | */ 1409 | public function execResize($exec, $w, $h) 1410 | { 1411 | return $this->browser->post( 1412 | $this->uri->expand( 1413 | 'exec/{exec}/resize{?w,h}', 1414 | array( 1415 | 'exec' => $exec, 1416 | 'w' => $w, 1417 | 'h' => $h 1418 | ) 1419 | ) 1420 | )->then(array($this->parser, 'expectEmpty')); 1421 | } 1422 | 1423 | /** 1424 | * Returns low-level information about the exec command id. 1425 | * 1426 | * Requires Docker Engine API v1.16+ / Docker v1.4+ 1427 | * 1428 | * @param string $exec exec ID 1429 | * @return PromiseInterface Promise 1430 | * @link https://docs.docker.com/engine/api/v1.40/#operation/ExecInspect 1431 | */ 1432 | public function execInspect($exec) 1433 | { 1434 | return $this->browser->get( 1435 | $this->uri->expand( 1436 | 'exec/{exec}/json', 1437 | array( 1438 | 'exec' => $exec 1439 | ) 1440 | ) 1441 | )->then(array($this->parser, 'expectJson')); 1442 | } 1443 | 1444 | /** 1445 | * List networks. 1446 | * 1447 | * @return PromiseInterface Promise 1448 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkList 1449 | */ 1450 | public function networkList() 1451 | { 1452 | return $this->browser->get( 1453 | $this->uri->expand( 1454 | 'networks', 1455 | array() 1456 | ) 1457 | )->then(array($this->parser, 'expectJson')); 1458 | } 1459 | 1460 | /** 1461 | * Inspect network. 1462 | * 1463 | * @param string $network The network id or name 1464 | * 1465 | * @return PromiseInterface Promise 1466 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkInspect 1467 | */ 1468 | public function networkInspect($network) 1469 | { 1470 | return $this->browser->get( 1471 | $this->uri->expand( 1472 | 'networks/{network}', 1473 | array( 1474 | 'network' => $network 1475 | ) 1476 | ) 1477 | )->then(array($this->parser, 'expectJson')); 1478 | } 1479 | 1480 | /** 1481 | * Remove network. 1482 | * 1483 | * @param string $network The network id or name 1484 | * 1485 | * @return PromiseInterface Promise 1486 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkRemove 1487 | */ 1488 | public function networkRemove($network) 1489 | { 1490 | return $this->browser->delete( 1491 | $this->uri->expand( 1492 | 'networks/{network}', 1493 | array( 1494 | 'network' => $network 1495 | ) 1496 | ) 1497 | )->then(array($this->parser, 'expectEmpty')); 1498 | } 1499 | 1500 | /** 1501 | * Create network. 1502 | * 1503 | * @param string $name The network name 1504 | * @param array $config (optional) The network configuration 1505 | * 1506 | * @return PromiseInterface Promise 1507 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkCreate 1508 | */ 1509 | public function networkCreate($name, $config = array()) 1510 | { 1511 | $config['Name'] = $name; 1512 | 1513 | return $this->postJson( 1514 | $this->uri->expand( 1515 | 'networks/create' 1516 | ), 1517 | $config 1518 | )->then(array($this->parser, 'expectJson')); 1519 | } 1520 | 1521 | /** 1522 | * Connect container to network 1523 | * 1524 | * @param string $network The network id or name 1525 | * @param string $container The id or name of the container to connect to network 1526 | * @param array $endpointConfig (optional) Configuration for a network endpoint 1527 | * 1528 | * @return PromiseInterface Promise 1529 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkConnect 1530 | */ 1531 | public function networkConnect($network, $container, $endpointConfig = array()) 1532 | { 1533 | return $this->postJson( 1534 | $this->uri->expand( 1535 | 'networks/{network}/connect', 1536 | array( 1537 | 'network' => $network 1538 | ) 1539 | ), 1540 | array( 1541 | 'Container' => $container, 1542 | 'EndpointConfig' => $endpointConfig ? json_encode($endpointConfig) : null 1543 | ) 1544 | )->then(array($this->parser, 'expectJson')); 1545 | } 1546 | 1547 | /** 1548 | * Disconnect container from network. 1549 | * 1550 | * @param string $network The id or name of network 1551 | * @param string $container The id or name of container to disconnect 1552 | * @param bool $force (optional) Force the disconnect 1553 | * 1554 | * @return PromiseInterface Promise 1555 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkDisconnect 1556 | */ 1557 | public function networkDisconnect($network, $container, $force = false) 1558 | { 1559 | return $this->postJson( 1560 | $this->uri->expand( 1561 | 'networks/{network}/disconnect', 1562 | array( 1563 | 'network' => $network 1564 | ) 1565 | ), 1566 | array( 1567 | 'Container' => $container, 1568 | 'Force' => $this->boolArg($force) 1569 | ) 1570 | )->then(array($this->parser, 'expectEmpty')); 1571 | } 1572 | 1573 | /** 1574 | * Remove all unused networks. 1575 | * 1576 | * @return PromiseInterface Promise 1577 | * @link https://docs.docker.com/engine/api/v1.40/#operation/NetworkPrune 1578 | */ 1579 | public function networkPrune() 1580 | { 1581 | return $this->postJson( 1582 | $this->uri->expand( 1583 | 'networks/prune', 1584 | array() 1585 | ), 1586 | array() 1587 | )->then(array($this->parser, 'expectJson')); 1588 | } 1589 | 1590 | private function postJson($url, $data) 1591 | { 1592 | $body = $this->json($data); 1593 | $headers = array('Content-Type' => 'application/json', 'Content-Length' => strlen($body)); 1594 | 1595 | return $this->browser->post($url, $headers, $body); 1596 | } 1597 | 1598 | private function json($data) 1599 | { 1600 | if ($data === array()) { 1601 | return '{}'; 1602 | } 1603 | return json_encode($data); 1604 | } 1605 | 1606 | /** 1607 | * Helper function to send an AuthConfig object via the X-Registry-Auth header 1608 | * 1609 | * If your API call returns a "500 Internal Server Error" response with the 1610 | * message "EOF", it probably means that the endpoint requires authorization 1611 | * and you did not supply this header. 1612 | * 1613 | * Description from Docker's docs (see links): 1614 | * 1615 | * AuthConfig, set as the X-Registry-Auth header, is currently a Base64 1616 | * encoded (JSON) string with the following structure: 1617 | * ``` 1618 | * {"username": "string", "password": "string", "email": "string", "serveraddress" : "string", "auth": ""} 1619 | * ``` 1620 | * 1621 | * Notice that auth is to be left empty, serveraddress is a domain/ip without 1622 | * protocol, and that double quotes (instead of single ones) are required. 1623 | * 1624 | * @param array $registryAuth 1625 | * @return array 1626 | * @link https://docs.docker.com/engine/api/v1.40/#section/Authentication for details about the AuthConfig object 1627 | * @link https://github.com/docker/docker/issues/9315 for error description 1628 | */ 1629 | private function authHeaders($registryAuth) 1630 | { 1631 | $headers = array(); 1632 | if ($registryAuth !== null) { 1633 | $headers['X-Registry-Auth'] = base64_encode($this->json($registryAuth)); 1634 | } 1635 | 1636 | return $headers; 1637 | } 1638 | 1639 | /** 1640 | * Internal helper function used to pass boolean true values to endpoints and omit boolean false values 1641 | * 1642 | * @param boolean $value 1643 | * @return int|null returns the integer `1` for boolean true values and a `null` for boolean false values 1644 | * @see Browser::resolve() 1645 | */ 1646 | private function boolArg($value) 1647 | { 1648 | return ($value ? 1 : null); 1649 | } 1650 | } 1651 | -------------------------------------------------------------------------------- /src/Io/ReadableDemultiplexStream.php: -------------------------------------------------------------------------------- 1 | multiplexed = $multiplexed; 29 | 30 | if ($stderrEvent === null) { 31 | $stderrEvent = 'data'; 32 | } 33 | 34 | $this->stderrEvent = $stderrEvent; 35 | 36 | $out = $this; 37 | $buffer =& $this->buffer; 38 | 39 | // pass all input data chunks through the parser 40 | $multiplexed->on('data', array($out, 'push')); 41 | 42 | // forward end event to output (unless parsing is still in progress) 43 | $multiplexed->on('end', function () use (&$buffer, $out) { 44 | // buffer must be empty on end, otherwise this is an error situation 45 | if ($buffer === '') { 46 | $out->emit('end'); 47 | } else { 48 | $out->emit('error', array(new \RuntimeException('Stream ended within incomplete multiplexed chunk'))); 49 | } 50 | $out->close(); 51 | }); 52 | 53 | // forward error event to output 54 | $multiplexed->on('error', function ($error) use ($out) { 55 | $out->emit('error', array($error)); 56 | $out->close(); 57 | }); 58 | 59 | // forward close event to output 60 | $multiplexed->on('close', function () use ($out) { 61 | $out->close(); 62 | }); 63 | } 64 | 65 | /** 66 | * push the given stream chunk into the parser buffer and try to extract all frames 67 | * 68 | * @internal 69 | * @param string $chunk 70 | */ 71 | public function push($chunk) 72 | { 73 | $this->buffer .= $chunk; 74 | 75 | while ($this->buffer !== '') { 76 | if (!isset($this->buffer[7])) { 77 | // last header byte not set => no complete header in buffer 78 | break; 79 | } 80 | 81 | $header = unpack('Cstream/x/x/x/Nlength', substr($this->buffer, 0, 8)); 82 | 83 | if (!isset($this->buffer[7 + $header['length']])) { 84 | // last payload byte not set => message payload is incomplete 85 | break; 86 | } 87 | 88 | $payload = substr($this->buffer, 8, $header['length']); 89 | $this->buffer = (string)substr($this->buffer, 8 + $header['length']); 90 | 91 | $this->emit( 92 | ($header['stream'] === 2) ? $this->stderrEvent : 'data', 93 | array($payload) 94 | ); 95 | } 96 | } 97 | 98 | public function pause() 99 | { 100 | $this->multiplexed->pause(); 101 | } 102 | 103 | public function resume() 104 | { 105 | $this->multiplexed->resume(); 106 | } 107 | 108 | public function isReadable() 109 | { 110 | return $this->multiplexed->isReadable(); 111 | } 112 | 113 | public function pipe(WritableStreamInterface $dest, array $options = array()) 114 | { 115 | return Util::pipe($this, $dest, $options); 116 | } 117 | 118 | public function close() 119 | { 120 | if ($this->closed) { 121 | return; 122 | } 123 | 124 | $this->closed = true; 125 | 126 | // closing output stream closes input stream 127 | $this->multiplexed->close(); 128 | $this->buffer = ''; 129 | 130 | $this->emit('close'); 131 | $this->removeAllListeners(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Io/ReadableJsonStream.php: -------------------------------------------------------------------------------- 1 | input = $input; 25 | $this->parser = $parser = new StreamingJsonParser(); 26 | if (!$input->isReadable()) { 27 | $this->close(); 28 | return; 29 | } 30 | 31 | // pass all input data chunks through the parser 32 | $input->on('data', array($this, 'handleData')); 33 | 34 | // forward end event to output 35 | $out = $this; 36 | $input->on('end', function () use ($out, $parser) { 37 | if ($parser->isEmpty()) { 38 | $out->emit('end'); 39 | } else { 40 | $out->emit('error', array(new \RuntimeException('Stream ended within incomplete JSON data'))); 41 | } 42 | $out->close(); 43 | }); 44 | 45 | // forward error event to output 46 | $input->on('error', function ($error) use ($out) { 47 | $out->emit('error', array($error)); 48 | $out->close(); 49 | }); 50 | 51 | // forward close event to output 52 | $input->on('close', function () use ($out) { 53 | $out->close(); 54 | }); 55 | } 56 | 57 | /** 58 | * push the given stream chunk into the parser buffer and try to extract all JSON messages 59 | * 60 | * @internal 61 | * @param string $data 62 | */ 63 | public function handleData($data) 64 | { 65 | // forward each data chunk to the streaming JSON parser 66 | try { 67 | $objects = $this->parser->push($data); 68 | } catch (\Exception $e) { 69 | $this->emit('error', array($e)); 70 | $this->close(); 71 | return; 72 | } 73 | 74 | foreach ($objects as $object) { 75 | // stop emitting data if stream is already closed 76 | if ($this->closed) { 77 | return; 78 | } 79 | 80 | if (isset($object['error'])) { 81 | $this->emit('error', array(new \RuntimeException($object['error']))); 82 | $this->close(); 83 | return; 84 | } 85 | $this->emit('data', array($object)); 86 | } 87 | } 88 | 89 | public function pause() 90 | { 91 | $this->input->pause(); 92 | } 93 | 94 | public function resume() 95 | { 96 | $this->input->resume(); 97 | } 98 | 99 | public function isReadable() 100 | { 101 | return $this->input->isReadable(); 102 | } 103 | 104 | public function pipe(WritableStreamInterface $dest, array $options = array()) 105 | { 106 | return Util::pipe($this, $dest, $options); 107 | } 108 | 109 | public function close() 110 | { 111 | if ($this->closed) { 112 | return; 113 | } 114 | 115 | $this->closed = true; 116 | 117 | // closing output stream closes input stream 118 | $this->input->close(); 119 | 120 | $this->emit('close'); 121 | $this->removeAllListeners(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Io/ResponseParser.php: -------------------------------------------------------------------------------- 1 | getBody(); 26 | } 27 | 28 | /** 29 | * Returns the parsed JSON body of the given $response 30 | * 31 | * @param ResponseInterface $response 32 | * @return array 33 | */ 34 | public function expectJson(ResponseInterface $response) 35 | { 36 | // application/json 37 | 38 | return json_decode((string)$response->getBody(), true); 39 | } 40 | 41 | /** 42 | * Returns the empty plain text body of the given $response 43 | * 44 | * @param ResponseInterface $response 45 | * @return string 46 | */ 47 | public function expectEmpty(ResponseInterface $response) 48 | { 49 | // 204 No Content 50 | // no content-type 51 | 52 | return $this->expectPlain($response); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Io/StreamingParser.php: -------------------------------------------------------------------------------- 1 | 24 | * @return ReadableStreamInterface 25 | * @uses self::parsePlainSream() 26 | */ 27 | public function parseJsonStream(PromiseInterface $promise) 28 | { 29 | // application/json 30 | 31 | return new ReadableJsonStream($this->parsePlainStream($promise)); 32 | } 33 | 34 | /** 35 | * Returns a readable plain text stream for the given ResponseInterface 36 | * 37 | * @param PromiseInterface $promise Promise 38 | * @return ReadableStreamInterface 39 | */ 40 | public function parsePlainStream(PromiseInterface $promise) 41 | { 42 | // text/plain 43 | 44 | return Stream\unwrapReadable($promise->then(function (ResponseInterface $response) { 45 | return $response->getBody(); 46 | })); 47 | } 48 | 49 | /** 50 | * Returns a readable plain text stream for the given multiplexed stream using Docker's "attach multiplexing protocol" 51 | * 52 | * @param ReadableStreamInterface $input 53 | * @param string $stderrEvent 54 | * @return ReadableStreamInterface 55 | */ 56 | public function demultiplexStream(ReadableStreamInterface $input, $stderrEvent = null) 57 | { 58 | return new ReadableDemultiplexStream($input, $stderrEvent); 59 | } 60 | 61 | /** 62 | * Returns a promise which resolves with the buffered stream contents of the given stream 63 | * 64 | * @param ReadableStreamInterface $stream 65 | * @return PromiseInterface Promise 66 | */ 67 | public function bufferedStream(ReadableStreamInterface $stream) 68 | { 69 | return Stream\buffer($stream); 70 | } 71 | 72 | /** 73 | * Returns a promise which resolves with an array of all "data" events 74 | * 75 | * @param ReadableStreamInterface $stream 76 | * @return PromiseInterface Promise 77 | */ 78 | public function deferredStream(ReadableStreamInterface $stream) 79 | { 80 | // cancelling the deferred will (try to) close the stream 81 | $deferred = new Deferred(function () use ($stream) { 82 | $stream->close(); 83 | 84 | throw new RuntimeException('Cancelled'); 85 | }); 86 | 87 | if ($stream->isReadable()) { 88 | // buffer all data events for deferred resolving 89 | $buffered = array(); 90 | $stream->on('data', function ($data) use (&$buffered) { 91 | $buffered []= $data; 92 | }); 93 | 94 | // error event rejects 95 | $stream->on('error', function ($error) use ($deferred) { 96 | $deferred->reject($error); 97 | }); 98 | 99 | // close event resolves with buffered events (unless already error'ed) 100 | $stream->on('close', function () use ($deferred, &$buffered) { 101 | $deferred->resolve($buffered); 102 | }); 103 | } else { 104 | $deferred->reject(new RuntimeException('Stream already ended, looks like it could not be opened')); 105 | } 106 | 107 | return $deferred->promise(); 108 | } 109 | } 110 | --------------------------------------------------------------------------------